操作本质上是元素级生命周期函数。它们对于以下情况很有用:
¥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
...
<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:
<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:
$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:
$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.
<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>