動作本質上是元素層級的生命週期函數。它們對於以下事項很有用:
- 與第三方函式庫介接
- 延遲載入的圖片
- 工具提示
- 新增自訂事件處理器
在這個應用程式中,你可以在 <canvas>
上塗鴉,並透過選單變更顏色和筆刷大小。但是,如果你開啟選單並使用 Tab 鍵循環瀏覽選項,你會很快發現焦點並未限制在模態視窗內。
我們可以使用動作來修正這個問題。從 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>
...然後使用 use:
指令將它新增到選單中
App
<div class="menu" use:trapFocus>
讓我們來看看 actions.svelte.js
中的 trapFocus
函數。當節點掛載到 DOM 時,會使用 node
呼叫動作函數 — 在我們的例子中是 <div class="menu">
。在動作內部,我們有一個 效果。
首先,我們需要新增一個事件監聽器,攔截 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 鍵循環瀏覽選項。
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>