@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.
Files changed (163) hide show
  1. package/LICENSE +21 -0
  2. package/THIRD_PARTY_NOTICES.md +201 -0
  3. package/dist/components/command/command.svelte.d.ts +30 -0
  4. package/dist/components/command/command.svelte.d.ts.map +1 -1
  5. package/dist/components/command/command.svelte.js +85 -4
  6. package/dist/components/command/components/command-group.svelte +25 -15
  7. package/dist/components/command/components/command-group.svelte.d.ts.map +1 -1
  8. package/dist/components/command/components/command-input.svelte +54 -5
  9. package/dist/components/command/components/command-input.svelte.d.ts +1 -1
  10. package/dist/components/command/components/command-input.svelte.d.ts.map +1 -1
  11. package/dist/components/command/components/command-list.svelte +22 -3
  12. package/dist/components/command/components/command-list.svelte.d.ts.map +1 -1
  13. package/dist/components/command/components/command-root.svelte +43 -4
  14. package/dist/components/command/components/command-root.svelte.d.ts.map +1 -1
  15. package/dist/components/command/index.d.ts +1 -0
  16. package/dist/components/command/index.d.ts.map +1 -1
  17. package/dist/components/command/types.d.ts +59 -4
  18. package/dist/components/command/types.d.ts.map +1 -1
  19. package/package.json +8 -2
  20. package/dist/components/__tests__/overlay-scrim-source.test.js +0 -134
  21. package/dist/components/accordion/__tests__/AccordionTest.svelte +0 -31
  22. package/dist/components/accordion/__tests__/accordion.test.svelte.js +0 -139
  23. package/dist/components/alert-dialog/__tests__/AlertDialogTest.svelte +0 -28
  24. package/dist/components/alert-dialog/__tests__/alert-dialog.a11y.test.js +0 -42
  25. package/dist/components/alert-dialog/__tests__/alert-dialog.integration.test.svelte.js +0 -51
  26. package/dist/components/alert-dialog/__tests__/alert-dialog.test.svelte.js +0 -113
  27. package/dist/components/app-bar/__tests__/app-bar.test.svelte.js +0 -30
  28. package/dist/components/aspect-ratio/__tests__/aspect-ratio.test.svelte.js +0 -22
  29. package/dist/components/avatar/__tests__/avatar.test.svelte.js +0 -26
  30. package/dist/components/badge/__tests__/badge.test.svelte.js +0 -30
  31. package/dist/components/bottom-sheet/__tests__/BottomSheetTest.svelte +0 -22
  32. package/dist/components/bottom-sheet/__tests__/bottom-sheet.test.svelte.js +0 -83
  33. package/dist/components/button/__tests__/button.test.svelte.js +0 -63
  34. package/dist/components/calendar/__tests__/CalendarTest.svelte +0 -56
  35. package/dist/components/calendar/__tests__/calendar.test.svelte.js +0 -190
  36. package/dist/components/card/__tests__/card.test.svelte.js +0 -30
  37. package/dist/components/checkbox/__tests__/checkbox.test.svelte.js +0 -44
  38. package/dist/components/chips/__tests__/chips.test.svelte.js +0 -26
  39. package/dist/components/collapsible/__tests__/CollapsibleTest.svelte +0 -18
  40. package/dist/components/collapsible/__tests__/collapsible.test.svelte.js +0 -125
  41. package/dist/components/combobox/__tests__/ComboboxTest.svelte +0 -40
  42. package/dist/components/combobox/__tests__/combobox.test.svelte.js +0 -255
  43. package/dist/components/command/__tests__/CommandTest.svelte +0 -77
  44. package/dist/components/command/__tests__/command-item-passthrough.test.svelte.js +0 -284
  45. package/dist/components/command/__tests__/command.a11y.test.js +0 -30
  46. package/dist/components/command/__tests__/command.integration.test.svelte.js +0 -68
  47. package/dist/components/command/__tests__/command.test.svelte.js +0 -230
  48. package/dist/components/context-menu/__tests__/ContextMenuNonInteractiveTest.svelte +0 -32
  49. package/dist/components/context-menu/__tests__/ContextMenuTest.svelte +0 -25
  50. package/dist/components/context-menu/__tests__/context-menu.test.svelte.js +0 -141
  51. package/dist/components/date-field/__tests__/DateFieldTest.svelte +0 -20
  52. package/dist/components/date-field/__tests__/date-field.test.svelte.js +0 -139
  53. package/dist/components/date-picker/__tests__/DatePickerTest.svelte +0 -20
  54. package/dist/components/date-picker/__tests__/date-picker.test.svelte.js +0 -190
  55. package/dist/components/date-range-field/__tests__/DateRangeFieldTest.svelte +0 -20
  56. package/dist/components/date-range-field/__tests__/date-range-field.test.svelte.js +0 -130
  57. package/dist/components/date-range-picker/__tests__/DateRangePickerTest.svelte +0 -18
  58. package/dist/components/date-range-picker/__tests__/date-range-picker.test.svelte.js +0 -156
  59. package/dist/components/dialog/__tests__/DialogTest.svelte +0 -34
  60. package/dist/components/dialog/__tests__/dialog.test.svelte.js +0 -184
  61. package/dist/components/drop-zone/__tests__/drop-zone.test.svelte.js +0 -26
  62. package/dist/components/dropdown-menu/__tests__/DropdownMenuRadioTest.svelte +0 -90
  63. package/dist/components/dropdown-menu/__tests__/DropdownMenuTest.svelte +0 -71
  64. package/dist/components/dropdown-menu/__tests__/dropdown-menu-radio-group.test.svelte.js +0 -306
  65. package/dist/components/dropdown-menu/__tests__/dropdown-menu.test.svelte.js +0 -567
  66. package/dist/components/empty-state/__tests__/empty-state.test.svelte.js +0 -26
  67. package/dist/components/fab/__tests__/fab.test.svelte.js +0 -30
  68. package/dist/components/field/__tests__/FieldTest.svelte +0 -26
  69. package/dist/components/field/__tests__/field.test.svelte.js +0 -74
  70. package/dist/components/grid-list/__tests__/GridListTest.svelte +0 -46
  71. package/dist/components/grid-list/__tests__/grid-list.test.svelte.js +0 -231
  72. package/dist/components/hover-card/__tests__/HoverCardTest.svelte +0 -30
  73. package/dist/components/hover-card/__tests__/hover-card.test.svelte.js +0 -236
  74. package/dist/components/indicator/__tests__/indicator.test.svelte.js +0 -30
  75. package/dist/components/input/__tests__/input.test.svelte.js +0 -67
  76. package/dist/components/label/__tests__/label.test.svelte.js +0 -22
  77. package/dist/components/language-select/__tests__/language-select.test.svelte.js +0 -199
  78. package/dist/components/language-select/__tests__/languages.test.js +0 -87
  79. package/dist/components/link-preview/__tests__/LinkPreviewTest.svelte +0 -37
  80. package/dist/components/link-preview/__tests__/link-preview.a11y.test.js +0 -28
  81. package/dist/components/link-preview/__tests__/link-preview.integration.test.svelte.js +0 -54
  82. package/dist/components/link-preview/__tests__/link-preview.test.svelte.js +0 -215
  83. package/dist/components/list/__tests__/list.test.svelte.js +0 -22
  84. package/dist/components/menubar/__tests__/MenubarTest.svelte +0 -52
  85. package/dist/components/menubar/__tests__/menubar.a11y.test.js +0 -28
  86. package/dist/components/menubar/__tests__/menubar.integration.test.svelte.js +0 -57
  87. package/dist/components/menubar/__tests__/menubar.test.svelte.js +0 -328
  88. package/dist/components/meter/__tests__/meter.test.svelte.js +0 -26
  89. package/dist/components/nav-bar/__tests__/nav-bar.test.svelte.js +0 -23
  90. package/dist/components/nav-drawer/__tests__/nav-drawer.test.svelte.js +0 -28
  91. package/dist/components/nav-rail/__tests__/nav-rail.test.svelte.js +0 -28
  92. package/dist/components/pagination/__tests__/pagination.test.svelte.js +0 -26
  93. package/dist/components/pin-input/__tests__/pin-input.test.svelte.js +0 -71
  94. package/dist/components/popover/__tests__/PopoverTest.svelte +0 -27
  95. package/dist/components/popover/__tests__/popover.test.svelte.js +0 -196
  96. package/dist/components/progress/__tests__/progress.test.svelte.js +0 -32
  97. package/dist/components/radio-group/__tests__/radio-group.test.svelte.js +0 -22
  98. package/dist/components/range-calendar/__tests__/RangeCalendarTest.svelte +0 -23
  99. package/dist/components/range-calendar/__tests__/range-calendar.test.svelte.js +0 -105
  100. package/dist/components/rating-group/__tests__/rating-group.test.svelte.js +0 -26
  101. package/dist/components/scroll-area/__tests__/scroll-area.test.svelte.js +0 -34
  102. package/dist/components/search-field/__tests__/SearchFieldTest.svelte +0 -34
  103. package/dist/components/search-field/__tests__/search-field.test.svelte.js +0 -118
  104. package/dist/components/select/__tests__/SelectLabelPersistenceTest.svelte +0 -31
  105. package/dist/components/select/__tests__/SelectTest.svelte +0 -33
  106. package/dist/components/select/__tests__/select.test.svelte.js +0 -221
  107. package/dist/components/separator/__tests__/separator.test.svelte.js +0 -39
  108. package/dist/components/sheet/__tests__/SheetTest.svelte +0 -34
  109. package/dist/components/sheet/__tests__/sheet.test.svelte.js +0 -193
  110. package/dist/components/side-sheet/__tests__/side-sheet.test.svelte.js +0 -138
  111. package/dist/components/sidebar/__tests__/SidebarFullTest.svelte +0 -45
  112. package/dist/components/sidebar/__tests__/SidebarTest.svelte +0 -9
  113. package/dist/components/sidebar/__tests__/sidebar.test.svelte.js +0 -186
  114. package/dist/components/simple-dialog/__tests__/SimpleDialogTest.svelte +0 -40
  115. package/dist/components/simple-dialog/__tests__/simple-dialog.test.svelte.js +0 -50
  116. package/dist/components/simple-tooltip/__tests__/SimpleTooltipTest.svelte +0 -19
  117. package/dist/components/simple-tooltip/__tests__/simple-tooltip.test.svelte.js +0 -61
  118. package/dist/components/slider/__tests__/slider.test.svelte.js +0 -42
  119. package/dist/components/sortable-list/__tests__/NestedSortableListTest.svelte +0 -49
  120. package/dist/components/sortable-list/__tests__/SortableListTest.svelte +0 -42
  121. package/dist/components/sortable-list/__tests__/sortable-list.nested.test.svelte.js +0 -112
  122. package/dist/components/sortable-list/__tests__/sortable-list.test.svelte.js +0 -175
  123. package/dist/components/splitter/__tests__/splitter.test.svelte.js +0 -26
  124. package/dist/components/switch/__tests__/switch.test.svelte.js +0 -44
  125. package/dist/components/table/__tests__/table.test.svelte.js +0 -22
  126. package/dist/components/tabs/__tests__/TabsTest.svelte +0 -32
  127. package/dist/components/tabs/__tests__/tabs.test.svelte.js +0 -227
  128. package/dist/components/textarea/__tests__/textarea.test.svelte.js +0 -55
  129. package/dist/components/time-field/__tests__/TimeFieldTest.svelte +0 -20
  130. package/dist/components/time-field/__tests__/time-field.test.svelte.js +0 -100
  131. package/dist/components/time-range-field/__tests__/TimeRangeFieldTest.svelte +0 -20
  132. package/dist/components/time-range-field/__tests__/time-range-field.test.svelte.js +0 -124
  133. package/dist/components/toast/__tests__/toast.test.svelte.js +0 -46
  134. package/dist/components/toggle/__tests__/toggle.test.svelte.js +0 -38
  135. package/dist/components/toggle-group/__tests__/toggle-group.test.svelte.js +0 -22
  136. package/dist/components/toolbar/__tests__/ToolbarCompositionTest.svelte +0 -18
  137. package/dist/components/toolbar/__tests__/ToolbarTabstopTest.svelte +0 -10
  138. package/dist/components/toolbar/__tests__/toolbar.test.svelte.js +0 -63
  139. package/dist/components/tooltip/__tests__/TooltipTest.svelte +0 -22
  140. package/dist/components/tooltip/__tests__/tooltip.test.svelte.js +0 -225
  141. package/dist/components/tree/__tests__/TreeLabelContentTest.svelte +0 -21
  142. package/dist/components/tree/__tests__/TreeTest.svelte +0 -24
  143. package/dist/components/tree/__tests__/tree.test.svelte.js +0 -229
  144. package/dist/components/virtualizer/__tests__/waterfall-layout.test.js +0 -82
  145. package/dist/internal/announcer/__tests__/announcer.test.js +0 -42
  146. package/dist/internal/dismissible-layer/__tests__/dismissible-layer.test.js +0 -196
  147. package/dist/internal/drag-drop/__tests__/auto-scroll.test.js +0 -104
  148. package/dist/internal/drag-drop/__tests__/drag-session.test.svelte.js +0 -803
  149. package/dist/internal/drag-drop/__tests__/drag-source.test.svelte.js +0 -130
  150. package/dist/internal/drag-drop/__tests__/drop-delegate.test.js +0 -223
  151. package/dist/internal/drag-drop/__tests__/types.test.js +0 -20
  152. package/dist/internal/escape-layer/__tests__/escape-layer.test.js +0 -113
  153. package/dist/internal/focus-scope/__tests__/focus-scope.test.js +0 -225
  154. package/dist/internal/grace-area/__tests__/grace-area.test.js +0 -67
  155. package/dist/internal/helpers/__tests__/helpers.test.js +0 -123
  156. package/dist/internal/portal/__tests__/PortalTest.svelte +0 -21
  157. package/dist/internal/portal/__tests__/portal.test.js +0 -29
  158. package/dist/internal/portal/__tests__/portal.test.svelte.js +0 -57
  159. package/dist/internal/scroll-lock/__tests__/scroll-lock.test.js +0 -160
  160. package/dist/internal/typeahead/__tests__/typeahead.test.js +0 -61
  161. package/src/themes/__tests__/carbon-dark-contrast.test.ts +0 -170
  162. package/src/themes/__tests__/generated-css-drift.test.ts +0 -53
  163. 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
