@spectral_labs/ui 1.6.1 → 1.8.0-next.0
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/LICENSE +21 -0
- package/THIRD_PARTY_NOTICES.md +201 -0
- package/dist/components/command/command.svelte.d.ts +30 -0
- package/dist/components/command/command.svelte.d.ts.map +1 -1
- package/dist/components/command/command.svelte.js +85 -4
- package/dist/components/command/components/command-group.svelte +25 -15
- package/dist/components/command/components/command-group.svelte.d.ts.map +1 -1
- package/dist/components/command/components/command-input.svelte +54 -5
- package/dist/components/command/components/command-input.svelte.d.ts +1 -1
- package/dist/components/command/components/command-input.svelte.d.ts.map +1 -1
- package/dist/components/command/components/command-list.svelte +22 -3
- package/dist/components/command/components/command-list.svelte.d.ts.map +1 -1
- package/dist/components/command/components/command-root.svelte +43 -4
- package/dist/components/command/components/command-root.svelte.d.ts.map +1 -1
- package/dist/components/command/index.d.ts +1 -0
- package/dist/components/command/index.d.ts.map +1 -1
- package/dist/components/command/types.d.ts +59 -4
- package/dist/components/command/types.d.ts.map +1 -1
- package/package.json +8 -2
- package/dist/components/__tests__/overlay-scrim-source.test.js +0 -134
- package/dist/components/accordion/__tests__/AccordionTest.svelte +0 -31
- package/dist/components/accordion/__tests__/accordion.test.svelte.js +0 -139
- package/dist/components/alert-dialog/__tests__/AlertDialogTest.svelte +0 -28
- package/dist/components/alert-dialog/__tests__/alert-dialog.a11y.test.js +0 -42
- package/dist/components/alert-dialog/__tests__/alert-dialog.integration.test.svelte.js +0 -51
- package/dist/components/alert-dialog/__tests__/alert-dialog.test.svelte.js +0 -113
- package/dist/components/app-bar/__tests__/app-bar.test.svelte.js +0 -30
- package/dist/components/aspect-ratio/__tests__/aspect-ratio.test.svelte.js +0 -22
- package/dist/components/avatar/__tests__/avatar.test.svelte.js +0 -26
- package/dist/components/badge/__tests__/badge.test.svelte.js +0 -30
- package/dist/components/bottom-sheet/__tests__/BottomSheetTest.svelte +0 -22
- package/dist/components/bottom-sheet/__tests__/bottom-sheet.test.svelte.js +0 -83
- package/dist/components/button/__tests__/button.test.svelte.js +0 -63
- package/dist/components/calendar/__tests__/CalendarTest.svelte +0 -56
- package/dist/components/calendar/__tests__/calendar.test.svelte.js +0 -190
- package/dist/components/card/__tests__/card.test.svelte.js +0 -30
- package/dist/components/checkbox/__tests__/checkbox.test.svelte.js +0 -44
- package/dist/components/chips/__tests__/chips.test.svelte.js +0 -26
- package/dist/components/collapsible/__tests__/CollapsibleTest.svelte +0 -18
- package/dist/components/collapsible/__tests__/collapsible.test.svelte.js +0 -125
- package/dist/components/combobox/__tests__/ComboboxTest.svelte +0 -40
- package/dist/components/combobox/__tests__/combobox.test.svelte.js +0 -255
- package/dist/components/command/__tests__/CommandTest.svelte +0 -77
- package/dist/components/command/__tests__/command-item-passthrough.test.svelte.js +0 -284
- package/dist/components/command/__tests__/command.a11y.test.js +0 -30
- package/dist/components/command/__tests__/command.integration.test.svelte.js +0 -68
- package/dist/components/command/__tests__/command.test.svelte.js +0 -230
- package/dist/components/context-menu/__tests__/ContextMenuNonInteractiveTest.svelte +0 -32
- package/dist/components/context-menu/__tests__/ContextMenuTest.svelte +0 -25
- package/dist/components/context-menu/__tests__/context-menu.test.svelte.js +0 -141
- package/dist/components/date-field/__tests__/DateFieldTest.svelte +0 -20
- package/dist/components/date-field/__tests__/date-field.test.svelte.js +0 -139
- package/dist/components/date-picker/__tests__/DatePickerTest.svelte +0 -20
- package/dist/components/date-picker/__tests__/date-picker.test.svelte.js +0 -190
- package/dist/components/date-range-field/__tests__/DateRangeFieldTest.svelte +0 -20
- package/dist/components/date-range-field/__tests__/date-range-field.test.svelte.js +0 -130
- package/dist/components/date-range-picker/__tests__/DateRangePickerTest.svelte +0 -18
- package/dist/components/date-range-picker/__tests__/date-range-picker.test.svelte.js +0 -156
- package/dist/components/dialog/__tests__/DialogTest.svelte +0 -34
- package/dist/components/dialog/__tests__/dialog.test.svelte.js +0 -184
- package/dist/components/drop-zone/__tests__/drop-zone.test.svelte.js +0 -26
- package/dist/components/dropdown-menu/__tests__/DropdownMenuRadioTest.svelte +0 -90
- package/dist/components/dropdown-menu/__tests__/DropdownMenuTest.svelte +0 -71
- package/dist/components/dropdown-menu/__tests__/dropdown-menu-radio-group.test.svelte.js +0 -306
- package/dist/components/dropdown-menu/__tests__/dropdown-menu.test.svelte.js +0 -567
- package/dist/components/empty-state/__tests__/empty-state.test.svelte.js +0 -26
- package/dist/components/fab/__tests__/fab.test.svelte.js +0 -30
- package/dist/components/field/__tests__/FieldTest.svelte +0 -26
- package/dist/components/field/__tests__/field.test.svelte.js +0 -74
- package/dist/components/grid-list/__tests__/GridListTest.svelte +0 -46
- package/dist/components/grid-list/__tests__/grid-list.test.svelte.js +0 -231
- package/dist/components/hover-card/__tests__/HoverCardTest.svelte +0 -30
- package/dist/components/hover-card/__tests__/hover-card.test.svelte.js +0 -236
- package/dist/components/indicator/__tests__/indicator.test.svelte.js +0 -30
- package/dist/components/input/__tests__/input.test.svelte.js +0 -67
- package/dist/components/label/__tests__/label.test.svelte.js +0 -22
- package/dist/components/language-select/__tests__/language-select.test.svelte.js +0 -199
- package/dist/components/language-select/__tests__/languages.test.js +0 -87
- package/dist/components/link-preview/__tests__/LinkPreviewTest.svelte +0 -37
- package/dist/components/link-preview/__tests__/link-preview.a11y.test.js +0 -28
- package/dist/components/link-preview/__tests__/link-preview.integration.test.svelte.js +0 -54
- package/dist/components/link-preview/__tests__/link-preview.test.svelte.js +0 -215
- package/dist/components/list/__tests__/list.test.svelte.js +0 -22
- package/dist/components/menubar/__tests__/MenubarTest.svelte +0 -52
- package/dist/components/menubar/__tests__/menubar.a11y.test.js +0 -28
- package/dist/components/menubar/__tests__/menubar.integration.test.svelte.js +0 -57
- package/dist/components/menubar/__tests__/menubar.test.svelte.js +0 -328
- package/dist/components/meter/__tests__/meter.test.svelte.js +0 -26
- package/dist/components/nav-bar/__tests__/nav-bar.test.svelte.js +0 -23
- package/dist/components/nav-drawer/__tests__/nav-drawer.test.svelte.js +0 -28
- package/dist/components/nav-rail/__tests__/nav-rail.test.svelte.js +0 -28
- package/dist/components/pagination/__tests__/pagination.test.svelte.js +0 -26
- package/dist/components/pin-input/__tests__/pin-input.test.svelte.js +0 -71
- package/dist/components/popover/__tests__/PopoverTest.svelte +0 -27
- package/dist/components/popover/__tests__/popover.test.svelte.js +0 -196
- package/dist/components/progress/__tests__/progress.test.svelte.js +0 -32
- package/dist/components/radio-group/__tests__/radio-group.test.svelte.js +0 -22
- package/dist/components/range-calendar/__tests__/RangeCalendarTest.svelte +0 -23
- package/dist/components/range-calendar/__tests__/range-calendar.test.svelte.js +0 -105
- package/dist/components/rating-group/__tests__/rating-group.test.svelte.js +0 -26
- package/dist/components/scroll-area/__tests__/scroll-area.test.svelte.js +0 -34
- package/dist/components/search-field/__tests__/SearchFieldTest.svelte +0 -34
- package/dist/components/search-field/__tests__/search-field.test.svelte.js +0 -118
- package/dist/components/select/__tests__/SelectLabelPersistenceTest.svelte +0 -31
- package/dist/components/select/__tests__/SelectTest.svelte +0 -33
- package/dist/components/select/__tests__/select.test.svelte.js +0 -221
- package/dist/components/separator/__tests__/separator.test.svelte.js +0 -39
- package/dist/components/sheet/__tests__/SheetTest.svelte +0 -34
- package/dist/components/sheet/__tests__/sheet.test.svelte.js +0 -193
- package/dist/components/side-sheet/__tests__/side-sheet.test.svelte.js +0 -138
- package/dist/components/sidebar/__tests__/SidebarFullTest.svelte +0 -45
- package/dist/components/sidebar/__tests__/SidebarTest.svelte +0 -9
- package/dist/components/sidebar/__tests__/sidebar.test.svelte.js +0 -186
- package/dist/components/simple-dialog/__tests__/SimpleDialogTest.svelte +0 -40
- package/dist/components/simple-dialog/__tests__/simple-dialog.test.svelte.js +0 -50
- package/dist/components/simple-tooltip/__tests__/SimpleTooltipTest.svelte +0 -19
- package/dist/components/simple-tooltip/__tests__/simple-tooltip.test.svelte.js +0 -61
- package/dist/components/slider/__tests__/slider.test.svelte.js +0 -42
- package/dist/components/sortable-list/__tests__/NestedSortableListTest.svelte +0 -49
- package/dist/components/sortable-list/__tests__/SortableListTest.svelte +0 -42
- package/dist/components/sortable-list/__tests__/sortable-list.nested.test.svelte.js +0 -112
- package/dist/components/sortable-list/__tests__/sortable-list.test.svelte.js +0 -175
- package/dist/components/splitter/__tests__/splitter.test.svelte.js +0 -26
- package/dist/components/switch/__tests__/switch.test.svelte.js +0 -44
- package/dist/components/table/__tests__/table.test.svelte.js +0 -22
- package/dist/components/tabs/__tests__/TabsTest.svelte +0 -32
- package/dist/components/tabs/__tests__/tabs.test.svelte.js +0 -227
- package/dist/components/textarea/__tests__/textarea.test.svelte.js +0 -55
- package/dist/components/time-field/__tests__/TimeFieldTest.svelte +0 -20
- package/dist/components/time-field/__tests__/time-field.test.svelte.js +0 -100
- package/dist/components/time-range-field/__tests__/TimeRangeFieldTest.svelte +0 -20
- package/dist/components/time-range-field/__tests__/time-range-field.test.svelte.js +0 -124
- package/dist/components/toast/__tests__/toast.test.svelte.js +0 -46
- package/dist/components/toggle/__tests__/toggle.test.svelte.js +0 -38
- package/dist/components/toggle-group/__tests__/toggle-group.test.svelte.js +0 -22
- package/dist/components/toolbar/__tests__/ToolbarCompositionTest.svelte +0 -18
- package/dist/components/toolbar/__tests__/ToolbarTabstopTest.svelte +0 -10
- package/dist/components/toolbar/__tests__/toolbar.test.svelte.js +0 -63
- package/dist/components/tooltip/__tests__/TooltipTest.svelte +0 -22
- package/dist/components/tooltip/__tests__/tooltip.test.svelte.js +0 -225
- package/dist/components/tree/__tests__/TreeLabelContentTest.svelte +0 -21
- package/dist/components/tree/__tests__/TreeTest.svelte +0 -24
- package/dist/components/tree/__tests__/tree.test.svelte.js +0 -229
- package/dist/components/virtualizer/__tests__/waterfall-layout.test.js +0 -82
- package/dist/internal/announcer/__tests__/announcer.test.js +0 -42
- package/dist/internal/dismissible-layer/__tests__/dismissible-layer.test.js +0 -196
- package/dist/internal/drag-drop/__tests__/auto-scroll.test.js +0 -104
- package/dist/internal/drag-drop/__tests__/drag-session.test.svelte.js +0 -803
- package/dist/internal/drag-drop/__tests__/drag-source.test.svelte.js +0 -130
- package/dist/internal/drag-drop/__tests__/drop-delegate.test.js +0 -223
- package/dist/internal/drag-drop/__tests__/types.test.js +0 -20
- package/dist/internal/escape-layer/__tests__/escape-layer.test.js +0 -113
- package/dist/internal/focus-scope/__tests__/focus-scope.test.js +0 -225
- package/dist/internal/grace-area/__tests__/grace-area.test.js +0 -67
- package/dist/internal/helpers/__tests__/helpers.test.js +0 -123
- package/dist/internal/portal/__tests__/PortalTest.svelte +0 -21
- package/dist/internal/portal/__tests__/portal.test.js +0 -29
- package/dist/internal/portal/__tests__/portal.test.svelte.js +0 -57
- package/dist/internal/scroll-lock/__tests__/scroll-lock.test.js +0 -160
- package/dist/internal/typeahead/__tests__/typeahead.test.js +0 -61
- package/src/themes/__tests__/carbon-dark-contrast.test.ts +0 -170
- package/src/themes/__tests__/generated-css-drift.test.ts +0 -53
- package/src/themes/__tests__/scrim-opaque.test.ts +0 -52
|
@@ -15,7 +15,8 @@
|
|
|
15
15
|
shouldFilter = true,
|
|
16
16
|
loop = true,
|
|
17
17
|
portalTo,
|
|
18
|
-
|
|
18
|
+
mode = 'modal',
|
|
19
|
+
trapFocus,
|
|
19
20
|
closeOnEscape = true,
|
|
20
21
|
closeOnOutsideClick = true,
|
|
21
22
|
onEscapeKeydown,
|
|
@@ -26,10 +27,27 @@
|
|
|
26
27
|
children,
|
|
27
28
|
}: CommandProps = $props()
|
|
28
29
|
|
|
30
|
+
/** A field living in an app bar must stay in the page's natural tab order,
|
|
31
|
+
* so `inline` flips the default — an explicit value still wins. */
|
|
32
|
+
const trapsFocus = $derived(trapFocus ?? mode !== 'inline')
|
|
33
|
+
/** Inline renders IN PLACE unless a target is given: the consumer's scoped
|
|
34
|
+
* CSS then applies, and the results can be anchored to the field. */
|
|
35
|
+
const portalDisabled = $derived(mode === 'inline' && portalTo === undefined)
|
|
36
|
+
|
|
29
37
|
const commandState = createCommandState({
|
|
30
38
|
get search() {
|
|
31
39
|
return value
|
|
32
40
|
},
|
|
41
|
+
get mode() {
|
|
42
|
+
return mode
|
|
43
|
+
},
|
|
44
|
+
get open() {
|
|
45
|
+
return open
|
|
46
|
+
},
|
|
47
|
+
onRequestOpen() {
|
|
48
|
+
open = true
|
|
49
|
+
onOpenChange?.(true)
|
|
50
|
+
},
|
|
33
51
|
get filter() {
|
|
34
52
|
return filter
|
|
35
53
|
},
|
|
@@ -83,11 +101,15 @@
|
|
|
83
101
|
},
|
|
84
102
|
})
|
|
85
103
|
|
|
104
|
+
// The two layers follow `open` in BOTH modes — which is exactly what makes a
|
|
105
|
+
// closed inline palette inert: no `document` listener, and no slot held in
|
|
106
|
+
// the global Escape / dismiss stacks that a surface below would have to
|
|
107
|
+
// yield to.
|
|
86
108
|
$effect(() => {
|
|
87
109
|
if (open && isBrowser) {
|
|
88
110
|
escapeLayer.mount()
|
|
89
111
|
dismissLayer.mount()
|
|
90
|
-
if (
|
|
112
|
+
if (trapsFocus && contentEl) {
|
|
91
113
|
focusScope.setElement(contentEl)
|
|
92
114
|
focusScope.mount()
|
|
93
115
|
}
|
|
@@ -109,12 +131,14 @@
|
|
|
109
131
|
})
|
|
110
132
|
</script>
|
|
111
133
|
|
|
112
|
-
|
|
113
|
-
|
|
134
|
+
<!-- Inline keeps the Root mounted whatever `open` is: the field is the point. -->
|
|
135
|
+
{#if mode === 'inline' || presence.isPresent}
|
|
136
|
+
<Portal to={portalTo} disabled={portalDisabled}>
|
|
114
137
|
<div
|
|
115
138
|
bind:this={contentEl}
|
|
116
139
|
class="sp-command {className ?? ''}"
|
|
117
140
|
data-command-root
|
|
141
|
+
data-mode={mode}
|
|
118
142
|
data-state={open ? 'open' : 'closed'}
|
|
119
143
|
>
|
|
120
144
|
{#if children}{@render children()}{/if}
|
|
@@ -132,4 +156,19 @@
|
|
|
132
156
|
background-color: var(--sp-sys-color-surface-container);
|
|
133
157
|
box-shadow: var(--sp-sys-elevation-3);
|
|
134
158
|
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Inline is not a panel: it is a field with results hanging off it. The
|
|
162
|
+
* panel chrome is dropped so the consumer can dress the field, and
|
|
163
|
+
* `overflow` is released so results positioned below are not clipped by the
|
|
164
|
+
* very element that owns them. `position: relative` makes the Root the
|
|
165
|
+
* containing block, which is what an anchored list needs.
|
|
166
|
+
*/
|
|
167
|
+
.sp-command[data-mode='inline'] {
|
|
168
|
+
position: relative;
|
|
169
|
+
overflow: visible;
|
|
170
|
+
border-radius: 0;
|
|
171
|
+
background-color: transparent;
|
|
172
|
+
box-shadow: none;
|
|
173
|
+
}
|
|
135
174
|
</style>
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"command-root.svelte.d.ts","sourceRoot":"","sources":["../../../../src/lib/components/command/components/command-root.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;
|
|
1
|
+
{"version":3,"file":"command-root.svelte.d.ts","sourceRoot":"","sources":["../../../../src/lib/components/command/components/command-root.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AA2J/C,QAAA,MAAM,WAAW,gEAAwC,CAAC;AAC1D,KAAK,WAAW,GAAG,UAAU,CAAC,OAAO,WAAW,CAAC,CAAC;AAClD,eAAe,WAAW,CAAC"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { defaultFilter } from './command.svelte.js';
|
|
2
|
+
export type { CommandMode } from './command.svelte.js';
|
|
2
3
|
export { default as Empty } from './components/command-empty.svelte';
|
|
3
4
|
export { default as Group } from './components/command-group.svelte';
|
|
4
5
|
export { default as Input } from './components/command-input.svelte';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/lib/components/command/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAA;AACnD,OAAO,EAAE,OAAO,IAAI,KAAK,EAAE,MAAM,mCAAmC,CAAA;AACpE,OAAO,EAAE,OAAO,IAAI,KAAK,EAAE,MAAM,mCAAmC,CAAA;AACpE,OAAO,EAAE,OAAO,IAAI,KAAK,EAAE,MAAM,mCAAmC,CAAA;AACpE,OAAO,EAAE,OAAO,IAAI,IAAI,EAAE,MAAM,kCAAkC,CAAA;AAClE,OAAO,EAAE,OAAO,IAAI,IAAI,EAAE,MAAM,kCAAkC,CAAA;AAClE,OAAO,EAAE,OAAO,IAAI,IAAI,EAAE,MAAM,kCAAkC,CAAA;AAClE,OAAO,EAAE,OAAO,IAAI,SAAS,EAAE,MAAM,uCAAuC,CAAA;AAC5E,YAAY,EACV,iBAAiB,EACjB,iBAAiB,EACjB,iBAAiB,EACjB,gBAAgB,EAChB,gBAAgB,EAChB,YAAY,EACZ,qBAAqB,GACtB,MAAM,YAAY,CAAA"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/lib/components/command/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAA;AACnD,YAAY,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAA;AACtD,OAAO,EAAE,OAAO,IAAI,KAAK,EAAE,MAAM,mCAAmC,CAAA;AACpE,OAAO,EAAE,OAAO,IAAI,KAAK,EAAE,MAAM,mCAAmC,CAAA;AACpE,OAAO,EAAE,OAAO,IAAI,KAAK,EAAE,MAAM,mCAAmC,CAAA;AACpE,OAAO,EAAE,OAAO,IAAI,IAAI,EAAE,MAAM,kCAAkC,CAAA;AAClE,OAAO,EAAE,OAAO,IAAI,IAAI,EAAE,MAAM,kCAAkC,CAAA;AAClE,OAAO,EAAE,OAAO,IAAI,IAAI,EAAE,MAAM,kCAAkC,CAAA;AAClE,OAAO,EAAE,OAAO,IAAI,SAAS,EAAE,MAAM,uCAAuC,CAAA;AAC5E,YAAY,EACV,iBAAiB,EACjB,iBAAiB,EACjB,iBAAiB,EACjB,gBAAgB,EAChB,gBAAgB,EAChB,YAAY,EACZ,qBAAqB,GACtB,MAAM,YAAY,CAAA"}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Snippet } from 'svelte';
|
|
2
|
-
import type { HTMLAttributes } from 'svelte/elements';
|
|
2
|
+
import type { HTMLAttributes, HTMLInputAttributes } from 'svelte/elements';
|
|
3
|
+
import type { CommandMode } from './command.svelte.js';
|
|
3
4
|
export interface CommandProps {
|
|
4
5
|
/** Current search query (bindable). */
|
|
5
6
|
value?: string;
|
|
@@ -9,13 +10,39 @@ export interface CommandProps {
|
|
|
9
10
|
shouldFilter?: boolean;
|
|
10
11
|
/** Whether keyboard navigation loops at edges. @default true */
|
|
11
12
|
loop?: boolean;
|
|
13
|
+
/**
|
|
14
|
+
* Composition mode.
|
|
15
|
+
*
|
|
16
|
+
* - `'modal'` (default, unchanged behaviour): `open` controls whether the
|
|
17
|
+
* palette is MOUNTED — closing unmounts the content, the field included,
|
|
18
|
+
* together with the Escape and outside-pointer layers.
|
|
19
|
+
* - `'inline'`: the Root and its `Command.Input` stay mounted whatever `open`
|
|
20
|
+
* is, so a search field can live permanently in a bar; `open` then governs
|
|
21
|
+
* only whether `Command.List` is RENDERED, and the two layers follow the
|
|
22
|
+
* results. A closed inline palette listens to nothing: no document
|
|
23
|
+
* listener, no slot held in the global layer stacks.
|
|
24
|
+
*
|
|
25
|
+
* @default 'modal'
|
|
26
|
+
*/
|
|
27
|
+
mode?: CommandMode;
|
|
12
28
|
/**
|
|
13
29
|
* Controls whether the palette is mounted. When true, render content. Default: true.
|
|
30
|
+
* In `'inline'` mode this controls the RESULTS only — see `mode`.
|
|
14
31
|
*/
|
|
15
32
|
open?: boolean;
|
|
16
|
-
/**
|
|
33
|
+
/**
|
|
34
|
+
* Portal target. Default `document.body` in `'modal'` mode; in `'inline'`
|
|
35
|
+
* mode the content is rendered IN PLACE by default, so the consumer's scoped
|
|
36
|
+
* CSS applies and the results can be anchored to the field. Passing a target
|
|
37
|
+
* portals in either mode.
|
|
38
|
+
*/
|
|
17
39
|
portalTo?: HTMLElement | string;
|
|
18
|
-
/**
|
|
40
|
+
/**
|
|
41
|
+
* Trap focus within the palette.
|
|
42
|
+
* @default `true` in `'modal'` mode, `false` in `'inline'` mode — a field
|
|
43
|
+
* sitting in a bar must stay part of the page's natural tab order. An
|
|
44
|
+
* explicit value is honoured in both modes.
|
|
45
|
+
*/
|
|
19
46
|
trapFocus?: boolean;
|
|
20
47
|
/** Close on Escape keydown. Default: true. */
|
|
21
48
|
closeOnEscape?: boolean;
|
|
@@ -35,10 +62,38 @@ export interface CommandProps {
|
|
|
35
62
|
class?: string;
|
|
36
63
|
children?: Snippet;
|
|
37
64
|
}
|
|
38
|
-
|
|
65
|
+
/**
|
|
66
|
+
* Extends the native `input` attributes. A consumer's `title`, `name`,
|
|
67
|
+
* `aria-label` / `aria-labelledby` / `aria-describedby`, `data-testid`,
|
|
68
|
+
* `style` and native event handlers reach the real `role="combobox"` node.
|
|
69
|
+
*
|
|
70
|
+
* The field's IDENTITY stays with the component: `role`, `id`, `type` and the
|
|
71
|
+
* three computed relations (`aria-expanded`, `aria-controls`,
|
|
72
|
+
* `aria-activedescendant`) are rejected by the type — they are derived from
|
|
73
|
+
* the palette's own state and a consumer value could only contradict it.
|
|
74
|
+
*
|
|
75
|
+
* Consumer handlers COMPOSE with the built-in ones: yours runs first, and
|
|
76
|
+
* calling `preventDefault()` cancels Spectral's local keyboard handling for
|
|
77
|
+
* that key. It does NOT retract a close already performed by the Escape layer,
|
|
78
|
+
* which runs earlier, on `document`, in the capture phase — `onEscapeKeydown`
|
|
79
|
+
* on `Command.Root` is the public hook for that.
|
|
80
|
+
*/
|
|
81
|
+
export interface CommandInputProps extends Omit<HTMLInputAttributes, 'class' | 'children' | 'value' | 'type' | 'role' | 'id' | 'autocomplete' | 'oninput' | 'aria-expanded' | 'aria-controls' | 'aria-activedescendant' | 'aria-autocomplete'> {
|
|
39
82
|
/** Placeholder text for the search input. */
|
|
40
83
|
placeholder?: string;
|
|
84
|
+
/** Class placed on the `<input>` itself. */
|
|
41
85
|
class?: string;
|
|
86
|
+
/**
|
|
87
|
+
* Class placed on the wrapper that holds the icon and the input — the box
|
|
88
|
+
* that carries the padding and the bottom border. This is the hook for
|
|
89
|
+
* restyling the field as, say, a pill inside an app bar.
|
|
90
|
+
*/
|
|
91
|
+
wrapperClass?: string;
|
|
92
|
+
/**
|
|
93
|
+
* The real input element, bindable. Lets the consumer focus the field
|
|
94
|
+
* (a `Cmd/Ctrl+K` shortcut, say) without reaching for an internal selector.
|
|
95
|
+
*/
|
|
96
|
+
element?: HTMLInputElement | null;
|
|
42
97
|
}
|
|
43
98
|
export interface CommandListProps {
|
|
44
99
|
/** Accessible label for the listbox. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/lib/components/command/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAA;AACrC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAA;
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/lib/components/command/types.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@spectral_labs/ui",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.8.0-next.0",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"registry": "https://registry.npmjs.org",
|
|
7
7
|
"access": "public"
|
|
@@ -13,6 +13,9 @@
|
|
|
13
13
|
"url": "https://github.com/negativemachine/spectral-ui.git",
|
|
14
14
|
"directory": "packages/spectral-ui"
|
|
15
15
|
},
|
|
16
|
+
"sideEffects": [
|
|
17
|
+
"**/*.css"
|
|
18
|
+
],
|
|
16
19
|
"exports": {
|
|
17
20
|
".": {
|
|
18
21
|
"types": "./dist/index.d.ts",
|
|
@@ -30,6 +33,7 @@
|
|
|
30
33
|
},
|
|
31
34
|
"types": "./dist/index.d.ts",
|
|
32
35
|
"files": [
|
|
36
|
+
"THIRD_PARTY_NOTICES.md",
|
|
33
37
|
"dist",
|
|
34
38
|
"src/base.css",
|
|
35
39
|
"src/css",
|
|
@@ -37,7 +41,9 @@
|
|
|
37
41
|
"src/fonts.css",
|
|
38
42
|
"src/themes",
|
|
39
43
|
"src/tokens.css",
|
|
40
|
-
"src/utilities.css"
|
|
44
|
+
"src/utilities.css",
|
|
45
|
+
"!dist/**/__tests__",
|
|
46
|
+
"!src/**/__tests__"
|
|
41
47
|
],
|
|
42
48
|
"svelte": "./dist/index.js",
|
|
43
49
|
"scripts": {
|
|
@@ -1,134 +0,0 @@
|
|
|
1
|
-
import { readFileSync } from 'node:fs';
|
|
2
|
-
import { dirname, resolve } from 'node:path';
|
|
3
|
-
import { fileURLToPath } from 'node:url';
|
|
4
|
-
import { describe, expect, it } from 'vitest';
|
|
5
|
-
/**
|
|
6
|
-
* spectral-ui#206 — the six covering surfaces paint the OPAQUE scrim token and
|
|
7
|
-
* own the veil's alpha through `opacity` (0.32, via the component's
|
|
8
|
-
* `--sp-comp-*-overlay-opacity` token where one exists). Exactly one owner:
|
|
9
|
-
* the colour is never translucent, the opacity is never missing or wrong.
|
|
10
|
-
*
|
|
11
|
-
* The two values are matched against an ALLOW-LIST, not a deny-list: any other
|
|
12
|
-
* spelling fails, so `#0000004d`, `oklch(0 0 0 / .3)`, `rgb(0 0 0 / 30%)` or a
|
|
13
|
-
* `color-mix()` cannot slip through a list of forbidden prefixes. Declarations
|
|
14
|
-
* are parsed (property, value) instead of substring-matched, because
|
|
15
|
-
* `--sp-comp-dialog-overlay-opacity: 0.32;` literally ends with
|
|
16
|
-
* `opacity: 0.32;` and would otherwise satisfy a naive regex while the real
|
|
17
|
-
* `opacity` declaration says something else.
|
|
18
|
-
*
|
|
19
|
-
* EVERY rule in the file that targets the class is inspected, not just the
|
|
20
|
-
* first one: in a Svelte component a later rule (a duplicate, a `@media`
|
|
21
|
-
* block) lives in the same scoped `<style>` and wins the cascade.
|
|
22
|
-
*
|
|
23
|
-
* Three of these surfaces (SideSheet, NavDrawer, BottomSheet) have no cell in
|
|
24
|
-
* `tests/e2e/scrim-alpha.spec.ts`, so this file is their only control — it has
|
|
25
|
-
* to be exact. The list is literal on purpose: a seventh surface that forgets
|
|
26
|
-
* the idiom has to be added here.
|
|
27
|
-
*/
|
|
28
|
-
const here = dirname(fileURLToPath(import.meta.url));
|
|
29
|
-
const SURFACES = [
|
|
30
|
-
{ file: 'dialog/components/dialog-overlay.svelte', selector: '.sp-dialog-overlay' },
|
|
31
|
-
{
|
|
32
|
-
file: 'alert-dialog/components/alert-dialog-overlay.svelte',
|
|
33
|
-
selector: '.sp-alert-dialog-overlay',
|
|
34
|
-
},
|
|
35
|
-
{ file: 'sheet/components/sheet-overlay.svelte', selector: '.sp-sheet-overlay' },
|
|
36
|
-
{ file: 'side-sheet/side-sheet.svelte', selector: '.sp-side-sheet-overlay' },
|
|
37
|
-
{ file: 'nav-drawer/components/nav-drawer-root.svelte', selector: '.sp-nav-drawer-overlay' },
|
|
38
|
-
{
|
|
39
|
-
file: 'bottom-sheet/components/bottom-sheet-root.svelte',
|
|
40
|
-
selector: '.sp-bottom-sheet-overlay',
|
|
41
|
-
},
|
|
42
|
-
];
|
|
43
|
-
/** `background-color` values that keep the colour opaque and token-driven. */
|
|
44
|
-
const ALLOWED_BACKGROUND = [
|
|
45
|
-
/^var\(--sp-sys-color-scrim\)$/,
|
|
46
|
-
/^var\(--sp-comp-[a-z-]+-overlay-color,\s*var\(--sp-sys-color-scrim\)\)$/,
|
|
47
|
-
];
|
|
48
|
-
/** `opacity` values that make the component the single owner of the alpha. */
|
|
49
|
-
const ALLOWED_OPACITY = [/^0\.32$/, /^var\(--sp-comp-[a-z-]+-overlay-opacity,\s*0\.32\)$/];
|
|
50
|
-
/** The component's scoped stylesheet, so template braces are never parsed. */
|
|
51
|
-
function styleBlock(componentSource, file) {
|
|
52
|
-
const match = componentSource.match(/<style>(.*)<\/style>/s);
|
|
53
|
-
if (!match)
|
|
54
|
-
throw new Error(`${file}: no <style> block`);
|
|
55
|
-
return match[1];
|
|
56
|
-
}
|
|
57
|
-
/** Every rule in `source` whose selector list mentions `selector`, in order. */
|
|
58
|
-
function rulesTargeting(source, selector) {
|
|
59
|
-
const rules = [];
|
|
60
|
-
for (let open = 0; open < source.length; open++) {
|
|
61
|
-
if (source[open] !== '{')
|
|
62
|
-
continue;
|
|
63
|
-
let start = open - 1;
|
|
64
|
-
while (start >= 0 && !'{};'.includes(source[start]))
|
|
65
|
-
start--;
|
|
66
|
-
const selectorList = source.slice(start + 1, open).trim();
|
|
67
|
-
if (!selectorList.includes(selector))
|
|
68
|
-
continue;
|
|
69
|
-
let depth = 1;
|
|
70
|
-
let close = open + 1;
|
|
71
|
-
for (; close < source.length && depth > 0; close++) {
|
|
72
|
-
if (source[close] === '{')
|
|
73
|
-
depth++;
|
|
74
|
-
else if (source[close] === '}')
|
|
75
|
-
depth--;
|
|
76
|
-
}
|
|
77
|
-
const declarations = source
|
|
78
|
-
.slice(open + 1, close - 1)
|
|
79
|
-
.split(';')
|
|
80
|
-
.map((chunk) => chunk.trim())
|
|
81
|
-
.filter((chunk) => chunk.includes(':') && !chunk.includes('{'))
|
|
82
|
-
.map((chunk) => ({
|
|
83
|
-
property: chunk.slice(0, chunk.indexOf(':')).trim(),
|
|
84
|
-
value: chunk.slice(chunk.indexOf(':') + 1).trim(),
|
|
85
|
-
}));
|
|
86
|
-
rules.push({ selectorList, declarations });
|
|
87
|
-
}
|
|
88
|
-
return rules;
|
|
89
|
-
}
|
|
90
|
-
const valueOf = (rule, property) => rule.declarations.findLast((d) => d.property === property)?.value;
|
|
91
|
-
describe('overlay surfaces own the scrim alpha through opacity (#206)', () => {
|
|
92
|
-
for (const { file, selector } of SURFACES) {
|
|
93
|
-
const source = readFileSync(resolve(here, '..', file), 'utf8');
|
|
94
|
-
const rules = rulesTargeting(styleBlock(source, file), selector);
|
|
95
|
-
// The veil rule is the one whose selector is the bare class; anything else
|
|
96
|
-
// (`[data-state='closed']`, a media-query variant) is a variant rule.
|
|
97
|
-
const veilRules = rules.filter((r) => r.selectorList === selector);
|
|
98
|
-
const variantRules = rules.filter((r) => r.selectorList !== selector);
|
|
99
|
-
it(`${selector} is painted by exactly one veil rule`, () => {
|
|
100
|
-
expect(veilRules).toHaveLength(1);
|
|
101
|
-
});
|
|
102
|
-
it(`${selector} paints the opaque scrim token`, () => {
|
|
103
|
-
const background = valueOf(veilRules[0], 'background-color');
|
|
104
|
-
expect(background, `${file}: background-color`).toBeDefined();
|
|
105
|
-
expect(ALLOWED_BACKGROUND.some((allowed) => allowed.test(background)), `${file}: background-color "${background}" is not an opaque token reference`).toBe(true);
|
|
106
|
-
});
|
|
107
|
-
it(`${selector} carries the veil's alpha as opacity 0.32`, () => {
|
|
108
|
-
const opacity = valueOf(veilRules[0], 'opacity');
|
|
109
|
-
expect(opacity, `${file}: opacity`).toBeDefined();
|
|
110
|
-
expect(ALLOWED_OPACITY.some((allowed) => allowed.test(opacity)), `${file}: opacity "${opacity}" is not the 0.32 the component owns`).toBe(true);
|
|
111
|
-
});
|
|
112
|
-
it(`${selector} declares no local overlay token that would change the veil`, () => {
|
|
113
|
-
for (const rule of rules) {
|
|
114
|
-
for (const { property, value } of rule.declarations) {
|
|
115
|
-
if (/^--sp-comp-[a-z-]+-overlay-opacity$/.test(property)) {
|
|
116
|
-
expect(value, `${file}: ${property}`).toBe('0.32');
|
|
117
|
-
}
|
|
118
|
-
if (/^--sp-comp-[a-z-]+-overlay-color$/.test(property)) {
|
|
119
|
-
expect(value, `${file}: ${property}`).toBe('var(--sp-sys-color-scrim)');
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
});
|
|
124
|
-
it(`${selector} variants only fade the veil out, never repaint it`, () => {
|
|
125
|
-
for (const rule of variantRules) {
|
|
126
|
-
expect(valueOf(rule, 'background-color'), `${file}: "${rule.selectorList}" repaints the veil`).toBeUndefined();
|
|
127
|
-
const opacity = valueOf(rule, 'opacity');
|
|
128
|
-
if (opacity !== undefined) {
|
|
129
|
-
expect(opacity, `${file}: "${rule.selectorList}" opacity`).toBe('0');
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
});
|
|
133
|
-
}
|
|
134
|
-
});
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
import AccordionContent from '../components/accordion-content.svelte'
|
|
3
|
-
import AccordionItem from '../components/accordion-item.svelte'
|
|
4
|
-
import AccordionRoot from '../components/accordion-root.svelte'
|
|
5
|
-
import AccordionTrigger from '../components/accordion-trigger.svelte'
|
|
6
|
-
|
|
7
|
-
let {
|
|
8
|
-
type = 'single',
|
|
9
|
-
value = $bindable(''),
|
|
10
|
-
disabled = false,
|
|
11
|
-
}: {
|
|
12
|
-
type?: 'single' | 'multiple'
|
|
13
|
-
value?: string | string[]
|
|
14
|
-
disabled?: boolean
|
|
15
|
-
} = $props()
|
|
16
|
-
</script>
|
|
17
|
-
|
|
18
|
-
<AccordionRoot bind:value {type} {disabled}>
|
|
19
|
-
<AccordionItem value="item-1">
|
|
20
|
-
<AccordionTrigger>Trigger 1</AccordionTrigger>
|
|
21
|
-
<AccordionContent>Content 1</AccordionContent>
|
|
22
|
-
</AccordionItem>
|
|
23
|
-
<AccordionItem value="item-2">
|
|
24
|
-
<AccordionTrigger>Trigger 2</AccordionTrigger>
|
|
25
|
-
<AccordionContent>Content 2</AccordionContent>
|
|
26
|
-
</AccordionItem>
|
|
27
|
-
<AccordionItem value="item-3" disabled={true}>
|
|
28
|
-
<AccordionTrigger>Trigger 3</AccordionTrigger>
|
|
29
|
-
<AccordionContent>Content 3</AccordionContent>
|
|
30
|
-
</AccordionItem>
|
|
31
|
-
</AccordionRoot>
|
|
@@ -1,139 +0,0 @@
|
|
|
1
|
-
import { flushSync, mount, unmount } from 'svelte';
|
|
2
|
-
import { afterEach, describe, expect, it } from 'vitest';
|
|
3
|
-
import AccordionRoot from '../components/accordion-root.svelte';
|
|
4
|
-
import AccordionTest from './AccordionTest.svelte';
|
|
5
|
-
describe('accordion', () => {
|
|
6
|
-
let target;
|
|
7
|
-
let instance;
|
|
8
|
-
function setup(props = {}) {
|
|
9
|
-
target = document.createElement('div');
|
|
10
|
-
document.body.appendChild(target);
|
|
11
|
-
instance = mount(AccordionTest, { target, props });
|
|
12
|
-
return target;
|
|
13
|
-
}
|
|
14
|
-
function setupRoot(props = {}) {
|
|
15
|
-
target = document.createElement('div');
|
|
16
|
-
document.body.appendChild(target);
|
|
17
|
-
instance = mount(AccordionRoot, { target, props });
|
|
18
|
-
return target;
|
|
19
|
-
}
|
|
20
|
-
afterEach(() => {
|
|
21
|
-
if (instance)
|
|
22
|
-
unmount(instance);
|
|
23
|
-
target?.remove();
|
|
24
|
-
});
|
|
25
|
-
// --- Basic rendering ---
|
|
26
|
-
it('renders with data-orientation', () => {
|
|
27
|
-
setupRoot();
|
|
28
|
-
expect(target.querySelector('[data-orientation]')).toBeTruthy();
|
|
29
|
-
});
|
|
30
|
-
it('renders all accordion items', () => {
|
|
31
|
-
setup();
|
|
32
|
-
const items = target.querySelectorAll('.sp-accordion-item');
|
|
33
|
-
expect(items.length).toBe(3);
|
|
34
|
-
});
|
|
35
|
-
it('renders trigger buttons', () => {
|
|
36
|
-
setup();
|
|
37
|
-
const triggers = target.querySelectorAll('.sp-accordion-trigger');
|
|
38
|
-
expect(triggers.length).toBe(3);
|
|
39
|
-
});
|
|
40
|
-
// --- ARIA attributes ---
|
|
41
|
-
it('triggers have aria-expanded="false" when collapsed', () => {
|
|
42
|
-
setup();
|
|
43
|
-
const triggers = target.querySelectorAll('.sp-accordion-trigger');
|
|
44
|
-
triggers.forEach((trigger) => {
|
|
45
|
-
expect(trigger.getAttribute('aria-expanded')).toBe('false');
|
|
46
|
-
});
|
|
47
|
-
});
|
|
48
|
-
it('trigger has aria-expanded="true" when item is open', () => {
|
|
49
|
-
setup({ value: 'item-1' });
|
|
50
|
-
const triggers = target.querySelectorAll('.sp-accordion-trigger');
|
|
51
|
-
expect(triggers[0]?.getAttribute('aria-expanded')).toBe('true');
|
|
52
|
-
expect(triggers[1]?.getAttribute('aria-expanded')).toBe('false');
|
|
53
|
-
});
|
|
54
|
-
it('item has data-state="open" when expanded', () => {
|
|
55
|
-
setup({ value: 'item-1' });
|
|
56
|
-
const items = target.querySelectorAll('.sp-accordion-item');
|
|
57
|
-
expect(items[0]?.getAttribute('data-state')).toBe('open');
|
|
58
|
-
expect(items[1]?.getAttribute('data-state')).toBe('closed');
|
|
59
|
-
});
|
|
60
|
-
it('content has role="region"', () => {
|
|
61
|
-
setup({ value: 'item-1' });
|
|
62
|
-
const content = target.querySelector('.sp-accordion-content');
|
|
63
|
-
expect(content?.getAttribute('role')).toBe('region');
|
|
64
|
-
});
|
|
65
|
-
// --- Single mode behavior ---
|
|
66
|
-
it('single mode: clicking one item closes the other', () => {
|
|
67
|
-
setup({ type: 'single', value: 'item-1' });
|
|
68
|
-
const triggers = target.querySelectorAll('.sp-accordion-trigger');
|
|
69
|
-
// item-1 is open
|
|
70
|
-
expect(target.querySelectorAll('.sp-accordion-content').length).toBe(1);
|
|
71
|
-
// Click item-2
|
|
72
|
-
triggers[1].click();
|
|
73
|
-
flushSync();
|
|
74
|
-
// item-2 should be open, item-1 closed
|
|
75
|
-
const items = target.querySelectorAll('.sp-accordion-item');
|
|
76
|
-
expect(items[0]?.getAttribute('data-state')).toBe('closed');
|
|
77
|
-
expect(items[1]?.getAttribute('data-state')).toBe('open');
|
|
78
|
-
expect(target.querySelectorAll('.sp-accordion-content').length).toBe(1);
|
|
79
|
-
});
|
|
80
|
-
it('single mode: clicking same item toggles it closed', () => {
|
|
81
|
-
setup({ type: 'single', value: 'item-1' });
|
|
82
|
-
const trigger = target.querySelector('.sp-accordion-trigger');
|
|
83
|
-
trigger.click();
|
|
84
|
-
flushSync();
|
|
85
|
-
expect(target.querySelector('.sp-accordion-content')).toBeNull();
|
|
86
|
-
});
|
|
87
|
-
// --- Multiple mode behavior ---
|
|
88
|
-
it('multiple mode: clicking items opens multiple', () => {
|
|
89
|
-
setup({ type: 'multiple', value: ['item-1'] });
|
|
90
|
-
const triggers = target.querySelectorAll('.sp-accordion-trigger');
|
|
91
|
-
// item-1 is already open, click item-2
|
|
92
|
-
triggers[1].click();
|
|
93
|
-
flushSync();
|
|
94
|
-
// Both should be open
|
|
95
|
-
const items = target.querySelectorAll('.sp-accordion-item');
|
|
96
|
-
expect(items[0]?.getAttribute('data-state')).toBe('open');
|
|
97
|
-
expect(items[1]?.getAttribute('data-state')).toBe('open');
|
|
98
|
-
expect(target.querySelectorAll('.sp-accordion-content').length).toBe(2);
|
|
99
|
-
});
|
|
100
|
-
// --- Disabled state ---
|
|
101
|
-
it('disabled item has data-disabled attribute', () => {
|
|
102
|
-
setup();
|
|
103
|
-
const items = target.querySelectorAll('.sp-accordion-item');
|
|
104
|
-
// item-3 is disabled
|
|
105
|
-
expect(items[2]?.hasAttribute('data-disabled')).toBe(true);
|
|
106
|
-
});
|
|
107
|
-
it('disabled item trigger is disabled', () => {
|
|
108
|
-
setup();
|
|
109
|
-
const triggers = target.querySelectorAll('.sp-accordion-trigger');
|
|
110
|
-
expect(triggers[2]?.disabled).toBe(true);
|
|
111
|
-
});
|
|
112
|
-
it('clicking disabled trigger does not toggle', () => {
|
|
113
|
-
setup({ type: 'single' });
|
|
114
|
-
const triggers = target.querySelectorAll('.sp-accordion-trigger');
|
|
115
|
-
triggers[2].click();
|
|
116
|
-
flushSync();
|
|
117
|
-
// item-3 should still be closed
|
|
118
|
-
const items = target.querySelectorAll('.sp-accordion-item');
|
|
119
|
-
expect(items[2]?.getAttribute('data-state')).toBe('closed');
|
|
120
|
-
});
|
|
121
|
-
it('root disabled prevents all toggles', () => {
|
|
122
|
-
setup({ disabled: true });
|
|
123
|
-
const triggers = target.querySelectorAll('.sp-accordion-trigger');
|
|
124
|
-
triggers[0].click();
|
|
125
|
-
flushSync();
|
|
126
|
-
// Nothing should be open
|
|
127
|
-
expect(target.querySelector('.sp-accordion-content')).toBeNull();
|
|
128
|
-
});
|
|
129
|
-
// --- Content rendering ---
|
|
130
|
-
it('content is not rendered for collapsed items', () => {
|
|
131
|
-
setup();
|
|
132
|
-
expect(target.querySelector('.sp-accordion-content')).toBeNull();
|
|
133
|
-
});
|
|
134
|
-
it('content is rendered for expanded item', () => {
|
|
135
|
-
setup({ value: 'item-1' });
|
|
136
|
-
const content = target.querySelector('.sp-accordion-content-inner');
|
|
137
|
-
expect(content?.textContent).toContain('Content 1');
|
|
138
|
-
});
|
|
139
|
-
});
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
import AlertDialogAction from '../components/alert-dialog-action.svelte'
|
|
3
|
-
import AlertDialogCancel from '../components/alert-dialog-cancel.svelte'
|
|
4
|
-
import AlertDialogContent from '../components/alert-dialog-content.svelte'
|
|
5
|
-
import AlertDialogDescription from '../components/alert-dialog-description.svelte'
|
|
6
|
-
import AlertDialogOverlay from '../components/alert-dialog-overlay.svelte'
|
|
7
|
-
import AlertDialogRoot from '../components/alert-dialog-root.svelte'
|
|
8
|
-
import AlertDialogTitle from '../components/alert-dialog-title.svelte'
|
|
9
|
-
import AlertDialogTrigger from '../components/alert-dialog-trigger.svelte'
|
|
10
|
-
|
|
11
|
-
let {
|
|
12
|
-
open = $bindable(false),
|
|
13
|
-
portalTo = undefined,
|
|
14
|
-
}: { open?: boolean; portalTo?: HTMLElement | string } = $props()
|
|
15
|
-
</script>
|
|
16
|
-
|
|
17
|
-
<AlertDialogRoot bind:open>
|
|
18
|
-
<AlertDialogTrigger>Delete</AlertDialogTrigger>
|
|
19
|
-
<AlertDialogOverlay />
|
|
20
|
-
<AlertDialogContent {portalTo}>
|
|
21
|
-
<AlertDialogTitle>Are you sure?</AlertDialogTitle>
|
|
22
|
-
<AlertDialogDescription>This action cannot be undone.</AlertDialogDescription>
|
|
23
|
-
<div class="sp-alert-dialog-actions">
|
|
24
|
-
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
|
25
|
-
<AlertDialogAction>Confirm</AlertDialogAction>
|
|
26
|
-
</div>
|
|
27
|
-
</AlertDialogContent>
|
|
28
|
-
</AlertDialogRoot>
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
import { mount, unmount } from 'svelte';
|
|
2
|
-
import { afterEach, describe, expect, it } from 'vitest';
|
|
3
|
-
import { configureAxe } from 'vitest-axe';
|
|
4
|
-
import * as matchers from 'vitest-axe/matchers';
|
|
5
|
-
import AlertDialogTest from './AlertDialogTest.svelte';
|
|
6
|
-
expect.extend(matchers);
|
|
7
|
-
const axe = configureAxe({
|
|
8
|
-
rules: {
|
|
9
|
-
region: { enabled: false },
|
|
10
|
-
'page-has-heading-one': { enabled: false },
|
|
11
|
-
'landmark-one-main': { enabled: false },
|
|
12
|
-
},
|
|
13
|
-
});
|
|
14
|
-
describe('alert-dialog a11y', () => {
|
|
15
|
-
let cleanup;
|
|
16
|
-
afterEach(() => cleanup?.());
|
|
17
|
-
it('has no axe violations when closed', async () => {
|
|
18
|
-
const container = document.createElement('div');
|
|
19
|
-
document.body.appendChild(container);
|
|
20
|
-
const instance = mount(AlertDialogTest, { target: container });
|
|
21
|
-
const results = await axe(container);
|
|
22
|
-
cleanup = () => {
|
|
23
|
-
unmount(instance);
|
|
24
|
-
container.remove();
|
|
25
|
-
};
|
|
26
|
-
expect(results).toHaveNoViolations();
|
|
27
|
-
});
|
|
28
|
-
it('has no axe violations when open', async () => {
|
|
29
|
-
const container = document.createElement('div');
|
|
30
|
-
document.body.appendChild(container);
|
|
31
|
-
const instance = mount(AlertDialogTest, {
|
|
32
|
-
target: container,
|
|
33
|
-
props: { open: true },
|
|
34
|
-
});
|
|
35
|
-
const results = await axe(container);
|
|
36
|
-
cleanup = () => {
|
|
37
|
-
unmount(instance);
|
|
38
|
-
container.remove();
|
|
39
|
-
};
|
|
40
|
-
expect(results).toHaveNoViolations();
|
|
41
|
-
});
|
|
42
|
-
});
|