Skip to main content
Svelte 基础
介绍
响应式
属性
逻辑
事件
绑定
类和样式
Actions
过渡动画
Svelte 进阶
高级响应性
复用内容
Motion
高级绑定
高级过渡效果
Context API
特殊元素
<script module>
后续步骤
SvelteKit 基础
介绍
路由
加载数据
请求头和 Cookie
共享模块
表单
API 路由
$app/state
错误和重定向
SvelteKit 进阶
钩子函数
页面选项
链接选项
高级路由
高级加载
环境变量
结论

Action 本质上是元素级别的生命周期函数。它们在以下场景很有用:

  • 与第三方库对接
  • 图片懒加载
  • 工具提示
  • 添加自定义事件处理程序

在这个应用中,你可以在 <canvas> 上涂鸦,并通过菜单改变颜色和笔刷大小。但是如果你打开菜单并使用 Tab 键在选项之间循环,你会发现焦点并没有被限制在模态框内。

我们可以用一个 action 来修复这个问题。从 actions.svelte.js 中导入 trapFocus...

App
<script>
	import Canvas from './Canvas.svelte';
	import { trapFocus } from './actions.svelte.js';

	const colors = ['red', 'orange', 'yellow', 'green', 'blue', 'indigo', 'violet', 'white', 'black'];

	let selected = $state(colors[0]);
	let size = $state(10);
	let showMenu = $state(true);
</script>
<script lang="ts">
	import Canvas from './Canvas.svelte';
	import { trapFocus } from './actions.svelte.js';

	const colors = ['red', 'orange', 'yellow', 'green', 'blue', 'indigo', 'violet', 'white', 'black'];

	let selected = $state(colors[0]);
	let size = $state(10);
	let showMenu = $state(true);
</script>

...然后使用 use: 指令将其添加到菜单上:

App
<div class="menu" use:trapFocus>

让我们看看 actions.svelte.js 中的 trapFocus 函数。当节点被挂载到 DOM 时,action 函数会被调用,传入一个 node 参数 — 在我们的例子中是 <div class="menu">。在 action 内部,我们有一个 effect

首先,我们需要添加一个事件监听器来拦截 Tab 键按下:

actions.svelte
$effect(() => {
	focusable()[0]?.focus();
	node.addEventListener('keydown', handleKeydown);
});

其次,当节点被卸载时,我们需要做一些清理工作 — 移除事件监听器,并将焦点恢复到元素挂载前的位置:

actions.svelte
$effect(() => {
	focusable()[0]?.focus();
	node.addEventListener('keydown', handleKeydown);

	return () => {
		node.removeEventListener('keydown', handleKeydown);
		previous?.focus();
	};
});

现在,当你打开菜单时,你可以使用 Tab 键在选项之间循环。

在 GitHub 编辑此页面

上一页 下一页
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
<script>
	import Canvas from './Canvas.svelte';
 
	const colors = ['red', 'orange', 'yellow', 'green', 'blue', 'indigo', 'violet', 'white', 'black'];
 
	let selected = $state(colors[0]);
	let size = $state(10);
	let showMenu = $state(true);
</script>
 
<div class="container">
	<Canvas color={selected} size={size} />
 
	{#if showMenu}
		<div
			role="presentation"
			class="modal-background"
			onclick={(event) => {
				if (event.target === event.currentTarget) {
					showMenu = false;
				}
			}}
			onkeydown={(e) => {
				if (e.key === 'Escape') {
					showMenu = false;
				}
			}}
		>
			<div class="menu">
				<div class="colors">
					{#each colors as color}
						<button
							class="color"
							aria-label={color}
							aria-current={selected === color}
							style="--color: {color}"
							onclick={() => {
								selected = color;
							}}
						></button>
					{/each}
				</div>
 
				<label>
					small
					<input type="range" bind:value={size} min="1" max="50" />
					large
				</label>
			</div>
		</div>
	{/if}
 
	<div class="controls">
		<button class="show-menu" onclick={() => showMenu = !showMenu}>
			{showMenu ? 'close' : 'menu'}
		</button>
	</div>
</div>
 
<style>
	.container {
		position: fixed;
		left: 0;
		top: 0;
		width: 100%;
		height: 100%;
	}
 
	.controls {
		position: absolute;
		left: 0;
		top: 0;
		padding: 1em;
	}
 
	.show-menu {
		width: 5em;
	}
 
	.modal-background {
		position: fixed;
		display: flex;
		justify-content: center;
		align-items: center;
		left: 0;
		top: 0;
		width: 100%;
		height: 100%;
		backdrop-filter: blur(20px);
	}
 
	.menu {
		position: relative;
		background: var(--bg-2);
		width: calc(100% - 2em);
		max-width: 28em;
		padding: 1em 1em 0.5em 1em;
		border-radius: 1em;
		box-sizing: border-box;
		user-select: none;
	}
 
	.colors {
		display: grid;
		align-items: center;
		grid-template-columns: repeat(9, 1fr);
		grid-gap: 0.5em;
	}
 
	.color {
		aspect-ratio: 1;
		border-radius: 50%;
		background: var(--color, #fff);
		transform: none;
		filter: drop-shadow(2px 2px 3px rgba(0,0,0,0.2));
		transition: all 0.1s;
	}
 
	.color[aria-current="true"] {
		transform: translate(1px, 1px);
		filter: none;
		box-shadow: inset 3px 3px 4px rgba(0,0,0,0.2);
	}
 
	.menu label {
		display: flex;
		width: 100%;
		margin: 1em 0 0 0;
	}
 
	.menu input {
		flex: 1;
	}
</style>