- trapFocus = true,
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 (trapFocus && contentEl) {
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
- {#if presence.isPresent}
113
- <Portal to={portalTo}>
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;AAoI/C,QAAA,MAAM,WAAW,gEAAwC,CAAC;AAC1D,KAAK,WAAW,GAAG,UAAU,CAAC,OAAO,WAAW,CAAC,CAAC;AAClD,eAAe,WAAW,CAAC"}
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
- /** Portal target. Default undefined (document.body). */
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
- /** Trap focus within the palette. Default: true. */
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
- export interface CommandInputProps {
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;AAErD,MAAM,WAAW,YAAY;IAC3B,uCAAuC;IACvC,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,gGAAgG;IAChG,MAAM,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,EAAE,KAAK,MAAM,CAAA;IACvE,0EAA0E;IAC1E,YAAY,CAAC,EAAE,OAAO,CAAA;IACtB,gEAAgE;IAChE,IAAI,CAAC,EAAE,OAAO,CAAA;IACd;;OAEG;IACH,IAAI,CAAC,EAAE,OAAO,CAAA;IACd,wDAAwD;IACxD,QAAQ,CAAC,EAAE,WAAW,GAAG,MAAM,CAAA;IAC/B,oDAAoD;IACpD,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,8CAA8C;IAC9C,aAAa,CAAC,EAAE,OAAO,CAAA;IACvB,mDAAmD;IACnD,mBAAmB,CAAC,EAAE,OAAO,CAAA;IAC7B,mDAAmD;IACnD,eAAe,CAAC,EAAE,CAAC,KAAK,EAAE,aAAa,KAAK,IAAI,CAAA;IAChD,qDAAqD;IACrD,iBAAiB,CAAC,EAAE,CAAC,KAAK,EAAE,YAAY,KAAK,IAAI,CAAA;IACjD,uFAAuF;IACvF,YAAY,CAAC,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAA;IACtC;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,IAAI,CAAA;IACpB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,QAAQ,CAAC,EAAE,OAAO,CAAA;CACnB;AAED,MAAM,WAAW,iBAAiB;IAChC,6CAA6C;IAC7C,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,KAAK,CAAC,EAAE,MAAM,CAAA;CACf;AAED,MAAM,WAAW,gBAAgB;IAC/B,wCAAwC;IACxC,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,QAAQ,CAAC,EAAE,OAAO,CAAA;CACnB;AAED,MAAM,WAAW,iBAAiB;IAChC,qCAAqC;IACrC,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,wEAAwE;IACxE,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,QAAQ,CAAC,EAAE,OAAO,CAAA;CACnB;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,WAAW,gBAAiB,SAAQ,IAAI,CAC5C,cAAc,CAAC,cAAc,CAAC,EAC9B,OAAO,GAAG,UAAU,GAAG,MAAM,GAAG,IAAI,GAAG,UAAU,GAAG,eAAe,GAAG,eAAe,CACtF;IACC,kCAAkC;IAClC,KAAK,EAAE,MAAM,CAAA;IACb,+DAA+D;IAC/D,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAA;IACnB,oCAAoC;IACpC,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB;;;;;;OAMG;IACH,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;IAClC,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,QAAQ,CAAC,EAAE,OAAO,CAAA;CACnB;AAED,MAAM,WAAW,iBAAiB;IAChC,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,QAAQ,CAAC,EAAE,OAAO,CAAA;CACnB;AAED,MAAM,WAAW,qBAAqB;IACpC,KAAK,CAAC,EAAE,MAAM,CAAA;CACf"}
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.6.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
- });