Skip to main content
Bases de Svelte
Introduction
Réactivité
Props
Logique
Évènements
Liaisons
Classes et styles
Attachements
Transitions
Svelte avancé
Réactivité avancée
Réutiliser du contenu
Mouvements
Liaisons avancées
Transitions avancées
API de contexte
Éléments spéciaux
<script module>
Next steps
Bases de SvelteKit
Introduction
Routing
Chargement de données
En-têtes et cookies
Modules partagés
Formulaires
Routes d’API
$app/state
Erreurs et redirections
SvelteKit avancé
Hooks
Options de page
Options de lien
Routing avancé
Chargement avancé
Variables d’environnement
Conclusion

Les attachements sont essentiellement des fonctions de cycle agissant au niveau de l'élément. Ils sont utiles pour des choses comme :

  • s'interfacer avec des librairies tierces
  • charger des images en différé ("lazy-loading")
  • afficher des tooltips
  • ajouter des gestionnaires d'évènements personnalisés

Dans cette application, vous pouvez gribouiller sur le <canvas>, et changer les couleurs et la taille du pinceau via le menu. Mais si vous ouvrez le menu et parcourez les options avec la touche Tab, vous constaterez que le focus n'est pas piégé au sein de la modale.

Safari ne fournit le focus avec la touche Tab qu'aux champs textes et aux menus pop-up, et ce par défaut. Pour suivre cet exercice dans Safari, utilisez Option + Tab ou activer l'option permettant d'utiliser Tab pour focaliser dans les options avancées de Safari.

Nous pouvons corriger cela avec un attachement. Importer trapFocus depuis attachments.svelte.js...

App
<script>
	import Canvas from './Canvas.svelte';
	import { trapFocus } from './attachments.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 './attachments.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>

... puis ajoutez-le au menu avec la balise {@attach} :

App
<div class="menu" {@attach trapFocus}>

Jetons un coup d'oeil à la fonction trapFocus dans le fichie attachments.svelte.js. Une fonction d'attachement est exécutée avec un node — l'élément <div class="menu"> dans notre cas — lorsque le noeud est monté dans le DOM. Les attachements sont exécutés dans des effets, et sont donc ré-exécutés à chaque fois qu'un état lu dans la fonction est mis à jour.

D'abord, nous avons besoin d'ajouter un gestionnaire d'évènement qui intercepte les utilisations de la touche Tab :

attachments.svelte
focusable()[0]?.focus();
const off = on(node, 'keydown', handleKeydown);

on est une surcouche par-dessus addEventListener qui utilise la délégation d'évènements. Elle renvoie une fonction qui supprime ce gestionnaire.

Puis, nous devons faire un peu de nettoyage lorsque le noeud est démonté — supprimer le gestionnaire d'évènements, et rétablir le focus à l'endroit où il se trouvait avant le montage de l'élément. Comme pour les effets, un attachement peut renvoyer une fonction de nettoyage, qui sera exécutée immédiatement avant que l'attachement ne soit ré-exécuté, ou après que l'élément ne soit supprimé du DOM :

attachments.svelte
focusable()[0]?.focus();
const off = on(node, 'keydown', handleKeydown);

return () => {
	off();
	previous?.focus();
};

Désormais, lorsque vous ouvrez le menu, vous pouvez parcourir en boucle les options avec la touche Tab.

Modifier cette page sur 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>
					petit
					<input type="range" bind:value={size} min="1" max="50" />
					grand
				</label>
			</div>
		</div>
	{/if}
 
	<div class="controls">
		<button class="show-menu" onclick={() => showMenu = !showMenu}>
			{showMenu ? 'fermer' : '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>