@db-vincent/components 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +3 -3
- package/src/astro/Avatar.astro +88 -0
- package/src/astro/AvatarGroup.astro +63 -0
- package/src/astro/Chip.astro +22 -3
- package/src/astro/Drawer.astro +30 -0
- package/src/astro/Grid.astro +7 -1
- package/src/astro/GridItem.astro +3 -1
- package/src/astro/Menu.astro +151 -0
- package/src/astro/MenuItem.astro +57 -0
- package/src/astro/Modal.astro +10 -3
- package/src/astro/Pagination.astro +82 -32
- package/src/astro/Popover.astro +51 -0
- package/src/astro/Progress.astro +71 -0
- package/src/astro/Select.astro +211 -194
- package/src/astro/Skeleton.astro +4 -2
- package/src/astro/Spinner.astro +1 -0
- package/src/astro/Stepper.astro +82 -0
- package/src/astro/Switch.astro +78 -0
- package/src/astro/Table.astro +90 -76
- package/src/astro/Tabs.astro +83 -0
- package/src/astro/TextField.astro +100 -82
- package/src/astro/Toast.astro +1 -1
- package/src/astro/Tooltip.astro +54 -0
- package/src/data/icons.ts +16 -0
- package/src/scripts/avatar.ts +33 -0
- package/src/scripts/menu.ts +218 -0
- package/src/scripts/tabs.ts +127 -0
- package/src/scripts/toast.ts +45 -11
- package/src/scripts/tooltip.ts +127 -0
- package/src/styles/accordion.css +2 -0
- package/src/styles/alert.css +2 -0
- package/src/styles/avatar.css +131 -0
- package/src/styles/badge.css +2 -0
- package/src/styles/base.css +2 -0
- package/src/styles/breadcrumbs.css +2 -0
- package/src/styles/button.css +13 -2
- package/src/styles/card.css +8 -4
- package/src/styles/chip.css +2 -0
- package/src/styles/choice.css +8 -2
- package/src/styles/code.css +2 -0
- package/src/styles/empty.css +2 -0
- package/src/styles/example.css +2 -0
- package/src/styles/grid.css +2 -0
- package/src/styles/input.css +11 -1
- package/src/styles/menu.css +151 -0
- package/src/styles/modal.css +169 -27
- package/src/styles/motion.css +21 -3
- package/src/styles/nav.css +2 -0
- package/src/styles/navbar.css +2 -0
- package/src/styles/page-header.css +3 -0
- package/src/styles/pagination.css +4 -0
- package/src/styles/popover.css +134 -0
- package/src/styles/progress.css +95 -0
- package/src/styles/section.css +2 -0
- package/src/styles/select.css +12 -1
- package/src/styles/skeleton.css +2 -0
- package/src/styles/spinner.css +2 -0
- package/src/styles/sr-only.css +14 -0
- package/src/styles/stepper.css +169 -0
- package/src/styles/switch.css +143 -0
- package/src/styles/table.css +2 -0
- package/src/styles/tabs.css +120 -0
- package/src/styles/theme-toggle.css +2 -0
- package/src/styles/toast.css +41 -23
- package/src/styles/tooltip.css +63 -0
- package/src/styles/topbar.css +2 -0
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
---
|
|
2
|
+
import Button from "./Button.astro";
|
|
3
|
+
import "../styles/popover.css";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Popover on the native `popover` attribute. The trigger's `popovertarget`
|
|
7
|
+
* supplies toggling, light dismiss, Escape and focus return, so no script is
|
|
8
|
+
* needed. Placement uses CSS anchor positioning and flips near the viewport
|
|
9
|
+
* edge. For a list of commands use Menu; for a hint use Tooltip.
|
|
10
|
+
*/
|
|
11
|
+
interface Props {
|
|
12
|
+
/** Accessible name for the surface. */
|
|
13
|
+
label: string;
|
|
14
|
+
triggerLabel?: string;
|
|
15
|
+
triggerVariant?: "primary" | "secondary" | "ghost" | "danger";
|
|
16
|
+
placement?: "top" | "bottom" | "start" | "end";
|
|
17
|
+
id?: string;
|
|
18
|
+
class?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const {
|
|
22
|
+
label,
|
|
23
|
+
triggerLabel = "Open",
|
|
24
|
+
triggerVariant = "secondary",
|
|
25
|
+
placement = "bottom",
|
|
26
|
+
id,
|
|
27
|
+
class: className,
|
|
28
|
+
} = Astro.props;
|
|
29
|
+
|
|
30
|
+
const surfaceId = id ?? `popover-${Math.random().toString(36).slice(2, 9)}`;
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
<span class="ds-anchor">
|
|
34
|
+
<Button
|
|
35
|
+
variant={triggerVariant}
|
|
36
|
+
type="button"
|
|
37
|
+
popovertarget={surfaceId}
|
|
38
|
+
aria-haspopup="dialog"
|
|
39
|
+
>
|
|
40
|
+
{triggerLabel}
|
|
41
|
+
</Button>
|
|
42
|
+
<div
|
|
43
|
+
class:list={["ds-popover", `ds-popover--${placement}`, className]}
|
|
44
|
+
id={surfaceId}
|
|
45
|
+
popover="auto"
|
|
46
|
+
role="dialog"
|
|
47
|
+
aria-label={label}
|
|
48
|
+
>
|
|
49
|
+
<slot />
|
|
50
|
+
</div>
|
|
51
|
+
</span>
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
---
|
|
2
|
+
import "../styles/progress.css";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Progress bar. Determinate when `value` is set, indeterminate otherwise.
|
|
6
|
+
* Uses role="progressbar" rather than <progress> so the bar can animate with
|
|
7
|
+
* a transform and fall back to a static bar under reduced motion.
|
|
8
|
+
*/
|
|
9
|
+
interface Props {
|
|
10
|
+
label: string;
|
|
11
|
+
/** Current value, 0 to `max`. Omit for an indeterminate wait. */
|
|
12
|
+
value?: number;
|
|
13
|
+
max?: number;
|
|
14
|
+
/** Overrides the visible and announced value, e.g. "3 of 5 files". */
|
|
15
|
+
valueText?: string;
|
|
16
|
+
tone?: "accent" | "success" | "warning" | "danger";
|
|
17
|
+
size?: "sm" | "md";
|
|
18
|
+
id?: string;
|
|
19
|
+
class?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const {
|
|
23
|
+
label,
|
|
24
|
+
value,
|
|
25
|
+
max = 100,
|
|
26
|
+
valueText,
|
|
27
|
+
tone = "accent",
|
|
28
|
+
size = "md",
|
|
29
|
+
id,
|
|
30
|
+
class: className,
|
|
31
|
+
} = Astro.props;
|
|
32
|
+
|
|
33
|
+
const labelId = `${id ?? `progress-${Math.random().toString(36).slice(2, 9)}`}-label`;
|
|
34
|
+
const determinate = value !== undefined;
|
|
35
|
+
const clamped = determinate ? Math.min(Math.max(value, 0), max) : 0;
|
|
36
|
+
const fraction = max > 0 ? clamped / max : 0;
|
|
37
|
+
const text =
|
|
38
|
+
valueText ?? (determinate ? `${Math.round(fraction * 100)}%` : undefined);
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
<div
|
|
42
|
+
class:list={[
|
|
43
|
+
"ds-progress",
|
|
44
|
+
tone !== "accent" && `ds-progress--${tone}`,
|
|
45
|
+
size !== "md" && `ds-progress--${size}`,
|
|
46
|
+
!determinate && "ds-progress--indeterminate",
|
|
47
|
+
className,
|
|
48
|
+
]}
|
|
49
|
+
id={id}
|
|
50
|
+
>
|
|
51
|
+
<div class="ds-progress__head">
|
|
52
|
+
<span class="ds-progress__label" id={labelId}>
|
|
53
|
+
{label}
|
|
54
|
+
</span>
|
|
55
|
+
{text && <span class="ds-progress__value">{text}</span>}
|
|
56
|
+
</div>
|
|
57
|
+
<div
|
|
58
|
+
class="ds-progress__track"
|
|
59
|
+
role="progressbar"
|
|
60
|
+
aria-labelledby={labelId}
|
|
61
|
+
aria-valuemin="0"
|
|
62
|
+
aria-valuemax={max}
|
|
63
|
+
aria-valuenow={determinate ? clamped : undefined}
|
|
64
|
+
aria-valuetext={text}
|
|
65
|
+
>
|
|
66
|
+
<div
|
|
67
|
+
class="ds-progress__bar"
|
|
68
|
+
style={determinate ? `--ds-progress-value: ${fraction}` : undefined}
|
|
69
|
+
></div>
|
|
70
|
+
</div>
|
|
71
|
+
</div>
|
package/src/astro/Select.astro
CHANGED
|
@@ -186,218 +186,235 @@ const display =
|
|
|
186
186
|
</div>
|
|
187
187
|
|
|
188
188
|
<script>
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
"[data-ds-select
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
list.querySelectorAll<HTMLElement>('[role="option"]'),
|
|
205
|
-
);
|
|
206
|
-
let active = -1;
|
|
207
|
-
|
|
208
|
-
const selected = () =>
|
|
209
|
-
options.filter(
|
|
210
|
-
(option) => option.getAttribute("aria-selected") === "true",
|
|
211
|
-
);
|
|
189
|
+
const bind = () => {
|
|
190
|
+
document
|
|
191
|
+
.querySelectorAll<HTMLElement>("[data-ds-select]")
|
|
192
|
+
.forEach((root) => {
|
|
193
|
+
if (root.dataset.dsBound) return;
|
|
194
|
+
root.dataset.dsBound = "true";
|
|
195
|
+
const trigger = root.querySelector<HTMLButtonElement>(
|
|
196
|
+
"[data-ds-select-trigger]",
|
|
197
|
+
);
|
|
198
|
+
const popup = root.querySelector<HTMLElement>("[data-ds-popup]");
|
|
199
|
+
const list = root.querySelector<HTMLElement>('[role="listbox"]');
|
|
200
|
+
const values = root.querySelector<HTMLElement>("[data-ds-values]");
|
|
201
|
+
const valueLabel = root.querySelector<HTMLElement>("[data-ds-value]");
|
|
202
|
+
const done = root.querySelector<HTMLButtonElement>("[data-ds-done]");
|
|
203
|
+
if (!trigger || !popup || !list || !valueLabel) return;
|
|
212
204
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
: `${chosen.length} selected`;
|
|
221
|
-
trigger.classList.toggle("is-placeholder", chosen.length === 0);
|
|
222
|
-
trigger.title = chosen.map((option) => option.dataset.label).join(", ");
|
|
223
|
-
};
|
|
205
|
+
const multiple = root.dataset.multiple === "true";
|
|
206
|
+
const name = root.dataset.name || "";
|
|
207
|
+
const placeholder = root.dataset.placeholder || "Select an option";
|
|
208
|
+
const options = Array.from(
|
|
209
|
+
list.querySelectorAll<HTMLElement>('[role="option"]'),
|
|
210
|
+
);
|
|
211
|
+
let active = -1;
|
|
224
212
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
const input = document.createElement("input");
|
|
230
|
-
input.type = "hidden";
|
|
231
|
-
input.name = name;
|
|
232
|
-
input.value = option.dataset.value || "";
|
|
233
|
-
values.append(input);
|
|
234
|
-
});
|
|
235
|
-
};
|
|
213
|
+
const selected = () =>
|
|
214
|
+
options.filter(
|
|
215
|
+
(option) => option.getAttribute("aria-selected") === "true",
|
|
216
|
+
);
|
|
236
217
|
|
|
237
|
-
|
|
238
|
-
|
|
218
|
+
const updateLabel = () => {
|
|
219
|
+
const chosen = selected();
|
|
220
|
+
valueLabel.textContent =
|
|
221
|
+
chosen.length === 0
|
|
222
|
+
? placeholder
|
|
223
|
+
: chosen.length === 1
|
|
224
|
+
? chosen[0].dataset.label || ""
|
|
225
|
+
: `${chosen.length} selected`;
|
|
226
|
+
trigger.classList.toggle("is-placeholder", chosen.length === 0);
|
|
227
|
+
trigger.title = chosen
|
|
228
|
+
.map((option) => option.dataset.label)
|
|
229
|
+
.join(", ");
|
|
230
|
+
};
|
|
239
231
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
};
|
|
232
|
+
const syncValues = () => {
|
|
233
|
+
if (!values) return;
|
|
234
|
+
values.replaceChildren();
|
|
235
|
+
selected().forEach((option) => {
|
|
236
|
+
const input = document.createElement("input");
|
|
237
|
+
input.type = "hidden";
|
|
238
|
+
input.name = name;
|
|
239
|
+
input.value = option.dataset.value || "";
|
|
240
|
+
values.append(input);
|
|
241
|
+
});
|
|
242
|
+
};
|
|
252
243
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
active = Math.max(0, Math.min(options.length - 1, index));
|
|
256
|
-
options.forEach((option, position) =>
|
|
257
|
-
option.classList.toggle("is-active", position === active),
|
|
258
|
-
);
|
|
259
|
-
if (focus) options[active]?.focus();
|
|
260
|
-
};
|
|
244
|
+
const enabled = (index: number) =>
|
|
245
|
+
options[index]?.getAttribute("aria-disabled") !== "true";
|
|
261
246
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
247
|
+
// Move from `from` in `direction`, skipping disabled options; stays put
|
|
248
|
+
// at either end.
|
|
249
|
+
const step = (from: number, direction: 1 | -1) => {
|
|
250
|
+
for (
|
|
251
|
+
let index = from + direction;
|
|
252
|
+
index >= 0 && index < options.length;
|
|
253
|
+
index += direction
|
|
254
|
+
) {
|
|
255
|
+
if (enabled(index)) return index;
|
|
256
|
+
}
|
|
257
|
+
return active;
|
|
258
|
+
};
|
|
268
259
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
setActive(current >= 0 ? current : step(-1, 1), true);
|
|
278
|
-
};
|
|
260
|
+
const setActive = (index: number, focus = false) => {
|
|
261
|
+
if (!options.length) return;
|
|
262
|
+
active = Math.max(0, Math.min(options.length - 1, index));
|
|
263
|
+
options.forEach((option, position) =>
|
|
264
|
+
option.classList.toggle("is-active", position === active),
|
|
265
|
+
);
|
|
266
|
+
if (focus) options[active]?.focus();
|
|
267
|
+
};
|
|
279
268
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
);
|
|
287
|
-
} else {
|
|
288
|
-
options.forEach((entry) =>
|
|
289
|
-
entry.setAttribute(
|
|
290
|
-
"aria-selected",
|
|
291
|
-
entry === option ? "true" : "false",
|
|
292
|
-
),
|
|
293
|
-
);
|
|
294
|
-
}
|
|
295
|
-
syncValues();
|
|
296
|
-
updateLabel();
|
|
297
|
-
root.dispatchEvent(
|
|
298
|
-
new CustomEvent("change", {
|
|
299
|
-
bubbles: true,
|
|
300
|
-
detail: { values: selected().map((entry) => entry.dataset.value) },
|
|
301
|
-
}),
|
|
302
|
-
);
|
|
303
|
-
if (multiple) {
|
|
304
|
-
option.focus();
|
|
305
|
-
} else {
|
|
306
|
-
close(true);
|
|
307
|
-
}
|
|
308
|
-
};
|
|
269
|
+
const close = (focusTrigger = false) => {
|
|
270
|
+
popup.hidden = true;
|
|
271
|
+
trigger.setAttribute("aria-expanded", "false");
|
|
272
|
+
root.classList.remove("is-open");
|
|
273
|
+
if (focusTrigger) trigger.focus();
|
|
274
|
+
};
|
|
309
275
|
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
276
|
+
const open = () => {
|
|
277
|
+
if (trigger.disabled) return;
|
|
278
|
+
popup.hidden = false;
|
|
279
|
+
trigger.setAttribute("aria-expanded", "true");
|
|
280
|
+
root.classList.add("is-open");
|
|
281
|
+
const current = options.findIndex(
|
|
282
|
+
(option) => option.getAttribute("aria-selected") === "true",
|
|
283
|
+
);
|
|
284
|
+
setActive(current >= 0 ? current : step(-1, 1), true);
|
|
285
|
+
};
|
|
314
286
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
287
|
+
const choose = (option: HTMLElement) => {
|
|
288
|
+
if (option.getAttribute("aria-disabled") === "true") return;
|
|
289
|
+
if (multiple) {
|
|
290
|
+
option.setAttribute(
|
|
291
|
+
"aria-selected",
|
|
292
|
+
option.getAttribute("aria-selected") === "true"
|
|
293
|
+
? "false"
|
|
294
|
+
: "true",
|
|
295
|
+
);
|
|
296
|
+
} else {
|
|
297
|
+
options.forEach((entry) =>
|
|
298
|
+
entry.setAttribute(
|
|
299
|
+
"aria-selected",
|
|
300
|
+
entry === option ? "true" : "false",
|
|
301
|
+
),
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
syncValues();
|
|
305
|
+
updateLabel();
|
|
306
|
+
root.dispatchEvent(
|
|
307
|
+
new CustomEvent("change", {
|
|
308
|
+
bubbles: true,
|
|
309
|
+
detail: {
|
|
310
|
+
values: selected().map((entry) => entry.dataset.value),
|
|
311
|
+
},
|
|
312
|
+
}),
|
|
313
|
+
);
|
|
314
|
+
if (multiple) {
|
|
315
|
+
option.focus();
|
|
316
|
+
} else {
|
|
317
|
+
close(true);
|
|
318
|
+
}
|
|
319
|
+
};
|
|
321
320
|
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
if (option) choose(option);
|
|
327
|
-
});
|
|
321
|
+
trigger.addEventListener("click", () => {
|
|
322
|
+
if (popup.hidden) open();
|
|
323
|
+
else close();
|
|
324
|
+
});
|
|
328
325
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
case "ArrowDown":
|
|
332
|
-
event.preventDefault();
|
|
333
|
-
setActive(step(active, 1), true);
|
|
334
|
-
break;
|
|
335
|
-
case "ArrowUp":
|
|
336
|
-
event.preventDefault();
|
|
337
|
-
setActive(step(active, -1), true);
|
|
338
|
-
break;
|
|
339
|
-
case "Home":
|
|
340
|
-
event.preventDefault();
|
|
341
|
-
setActive(step(-1, 1), true);
|
|
342
|
-
break;
|
|
343
|
-
case "End":
|
|
344
|
-
event.preventDefault();
|
|
345
|
-
setActive(step(options.length, -1), true);
|
|
346
|
-
break;
|
|
347
|
-
case "Enter":
|
|
348
|
-
case " ":
|
|
349
|
-
event.preventDefault();
|
|
350
|
-
if (active >= 0) choose(options[active]);
|
|
351
|
-
break;
|
|
352
|
-
case "Escape":
|
|
353
|
-
event.preventDefault();
|
|
354
|
-
close(true);
|
|
355
|
-
break;
|
|
356
|
-
case "Tab":
|
|
357
|
-
// In multiple mode Tab reaches the Done button before leaving.
|
|
358
|
-
if (multiple && done && !event.shiftKey) {
|
|
326
|
+
trigger.addEventListener("keydown", (event) => {
|
|
327
|
+
if (["ArrowDown", "ArrowUp", "Enter", " "].includes(event.key)) {
|
|
359
328
|
event.preventDefault();
|
|
360
|
-
|
|
361
|
-
break;
|
|
329
|
+
if (popup.hidden) open();
|
|
362
330
|
}
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
list.addEventListener("click", (event) => {
|
|
334
|
+
const option = (event.target as HTMLElement).closest<HTMLElement>(
|
|
335
|
+
'[role="option"]',
|
|
336
|
+
);
|
|
337
|
+
if (option) choose(option);
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
list.addEventListener("keydown", (event) => {
|
|
341
|
+
switch (event.key) {
|
|
342
|
+
case "ArrowDown":
|
|
343
|
+
event.preventDefault();
|
|
344
|
+
setActive(step(active, 1), true);
|
|
345
|
+
break;
|
|
346
|
+
case "ArrowUp":
|
|
347
|
+
event.preventDefault();
|
|
348
|
+
setActive(step(active, -1), true);
|
|
349
|
+
break;
|
|
350
|
+
case "Home":
|
|
351
|
+
event.preventDefault();
|
|
352
|
+
setActive(step(-1, 1), true);
|
|
353
|
+
break;
|
|
354
|
+
case "End":
|
|
355
|
+
event.preventDefault();
|
|
356
|
+
setActive(step(options.length, -1), true);
|
|
357
|
+
break;
|
|
358
|
+
case "Enter":
|
|
359
|
+
case " ":
|
|
360
|
+
event.preventDefault();
|
|
361
|
+
if (active >= 0) choose(options[active]);
|
|
362
|
+
break;
|
|
363
|
+
case "Escape":
|
|
364
|
+
event.preventDefault();
|
|
365
|
+
close(true);
|
|
366
|
+
break;
|
|
367
|
+
case "Tab":
|
|
368
|
+
// In multiple mode Tab reaches the Done button before leaving.
|
|
369
|
+
if (multiple && done && !event.shiftKey) {
|
|
370
|
+
event.preventDefault();
|
|
371
|
+
done.focus();
|
|
376
372
|
break;
|
|
377
373
|
}
|
|
378
|
-
|
|
374
|
+
close();
|
|
375
|
+
trigger.focus();
|
|
376
|
+
break;
|
|
377
|
+
default:
|
|
378
|
+
if (event.key.length === 1) {
|
|
379
|
+
const letter = event.key.toLowerCase();
|
|
380
|
+
// Wrap-search from the option after the active one; no match
|
|
381
|
+
// keeps the current option.
|
|
382
|
+
for (let offset = 1; offset <= options.length; offset += 1) {
|
|
383
|
+
const index = (active + offset) % options.length;
|
|
384
|
+
const label = (
|
|
385
|
+
options[index].dataset.label || ""
|
|
386
|
+
).toLowerCase();
|
|
387
|
+
if (enabled(index) && label.startsWith(letter)) {
|
|
388
|
+
setActive(index, true);
|
|
389
|
+
break;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
}
|
|
379
393
|
}
|
|
380
|
-
|
|
381
|
-
});
|
|
394
|
+
});
|
|
382
395
|
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
396
|
+
done?.addEventListener("click", () => close(true));
|
|
397
|
+
done?.addEventListener("keydown", (event) => {
|
|
398
|
+
if (event.key === "Escape") {
|
|
399
|
+
event.preventDefault();
|
|
400
|
+
close(true);
|
|
401
|
+
} else if (event.key === "Tab" && event.shiftKey) {
|
|
402
|
+
event.preventDefault();
|
|
403
|
+
setActive(active, true);
|
|
404
|
+
} else if (event.key === "Tab") {
|
|
405
|
+
close();
|
|
406
|
+
}
|
|
407
|
+
});
|
|
395
408
|
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
409
|
+
document.addEventListener("pointerdown", (event) => {
|
|
410
|
+
if (!popup.hidden && !root.contains(event.target as Node)) close();
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
syncValues();
|
|
414
|
+
updateLabel();
|
|
415
|
+
});
|
|
416
|
+
};
|
|
399
417
|
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
});
|
|
418
|
+
bind();
|
|
419
|
+
document.addEventListener("astro:page-load", bind);
|
|
403
420
|
</script>
|
package/src/astro/Skeleton.astro
CHANGED
|
@@ -18,13 +18,15 @@ const {
|
|
|
18
18
|
width,
|
|
19
19
|
class: className,
|
|
20
20
|
} = Astro.props;
|
|
21
|
+
const safeWidth =
|
|
22
|
+
width && /^\d+(\.\d+)?(rem|em|px|%|ch|vw)$/.test(width) ? width : undefined;
|
|
21
23
|
---
|
|
22
24
|
|
|
23
25
|
{variant === "paragraph" ? (
|
|
24
26
|
<div
|
|
25
27
|
class:list={["ds-skeleton-stack", className]}
|
|
26
28
|
aria-hidden="true"
|
|
27
|
-
style={
|
|
29
|
+
style={safeWidth ? `width:${safeWidth}` : undefined}
|
|
28
30
|
>
|
|
29
31
|
{Array.from({ length: lines }).map(() => (
|
|
30
32
|
<span class="ds-skeleton ds-skeleton--text"></span>
|
|
@@ -34,6 +36,6 @@ const {
|
|
|
34
36
|
<span
|
|
35
37
|
class:list={["ds-skeleton", `ds-skeleton--${variant}`, className]}
|
|
36
38
|
aria-hidden="true"
|
|
37
|
-
style={
|
|
39
|
+
style={safeWidth ? `width:${safeWidth}` : undefined}
|
|
38
40
|
></span>
|
|
39
41
|
)}
|
package/src/astro/Spinner.astro
CHANGED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
---
|
|
2
|
+
import Icon from "./Icon.astro";
|
|
3
|
+
import "../styles/stepper.css";
|
|
4
|
+
import "../styles/sr-only.css";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Progress through an ordered process. Presentational: an <ol> where the
|
|
8
|
+
* current step carries aria-current="step". State is shown by marker shape
|
|
9
|
+
* (number, check, cross) and read out as visually hidden text, never by
|
|
10
|
+
* colour alone.
|
|
11
|
+
*/
|
|
12
|
+
interface Step {
|
|
13
|
+
label: string;
|
|
14
|
+
description?: string;
|
|
15
|
+
state?: "complete" | "current" | "upcoming" | "error";
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface Props {
|
|
19
|
+
steps: Step[];
|
|
20
|
+
orientation?: "horizontal" | "vertical";
|
|
21
|
+
/** Accessible name for the list, e.g. "Setup progress". */
|
|
22
|
+
label?: string;
|
|
23
|
+
class?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const {
|
|
27
|
+
steps,
|
|
28
|
+
orientation = "horizontal",
|
|
29
|
+
label,
|
|
30
|
+
class: className,
|
|
31
|
+
} = Astro.props;
|
|
32
|
+
|
|
33
|
+
const STATE_TEXT = {
|
|
34
|
+
complete: "Completed",
|
|
35
|
+
error: "Error",
|
|
36
|
+
upcoming: "Not started",
|
|
37
|
+
current: "",
|
|
38
|
+
};
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
<ol
|
|
42
|
+
class:list={[
|
|
43
|
+
"ds-stepper",
|
|
44
|
+
orientation === "vertical" && "ds-stepper--vertical",
|
|
45
|
+
className,
|
|
46
|
+
]}
|
|
47
|
+
aria-label={label}
|
|
48
|
+
>
|
|
49
|
+
{steps.map((step, index) => {
|
|
50
|
+
const state = step.state ?? "upcoming";
|
|
51
|
+
return (
|
|
52
|
+
<li
|
|
53
|
+
class="ds-stepper__item"
|
|
54
|
+
data-state={state}
|
|
55
|
+
aria-current={state === "current" ? "step" : undefined}
|
|
56
|
+
>
|
|
57
|
+
<span class="ds-stepper__marker" aria-hidden="true">
|
|
58
|
+
{state === "complete" ? (
|
|
59
|
+
<Icon name="check" size={16} />
|
|
60
|
+
) : state === "error" ? (
|
|
61
|
+
<Icon name="x" size={16} />
|
|
62
|
+
) : (
|
|
63
|
+
index + 1
|
|
64
|
+
)}
|
|
65
|
+
</span>
|
|
66
|
+
<span class="ds-stepper__body">
|
|
67
|
+
<span class="ds-stepper__label">
|
|
68
|
+
{step.label}
|
|
69
|
+
{STATE_TEXT[state] && (
|
|
70
|
+
<span class="ds-sr-only">
|
|
71
|
+
, {STATE_TEXT[state].toLowerCase()}
|
|
72
|
+
</span>
|
|
73
|
+
)}
|
|
74
|
+
</span>
|
|
75
|
+
{step.description && (
|
|
76
|
+
<span class="ds-stepper__description">{step.description}</span>
|
|
77
|
+
)}
|
|
78
|
+
</span>
|
|
79
|
+
</li>
|
|
80
|
+
);
|
|
81
|
+
})}
|
|
82
|
+
</ol>
|