Skip to main content
基本 Svelte
介绍
反应性
属性
逻辑
事件
绑定
类和样式
动作
转换
高级 Svelte
高级反应性
重用内容
运动
高级绑定
高级转换
上下文 API
特殊元素
<script module>
后续步骤
基本 SvelteKit
介绍
路由
加载数据
标题和 cookie
共享模块
表单
API 路由
$app/state
错误和重定向
高级 SvelteKit
钩子
页面选项
链接选项
高级路由
高级加载
环境变量
结论

操作本质上是元素级生命周期函数。它们对于以下情况很有用:

¥Actions are essentially element-level lifecycle functions. They’re useful for things like:

  • 与第三方库交互

    ¥interfacing with third-party libraries

  • 延迟加载的图片

    ¥lazy-loaded images

  • tooltips

  • 添加自定义事件处理程序

    ¥adding custom event handlers

在这个应用中,你可以在 <canvas> 上涂鸦,并通过菜单更改颜色和画笔大小。但如果你打开菜单并使用 Tab 键循环浏览选项,你很快就会发现焦点没有被困在模式中。

¥In this app, you can scribble on the <canvas>, and change colours and brush size via the menu. But if you open the menu and cycle through the options with the Tab key, you’ll soon find that the focus isn’t trapped inside the modal.

我们可以通过操作来解决这个问题。从 actions.svelte.js 导入 trapFocus...

¥We can fix that with an action. Import trapFocus from actions.svelte.js...

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: 指令将其添加到菜单:

¥...then add it to the menu with the use: directive:

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

让我们看一下 actions.svelte.js 中的 trapFocus 函数。当节点安装到 DOM 时,使用 node(在我们的例子中是 <div class="menu">)调用操作函数。在操作中,我们有一个 effect

¥Let’s take a look at the trapFocus function in actions.svelte.js. An action function is called with a node — the <div class="menu"> in our case — when the node is mounted to the DOM. Inside the action, we have an effect.

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

¥First, we need to add an event listener that intercepts Tab key presses:

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

其次,我们需要在卸载节点时进行一些清理 — 删除事件监听器,并将焦点恢复到元素挂载之前的位置:

¥Second, we need to do some cleanup when the node is unmounted — removing the event listener, and restoring focus to where it was before the element mounted:

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

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

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

¥Now, when you open the menu, you can cycle through the options with the Tab key.

上一页 下一页
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>