@happyvertical/smrt-ui 0.51.39 → 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
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
SortableHeader — a sortable column header for a hand-rolled table.
|
|
3
|
+
|
|
4
|
+
Renders the `<th>` itself so `aria-sort` sits on the column header. The
|
|
5
|
+
control inside is a link when `href` is given (server-sorted lists keep the
|
|
6
|
+
sort in the URL; build it with `listSortHref`) or a button calling `onSort`
|
|
7
|
+
(client-sorted lists; compute the next sort with `toggleListSort`) — or
|
|
8
|
+
pass a list's state (`createUrlListSort` / `createLocalListSort`) as `list`
|
|
9
|
+
and it supplies all three. Both are
|
|
10
|
+
reachable by Tab and activated with Enter (a button also with Space). The
|
|
11
|
+
indicator shows ↑/↓ on the sorted column and a faint ↕ on the others.
|
|
12
|
+
|
|
13
|
+
Use `as="div"` with `role="columnheader"` semantics for grid layouts that
|
|
14
|
+
are not a `<table>`.
|
|
15
|
+
|
|
16
|
+
Style hooks (set on the header or an ancestor): `--sortable-header-padding`
|
|
17
|
+
(the control's padding) and `--sortable-header-font-weight` (default 600;
|
|
18
|
+
`inherit` takes the surrounding head row's weight).
|
|
19
|
+
-->
|
|
20
|
+
<script lang="ts">
|
|
21
|
+
import type { Snippet } from 'svelte';
|
|
22
|
+
import {
|
|
23
|
+
type ListSort,
|
|
24
|
+
type ListSortSpec,
|
|
25
|
+
listSortActionLabel,
|
|
26
|
+
listSortAria,
|
|
27
|
+
} from './list-sort.js';
|
|
28
|
+
import type { ListSortState } from './list-sort-state.svelte.js';
|
|
29
|
+
|
|
30
|
+
interface Props {
|
|
31
|
+
/** Id of the column this header sorts by. */
|
|
32
|
+
columnId: string;
|
|
33
|
+
/** Column name shown in the header and used in the sort announcement. */
|
|
34
|
+
label: string;
|
|
35
|
+
/** The list's sort state; supplies `sort`, `spec`, `href`, and `onSort`. */
|
|
36
|
+
list?: ListSortState;
|
|
37
|
+
/** Current sort of the list. */
|
|
38
|
+
sort?: ListSort | null;
|
|
39
|
+
/** Link to the list sorted by this column (server-sorted lists). */
|
|
40
|
+
href?: string;
|
|
41
|
+
/** Called on activation (client-sorted lists). */
|
|
42
|
+
onSort?: (columnId: string) => void;
|
|
43
|
+
/** The list's sort spec; gives the column's first direction for the label. */
|
|
44
|
+
spec?: ListSortSpec;
|
|
45
|
+
/** Render a table `th` (default) or a `div` column header for grid layouts. */
|
|
46
|
+
as?: 'th' | 'div';
|
|
47
|
+
/** Horizontal alignment of the header content. */
|
|
48
|
+
align?: 'start' | 'center' | 'end';
|
|
49
|
+
/** Extra class names. */
|
|
50
|
+
class?: string;
|
|
51
|
+
children?: Snippet;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
let {
|
|
55
|
+
columnId,
|
|
56
|
+
label,
|
|
57
|
+
list,
|
|
58
|
+
sort: sortProp,
|
|
59
|
+
href: hrefProp,
|
|
60
|
+
onSort: onSortProp,
|
|
61
|
+
spec: specProp,
|
|
62
|
+
as = 'th',
|
|
63
|
+
align = 'start',
|
|
64
|
+
class: className = '',
|
|
65
|
+
children,
|
|
66
|
+
}: Props = $props();
|
|
67
|
+
|
|
68
|
+
const sort = $derived(list ? list.sort : sortProp);
|
|
69
|
+
const spec = $derived(list ? list.spec : specProp);
|
|
70
|
+
const href = $derived(list ? list.href(columnId) : hrefProp);
|
|
71
|
+
const onSort = $derived(
|
|
72
|
+
list ? (id: string) => void list.toggle(id) : onSortProp,
|
|
73
|
+
);
|
|
74
|
+
const aria = $derived(listSortAria(sort, columnId));
|
|
75
|
+
const active = $derived(aria !== 'none');
|
|
76
|
+
const actionLabel = $derived(listSortActionLabel(sort, columnId, label, spec));
|
|
77
|
+
</script>
|
|
78
|
+
|
|
79
|
+
{#snippet content()}
|
|
80
|
+
<span class="sortable-header__label">
|
|
81
|
+
{#if children}{@render children()}{:else}{label}{/if}
|
|
82
|
+
</span>
|
|
83
|
+
<span class="sortable-header__icon" class:is-active={active} aria-hidden="true">
|
|
84
|
+
{#if aria === 'ascending'}↑{:else if aria === 'descending'}↓{:else}↕{/if}
|
|
85
|
+
</span>
|
|
86
|
+
{/snippet}
|
|
87
|
+
|
|
88
|
+
<svelte:element
|
|
89
|
+
this={as}
|
|
90
|
+
class={`sortable-header sortable-header--${align} ${className}`}
|
|
91
|
+
class:is-sorted={active}
|
|
92
|
+
scope={as === 'th' ? 'col' : undefined}
|
|
93
|
+
role={as === 'div' ? 'columnheader' : undefined}
|
|
94
|
+
aria-sort={aria}
|
|
95
|
+
data-sort-column={columnId}
|
|
96
|
+
>
|
|
97
|
+
{#if href}
|
|
98
|
+
<a
|
|
99
|
+
class="sortable-header__control"
|
|
100
|
+
{href}
|
|
101
|
+
aria-label={actionLabel}
|
|
102
|
+
data-sveltekit-noscroll
|
|
103
|
+
data-sveltekit-keepfocus
|
|
104
|
+
data-sveltekit-replacestate
|
|
105
|
+
onclick={() => onSort?.(columnId)}
|
|
106
|
+
>
|
|
107
|
+
{@render content()}
|
|
108
|
+
</a>
|
|
109
|
+
{:else}
|
|
110
|
+
<button
|
|
111
|
+
type="button"
|
|
112
|
+
class="sortable-header__control"
|
|
113
|
+
aria-label={actionLabel}
|
|
114
|
+
onclick={() => onSort?.(columnId)}
|
|
115
|
+
>
|
|
116
|
+
{@render content()}
|
|
117
|
+
</button>
|
|
118
|
+
{/if}
|
|
119
|
+
</svelte:element>
|
|
120
|
+
|
|
121
|
+
<style>
|
|
122
|
+
.sortable-header {
|
|
123
|
+
padding: 0;
|
|
124
|
+
font-weight: inherit;
|
|
125
|
+
text-align: start;
|
|
126
|
+
}
|
|
127
|
+
.sortable-header--center {
|
|
128
|
+
text-align: center;
|
|
129
|
+
}
|
|
130
|
+
.sortable-header--end {
|
|
131
|
+
text-align: end;
|
|
132
|
+
}
|
|
133
|
+
.sortable-header__control {
|
|
134
|
+
display: inline-flex;
|
|
135
|
+
align-items: center;
|
|
136
|
+
gap: 0.25rem;
|
|
137
|
+
min-height: 2.75rem;
|
|
138
|
+
width: 100%;
|
|
139
|
+
padding: var(--sortable-header-padding, 0.5rem 0.75rem);
|
|
140
|
+
border: 0;
|
|
141
|
+
background: transparent;
|
|
142
|
+
color: inherit;
|
|
143
|
+
font: inherit;
|
|
144
|
+
font-weight: var(--sortable-header-font-weight, 600);
|
|
145
|
+
text-align: inherit;
|
|
146
|
+
text-decoration: none;
|
|
147
|
+
cursor: pointer;
|
|
148
|
+
}
|
|
149
|
+
.sortable-header--center .sortable-header__control {
|
|
150
|
+
justify-content: center;
|
|
151
|
+
}
|
|
152
|
+
.sortable-header--end .sortable-header__control {
|
|
153
|
+
justify-content: flex-end;
|
|
154
|
+
}
|
|
155
|
+
.sortable-header__control:hover .sortable-header__label {
|
|
156
|
+
text-decoration: underline;
|
|
157
|
+
}
|
|
158
|
+
.sortable-header__control:focus-visible {
|
|
159
|
+
outline: 2px solid var(--color-focus, var(--color-primary, currentColor));
|
|
160
|
+
outline-offset: -2px;
|
|
161
|
+
}
|
|
162
|
+
.sortable-header__icon {
|
|
163
|
+
opacity: 0.35;
|
|
164
|
+
font-size: 0.85em;
|
|
165
|
+
}
|
|
166
|
+
.sortable-header__icon.is-active {
|
|
167
|
+
opacity: 1;
|
|
168
|
+
}
|
|
169
|
+
</style>
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
import { type ListSort, type ListSortSpec } from './list-sort.js';
|
|
3
|
+
import type { ListSortState } from './list-sort-state.svelte.js';
|
|
4
|
+
interface Props {
|
|
5
|
+
/** Id of the column this header sorts by. */
|
|
6
|
+
columnId: string;
|
|
7
|
+
/** Column name shown in the header and used in the sort announcement. */
|
|
8
|
+
label: string;
|
|
9
|
+
/** The list's sort state; supplies `sort`, `spec`, `href`, and `onSort`. */
|
|
10
|
+
list?: ListSortState;
|
|
11
|
+
/** Current sort of the list. */
|
|
12
|
+
sort?: ListSort | null;
|
|
13
|
+
/** Link to the list sorted by this column (server-sorted lists). */
|
|
14
|
+
href?: string;
|
|
15
|
+
/** Called on activation (client-sorted lists). */
|
|
16
|
+
onSort?: (columnId: string) => void;
|
|
17
|
+
/** The list's sort spec; gives the column's first direction for the label. */
|
|
18
|
+
spec?: ListSortSpec;
|
|
19
|
+
/** Render a table `th` (default) or a `div` column header for grid layouts. */
|
|
20
|
+
as?: 'th' | 'div';
|
|
21
|
+
/** Horizontal alignment of the header content. */
|
|
22
|
+
align?: 'start' | 'center' | 'end';
|
|
23
|
+
/** Extra class names. */
|
|
24
|
+
class?: string;
|
|
25
|
+
children?: Snippet;
|
|
26
|
+
}
|
|
27
|
+
declare const SortableHeader: import("svelte").Component<Props, {}, "">;
|
|
28
|
+
type SortableHeader = ReturnType<typeof SortableHeader>;
|
|
29
|
+
export default SortableHeader;
|
|
30
|
+
//# sourceMappingURL=SortableHeader.svelte.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SortableHeader.svelte.d.ts","sourceRoot":"","sources":["../../../src/components/data/SortableHeader.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AACtC,OAAO,EACL,KAAK,QAAQ,EACb,KAAK,YAAY,EAGlB,MAAM,gBAAgB,CAAC;AACxB,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAC;AAGjE,UAAU,KAAK;IACb,6CAA6C;IAC7C,QAAQ,EAAE,MAAM,CAAC;IACjB,yEAAyE;IACzE,KAAK,EAAE,MAAM,CAAC;IACd,4EAA4E;IAC5E,IAAI,CAAC,EAAE,aAAa,CAAC;IACrB,gCAAgC;IAChC,IAAI,CAAC,EAAE,QAAQ,GAAG,IAAI,CAAC;IACvB,oEAAoE;IACpE,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,kDAAkD;IAClD,MAAM,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;IACpC,8EAA8E;IAC9E,IAAI,CAAC,EAAE,YAAY,CAAC;IACpB,+EAA+E;IAC/E,EAAE,CAAC,EAAE,IAAI,GAAG,KAAK,CAAC;IAClB,kDAAkD;IAClD,KAAK,CAAC,EAAE,OAAO,GAAG,QAAQ,GAAG,KAAK,CAAC;IACnC,yBAAyB;IACzB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AA0DD,QAAA,MAAM,cAAc,2CAAwC,CAAC;AAC7D,KAAK,cAAc,GAAG,UAAU,CAAC,OAAO,cAAc,CAAC,CAAC;AACxD,eAAe,cAAc,CAAC"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { render, screen } from '@testing-library/svelte';
|
|
2
|
+
import userEvent from '@testing-library/user-event';
|
|
3
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
4
|
+
import CollectionList from '../CollectionList.svelte';
|
|
5
|
+
const items = [
|
|
6
|
+
{ id: 'a', title: 'First' },
|
|
7
|
+
{ id: 'b', title: 'Second' },
|
|
8
|
+
];
|
|
9
|
+
describe('CollectionList', () => {
|
|
10
|
+
it.each([
|
|
11
|
+
'list',
|
|
12
|
+
'divided',
|
|
13
|
+
'grid',
|
|
14
|
+
'gallery',
|
|
15
|
+
])('renders the %s layout', (layout) => {
|
|
16
|
+
const { container } = render(CollectionList, {
|
|
17
|
+
props: { items, itemKey: 'id', title: 'title', layout },
|
|
18
|
+
});
|
|
19
|
+
expect(container.querySelector(`ul.collection--${layout}`)).not.toBeNull();
|
|
20
|
+
expect(screen.getByText('First')).toBeInTheDocument();
|
|
21
|
+
});
|
|
22
|
+
it('selects an item with its labelled checkbox', async () => {
|
|
23
|
+
const onselectionchange = vi.fn();
|
|
24
|
+
render(CollectionList, {
|
|
25
|
+
props: {
|
|
26
|
+
items,
|
|
27
|
+
itemKey: 'id',
|
|
28
|
+
title: 'title',
|
|
29
|
+
layout: 'gallery',
|
|
30
|
+
selectable: true,
|
|
31
|
+
onselectionchange,
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
await userEvent.click(screen.getByRole('checkbox', { name: 'Select Second' }));
|
|
35
|
+
expect(onselectionchange).toHaveBeenCalledWith(new Set(['b']));
|
|
36
|
+
});
|
|
37
|
+
});
|
|
@@ -53,6 +53,35 @@ describe('DataTable', () => {
|
|
|
53
53
|
await userEvent.click(screen.getByRole('button', { name: 'Clear sorting for Name' }));
|
|
54
54
|
expect(nameHeader).not.toHaveAttribute('aria-sort');
|
|
55
55
|
});
|
|
56
|
+
it('starts a date/count column descending and toggles without clearing when not clearable', async () => {
|
|
57
|
+
render(DataTable, {
|
|
58
|
+
props: {
|
|
59
|
+
data,
|
|
60
|
+
columns: [
|
|
61
|
+
columns[0],
|
|
62
|
+
{
|
|
63
|
+
id: 'age',
|
|
64
|
+
label: 'Age',
|
|
65
|
+
accessor: 'age',
|
|
66
|
+
sortable: true,
|
|
67
|
+
sortFirstDirection: 'desc',
|
|
68
|
+
},
|
|
69
|
+
],
|
|
70
|
+
sortable: true,
|
|
71
|
+
sortClearable: false,
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
const ageHeader = screen.getByRole('columnheader', { name: 'Age' });
|
|
75
|
+
await userEvent.click(screen.getByRole('button', { name: 'Sort Age descending' }));
|
|
76
|
+
expect(ageHeader).toHaveAttribute('aria-sort', 'descending');
|
|
77
|
+
const firstCell = () => screen.getAllByRole('row')[1]?.textContent ?? '';
|
|
78
|
+
expect(firstCell()).toContain('Linus');
|
|
79
|
+
await userEvent.click(screen.getByRole('button', { name: 'Sort Age ascending' }));
|
|
80
|
+
expect(ageHeader).toHaveAttribute('aria-sort', 'ascending');
|
|
81
|
+
expect(firstCell()).toContain('Ada');
|
|
82
|
+
await userEvent.click(screen.getByRole('button', { name: 'Sort Age descending' }));
|
|
83
|
+
expect(ageHeader).toHaveAttribute('aria-sort', 'descending');
|
|
84
|
+
});
|
|
56
85
|
it('announces the next action for each rule in a multi-column sort', async () => {
|
|
57
86
|
const multiSortColumns = [columns[0], { ...columns[1], sortable: true }];
|
|
58
87
|
render(DataTable, {
|
|
@@ -729,3 +758,50 @@ describe('DataTable', () => {
|
|
|
729
758
|
await expectNoA11yViolations(container);
|
|
730
759
|
});
|
|
731
760
|
});
|
|
761
|
+
describe('DataTable phoneLayout', () => {
|
|
762
|
+
it('keeps the scrolling table unless asked for cards', () => {
|
|
763
|
+
const { container } = render(DataTable, {
|
|
764
|
+
props: { data, columns, caption: 'People' },
|
|
765
|
+
});
|
|
766
|
+
expect(container.querySelector('.data-table-container--phone-cards')).toBeNull();
|
|
767
|
+
});
|
|
768
|
+
it('marks the container for the phone card layout and keeps the table semantics', () => {
|
|
769
|
+
const { container } = render(DataTable, {
|
|
770
|
+
props: { data, columns, caption: 'People', phoneLayout: 'cards' },
|
|
771
|
+
});
|
|
772
|
+
expect(container.querySelector('.data-table-container.data-table-container--phone-cards')).not.toBeNull();
|
|
773
|
+
expect(screen.getByRole('table', { name: 'People' })).toBeInTheDocument();
|
|
774
|
+
expect(screen.getAllByRole('row')).toHaveLength(3);
|
|
775
|
+
});
|
|
776
|
+
it('gives cards explicit table roles and a visible column label per cell', async () => {
|
|
777
|
+
const { container } = render(DataTable, {
|
|
778
|
+
props: { data, columns, caption: 'People', phoneLayout: 'cards' },
|
|
779
|
+
});
|
|
780
|
+
// Explicit roles survive the block/grid restyle (WebKit drops implicit
|
|
781
|
+
// table roles once display changes).
|
|
782
|
+
expect(container.querySelector('table')).toHaveAttribute('role', 'table');
|
|
783
|
+
for (const row of container.querySelectorAll('tr')) {
|
|
784
|
+
expect(row).toHaveAttribute('role', 'row');
|
|
785
|
+
}
|
|
786
|
+
for (const header of container.querySelectorAll('thead th')) {
|
|
787
|
+
expect(header).toHaveAttribute('role', 'columnheader');
|
|
788
|
+
}
|
|
789
|
+
const cells = container.querySelectorAll('tbody td[data-column-id]');
|
|
790
|
+
expect(cells.length).toBeGreaterThan(0);
|
|
791
|
+
for (const cell of cells) {
|
|
792
|
+
expect(cell).toHaveAttribute('role', 'cell');
|
|
793
|
+
const column = columns.find((candidate) => candidate.id === cell.getAttribute('data-column-id'));
|
|
794
|
+
expect(cell).toHaveAttribute('data-label', column?.label);
|
|
795
|
+
}
|
|
796
|
+
// The header stays in the accessibility tree (visually hidden, not removed).
|
|
797
|
+
expect(screen.getAllByRole('columnheader').length).toBe(columns.length);
|
|
798
|
+
await expectNoA11yViolations(container);
|
|
799
|
+
});
|
|
800
|
+
it('adds no explicit roles or labels outside cards mode', () => {
|
|
801
|
+
const { container } = render(DataTable, {
|
|
802
|
+
props: { data, columns, caption: 'People' },
|
|
803
|
+
});
|
|
804
|
+
expect(container.querySelector('table')).not.toHaveAttribute('role');
|
|
805
|
+
expect(container.querySelector('[data-label]')).toBeNull();
|
|
806
|
+
});
|
|
807
|
+
});
|
|
@@ -118,6 +118,41 @@ describe('DataTableController', () => {
|
|
|
118
118
|
{ columnId: 'age', direction: 'asc' },
|
|
119
119
|
]);
|
|
120
120
|
});
|
|
121
|
+
it('starts a column at its first direction and toggles without clearing when not clearable', () => {
|
|
122
|
+
const controller = createDataTableController({ initialState: state });
|
|
123
|
+
const toggle = () => controller.dispatch({
|
|
124
|
+
type: 'toggleSorting',
|
|
125
|
+
columnId: 'age',
|
|
126
|
+
firstDirection: 'desc',
|
|
127
|
+
clearable: false,
|
|
128
|
+
});
|
|
129
|
+
toggle();
|
|
130
|
+
expect(controller.snapshot().state.sorting).toEqual([
|
|
131
|
+
{ columnId: 'age', direction: 'desc' },
|
|
132
|
+
]);
|
|
133
|
+
toggle();
|
|
134
|
+
expect(controller.snapshot().state.sorting).toEqual([
|
|
135
|
+
{ columnId: 'age', direction: 'asc' },
|
|
136
|
+
]);
|
|
137
|
+
toggle();
|
|
138
|
+
expect(controller.snapshot().state.sorting).toEqual([
|
|
139
|
+
{ columnId: 'age', direction: 'desc' },
|
|
140
|
+
]);
|
|
141
|
+
// Clearable (default) with a desc-first column: desc -> asc -> cleared.
|
|
142
|
+
const cycling = createDataTableController({ initialState: state });
|
|
143
|
+
const cycle = () => cycling.dispatch({
|
|
144
|
+
type: 'toggleSorting',
|
|
145
|
+
columnId: 'age',
|
|
146
|
+
firstDirection: 'desc',
|
|
147
|
+
});
|
|
148
|
+
cycle();
|
|
149
|
+
cycle();
|
|
150
|
+
expect(cycling.snapshot().state.sorting).toEqual([
|
|
151
|
+
{ columnId: 'age', direction: 'asc' },
|
|
152
|
+
]);
|
|
153
|
+
cycle();
|
|
154
|
+
expect(cycling.snapshot().state.sorting).toEqual([]);
|
|
155
|
+
});
|
|
121
156
|
it('proposes controlled transitions without mutating until the host reconciles state', () => {
|
|
122
157
|
const onStateChange = vi.fn();
|
|
123
158
|
const controller = createDataTableController({ state, onStateChange });
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { fireEvent, render, screen } from '@testing-library/svelte';
|
|
4
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
5
|
+
import ListSortSelect from '../ListSortSelect.svelte';
|
|
6
|
+
import SortableHeader from '../SortableHeader.svelte';
|
|
7
|
+
const spec = {
|
|
8
|
+
columns: [{ id: 'published', firstDirection: 'desc' }, 'title'],
|
|
9
|
+
default: { columnId: 'published', direction: 'desc' },
|
|
10
|
+
};
|
|
11
|
+
describe('SortableHeader', () => {
|
|
12
|
+
it('marks the sorted column with aria-sort and an arrow, and labels the next action', () => {
|
|
13
|
+
render(SortableHeader, {
|
|
14
|
+
props: {
|
|
15
|
+
columnId: 'published',
|
|
16
|
+
label: 'Published',
|
|
17
|
+
sort: spec.default,
|
|
18
|
+
spec,
|
|
19
|
+
as: 'div',
|
|
20
|
+
},
|
|
21
|
+
});
|
|
22
|
+
const header = screen.getByRole('columnheader');
|
|
23
|
+
expect(header.getAttribute('aria-sort')).toBe('descending');
|
|
24
|
+
expect(header.textContent).toContain('↓');
|
|
25
|
+
expect(screen.getByRole('button', { name: 'Sort by Published, ascending' })).toBeTruthy();
|
|
26
|
+
});
|
|
27
|
+
it('calls onSort from a button (keyboard-activatable) for client lists', async () => {
|
|
28
|
+
const onSort = vi.fn();
|
|
29
|
+
render(SortableHeader, {
|
|
30
|
+
props: {
|
|
31
|
+
columnId: 'title',
|
|
32
|
+
label: 'Title',
|
|
33
|
+
sort: spec.default,
|
|
34
|
+
spec,
|
|
35
|
+
onSort,
|
|
36
|
+
as: 'div',
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
const header = screen.getByRole('columnheader');
|
|
40
|
+
expect(header.getAttribute('aria-sort')).toBe('none');
|
|
41
|
+
const button = screen.getByRole('button', {
|
|
42
|
+
name: 'Sort by Title, ascending',
|
|
43
|
+
});
|
|
44
|
+
await fireEvent.click(button);
|
|
45
|
+
expect(onSort).toHaveBeenCalledWith('title');
|
|
46
|
+
});
|
|
47
|
+
it('renders a link for server-sorted lists', () => {
|
|
48
|
+
render(SortableHeader, {
|
|
49
|
+
props: {
|
|
50
|
+
columnId: 'title',
|
|
51
|
+
label: 'Title',
|
|
52
|
+
sort: { columnId: 'title', direction: 'asc' },
|
|
53
|
+
spec,
|
|
54
|
+
href: '/list?sort=title&dir=desc',
|
|
55
|
+
as: 'div',
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
const link = screen.getByRole('link', {
|
|
59
|
+
name: 'Sort by Title, descending',
|
|
60
|
+
});
|
|
61
|
+
expect(link.getAttribute('href')).toBe('/list?sort=title&dir=desc');
|
|
62
|
+
expect(screen.getByRole('columnheader').getAttribute('aria-sort')).toBe('ascending');
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
describe('ListSortSelect', () => {
|
|
66
|
+
it('offers plain-language orders and reports the chosen sort', async () => {
|
|
67
|
+
const onChange = vi.fn();
|
|
68
|
+
render(ListSortSelect, {
|
|
69
|
+
props: {
|
|
70
|
+
columns: [
|
|
71
|
+
{ id: 'published', label: 'Published', kind: 'date' },
|
|
72
|
+
{ id: 'title', label: 'Title' },
|
|
73
|
+
],
|
|
74
|
+
sort: spec.default,
|
|
75
|
+
onChange,
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
const select = screen.getByLabelText('Sort by');
|
|
79
|
+
expect([...select.options].map((option) => option.textContent)).toEqual([
|
|
80
|
+
'Published, newest first',
|
|
81
|
+
'Published, oldest first',
|
|
82
|
+
'Title, A–Z',
|
|
83
|
+
'Title, Z–A',
|
|
84
|
+
]);
|
|
85
|
+
expect(select.value).toBe('published:desc');
|
|
86
|
+
await fireEvent.change(select, { target: { value: 'title:asc' } });
|
|
87
|
+
expect(onChange).toHaveBeenCalledWith({
|
|
88
|
+
columnId: 'title',
|
|
89
|
+
direction: 'asc',
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
describe('SortableHeader style hooks', () => {
|
|
94
|
+
it('reads its control weight from --sortable-header-font-weight (default 600)', () => {
|
|
95
|
+
const source = readFileSync(join(process.cwd().endsWith('packages/smrt-ui')
|
|
96
|
+
? process.cwd()
|
|
97
|
+
: join(process.cwd(), 'packages/smrt-ui'), 'src/components/data/SortableHeader.svelte'), 'utf8');
|
|
98
|
+
expect(source).toMatch(/font-weight:\s*var\(--sortable-header-font-weight,\s*600\)/);
|
|
99
|
+
expect(source).toMatch(/padding:\s*var\(--sortable-header-padding,/);
|
|
100
|
+
});
|
|
101
|
+
});
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { createDataSurfaceRegistry } from '../data-surface.js';
|
|
3
|
+
import { registerLinkSurface, resolveLinkTarget } from '../link-surface.js';
|
|
4
|
+
import { registerStepSurface } from '../step-surface.js';
|
|
5
|
+
const links = [
|
|
6
|
+
{ id: 'events', label: 'Events', href: '/s/events', group: 'Content' },
|
|
7
|
+
{ id: 'articles', label: 'Articles', href: '/s/articles', group: 'Content' },
|
|
8
|
+
{ id: 'ads', label: 'Advertising', href: '/s/ads' },
|
|
9
|
+
];
|
|
10
|
+
let sequence = 0;
|
|
11
|
+
function command(surfaceId, kind, controlId, expectedRevision, payload) {
|
|
12
|
+
sequence += 1;
|
|
13
|
+
return {
|
|
14
|
+
version: 1,
|
|
15
|
+
commandId: `c${sequence}`,
|
|
16
|
+
identity: { surfaceId, kind },
|
|
17
|
+
expectedRevision,
|
|
18
|
+
controlId,
|
|
19
|
+
...(payload === undefined ? {} : { payload: payload }),
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
describe('link surface', () => {
|
|
23
|
+
it('resolves by id, label, and unique prefix; refuses ambiguity', () => {
|
|
24
|
+
expect(resolveLinkTarget('events', links)).toBe('/s/events');
|
|
25
|
+
expect(resolveLinkTarget({ target: 'ARTICLES' }, links)).toBe('/s/articles');
|
|
26
|
+
expect(resolveLinkTarget({ page: 'adv' }, links)).toBe('/s/ads');
|
|
27
|
+
expect(resolveLinkTarget('a', links)).toBeNull();
|
|
28
|
+
expect(resolveLinkTarget({ nope: 1 }, links)).toBeNull();
|
|
29
|
+
});
|
|
30
|
+
it('publishes links without hrefs and navigates through the open control', async () => {
|
|
31
|
+
const registry = createDataSurfaceRegistry();
|
|
32
|
+
const navigate = vi.fn();
|
|
33
|
+
const handle = registerLinkSurface({
|
|
34
|
+
registry,
|
|
35
|
+
surfaceId: 'site-sections',
|
|
36
|
+
label: 'Site sections',
|
|
37
|
+
description: 'Pages in this site',
|
|
38
|
+
links,
|
|
39
|
+
navigate,
|
|
40
|
+
});
|
|
41
|
+
const snapshot = registry.inspect({
|
|
42
|
+
surfaceId: 'site-sections',
|
|
43
|
+
kind: 'list',
|
|
44
|
+
});
|
|
45
|
+
expect(JSON.stringify(snapshot?.state)).not.toContain('/s/');
|
|
46
|
+
expect(snapshot?.state.links).toEqual([
|
|
47
|
+
{ id: 'events', label: 'Events', group: 'Content' },
|
|
48
|
+
{ id: 'articles', label: 'Articles', group: 'Content' },
|
|
49
|
+
{ id: 'ads', label: 'Advertising' },
|
|
50
|
+
]);
|
|
51
|
+
const ok = await registry.execute(command('site-sections', 'list', 'open', snapshot?.revision ?? 0, {
|
|
52
|
+
target: 'Events',
|
|
53
|
+
}));
|
|
54
|
+
expect(ok.ok).toBe(true);
|
|
55
|
+
expect(navigate).toHaveBeenCalledWith('/s/events');
|
|
56
|
+
const refused = await registry.execute(command('site-sections', 'list', 'open', snapshot?.revision ?? 0, {
|
|
57
|
+
target: 'https://evil.example',
|
|
58
|
+
}));
|
|
59
|
+
expect(refused.ok).toBe(false);
|
|
60
|
+
handle.update({ state: { active: 'events' } });
|
|
61
|
+
expect(registry.inspect({ surfaceId: 'site-sections', kind: 'list' })?.revision).toBe((snapshot?.revision ?? 0) + 1);
|
|
62
|
+
handle.destroy();
|
|
63
|
+
expect(registry.list()).toHaveLength(0);
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
describe('step surface', () => {
|
|
67
|
+
const steps = [
|
|
68
|
+
{ id: 'town', label: 'Your town' },
|
|
69
|
+
{ id: 'features', label: 'Features' },
|
|
70
|
+
{ id: 'live', label: 'Go live' },
|
|
71
|
+
];
|
|
72
|
+
const identity = { surfaceId: 'new-site-steps', kind: 'custom' };
|
|
73
|
+
it('advances a non-writing step through the page handler', async () => {
|
|
74
|
+
const registry = createDataSurfaceRegistry();
|
|
75
|
+
const next = vi.fn(() => true);
|
|
76
|
+
const handle = registerStepSurface({
|
|
77
|
+
registry,
|
|
78
|
+
surfaceId: 'new-site-steps',
|
|
79
|
+
label: 'New site steps',
|
|
80
|
+
description: 'Start a new town site',
|
|
81
|
+
steps,
|
|
82
|
+
current: 'town',
|
|
83
|
+
next,
|
|
84
|
+
});
|
|
85
|
+
const before = registry.inspect(identity);
|
|
86
|
+
expect(before?.state).toMatchObject({
|
|
87
|
+
current: 'town',
|
|
88
|
+
currentLabel: 'Your town',
|
|
89
|
+
stepNumber: 1,
|
|
90
|
+
stepCount: 3,
|
|
91
|
+
nextWrites: false,
|
|
92
|
+
awaitingPerson: false,
|
|
93
|
+
});
|
|
94
|
+
const result = await registry.execute(command('new-site-steps', 'custom', 'next', before?.revision ?? 0));
|
|
95
|
+
expect(result.ok).toBe(true);
|
|
96
|
+
expect(next).toHaveBeenCalledTimes(1);
|
|
97
|
+
handle.update({
|
|
98
|
+
current: 'features',
|
|
99
|
+
steps: [{ ...steps[0], complete: true }, steps[1], steps[2]],
|
|
100
|
+
});
|
|
101
|
+
const after = registry.inspect(identity);
|
|
102
|
+
expect(after?.state.current).toBe('features');
|
|
103
|
+
expect(after?.state.steps).toEqual([
|
|
104
|
+
{ id: 'town', label: 'Your town', status: 'complete' },
|
|
105
|
+
{ id: 'features', label: 'Features', status: 'current' },
|
|
106
|
+
{ id: 'live', label: 'Go live', status: 'upcoming' },
|
|
107
|
+
]);
|
|
108
|
+
});
|
|
109
|
+
it('refuses when the page handler rejects the step (validation failed)', async () => {
|
|
110
|
+
const registry = createDataSurfaceRegistry();
|
|
111
|
+
registerStepSurface({
|
|
112
|
+
registry,
|
|
113
|
+
surfaceId: 'new-site-steps',
|
|
114
|
+
label: 'New site steps',
|
|
115
|
+
description: 'Start a new town site',
|
|
116
|
+
steps,
|
|
117
|
+
current: 'town',
|
|
118
|
+
next: () => false,
|
|
119
|
+
});
|
|
120
|
+
const result = await registry.execute(command('new-site-steps', 'custom', 'next', 1));
|
|
121
|
+
expect(result.ok).toBe(false);
|
|
122
|
+
});
|
|
123
|
+
it('never presses a writing step: it shows the button and waits for the person', async () => {
|
|
124
|
+
const registry = createDataSurfaceRegistry();
|
|
125
|
+
const create = vi.fn();
|
|
126
|
+
const showNext = vi.fn();
|
|
127
|
+
const handle = registerStepSurface({
|
|
128
|
+
registry,
|
|
129
|
+
surfaceId: 'new-site-steps',
|
|
130
|
+
label: 'New site steps',
|
|
131
|
+
description: 'Start a new town site',
|
|
132
|
+
steps,
|
|
133
|
+
current: 'features',
|
|
134
|
+
next: create,
|
|
135
|
+
nextWrites: true,
|
|
136
|
+
showNext,
|
|
137
|
+
});
|
|
138
|
+
const result = await registry.execute(command('new-site-steps', 'custom', 'next', 1));
|
|
139
|
+
expect(result.ok).toBe(true);
|
|
140
|
+
expect(create).not.toHaveBeenCalled();
|
|
141
|
+
expect(showNext).toHaveBeenCalledTimes(1);
|
|
142
|
+
expect(registry.inspect(identity)?.state).toMatchObject({
|
|
143
|
+
nextWrites: true,
|
|
144
|
+
awaitingPerson: true,
|
|
145
|
+
});
|
|
146
|
+
handle.update({ current: 'live', nextWrites: false });
|
|
147
|
+
expect(registry.inspect(identity)?.state.awaitingPerson).toBe(false);
|
|
148
|
+
});
|
|
149
|
+
it('goes back and jumps only to available steps', async () => {
|
|
150
|
+
const registry = createDataSurfaceRegistry();
|
|
151
|
+
const back = vi.fn();
|
|
152
|
+
const goTo = vi.fn();
|
|
153
|
+
registerStepSurface({
|
|
154
|
+
registry,
|
|
155
|
+
surfaceId: 'new-site-steps',
|
|
156
|
+
label: 'New site steps',
|
|
157
|
+
description: 'Start a new town site',
|
|
158
|
+
steps: [{ ...steps[0], complete: true }, steps[1], steps[2]],
|
|
159
|
+
current: 'features',
|
|
160
|
+
back,
|
|
161
|
+
goTo,
|
|
162
|
+
});
|
|
163
|
+
expect((await registry.execute(command('new-site-steps', 'custom', 'back', 1)))
|
|
164
|
+
.ok).toBe(true);
|
|
165
|
+
expect(back).toHaveBeenCalled();
|
|
166
|
+
expect((await registry.execute(command('new-site-steps', 'custom', 'go-to', 1, {
|
|
167
|
+
target: 'Go live',
|
|
168
|
+
}))).ok).toBe(false);
|
|
169
|
+
expect((await registry.execute(command('new-site-steps', 'custom', 'go-to', 1, 'your town'))).ok).toBe(true);
|
|
170
|
+
expect(goTo).toHaveBeenCalledWith('town');
|
|
171
|
+
});
|
|
172
|
+
});
|