Angular Material 18+ 高级教程 – Material Tooltip
前言
一个常见的 Tooltip 使用场景是
当有 ellipsis 时,hover 显示全文。
Tooltip 算是一种 Popover,我们之前有讲过,要搞 Popover 可以使用底层的 CDK Overlay 来实现。
而 Angular Material Tooltip 便是基于 CDK Overlay 实现的。
基本用法
首先,我们有一个 ellipsis。
<div class="container">
<p>Lorem ipsum dolor sit amet consectetur adipisicing elit. Inventore, natus!</p>
</div>
Styles
.container {
margin-top: 64px;
margin-inline: auto;
max-width: 360px;
border: 1px solid black;
padding: 16px; p {
white-space: nowrap;
overflow-x: hidden;
text-overflow: ellipsis;
}
}
效果
后半段的内容被省略了,我们希望 hover 它能显示全文。
MatTooltip 指令
import MatTooltipModule 或者 MatTooltip 指令 (它是 Standalone Component,所以也可以单独使用)
接着 App Template
<div class="container">
<p matTooltip="Lorem ipsum dolor sit amet consectetur adipisicing elit. Inventore, natus!" >
Lorem ipsum dolor sit amet consectetur adipisicing elit. Inventore, natus!
</p>
</div>
效果
hover 会立刻 popup Tooltip,如果是 touch device 则是 long press 500ms 会 popup Tooltip。
Tooltip 优先选择 popup 在下方 (CDK Overlay Preferred Positions 概念)。
如果字太长 (超过 200px width) 它会 break to multiple line。
Options
我们可以通过一些 options 控制它的体验。
Delay show / hide
by default,hover 会立刻 popup Tooltip,如果我们希望它慢一点可以传入 @Input matTooltipShowDelay 或 matTooltipHideDelay
<p
matTooltip="Lorem ipsum dolor sit amet consectetur adipisicing elit. Inventore, natus!"
matTooltipShowDelay="1000"
matTooltipHideDelay="1000"
>
Lorem ipsum dolor sit amet consectetur adipisicing elit. Inventore, natus!
</p>
这样 show / hide 就会 delay 1 秒 (1000 milliseconds)。
效果
Disabled
<p
matTooltip="Lorem ipsum dolor sit amet consectetur adipisicing elit. Inventore, natus!"
matTooltipDisabled
>
也是通过 @Input
配上 signal 是这样
<p
matTooltip="Lorem ipsum dolor sit amet consectetur adipisicing elit. Inventore, natus!"
[matTooltipDisabled]="disabled()"
>
App 组件
export class AppComponent {
readonly disabled = signal(true);
}
Preferred positions
我们可以选择其中一个位置作为优先 popup 的地方,by default 是 below。
before 和 after 是 Logical Properties 概念,LTR (left to right) 情况下 before 指的是 left,after 指的是 right。
<p
matTooltip="Lorem ipsum dolor sit amet consectetur adipisicing elit. Inventore, natus!"
matTooltipPosition="left"
>
效果
虽然只能设置一个 position (不像 CDK Overlay 的 withPositions 可以放多个),但是它内部其实是有多个 preferred positions,
所以也会兼顾空间不足的情况。
Position at origin
by default,Tooltip 会定位在 <p> 的外面,而且和我们 hover 的地方无关。
而开启 position at origin 后,Tooltip 会依据我们 hover 的地方做显示 Tooltip。
<p
matTooltip="Lorem ipsum dolor sit amet consectetur adipisicing elit. Inventore, natus!"
matTooltipPositionAtOrigin
>
效果
Programmatic show / hide
想用 JS 控制 show / hide 也行
<p
#tooltip
matTooltip="Lorem ipsum dolor sit amet consectetur adipisicing elit. Inventore, natus!"
>
加上 #tooltip template variables,然后 query MatTooltip 调用 show / hide 方法就可以了
export class AppComponent {
readonly tooltip = viewChild.required('tooltip', { read: MatTooltip }); constructor() {
afterNextRender(() => {
window.setTimeout(() => this.tooltip().show(), 1000);
window.setTimeout(() => this.tooltip().hide(), 3000);
});
}
}
注:如果 Tooltip 处于 disabled 状态,那 show / hide 方法会失效。
Overriding styles
Tooltip by default 超过 200px 就会 break to multiple line,如果我们不喜欢,可以 override 掉它。
首先用 @Input matTooltipClass 添加一个 class
<p
matTooltip="Lorem ipsum dolor sit amet consectetur adipisicing elit. Inventore, natus!"
matTooltipClass="my-tooltip"
>
Lorem ipsum dolor sit amet consectetur adipisicing elit. Inventore, natus!
</p>
@Input matTooltipClass 也支持 ngClass 的写法,因为它内部就是使用了 ngClass。
然后在 styles.scss 写上 override CSS
.mat-mdc-tooltip.my-tooltip .mdc-tooltip__surface {
max-width: unset;
}
Tooltip 是 Overlay 做的,所以它会被 append to body,要修改它的 styles 我们需要把 styles apply to global (e.g. styles.scss)
这是它的 HTML 结构
效果
Global options
上面教的都是用 @Input 挨个设置 options,假如我们想一次性设置所有 Tooltip,那可以使用 Dependency Injection provide MAT_TOOLTIP_DEFAULT_OPTIONS token。
app.config.ts (也可以 provide to specify 组件 override default options)
export const appConfig: ApplicationConfig = {
providers: [
provideExperimentalZonelessChangeDetection(),
provideAnimationsAsync(),
{
provide: MAT_TOOLTIP_DEFAULT_OPTIONS,
useValue: {
hideDelay: 0,
showDelay: 0,
position: 'below',
positionAtOrigin: false, touchGestures: 'auto',
touchLongPressShowDelay: 500,
touchendHideDelay: 1500,
disableTooltipInteractivity: false,
} satisfies MatTooltipDefaultOptions,
},
],
};
上面写的都是默认值。
有几个 options 是无法透过 @Input 设置的,只能透过 MatTooltipDefaultOptions
- touchGestures
它有 3 个选择:'auto' | 'on' | 'off'。
在 touch device 情况下,long press 是触发 Tooltip 的手势。
但是游览器有一些 element 有原生的 long press 行为,这时候 Tooltip 和游览器就撞了。
‘auto’ 表示让游览器赢
‘on’ 表示让 Tooltip 赢
'off' 表示不支持 Tooltip 不支持 touch device
touchLongPressShowDelay
在 touch device 情况下,long press 多久后会 popup Tooltip。
touchendHideDelay
在 touch device 情况下,Tooltip popup 出来后多久它会自动 hide 起来
disableTooltipInteractivity
禁止 Tooltip 交互。
所谓的禁止就是 pointer-events: none
当 mouseleave 遇上 Tooltip
这是一个 sidebar 交互体验。
by right,当 mouse 离开 sidebar 的时候 sidebar 才会关闭。
但是由于 Tooltip 在 body,所以当 mouse 移动到 Tooltip 时,它也算是离开了 sidebar,所以 sidebar 被关闭了。
Reproduction
我们做个单元测试,还原这个问题。
App Template
<div class="container" #container>
<button
matTooltip="Lorem ipsum dolor sit amet consectetur adipisicing elit.
Voluptatibus molestiae et saepe voluptas dolore dolorum cupiditate accusantium
aliquam aut reiciendis laboriosam qui alias quisquam, ab minima adipisci tempore cumque sunt."
>
Submit
</button>
</div>
Styles
.container {
margin-top: 64px;
margin-inline: auto;
max-width: 428px;
border: 1px solid black;
padding: 16px; button {
background-color: pink;
border-width: 0;
padding: 16px;
border-radius: 4px;
cursor: pointer;
}
}
App 组件
export class AppComponent {
readonly container = viewChild.required<string, ElementRef<HTMLElement>>('container', { read: ElementRef }); constructor() {
afterNextRender(() => {
const container = this.container().nativeElement;
container.addEventListener('mouseenter', () => {
container.style.backgroundColor = 'lightblue';
}); container.addEventListener('mouseleave', () => {
container.style.removeProperty('background-color');
});
});
}
}
当 container mouse enter 就给它一个浅蓝色,mouse leave 就拿掉浅蓝色。
效果
可以看到,当 mouse 移动进 Tooltip (button 和 tooltip 之间白色的区域也是 tooltip 范围来的) 时,浅蓝色被拿掉了,
因为 Tooltip 在 body,不在 container 内,所以 mouse 算是离开 (mouse leave) 了 container。
Solution
这种鸟问题通常解决方案都不太优雅。首先我们可以先参考 Tooltip 自己怎么处理 hover。
当 mouse leave Tooltip,Tooltip 会消失。
但是,如果 mouse leave 是去到 button (the trigger of Tooltip) 则 Tooltip 不会消失。
我们可以在 trigger 和 Tooltip 之间来回游走,Tooltip 都不会消失。
Tooltip 的源码在 tooltip.ts
Tooltip 组件监听了 mouse leave 事件
relatedTarget 是指离开后,去到了哪一个 element,
如果 relatedTarget 是或包含在 trigger element (button) 里, 那就不会 hide Tooltip。
它会等到 trigger element 触发 mouse leave 而且不是 leave into Tooltip 才会 hide。
好,清楚了,那我们也可以学它,用 relatedTarget 做判断。
export class AppComponent {
readonly container = viewChild.required<string, ElementRef<HTMLElement>>('container', { read: ElementRef }); // 需要加个 class selector 做识别
readonly tooltipClassName = 'my-tooltip'; constructor() {
afterNextRender(() => {
const container = this.container().nativeElement; // 判断 relatedTarget 是不是 Tooltip
const isTooltip = (relatedTarget: EventTarget | null): relatedTarget is HTMLElement =>
relatedTarget instanceof HTMLElement && relatedTarget.closest(`.${this.tooltipClassName}`) !== null; // 判断 relatedTarget 是不是 Tooltip 组件
// Tooltip 组件是 Tooltip 的 parent
// 小心坑:
// 直觉上,Tooltip 和 Tooltip 组件应该是同一个 element 才对,或者说 Tooltip 组件只是一个简单的 wrapper 也行。
// 但实际上却不是这样,当我们 mouse leave 时,有些情况会进入到 Tooltip 组件,有些情况则会进入到 Tooltip...
// 所以必须分清楚。
const isTooltipComponent = (relatedTarget: EventTarget | null): relatedTarget is HTMLElement =>
relatedTarget instanceof HTMLElement &&
// 提醒:
// Angular Material 没有公开 Tooltip 组件的信息
// 这里我们只能依据它目前的源码,hardcode 上它的 tag name 做识别。这里未来是可能遭遇 breaking changes 的哦。
relatedTarget.tagName === 'MAT-TOOLTIP-COMPONENT' &&
// 提醒:
// 目前 Tooltip 组件只有一个 child element,那就是 Tooltip。
// 这个结构未来也有可能遭遇 breaking changes
relatedTarget.firstElementChild!.classList.contains(this.tooltipClassName); // 判断 relatedTarget 是不是 container
const isContainer = (relatedTarget: EventTarget | null): boolean =>
relatedTarget instanceof HTMLElement && container.contains(relatedTarget); container.addEventListener('mouseenter', ({ relatedTarget }) => {
// 如果 enter 是 from Tooltip 那不需要处理
if (isTooltip(relatedTarget) || isTooltipComponent(relatedTarget)) return;
container.style.backgroundColor = 'lightblue';
}); container.addEventListener('mouseleave', ({ relatedTarget }) => {
// 处理 on container mouse leave
const handleLeaveContainer = () => container.style.removeProperty('background-color'); // 如果是 leave to Tooltip,那不算 leave container
if (isTooltip(relatedTarget) || isTooltipComponent(relatedTarget)) {
const tooltipComponent = isTooltip(relatedTarget) ? relatedTarget.parentElement! : relatedTarget; // 监听 Tooltip 组件 mouse leave,如果是回来 container 那就 do nothing
// 如果是 leave to 其它地方,那就等价于 leave container 了
tooltipComponent.addEventListener(
'mouseleave',
({ relatedTarget }) => !isContainer(relatedTarget) && handleLeaveContainer(),
{ once: true },
);
return;
}
handleLeaveContainer();
});
});
}
}
效果
目录
上一篇 Angular Material 18+ 高级教程 – CDK Overlay
下一篇 Angular Material 18+ 高级教程 – CDK Table
想查看目录,请移步 Angular 18+ 高级教程 – 目录
喜欢请点推荐,若发现教程内容以新版脱节请评论通知我。happy coding
Angular Material 18+ 高级教程 – Material Tooltip的更多相关文章
- Siki_Unity_2-9_C#高级教程(未完)
Unity 2-9 C#高级教程 任务1:字符串和正则表达式任务1-1&1-2:字符串类string System.String类(string为别名) 注:string创建的字符串是不可变的 ...
- Pandas之:Pandas高级教程以铁达尼号真实数据为例
Pandas之:Pandas高级教程以铁达尼号真实数据为例 目录 简介 读写文件 DF的选择 选择列数据 选择行数据 同时选择行和列 使用plots作图 使用现有的列创建新的列 进行统计 DF重组 简 ...
- ios cocopods 安装使用及高级教程
CocoaPods简介 每种语言发展到一个阶段,就会出现相应的依赖管理工具,例如Java语言的Maven,nodejs的npm.随着iOS开发者的增多,业界也出现了为iOS程序提供依赖管理的工具,它的 ...
- 【读书笔记】.Net并行编程高级教程(二)-- 任务并行
前面一篇提到例子都是数据并行,但这并不是并行化的唯一形式,在.Net4之前,必须要创建多个线程或者线程池来利用多核技术.现在只需要使用新的Task实例就可以通过更简单的代码解决命令式任务并行问题. 1 ...
- 【读书笔记】.Net并行编程高级教程--Parallel
一直觉得自己对并发了解不够深入,特别是看了<代码整洁之道>觉得自己有必要好好学学并发编程,因为性能也是衡量代码整洁的一大标准.而且在<失控>这本书中也多次提到并发,不管是计算机 ...
- 分享25个新鲜出炉的 Photoshop 高级教程
网络上众多优秀的 Photoshop 实例教程是提高 Photoshop 技能的最佳学习途径.今天,我向大家分享25个新鲜出炉的 Photoshop 高级教程,提高你的设计技巧,制作时尚的图片效果.这 ...
- 展讯NAND Flash高级教程【转】
转自:http://wenku.baidu.com/view/d236e6727fd5360cba1adb9e.html 展讯NAND Flash高级教程
- Net并行编程高级教程--Parallel
Net并行编程高级教程--Parallel 一直觉得自己对并发了解不够深入,特别是看了<代码整洁之道>觉得自己有必要好好学学并发编程,因为性能也是衡量代码整洁的一大标准.而且在<失控 ...
- [转帖]tar高级教程:增量备份、定时备份、网络备份
tar高级教程:增量备份.定时备份.网络备份 作者: lesca 分类: Tutorials, Ubuntu 发布时间: 2012-03-01 11:42 ė浏览 27,065 次 61条评论 一.概 ...
- Django 2.0.1 官方文档翻译: 高级教程:如何编写可重用的app (page 13)
高级教程:如何编写可重用的app (page 13) 本节教程上接第七部分(Page 12).我们会把我们的 web-poll应用转换成一个独立的python包,你可以在新的项目中重用或者把它分享给其 ...
随机推荐
- thinkphp模型hasOne、hasMany、belongsTo详解
在ThinkPHP框架中,hasOne.hasMany和belongsTo是用于定义模型间一对多(1:n).一对一(1:1)和多对一(n:1)关联关系的方法.以下是一些简单的示例来解释这些关系: 1. ...
- MySQL之DCL
DCL * 一个项目创建一个用户!一个项目对应的数据库只有一个! * 这个用户只能对这个数据库有权限,其他数据库你就操作不了了! 1. 创建用户 * CREATE USER 用户名@IP地址 ID ...
- Django 通过自定义context_processors实现自定义tag
通过自定义context_processors实现自定义tag by:授客 QQ:1033553122 测试环境 Win7 Django 1.11 实践 步骤1 应用根目录下,新建自定义context ...
- 单细胞测序最好的教程(十):细胞类型注释迁移|万能的Transformer
作者按 本章节主要讲解了基于transformer的迁移注释方法TOSICA,该算法在迁移注释上达到了SOTA的水平,在注释这么卷的赛道愣是杀出了一条血路.本教程首发于单细胞最好的中文教程,未经授权许 ...
- 用.Net实现GraphRag:从零开始构建智能知识图谱
近来,大模型技术日新月异,使得与其相关的研发项目也层出不穷.其中一个备受关注的技术便是RAG(Retrieval Augmented Generation).今天,我要跟大家分享一个出色的项目:Gra ...
- STM32开发环境配置记录——关于PlatformIO + VSCode + CubeMX的集成环境配置
前言 为什么配置这样的一个环境呢?鄙人受够了Keil5那个简陋的工作环境了,实在是用不下去,调试上很容易跟CubeMX的代码产生不协调导致调试--发布代码不一致造成的一系列问题.CubeIDE虽说 ...
- RDD | 算子 | 持久化
分布式集合对象上的API称之为算子 算子分为两类: transformation算子:指返回值仍然是rdd,类似于stream里的中间流 这类算子与中间流相同,是懒加载的 action算子:返回值不是 ...
- 计算机类的短周期的SCI期刊
<Human-centric Computing and Information Sciences> 韩国人办的,Open Access,周期短,费用高,SCI二区,水毕业可用. 以下引自 ...
- 为什么阿波罗机器人(Apollo)是外观最帅的机器人 ?
资料: https://www.youtube.com/watch?v=3CdwPGC9nyk 答案很简单,那就是这个公司单独找了一个外观设计团队,单独设计的外观. 看来啥事情要想搞的好,那就得多花钱 ...
- MindSpore计算框架如何发布训练好的模型到官方模型仓库MindSpore_Hub上
相关官方资料: https://www.mindspore.cn/tutorial/training/zh-CN/r1.2/use/publish_model.html 参考地址: https://g ...