@happyvertical/smrt-ui 0.51.38 → 0.52.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/AGENTS.md +14 -2
- package/README.md +257 -5
- package/dist/actions/__tests__/swipe-dismiss.test.js +69 -0
- package/dist/actions/swipe-dismiss.d.ts +42 -0
- package/dist/actions/swipe-dismiss.d.ts.map +1 -0
- package/dist/actions/swipe-dismiss.js +69 -0
- package/dist/components/calendar/Calendar.svelte +5 -0
- package/dist/components/calendar/Calendar.svelte.d.ts.map +1 -1
- package/dist/components/calendar/CalendarView.svelte +1250 -0
- package/dist/components/calendar/CalendarView.svelte.d.ts +66 -0
- package/dist/components/calendar/CalendarView.svelte.d.ts.map +1 -0
- package/dist/components/calendar/DayView.svelte +5 -0
- package/dist/components/calendar/DayView.svelte.d.ts.map +1 -1
- package/dist/components/calendar/__tests__/CalendarView.test.js +226 -0
- package/dist/components/calendar/__tests__/calendar-model.test.js +138 -0
- package/dist/components/calendar/calendar-model.d.ts +178 -0
- package/dist/components/calendar/calendar-model.d.ts.map +1 -0
- package/dist/components/calendar/calendar-model.js +362 -0
- package/dist/components/calendar/index.d.ts +12 -1
- package/dist/components/calendar/index.d.ts.map +1 -1
- package/dist/components/calendar/index.js +11 -1
- package/dist/components/data/CollectionList.svelte +23 -5
- package/dist/components/data/CollectionList.svelte.d.ts +8 -2
- package/dist/components/data/CollectionList.svelte.d.ts.map +1 -1
- package/dist/components/data/DataTable.svelte +118 -40
- package/dist/components/data/DataTable.svelte.d.ts.map +1 -1
- package/dist/components/data/DataTableController.d.ts +4 -0
- package/dist/components/data/DataTableController.d.ts.map +1 -1
- package/dist/components/data/DataTableController.js +11 -4
- package/dist/components/data/ListSortSelect.svelte +121 -0
- package/dist/components/data/ListSortSelect.svelte.d.ts +22 -0
- package/dist/components/data/ListSortSelect.svelte.d.ts.map +1 -0
- package/dist/components/data/SortableHeader.svelte +169 -0
- package/dist/components/data/SortableHeader.svelte.d.ts +30 -0
- package/dist/components/data/SortableHeader.svelte.d.ts.map +1 -0
- package/dist/components/data/__tests__/CollectionList.test.js +37 -0
- package/dist/components/data/__tests__/DataTable.test.js +76 -0
- package/dist/components/data/__tests__/DataTableController.test.js +35 -0
- package/dist/components/data/__tests__/SortableHeader.test.js +101 -0
- package/dist/components/data/__tests__/link-step-surface.test.js +172 -0
- package/dist/components/data/__tests__/list-sort-state.svelte.test.js +71 -0
- package/dist/components/data/__tests__/list-sort.test.js +91 -0
- package/dist/components/data/__tests__/surface-navigation.test.js +121 -0
- package/dist/components/data/data-surface.d.ts +1 -0
- package/dist/components/data/data-surface.d.ts.map +1 -1
- package/dist/components/data/data-surface.js +1 -0
- package/dist/components/data/index.d.ts +6 -0
- package/dist/components/data/index.d.ts.map +1 -1
- package/dist/components/data/index.js +6 -0
- package/dist/components/data/link-surface.d.ts +79 -0
- package/dist/components/data/link-surface.d.ts.map +1 -0
- package/dist/components/data/link-surface.js +155 -0
- package/dist/components/data/list-sort-state.svelte.d.ts +39 -0
- package/dist/components/data/list-sort-state.svelte.d.ts.map +1 -0
- package/dist/components/data/list-sort-state.svelte.js +47 -0
- package/dist/components/data/list-sort.d.ts +94 -0
- package/dist/components/data/list-sort.d.ts.map +1 -0
- package/dist/components/data/list-sort.js +182 -0
- package/dist/components/data/step-surface.d.ts +73 -0
- package/dist/components/data/step-surface.d.ts.map +1 -0
- package/dist/components/data/step-surface.js +177 -0
- package/dist/components/data/surface-navigation.d.ts +49 -0
- package/dist/components/data/surface-navigation.d.ts.map +1 -0
- package/dist/components/data/surface-navigation.js +100 -0
- package/dist/components/data/types.d.ts +19 -0
- package/dist/components/data/types.d.ts.map +1 -1
- package/dist/components/feedback/PhoneSheet.svelte +224 -0
- package/dist/components/feedback/PhoneSheet.svelte.d.ts +35 -0
- package/dist/components/feedback/PhoneSheet.svelte.d.ts.map +1 -0
- package/dist/components/feedback/WorkingStrip.svelte +426 -0
- package/dist/components/feedback/WorkingStrip.svelte.d.ts +37 -0
- package/dist/components/feedback/WorkingStrip.svelte.d.ts.map +1 -0
- package/dist/components/feedback/__tests__/PhoneSheet.test.js +101 -0
- package/dist/components/feedback/__tests__/WorkingStrip.test.js +172 -0
- package/dist/components/feedback/index.d.ts +4 -0
- package/dist/components/feedback/index.d.ts.map +1 -1
- package/dist/components/feedback/index.js +5 -0
- package/dist/components/feedback/working-status.d.ts +34 -0
- package/dist/components/feedback/working-status.d.ts.map +1 -0
- package/dist/components/feedback/working-status.js +5 -0
- package/dist/components/forms/Checkbox.svelte +26 -2
- package/dist/components/forms/Combobox.svelte +68 -12
- package/dist/components/forms/Combobox.svelte.d.ts +6 -0
- package/dist/components/forms/Combobox.svelte.d.ts.map +1 -1
- package/dist/components/forms/DictationButton.svelte +128 -0
- package/dist/components/forms/DictationButton.svelte.d.ts +19 -0
- package/dist/components/forms/DictationButton.svelte.d.ts.map +1 -0
- package/dist/components/forms/DictationStatus.svelte +164 -0
- package/dist/components/forms/DictationStatus.svelte.d.ts +11 -0
- package/dist/components/forms/DictationStatus.svelte.d.ts.map +1 -0
- package/dist/components/forms/Fieldset.svelte +5 -1
- package/dist/components/forms/Fieldset.svelte.d.ts +2 -0
- package/dist/components/forms/Fieldset.svelte.d.ts.map +1 -1
- package/dist/components/forms/FormActionBar.svelte +84 -0
- package/dist/components/forms/FormActionBar.svelte.d.ts +23 -0
- package/dist/components/forms/FormActionBar.svelte.d.ts.map +1 -0
- package/dist/components/forms/Input.svelte +9 -0
- package/dist/components/forms/Radio.svelte +15 -4
- package/dist/components/forms/SearchInput.svelte +207 -0
- package/dist/components/forms/SearchInput.svelte.d.ts +22 -0
- package/dist/components/forms/SearchInput.svelte.d.ts.map +1 -0
- package/dist/components/forms/Select.svelte +9 -0
- package/dist/components/forms/StagedControlReview.svelte +21 -12
- package/dist/components/forms/StagedControlReview.svelte.d.ts.map +1 -1
- package/dist/components/forms/Switch.svelte +11 -5
- package/dist/components/forms/TagsInput.svelte +8 -1
- package/dist/components/forms/Textarea.svelte +8 -0
- package/dist/components/forms/__tests__/Combobox.test.js +87 -0
- package/dist/components/forms/__tests__/ControlHitArea.test.js +73 -0
- package/dist/components/forms/__tests__/DictationButton.test.js +176 -0
- package/dist/components/forms/__tests__/Fieldset.test.js +37 -0
- package/dist/components/forms/__tests__/FormActionBar.test.js +22 -0
- package/dist/components/forms/__tests__/SearchInput.test.js +92 -0
- package/dist/components/forms/__tests__/StagedControlReview.test.js +37 -3
- package/dist/components/forms/__tests__/combobox-labels.fixture.svelte +19 -0
- package/dist/components/forms/__tests__/combobox-labels.fixture.svelte.d.ts +11 -0
- package/dist/components/forms/__tests__/combobox-labels.fixture.svelte.d.ts.map +1 -0
- package/dist/components/forms/__tests__/composite-registration.fixture.svelte +29 -0
- package/dist/components/forms/__tests__/composite-registration.fixture.svelte.d.ts +8 -0
- package/dist/components/forms/__tests__/composite-registration.fixture.svelte.d.ts.map +1 -0
- package/dist/components/forms/__tests__/control-interaction.test.js +19 -1
- package/dist/components/forms/__tests__/control-proposals.test.js +63 -0
- package/dist/components/forms/__tests__/dictation-recording.test.js +582 -0
- package/dist/components/forms/__tests__/dictation.test.js +486 -0
- package/dist/components/forms/__tests__/long-press.test.js +175 -0
- package/dist/components/forms/__tests__/place-picker.fixture.svelte +33 -0
- package/dist/components/forms/__tests__/place-picker.fixture.svelte.d.ts +12 -0
- package/dist/components/forms/__tests__/place-picker.fixture.svelte.d.ts.map +1 -0
- package/dist/components/forms/audio-capture.d.ts +69 -0
- package/dist/components/forms/audio-capture.d.ts.map +1 -0
- package/dist/components/forms/audio-capture.js +193 -0
- package/dist/components/forms/control-interaction.d.ts +6 -0
- package/dist/components/forms/control-interaction.d.ts.map +1 -1
- package/dist/components/forms/control-interaction.js +12 -1
- package/dist/components/forms/control-proposals.d.ts +55 -0
- package/dist/components/forms/control-proposals.d.ts.map +1 -0
- package/dist/components/forms/control-proposals.js +188 -0
- package/dist/components/forms/dictation-transcribe.d.ts +36 -0
- package/dist/components/forms/dictation-transcribe.d.ts.map +1 -0
- package/dist/components/forms/dictation-transcribe.js +65 -0
- package/dist/components/forms/dictation.svelte.d.ts +200 -0
- package/dist/components/forms/dictation.svelte.d.ts.map +1 -0
- package/dist/components/forms/dictation.svelte.js +686 -0
- package/dist/components/forms/index.d.ts +14 -1
- package/dist/components/forms/index.d.ts.map +1 -1
- package/dist/components/forms/index.js +14 -1
- package/dist/components/forms/insert-text.d.ts +12 -0
- package/dist/components/forms/insert-text.d.ts.map +1 -0
- package/dist/components/forms/insert-text.js +38 -0
- package/dist/components/forms/long-press.d.ts +88 -0
- package/dist/components/forms/long-press.d.ts.map +1 -0
- package/dist/components/forms/long-press.js +213 -0
- package/dist/components/forms/ready-beep.d.ts +24 -0
- package/dist/components/forms/ready-beep.d.ts.map +1 -0
- package/dist/components/forms/ready-beep.js +82 -0
- package/dist/components/layout/PageHeader.svelte +151 -17
- package/dist/components/layout/PageHeader.svelte.d.ts +47 -7
- package/dist/components/layout/PageHeader.svelte.d.ts.map +1 -1
- package/dist/components/layout/__tests__/PageHeader.test.js +87 -1
- package/dist/components/layout/index.d.ts +1 -0
- package/dist/components/layout/index.d.ts.map +1 -1
- package/dist/components/layout/index.js +1 -0
- package/dist/components/layout/page-header-context.d.ts +25 -0
- package/dist/components/layout/page-header-context.d.ts.map +1 -0
- package/dist/components/layout/page-header-context.js +16 -0
- package/dist/components/nav/Tabs.svelte +241 -0
- package/dist/components/nav/Tabs.svelte.d.ts +15 -0
- package/dist/components/nav/Tabs.svelte.d.ts.map +1 -1
- package/dist/components/nav/__tests__/LinkTabs.test.js +117 -0
- package/dist/components/nav/index.d.ts +1 -0
- package/dist/components/nav/index.d.ts.map +1 -1
- package/dist/components/nav/index.js +1 -0
- package/dist/components/nav/tabs-overflow.d.ts +12 -0
- package/dist/components/nav/tabs-overflow.d.ts.map +1 -0
- package/dist/components/nav/tabs-overflow.js +17 -0
- package/dist/components/nav/types.d.ts +8 -0
- package/dist/components/nav/types.d.ts.map +1 -1
- package/dist/components/ui/Button.svelte +7 -0
- package/dist/components/ui/Card.svelte +14 -5
- package/dist/components/ui/Dropdown.svelte +55 -1
- package/dist/components/ui/Dropdown.svelte.d.ts +8 -0
- package/dist/components/ui/Dropdown.svelte.d.ts.map +1 -1
- package/dist/components/ui/IconToggle.svelte +139 -0
- package/dist/components/ui/IconToggle.svelte.d.ts +32 -0
- package/dist/components/ui/IconToggle.svelte.d.ts.map +1 -0
- package/dist/components/ui/IconToggleGroup.svelte +56 -0
- package/dist/components/ui/IconToggleGroup.svelte.d.ts +21 -0
- package/dist/components/ui/IconToggleGroup.svelte.d.ts.map +1 -0
- package/dist/components/ui/Pagination.svelte +1 -1
- package/dist/components/ui/Popover.svelte +17 -1
- package/dist/components/ui/Popover.svelte.d.ts +8 -0
- package/dist/components/ui/Popover.svelte.d.ts.map +1 -1
- package/dist/components/ui/__tests__/IconToggle.test.js +59 -0
- package/dist/components/ui/__tests__/IconToggleGroup.test.js +30 -0
- package/dist/components/ui/__tests__/Popover.test.js +44 -0
- package/dist/components/ui/__tests__/dropdown-popover-icon.test.js +125 -0
- package/dist/components/ui/__tests__/phone-touch-targets.test.js +39 -0
- package/dist/components/ui/index.d.ts +2 -0
- package/dist/components/ui/index.d.ts.map +1 -1
- package/dist/components/ui/index.js +2 -0
- package/dist/i18n/strings.ui.d.ts +41 -0
- package/dist/i18n/strings.ui.d.ts.map +1 -1
- package/dist/i18n/strings.ui.js +46 -0
- package/dist/svelte/__tests__/playground.test.js +1 -0
- package/dist/svelte/playground/CalendarPreview.svelte +140 -0
- package/dist/svelte/playground/CalendarPreview.svelte.d.ts +4 -0
- package/dist/svelte/playground/CalendarPreview.svelte.d.ts.map +1 -0
- package/dist/svelte/playground.d.ts.map +1 -1
- package/dist/svelte/playground.js +10 -0
- package/dist/types-generic.d.ts +2 -0
- package/dist/types-generic.d.ts.map +1 -1
- package/package.json +3 -3
|
@@ -18,6 +18,14 @@ export interface Props {
|
|
|
18
18
|
label?: string;
|
|
19
19
|
/** Custom trigger content. */
|
|
20
20
|
trigger?: Snippet;
|
|
21
|
+
/** Accessible name of the trigger button when it shows only an icon (`aria-label`); omit to use its visible content (an icon trigger then falls back to `label`). */
|
|
22
|
+
triggerLabel?: string;
|
|
23
|
+
/**
|
|
24
|
+
* Trigger look: `button` (bordered, the default) or `icon`, a borderless 44px
|
|
25
|
+
* round icon button (a "more" or row-actions menu) that takes the surrounding
|
|
26
|
+
* text colour.
|
|
27
|
+
*/
|
|
28
|
+
variant?: 'button' | 'icon';
|
|
21
29
|
/** Menu items. */
|
|
22
30
|
items: MenuItem[];
|
|
23
31
|
/** Menu placement relative to the trigger. */
|
|
@@ -31,6 +39,8 @@ export interface Props {
|
|
|
31
39
|
const {
|
|
32
40
|
label,
|
|
33
41
|
trigger,
|
|
42
|
+
triggerLabel,
|
|
43
|
+
variant = 'button',
|
|
34
44
|
items,
|
|
35
45
|
placement = 'bottom-start',
|
|
36
46
|
onselect,
|
|
@@ -39,6 +49,16 @@ const {
|
|
|
39
49
|
|
|
40
50
|
const instanceId = $props.id();
|
|
41
51
|
const menuId = `smrt-menu-${instanceId}`;
|
|
52
|
+
|
|
53
|
+
// An icon trigger shows no text, so without triggerLabel or label the button
|
|
54
|
+
// has no accessible name.
|
|
55
|
+
$effect(() => {
|
|
56
|
+
if (variant === 'icon' && !triggerLabel && !label) {
|
|
57
|
+
console.warn(
|
|
58
|
+
'[Dropdown] variant="icon" needs triggerLabel (or label): the trigger button has no accessible name.',
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
});
|
|
42
62
|
let open = $state(false);
|
|
43
63
|
let wrapEl = $state<HTMLElement | null>(null);
|
|
44
64
|
let triggerEl = $state<HTMLButtonElement | null>(null);
|
|
@@ -132,6 +152,8 @@ $effect(() => {
|
|
|
132
152
|
bind:this={triggerEl}
|
|
133
153
|
type="button"
|
|
134
154
|
class="dropdown__trigger"
|
|
155
|
+
class:dropdown__trigger--icon={variant === 'icon'}
|
|
156
|
+
aria-label={triggerLabel ?? (variant === 'icon' ? label : undefined)}
|
|
135
157
|
aria-haspopup="menu"
|
|
136
158
|
aria-expanded={open}
|
|
137
159
|
aria-controls={open ? menuId : undefined}
|
|
@@ -160,7 +182,10 @@ $effect(() => {
|
|
|
160
182
|
tabindex={-1}
|
|
161
183
|
onclick={() => selectItem(item)}
|
|
162
184
|
>
|
|
163
|
-
{item.
|
|
185
|
+
{#if item.icon}
|
|
186
|
+
<span class="dropdown__item-icon" aria-hidden="true">{@render item.icon()}</span>
|
|
187
|
+
{/if}
|
|
188
|
+
<span class="dropdown__item-label">{item.label}</span>
|
|
164
189
|
</button>
|
|
165
190
|
{/each}
|
|
166
191
|
</div>
|
|
@@ -187,6 +212,24 @@ $effect(() => {
|
|
|
187
212
|
outline: 2px solid var(--smrt-color-primary);
|
|
188
213
|
outline-offset: 2px;
|
|
189
214
|
}
|
|
215
|
+
/* Icon trigger: a 44px round "more" button in the surrounding text colour. */
|
|
216
|
+
.dropdown__trigger--icon {
|
|
217
|
+
display: inline-flex;
|
|
218
|
+
align-items: center;
|
|
219
|
+
justify-content: center;
|
|
220
|
+
box-sizing: border-box;
|
|
221
|
+
inline-size: 2.75rem;
|
|
222
|
+
block-size: 2.75rem;
|
|
223
|
+
padding: 0;
|
|
224
|
+
border: none;
|
|
225
|
+
border-radius: var(--smrt-radius-full, 999px);
|
|
226
|
+
background: transparent;
|
|
227
|
+
color: inherit;
|
|
228
|
+
}
|
|
229
|
+
.dropdown__trigger--icon:hover:not(:disabled),
|
|
230
|
+
.dropdown__trigger--icon[aria-expanded='true'] {
|
|
231
|
+
background: color-mix(in srgb, currentColor 10%, transparent);
|
|
232
|
+
}
|
|
190
233
|
.dropdown__trigger:disabled {
|
|
191
234
|
opacity: 0.5;
|
|
192
235
|
cursor: not-allowed;
|
|
@@ -226,6 +269,12 @@ $effect(() => {
|
|
|
226
269
|
|
|
227
270
|
.dropdown__item {
|
|
228
271
|
appearance: none;
|
|
272
|
+
display: flex;
|
|
273
|
+
align-items: center;
|
|
274
|
+
gap: var(--smrt-spacing-2, 8px);
|
|
275
|
+
/* A touch target: menu rows are at least 44px tall. */
|
|
276
|
+
min-block-size: 2.75rem;
|
|
277
|
+
box-sizing: border-box;
|
|
229
278
|
font: inherit;
|
|
230
279
|
text-align: left;
|
|
231
280
|
cursor: pointer;
|
|
@@ -236,6 +285,11 @@ $effect(() => {
|
|
|
236
285
|
color: var(--smrt-color-on-surface);
|
|
237
286
|
white-space: nowrap;
|
|
238
287
|
}
|
|
288
|
+
.dropdown__item-icon {
|
|
289
|
+
display: inline-flex;
|
|
290
|
+
flex: none;
|
|
291
|
+
color: var(--smrt-color-on-surface-variant);
|
|
292
|
+
}
|
|
239
293
|
.dropdown__item:hover:not(:disabled) {
|
|
240
294
|
background: var(--smrt-color-surface-container-high);
|
|
241
295
|
}
|
|
@@ -14,6 +14,14 @@ export interface Props {
|
|
|
14
14
|
label?: string;
|
|
15
15
|
/** Custom trigger content. */
|
|
16
16
|
trigger?: Snippet;
|
|
17
|
+
/** Accessible name of the trigger button when it shows only an icon (`aria-label`); omit to use its visible content (an icon trigger then falls back to `label`). */
|
|
18
|
+
triggerLabel?: string;
|
|
19
|
+
/**
|
|
20
|
+
* Trigger look: `button` (bordered, the default) or `icon`, a borderless 44px
|
|
21
|
+
* round icon button (a "more" or row-actions menu) that takes the surrounding
|
|
22
|
+
* text colour.
|
|
23
|
+
*/
|
|
24
|
+
variant?: 'button' | 'icon';
|
|
17
25
|
/** Menu items. */
|
|
18
26
|
items: MenuItem[];
|
|
19
27
|
/** Menu placement relative to the trigger. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Dropdown.svelte.d.ts","sourceRoot":"","sources":["../../../src/components/ui/Dropdown.svelte.ts"],"names":[],"mappings":"AAGA;;;;;;;;GAQG;AACH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AAEtC,OAAO,KAAK,EAAE,iBAAiB,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAGvE,MAAM,WAAW,KAAK;IACpB,uEAAuE;IACvE,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,8BAA8B;IAC9B,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,kBAAkB;IAClB,KAAK,EAAE,QAAQ,EAAE,CAAC;IAClB,8CAA8C;IAC9C,SAAS,CAAC,EAAE,iBAAiB,CAAC;IAC9B,0CAA0C;IAC1C,QAAQ,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;IAChC,2BAA2B;IAC3B,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;
|
|
1
|
+
{"version":3,"file":"Dropdown.svelte.d.ts","sourceRoot":"","sources":["../../../src/components/ui/Dropdown.svelte.ts"],"names":[],"mappings":"AAGA;;;;;;;;GAQG;AACH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AAEtC,OAAO,KAAK,EAAE,iBAAiB,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAGvE,MAAM,WAAW,KAAK;IACpB,uEAAuE;IACvE,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,8BAA8B;IAC9B,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,qKAAqK;IACrK,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;;OAIG;IACH,OAAO,CAAC,EAAE,QAAQ,GAAG,MAAM,CAAC;IAC5B,kBAAkB;IAClB,KAAK,EAAE,QAAQ,EAAE,CAAC;IAClB,8CAA8C;IAC9C,SAAS,CAAC,EAAE,iBAAiB,CAAC;IAC9B,0CAA0C;IAC1C,QAAQ,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;IAChC,2BAA2B;IAC3B,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AA+ID,QAAA,MAAM,QAAQ,2CAAwC,CAAC;AACvD,KAAK,QAAQ,GAAG,UAAU,CAAC,OAAO,QAAQ,CAAC,CAAC;AAC5C,eAAe,QAAQ,CAAC"}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* IconToggle — an icon-only toggle button for dense filter and view-switcher
|
|
4
|
+
* rows. The icon is the `children` snippet (any icon set); `label` is both the
|
|
5
|
+
* accessible name and the tooltip text. `pressed` is reflected with
|
|
6
|
+
* `aria-pressed`, so a row of them is a one-of-many (or a many) toggle group;
|
|
7
|
+
* the owner decides what pressing means (this component keeps no state). An
|
|
8
|
+
* optional `count` shows as a small badge and is part of the accessible name
|
|
9
|
+
* and tooltip. The hit area is at least 44px square.
|
|
10
|
+
*/
|
|
11
|
+
import type { Snippet } from 'svelte';
|
|
12
|
+
import type { HTMLButtonAttributes } from 'svelte/elements';
|
|
13
|
+
import type { TooltipPlacement } from '../../types-generic';
|
|
14
|
+
import Tooltip from './Tooltip.svelte';
|
|
15
|
+
|
|
16
|
+
export interface Props
|
|
17
|
+
extends Omit<
|
|
18
|
+
HTMLButtonAttributes,
|
|
19
|
+
'class' | 'children' | 'aria-label' | 'aria-pressed' | 'title'
|
|
20
|
+
> {
|
|
21
|
+
/** Accessible name and tooltip text (what the button does or filters). */
|
|
22
|
+
label: string;
|
|
23
|
+
/** Whether the toggle is on (`aria-pressed`). */
|
|
24
|
+
pressed?: boolean;
|
|
25
|
+
/** Optional count badge; included in the accessible name and tooltip. */
|
|
26
|
+
count?: number;
|
|
27
|
+
/** A CSS colour (a token such as `var(--status-draft)` or a value) shown when the toggle is pressed. */
|
|
28
|
+
tone?: string;
|
|
29
|
+
/** The icon. Decorative: it is hidden from assistive technology. */
|
|
30
|
+
children: Snippet;
|
|
31
|
+
/** Where the tooltip opens. */
|
|
32
|
+
tooltipPlacement?: TooltipPlacement;
|
|
33
|
+
/** Additional CSS class names. */
|
|
34
|
+
class?: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const {
|
|
38
|
+
label,
|
|
39
|
+
pressed = false,
|
|
40
|
+
count,
|
|
41
|
+
tone,
|
|
42
|
+
children,
|
|
43
|
+
tooltipPlacement = 'top',
|
|
44
|
+
disabled = false,
|
|
45
|
+
type = 'button',
|
|
46
|
+
class: className = '',
|
|
47
|
+
...rest
|
|
48
|
+
}: Props = $props();
|
|
49
|
+
|
|
50
|
+
const name = $derived(count === undefined ? label : `${label} (${count})`);
|
|
51
|
+
</script>
|
|
52
|
+
|
|
53
|
+
<Tooltip text={name} placement={tooltipPlacement}>
|
|
54
|
+
<button
|
|
55
|
+
{...rest}
|
|
56
|
+
{type}
|
|
57
|
+
{disabled}
|
|
58
|
+
class="icon-toggle {className}"
|
|
59
|
+
class:pressed
|
|
60
|
+
class:toned={tone !== undefined}
|
|
61
|
+
style:--icon-toggle-tone={tone}
|
|
62
|
+
aria-pressed={pressed}
|
|
63
|
+
aria-label={name}
|
|
64
|
+
>
|
|
65
|
+
<span class="icon" aria-hidden="true">{@render children()}</span>
|
|
66
|
+
{#if count !== undefined}<span class="count" aria-hidden="true">{count}</span>{/if}
|
|
67
|
+
</button>
|
|
68
|
+
</Tooltip>
|
|
69
|
+
|
|
70
|
+
<style>
|
|
71
|
+
.icon-toggle {
|
|
72
|
+
position: relative;
|
|
73
|
+
display: inline-flex;
|
|
74
|
+
align-items: center;
|
|
75
|
+
justify-content: center;
|
|
76
|
+
box-sizing: border-box;
|
|
77
|
+
min-width: 2.75rem;
|
|
78
|
+
min-height: 2.75rem;
|
|
79
|
+
padding: 0;
|
|
80
|
+
border: 1px solid var(--smrt-color-outline-variant, var(--smrt-color-outline));
|
|
81
|
+
border-radius: var(--smrt-radius-full, 999px);
|
|
82
|
+
background: var(--smrt-color-surface-container, var(--smrt-color-surface));
|
|
83
|
+
color: var(--smrt-color-on-surface);
|
|
84
|
+
font: inherit;
|
|
85
|
+
cursor: pointer;
|
|
86
|
+
}
|
|
87
|
+
.icon-toggle:hover:not(:disabled) {
|
|
88
|
+
background: var(--smrt-color-surface-container-high, var(--smrt-color-surface-container));
|
|
89
|
+
}
|
|
90
|
+
/* Pressed is not told by colour alone (WCAG 1.4.1): it also draws a 2px ring
|
|
91
|
+
in its text colour, where unpressed has a 1px outline. */
|
|
92
|
+
.icon-toggle.pressed {
|
|
93
|
+
border-color: transparent;
|
|
94
|
+
background: var(--smrt-color-primary-container, var(--smrt-color-secondary-container));
|
|
95
|
+
color: var(--smrt-color-on-primary-container, var(--smrt-color-on-secondary-container));
|
|
96
|
+
box-shadow: inset 0 0 0 2px currentColor;
|
|
97
|
+
}
|
|
98
|
+
/* A toned toggle is neutral until pressed, then wears its tone. */
|
|
99
|
+
.icon-toggle.toned {
|
|
100
|
+
color: var(--smrt-color-on-surface-variant, var(--smrt-color-on-surface));
|
|
101
|
+
}
|
|
102
|
+
.icon-toggle.toned.pressed {
|
|
103
|
+
background: color-mix(in srgb, var(--icon-toggle-tone) 16%, var(--smrt-color-surface));
|
|
104
|
+
color: var(--icon-toggle-tone);
|
|
105
|
+
box-shadow: inset 0 0 0 2px var(--icon-toggle-tone);
|
|
106
|
+
}
|
|
107
|
+
.icon-toggle:focus-visible {
|
|
108
|
+
outline: 2px solid var(--smrt-color-primary);
|
|
109
|
+
outline-offset: 2px;
|
|
110
|
+
}
|
|
111
|
+
.icon-toggle:disabled {
|
|
112
|
+
opacity: 0.5;
|
|
113
|
+
cursor: not-allowed;
|
|
114
|
+
}
|
|
115
|
+
.icon {
|
|
116
|
+
display: inline-flex;
|
|
117
|
+
}
|
|
118
|
+
.count {
|
|
119
|
+
position: absolute;
|
|
120
|
+
z-index: 2;
|
|
121
|
+
top: -0.3rem;
|
|
122
|
+
right: -0.3rem;
|
|
123
|
+
box-sizing: border-box;
|
|
124
|
+
min-width: 1.15rem;
|
|
125
|
+
padding: 0 0.3rem;
|
|
126
|
+
border-radius: var(--smrt-radius-full, 999px);
|
|
127
|
+
/* Outlined, not filled: a surface-coloured face (so the button's edge does
|
|
128
|
+
not show through where the badge overlaps it), a 1px theme outline and
|
|
129
|
+
neutral text, readable on light and dark themes alike. */
|
|
130
|
+
border: 1px solid var(--smrt-color-outline);
|
|
131
|
+
background: var(--smrt-color-surface);
|
|
132
|
+
color: var(--smrt-color-on-surface-variant);
|
|
133
|
+
font-size: 0.68rem;
|
|
134
|
+
font-weight: 700;
|
|
135
|
+
line-height: 1.15rem;
|
|
136
|
+
text-align: center;
|
|
137
|
+
font-variant-numeric: tabular-nums;
|
|
138
|
+
}
|
|
139
|
+
</style>
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* IconToggle — an icon-only toggle button for dense filter and view-switcher
|
|
3
|
+
* rows. The icon is the `children` snippet (any icon set); `label` is both the
|
|
4
|
+
* accessible name and the tooltip text. `pressed` is reflected with
|
|
5
|
+
* `aria-pressed`, so a row of them is a one-of-many (or a many) toggle group;
|
|
6
|
+
* the owner decides what pressing means (this component keeps no state). An
|
|
7
|
+
* optional `count` shows as a small badge and is part of the accessible name
|
|
8
|
+
* and tooltip. The hit area is at least 44px square.
|
|
9
|
+
*/
|
|
10
|
+
import type { Snippet } from 'svelte';
|
|
11
|
+
import type { HTMLButtonAttributes } from 'svelte/elements';
|
|
12
|
+
import type { TooltipPlacement } from '../../types-generic';
|
|
13
|
+
export interface Props extends Omit<HTMLButtonAttributes, 'class' | 'children' | 'aria-label' | 'aria-pressed' | 'title'> {
|
|
14
|
+
/** Accessible name and tooltip text (what the button does or filters). */
|
|
15
|
+
label: string;
|
|
16
|
+
/** Whether the toggle is on (`aria-pressed`). */
|
|
17
|
+
pressed?: boolean;
|
|
18
|
+
/** Optional count badge; included in the accessible name and tooltip. */
|
|
19
|
+
count?: number;
|
|
20
|
+
/** A CSS colour (a token such as `var(--status-draft)` or a value) shown when the toggle is pressed. */
|
|
21
|
+
tone?: string;
|
|
22
|
+
/** The icon. Decorative: it is hidden from assistive technology. */
|
|
23
|
+
children: Snippet;
|
|
24
|
+
/** Where the tooltip opens. */
|
|
25
|
+
tooltipPlacement?: TooltipPlacement;
|
|
26
|
+
/** Additional CSS class names. */
|
|
27
|
+
class?: string;
|
|
28
|
+
}
|
|
29
|
+
declare const IconToggle: import("svelte").Component<Props, {}, "">;
|
|
30
|
+
type IconToggle = ReturnType<typeof IconToggle>;
|
|
31
|
+
export default IconToggle;
|
|
32
|
+
//# sourceMappingURL=IconToggle.svelte.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"IconToggle.svelte.d.ts","sourceRoot":"","sources":["../../../src/components/ui/IconToggle.svelte.ts"],"names":[],"mappings":"AAGA;;;;;;;;GAQG;AACH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AACtC,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAC;AAC5D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAI5D,MAAM,WAAW,KACf,SAAQ,IAAI,CACV,oBAAoB,EACpB,OAAO,GAAG,UAAU,GAAG,YAAY,GAAG,cAAc,GAAG,OAAO,CAC/D;IACD,0EAA0E;IAC1E,KAAK,EAAE,MAAM,CAAC;IACd,iDAAiD;IACjD,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,yEAAyE;IACzE,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,wGAAwG;IACxG,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,oEAAoE;IACpE,QAAQ,EAAE,OAAO,CAAC;IAClB,+BAA+B;IAC/B,gBAAgB,CAAC,EAAE,gBAAgB,CAAC;IACpC,kCAAkC;IAClC,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAoCD,QAAA,MAAM,UAAU,2CAAwC,CAAC;AACzD,KAAK,UAAU,GAAG,UAAU,CAAC,OAAO,UAAU,CAAC,CAAC;AAChD,eAAe,UAAU,CAAC"}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* IconToggleGroup — joins a row of `IconToggle`s into one labelled group: a
|
|
4
|
+
* single segmented strip with shared edges and rounded ends, exposed to
|
|
5
|
+
* assistive technology as `role="group"` named by `label` ("Filter by type").
|
|
6
|
+
* It adds no behaviour; each toggle keeps its own `pressed` state and
|
|
7
|
+
* `onclick`, so it works for one-of-many and many-of-many alike. Put one group
|
|
8
|
+
* per kind of filter and lay the groups out yourself.
|
|
9
|
+
*/
|
|
10
|
+
import type { Snippet } from 'svelte';
|
|
11
|
+
|
|
12
|
+
export interface Props {
|
|
13
|
+
/** Names the group for assistive technology (what these toggles filter). */
|
|
14
|
+
label: string;
|
|
15
|
+
/** The `IconToggle`s. */
|
|
16
|
+
children: Snippet;
|
|
17
|
+
/** Additional CSS class names. */
|
|
18
|
+
class?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const { label, children, class: className = '' }: Props = $props();
|
|
22
|
+
</script>
|
|
23
|
+
|
|
24
|
+
<div class="icon-toggle-group {className}" role="group" aria-label={label}>
|
|
25
|
+
{@render children()}
|
|
26
|
+
</div>
|
|
27
|
+
|
|
28
|
+
<style>
|
|
29
|
+
.icon-toggle-group {
|
|
30
|
+
display: inline-flex;
|
|
31
|
+
flex-wrap: wrap;
|
|
32
|
+
max-width: 100%;
|
|
33
|
+
isolation: isolate;
|
|
34
|
+
}
|
|
35
|
+
/* Shared edges: one border between neighbours, square inside, round ends.
|
|
36
|
+
Each toggle sits in tooltip wrappers, so the rules reach it by descendant. */
|
|
37
|
+
.icon-toggle-group :global(.icon-toggle) {
|
|
38
|
+
border-radius: 0;
|
|
39
|
+
}
|
|
40
|
+
.icon-toggle-group > :global(* + *) {
|
|
41
|
+
margin-inline-start: -1px;
|
|
42
|
+
}
|
|
43
|
+
.icon-toggle-group > :global(:first-child .icon-toggle) {
|
|
44
|
+
border-start-start-radius: var(--smrt-radius-full, 999px);
|
|
45
|
+
border-end-start-radius: var(--smrt-radius-full, 999px);
|
|
46
|
+
}
|
|
47
|
+
.icon-toggle-group > :global(:last-child .icon-toggle) {
|
|
48
|
+
border-start-end-radius: var(--smrt-radius-full, 999px);
|
|
49
|
+
border-end-end-radius: var(--smrt-radius-full, 999px);
|
|
50
|
+
}
|
|
51
|
+
/* The pressed one sits above its neighbours so its tint is not clipped by them. */
|
|
52
|
+
.icon-toggle-group :global(.icon-toggle.pressed),
|
|
53
|
+
.icon-toggle-group :global(.icon-toggle:focus-visible) {
|
|
54
|
+
z-index: 1;
|
|
55
|
+
}
|
|
56
|
+
</style>
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* IconToggleGroup — joins a row of `IconToggle`s into one labelled group: a
|
|
3
|
+
* single segmented strip with shared edges and rounded ends, exposed to
|
|
4
|
+
* assistive technology as `role="group"` named by `label` ("Filter by type").
|
|
5
|
+
* It adds no behaviour; each toggle keeps its own `pressed` state and
|
|
6
|
+
* `onclick`, so it works for one-of-many and many-of-many alike. Put one group
|
|
7
|
+
* per kind of filter and lay the groups out yourself.
|
|
8
|
+
*/
|
|
9
|
+
import type { Snippet } from 'svelte';
|
|
10
|
+
export interface Props {
|
|
11
|
+
/** Names the group for assistive technology (what these toggles filter). */
|
|
12
|
+
label: string;
|
|
13
|
+
/** The `IconToggle`s. */
|
|
14
|
+
children: Snippet;
|
|
15
|
+
/** Additional CSS class names. */
|
|
16
|
+
class?: string;
|
|
17
|
+
}
|
|
18
|
+
declare const IconToggleGroup: import("svelte").Component<Props, {}, "">;
|
|
19
|
+
type IconToggleGroup = ReturnType<typeof IconToggleGroup>;
|
|
20
|
+
export default IconToggleGroup;
|
|
21
|
+
//# sourceMappingURL=IconToggleGroup.svelte.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"IconToggleGroup.svelte.d.ts","sourceRoot":"","sources":["../../../src/components/ui/IconToggleGroup.svelte.ts"],"names":[],"mappings":"AAGA;;;;;;;GAOG;AACH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AAGtC,MAAM,WAAW,KAAK;IACpB,4EAA4E;IAC5E,KAAK,EAAE,MAAM,CAAC;IACd,yBAAyB;IACzB,QAAQ,EAAE,OAAO,CAAC;IAClB,kCAAkC;IAClC,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAiBD,QAAA,MAAM,eAAe,2CAAwC,CAAC;AAC9D,KAAK,eAAe,GAAG,UAAU,CAAC,OAAO,eAAe,CAAC,CAAC;AAC1D,eAAe,eAAe,CAAC"}
|
|
@@ -6,6 +6,14 @@ export interface Props {
|
|
|
6
6
|
label: string;
|
|
7
7
|
/** Content rendered inside the trigger button in place of the label text. */
|
|
8
8
|
trigger?: Snippet;
|
|
9
|
+
/** Accessible name of the trigger button when it shows only an icon (`aria-label`); omit to use its visible content (an icon trigger then falls back to `label`). */
|
|
10
|
+
triggerLabel?: string;
|
|
11
|
+
/**
|
|
12
|
+
* Trigger look: `button` (bordered, the default) or `icon`, a borderless
|
|
13
|
+
* 44px round icon button that takes the surrounding text colour (set the
|
|
14
|
+
* colour on the popover's `class`).
|
|
15
|
+
*/
|
|
16
|
+
variant?: 'button' | 'icon';
|
|
9
17
|
/** The popover content. */
|
|
10
18
|
children?: Snippet;
|
|
11
19
|
/** Whether the popover is visible. */
|
|
@@ -25,6 +33,8 @@ export interface Props {
|
|
|
25
33
|
let {
|
|
26
34
|
label,
|
|
27
35
|
trigger,
|
|
36
|
+
triggerLabel,
|
|
37
|
+
variant = 'button',
|
|
28
38
|
children,
|
|
29
39
|
open = $bindable(false),
|
|
30
40
|
disabled = false,
|
|
@@ -74,7 +84,9 @@ $effect(() => {
|
|
|
74
84
|
bind:this={triggerEl}
|
|
75
85
|
type="button"
|
|
76
86
|
class="popover__trigger"
|
|
87
|
+
class:popover__trigger--icon={variant === 'icon'}
|
|
77
88
|
aria-haspopup="dialog"
|
|
89
|
+
aria-label={triggerLabel ?? (variant === 'icon' ? label : undefined)}
|
|
78
90
|
aria-expanded={open}
|
|
79
91
|
aria-controls={open ? panelId : undefined}
|
|
80
92
|
{disabled}
|
|
@@ -103,7 +115,11 @@ $effect(() => {
|
|
|
103
115
|
.popover__trigger { appearance: none; padding: var(--smrt-spacing-2) var(--smrt-spacing-3); border: 1px solid var(--smrt-color-outline); border-radius: var(--smrt-radius-medium); background: var(--smrt-color-surface); color: var(--smrt-color-on-surface); font: inherit; cursor: pointer; }
|
|
104
116
|
.popover__trigger:focus-visible, .popover__panel:focus-visible { outline: 2px solid var(--smrt-color-primary); outline-offset: 2px; }
|
|
105
117
|
.popover__trigger:disabled { opacity: .5; cursor: not-allowed; }
|
|
106
|
-
|
|
118
|
+
/* Icon trigger: a 44px round glyph in the surrounding colour. Hover and open nudge the glyph; focus keeps the standard primary ring (a soft ring in the glyph's colour fell under 3:1 on ordinary surfaces). */
|
|
119
|
+
.popover__trigger--icon { display: inline-flex; align-items: center; justify-content: center; box-sizing: border-box; inline-size: 2.75rem; block-size: 2.75rem; padding: 0; border: none; border-radius: var(--smrt-radius-full, 999px); background: transparent; color: inherit; }
|
|
120
|
+
.popover__trigger--icon:hover:not(:disabled), .popover__trigger--icon[aria-expanded='true'] { background: transparent; color: color-mix(in srgb, currentColor 80%, var(--smrt-color-on-surface)); }
|
|
121
|
+
.popover__trigger--icon:focus-visible { background: transparent; }
|
|
122
|
+
.popover__panel { position: absolute; z-index: var(--smrt-z-index-popover, 1000); box-sizing: border-box; inline-size: var(--smrt-popover-panel-width, auto); min-width: min(16rem, calc(100vw - 2rem)); max-width: calc(100vw - 2rem); padding: var(--smrt-popover-panel-padding, var(--smrt-spacing-4)); border: 1px solid var(--smrt-color-outline-variant); border-radius: var(--smrt-radius-large); background: var(--smrt-color-surface-container); color: var(--smrt-color-on-surface); box-shadow: var(--smrt-elevation-3); }
|
|
107
123
|
.popover__panel--bottom-start { top: calc(100% + var(--smrt-spacing-1)); left: 0; }
|
|
108
124
|
.popover__panel--bottom-end { top: calc(100% + var(--smrt-spacing-1)); right: 0; }
|
|
109
125
|
.popover__panel--top-start { bottom: calc(100% + var(--smrt-spacing-1)); left: 0; }
|
|
@@ -4,6 +4,14 @@ export interface Props {
|
|
|
4
4
|
label: string;
|
|
5
5
|
/** Content rendered inside the trigger button in place of the label text. */
|
|
6
6
|
trigger?: Snippet;
|
|
7
|
+
/** Accessible name of the trigger button when it shows only an icon (`aria-label`); omit to use its visible content (an icon trigger then falls back to `label`). */
|
|
8
|
+
triggerLabel?: string;
|
|
9
|
+
/**
|
|
10
|
+
* Trigger look: `button` (bordered, the default) or `icon`, a borderless
|
|
11
|
+
* 44px round icon button that takes the surrounding text colour (set the
|
|
12
|
+
* colour on the popover's `class`).
|
|
13
|
+
*/
|
|
14
|
+
variant?: 'button' | 'icon';
|
|
7
15
|
/** The popover content. */
|
|
8
16
|
children?: Snippet;
|
|
9
17
|
/** Whether the popover is visible. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Popover.svelte.d.ts","sourceRoot":"","sources":["../../../src/components/ui/Popover.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,KAAK,OAAO,EAAQ,MAAM,QAAQ,CAAC;AAG5C,MAAM,WAAW,KAAK;IACpB,oEAAoE;IACpE,KAAK,EAAE,MAAM,CAAC;IACd,6EAA6E;IAC7E,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,2BAA2B;IAC3B,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,sCAAsC;IACtC,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,kCAAkC;IAClC,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,uDAAuD;IACvD,SAAS,CAAC,EAAE,cAAc,GAAG,YAAY,GAAG,WAAW,GAAG,SAAS,CAAC;IACpE,sDAAsD;IACtD,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAC9B,yCAAyC;IACzC,YAAY,CAAC,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAC;IACvC,kCAAkC;IAClC,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;
|
|
1
|
+
{"version":3,"file":"Popover.svelte.d.ts","sourceRoot":"","sources":["../../../src/components/ui/Popover.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,KAAK,OAAO,EAAQ,MAAM,QAAQ,CAAC;AAG5C,MAAM,WAAW,KAAK;IACpB,oEAAoE;IACpE,KAAK,EAAE,MAAM,CAAC;IACd,6EAA6E;IAC7E,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,qKAAqK;IACrK,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;;OAIG;IACH,OAAO,CAAC,EAAE,QAAQ,GAAG,MAAM,CAAC;IAC5B,2BAA2B;IAC3B,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,sCAAsC;IACtC,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,kCAAkC;IAClC,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,uDAAuD;IACvD,SAAS,CAAC,EAAE,cAAc,GAAG,YAAY,GAAG,WAAW,GAAG,SAAS,CAAC;IACpE,sDAAsD;IACtD,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAC9B,yCAAyC;IACzC,YAAY,CAAC,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAC;IACvC,kCAAkC;IAClC,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAqED,QAAA,MAAM,OAAO,+CAAwC,CAAC;AACtD,KAAK,OAAO,GAAG,UAAU,CAAC,OAAO,OAAO,CAAC,CAAC;AAC1C,eAAe,OAAO,CAAC"}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { render, screen } from '@testing-library/svelte';
|
|
2
|
+
import userEvent from '@testing-library/user-event';
|
|
3
|
+
import { createRawSnippet } from 'svelte';
|
|
4
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
5
|
+
import { expectNoA11yViolations } from '../../../test-support/a11y';
|
|
6
|
+
import IconToggle from '../IconToggle.svelte';
|
|
7
|
+
const icon = createRawSnippet(() => ({
|
|
8
|
+
render: () => '<svg viewBox="0 0 24 24" width="18" height="18"></svg>',
|
|
9
|
+
}));
|
|
10
|
+
describe('IconToggle', () => {
|
|
11
|
+
it('is a labelled toggle button reflecting pressed', () => {
|
|
12
|
+
render(IconToggle, {
|
|
13
|
+
props: { label: 'Grid', pressed: true, children: icon },
|
|
14
|
+
});
|
|
15
|
+
const button = screen.getByRole('button', { name: 'Grid' });
|
|
16
|
+
expect(button).toHaveAttribute('aria-pressed', 'true');
|
|
17
|
+
expect(button).toHaveAttribute('type', 'button');
|
|
18
|
+
});
|
|
19
|
+
it('shows a count badge that is part of the name and the tooltip', () => {
|
|
20
|
+
render(IconToggle, {
|
|
21
|
+
props: { label: 'Draft', count: 12, children: icon },
|
|
22
|
+
});
|
|
23
|
+
const button = screen.getByRole('button', { name: 'Draft (12)' });
|
|
24
|
+
expect(button).toHaveAttribute('aria-pressed', 'false');
|
|
25
|
+
expect(screen.getByRole('tooltip')).toHaveTextContent('Draft (12)');
|
|
26
|
+
expect(button.querySelector('.count')).toHaveTextContent('12');
|
|
27
|
+
});
|
|
28
|
+
it('keeps no state of its own: it calls onclick and the owner decides', async () => {
|
|
29
|
+
const onclick = vi.fn();
|
|
30
|
+
render(IconToggle, {
|
|
31
|
+
props: { label: 'Cards', pressed: false, onclick, children: icon },
|
|
32
|
+
});
|
|
33
|
+
await userEvent.click(screen.getByRole('button', { name: 'Cards' }));
|
|
34
|
+
expect(onclick).toHaveBeenCalledTimes(1);
|
|
35
|
+
expect(screen.getByRole('button', { name: 'Cards' })).toHaveAttribute('aria-pressed', 'false');
|
|
36
|
+
});
|
|
37
|
+
it('is axe-clean', async () => {
|
|
38
|
+
const { container } = render(IconToggle, {
|
|
39
|
+
props: { label: 'Grid', count: 3, children: icon },
|
|
40
|
+
});
|
|
41
|
+
await expectNoA11yViolations(container);
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
describe('IconToggle tone', () => {
|
|
45
|
+
it('carries its tone as a custom property and a toned class', () => {
|
|
46
|
+
render(IconToggle, {
|
|
47
|
+
props: { label: 'Draft', tone: 'var(--status-draft)', children: icon },
|
|
48
|
+
});
|
|
49
|
+
const button = screen.getByRole('button', { name: 'Draft' });
|
|
50
|
+
expect(button).toHaveClass('toned');
|
|
51
|
+
expect(button.style.getPropertyValue('--icon-toggle-tone')).toBe('var(--status-draft)');
|
|
52
|
+
});
|
|
53
|
+
it('has no tone styling unless asked', () => {
|
|
54
|
+
render(IconToggle, { props: { label: 'Grid', children: icon } });
|
|
55
|
+
const button = screen.getByRole('button', { name: 'Grid' });
|
|
56
|
+
expect(button).not.toHaveClass('toned');
|
|
57
|
+
expect(button.style.getPropertyValue('--icon-toggle-tone')).toBe('');
|
|
58
|
+
});
|
|
59
|
+
});
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { render, screen } from '@testing-library/svelte';
|
|
2
|
+
import { createRawSnippet } from 'svelte';
|
|
3
|
+
import { describe, expect, it } from 'vitest';
|
|
4
|
+
import { expectNoA11yViolations } from '../../../test-support/a11y';
|
|
5
|
+
import IconToggleGroup from '../IconToggleGroup.svelte';
|
|
6
|
+
const buttons = createRawSnippet(() => ({
|
|
7
|
+
render: () => '<span><button type="button" aria-pressed="true" aria-label="Draft (2)">D</button><button type="button" aria-pressed="false" aria-label="Review (0)">R</button></span>',
|
|
8
|
+
}));
|
|
9
|
+
describe('IconToggleGroup', () => {
|
|
10
|
+
it('is a named group around its toggles', () => {
|
|
11
|
+
render(IconToggleGroup, {
|
|
12
|
+
props: { label: 'Filter by status', children: buttons },
|
|
13
|
+
});
|
|
14
|
+
const group = screen.getByRole('group', { name: 'Filter by status' });
|
|
15
|
+
expect(group.querySelectorAll('button')).toHaveLength(2);
|
|
16
|
+
});
|
|
17
|
+
it('adds no state: the toggles keep their own pressed value', () => {
|
|
18
|
+
render(IconToggleGroup, {
|
|
19
|
+
props: { label: 'Filter by status', children: buttons },
|
|
20
|
+
});
|
|
21
|
+
expect(screen.getByRole('button', { name: 'Draft (2)' })).toHaveAttribute('aria-pressed', 'true');
|
|
22
|
+
expect(screen.getByRole('button', { name: 'Review (0)' })).toHaveAttribute('aria-pressed', 'false');
|
|
23
|
+
});
|
|
24
|
+
it('is axe-clean', async () => {
|
|
25
|
+
const { container } = render(IconToggleGroup, {
|
|
26
|
+
props: { label: 'Filter by status', children: buttons },
|
|
27
|
+
});
|
|
28
|
+
await expectNoA11yViolations(container);
|
|
29
|
+
});
|
|
30
|
+
});
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { render, screen } from '@testing-library/svelte';
|
|
2
|
+
import userEvent from '@testing-library/user-event';
|
|
3
|
+
import { createRawSnippet } from 'svelte';
|
|
4
|
+
import { describe, expect, it } from 'vitest';
|
|
5
|
+
import { expectNoA11yViolations } from '../../../test-support/a11y';
|
|
6
|
+
import Popover from '../Popover.svelte';
|
|
7
|
+
const icon = createRawSnippet(() => ({
|
|
8
|
+
render: () => '<svg viewBox="0 0 24 24" width="18" height="18"></svg>',
|
|
9
|
+
}));
|
|
10
|
+
const body = createRawSnippet(() => ({
|
|
11
|
+
render: () => '<p>No main picture</p>',
|
|
12
|
+
}));
|
|
13
|
+
describe('Popover', () => {
|
|
14
|
+
it('names an icon-only trigger with triggerLabel and opens its panel', async () => {
|
|
15
|
+
render(Popover, {
|
|
16
|
+
props: {
|
|
17
|
+
label: 'Issues',
|
|
18
|
+
triggerLabel: '3 issues',
|
|
19
|
+
trigger: icon,
|
|
20
|
+
children: body,
|
|
21
|
+
},
|
|
22
|
+
});
|
|
23
|
+
const trigger = screen.getByRole('button', { name: '3 issues' });
|
|
24
|
+
expect(trigger).toHaveAttribute('aria-expanded', 'false');
|
|
25
|
+
await userEvent.click(trigger);
|
|
26
|
+
expect(trigger).toHaveAttribute('aria-expanded', 'true');
|
|
27
|
+
expect(screen.getByRole('dialog', { name: 'Issues' })).toHaveTextContent('No main picture');
|
|
28
|
+
});
|
|
29
|
+
it('is named by its visible content when there is no triggerLabel', () => {
|
|
30
|
+
render(Popover, { props: { label: 'Filters', children: body } });
|
|
31
|
+
expect(screen.getByRole('button', { name: 'Filters' })).not.toHaveAttribute('aria-label');
|
|
32
|
+
});
|
|
33
|
+
it('is axe-clean with an icon-only trigger', async () => {
|
|
34
|
+
const { container } = render(Popover, {
|
|
35
|
+
props: {
|
|
36
|
+
label: 'Issues',
|
|
37
|
+
triggerLabel: '3 issues',
|
|
38
|
+
trigger: icon,
|
|
39
|
+
children: body,
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
await expectNoA11yViolations(container);
|
|
43
|
+
});
|
|
44
|
+
});
|