@llui/components 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +143 -0
- package/dist/components/accordion.d.ts +115 -0
- package/dist/components/accordion.d.ts.map +1 -0
- package/dist/components/accordion.js +138 -0
- package/dist/components/alert-dialog.d.ts +45 -0
- package/dist/components/alert-dialog.d.ts.map +1 -0
- package/dist/components/alert-dialog.js +12 -0
- package/dist/components/angle-slider.d.ts +121 -0
- package/dist/components/angle-slider.d.ts.map +1 -0
- package/dist/components/angle-slider.js +145 -0
- package/dist/components/async-list.d.ts +104 -0
- package/dist/components/async-list.d.ts.map +1 -0
- package/dist/components/async-list.js +117 -0
- package/dist/components/avatar.d.ts +58 -0
- package/dist/components/avatar.d.ts.map +1 -0
- package/dist/components/avatar.js +43 -0
- package/dist/components/carousel.d.ts +128 -0
- package/dist/components/carousel.d.ts.map +1 -0
- package/dist/components/carousel.js +131 -0
- package/dist/components/cascade-select.d.ts +95 -0
- package/dist/components/cascade-select.d.ts.map +1 -0
- package/dist/components/cascade-select.js +100 -0
- package/dist/components/checkbox.d.ts +74 -0
- package/dist/components/checkbox.d.ts.map +1 -0
- package/dist/components/checkbox.js +73 -0
- package/dist/components/clipboard.d.ts +72 -0
- package/dist/components/clipboard.d.ts.map +1 -0
- package/dist/components/clipboard.js +73 -0
- package/dist/components/collapsible.d.ts +64 -0
- package/dist/components/collapsible.d.ts.map +1 -0
- package/dist/components/collapsible.js +51 -0
- package/dist/components/color-picker.d.ts +125 -0
- package/dist/components/color-picker.d.ts.map +1 -0
- package/dist/components/color-picker.js +169 -0
- package/dist/components/combobox.d.ts +163 -0
- package/dist/components/combobox.d.ts.map +1 -0
- package/dist/components/combobox.js +345 -0
- package/dist/components/context-menu.d.ts +105 -0
- package/dist/components/context-menu.d.ts.map +1 -0
- package/dist/components/context-menu.js +177 -0
- package/dist/components/date-input.d.ts +117 -0
- package/dist/components/date-input.d.ts.map +1 -0
- package/dist/components/date-input.js +149 -0
- package/dist/components/date-picker.d.ts +142 -0
- package/dist/components/date-picker.d.ts.map +1 -0
- package/dist/components/date-picker.js +294 -0
- package/dist/components/dialog.d.ts +152 -0
- package/dist/components/dialog.d.ts.map +1 -0
- package/dist/components/dialog.js +140 -0
- package/dist/components/drawer.d.ts +106 -0
- package/dist/components/drawer.d.ts.map +1 -0
- package/dist/components/drawer.js +136 -0
- package/dist/components/editable.d.ts +92 -0
- package/dist/components/editable.d.ts.map +1 -0
- package/dist/components/editable.js +112 -0
- package/dist/components/file-upload.d.ts +251 -0
- package/dist/components/file-upload.d.ts.map +1 -0
- package/dist/components/file-upload.js +324 -0
- package/dist/components/floating-panel.d.ts +171 -0
- package/dist/components/floating-panel.d.ts.map +1 -0
- package/dist/components/floating-panel.js +198 -0
- package/dist/components/hover-card.d.ts +85 -0
- package/dist/components/hover-card.d.ts.map +1 -0
- package/dist/components/hover-card.js +128 -0
- package/dist/components/image-cropper.d.ts +129 -0
- package/dist/components/image-cropper.d.ts.map +1 -0
- package/dist/components/image-cropper.js +208 -0
- package/dist/components/index.d.ts +109 -0
- package/dist/components/index.d.ts.map +1 -0
- package/dist/components/index.js +54 -0
- package/dist/components/listbox.d.ts +98 -0
- package/dist/components/listbox.d.ts.map +1 -0
- package/dist/components/listbox.js +174 -0
- package/dist/components/marquee.d.ts +84 -0
- package/dist/components/marquee.d.ts.map +1 -0
- package/dist/components/marquee.js +73 -0
- package/dist/components/menu.d.ts +131 -0
- package/dist/components/menu.d.ts.map +1 -0
- package/dist/components/menu.js +262 -0
- package/dist/components/navigation-menu.d.ts +111 -0
- package/dist/components/navigation-menu.d.ts.map +1 -0
- package/dist/components/navigation-menu.js +102 -0
- package/dist/components/number-input.d.ts +106 -0
- package/dist/components/number-input.d.ts.map +1 -0
- package/dist/components/number-input.js +178 -0
- package/dist/components/pagination.d.ts +113 -0
- package/dist/components/pagination.d.ts.map +1 -0
- package/dist/components/pagination.js +135 -0
- package/dist/components/password-input.d.ts +64 -0
- package/dist/components/password-input.d.ts.map +1 -0
- package/dist/components/password-input.js +52 -0
- package/dist/components/pin-input.d.ts +89 -0
- package/dist/components/pin-input.d.ts.map +1 -0
- package/dist/components/pin-input.js +139 -0
- package/dist/components/popover.d.ts +116 -0
- package/dist/components/popover.d.ts.map +1 -0
- package/dist/components/popover.js +146 -0
- package/dist/components/presence.d.ts +71 -0
- package/dist/components/presence.d.ts.map +1 -0
- package/dist/components/presence.js +57 -0
- package/dist/components/progress.d.ts +74 -0
- package/dist/components/progress.d.ts.map +1 -0
- package/dist/components/progress.js +80 -0
- package/dist/components/qr-code.d.ts +114 -0
- package/dist/components/qr-code.d.ts.map +1 -0
- package/dist/components/qr-code.js +108 -0
- package/dist/components/radio-group.d.ts +89 -0
- package/dist/components/radio-group.d.ts.map +1 -0
- package/dist/components/radio-group.js +161 -0
- package/dist/components/rating-group.d.ts +88 -0
- package/dist/components/rating-group.d.ts.map +1 -0
- package/dist/components/rating-group.js +122 -0
- package/dist/components/scroll-area.d.ts +124 -0
- package/dist/components/scroll-area.d.ts.map +1 -0
- package/dist/components/scroll-area.js +152 -0
- package/dist/components/select.d.ts +161 -0
- package/dist/components/select.d.ts.map +1 -0
- package/dist/components/select.js +333 -0
- package/dist/components/signature-pad.d.ts +138 -0
- package/dist/components/signature-pad.d.ts.map +1 -0
- package/dist/components/signature-pad.js +142 -0
- package/dist/components/slider.d.ts +117 -0
- package/dist/components/slider.d.ts.map +1 -0
- package/dist/components/slider.js +210 -0
- package/dist/components/splitter.d.ts +87 -0
- package/dist/components/splitter.d.ts.map +1 -0
- package/dist/components/splitter.js +119 -0
- package/dist/components/steps.d.ts +104 -0
- package/dist/components/steps.d.ts.map +1 -0
- package/dist/components/steps.js +133 -0
- package/dist/components/switch.d.ts +66 -0
- package/dist/components/switch.d.ts.map +1 -0
- package/dist/components/switch.js +59 -0
- package/dist/components/tabs.d.ts +146 -0
- package/dist/components/tabs.d.ts.map +1 -0
- package/dist/components/tabs.js +244 -0
- package/dist/components/tags-input.d.ts +118 -0
- package/dist/components/tags-input.d.ts.map +1 -0
- package/dist/components/tags-input.js +168 -0
- package/dist/components/time-picker.d.ts +121 -0
- package/dist/components/time-picker.d.ts.map +1 -0
- package/dist/components/time-picker.js +147 -0
- package/dist/components/timer.d.ts +131 -0
- package/dist/components/timer.d.ts.map +1 -0
- package/dist/components/timer.js +117 -0
- package/dist/components/toast.d.ts +119 -0
- package/dist/components/toast.d.ts.map +1 -0
- package/dist/components/toast.js +102 -0
- package/dist/components/toc.d.ts +119 -0
- package/dist/components/toc.d.ts.map +1 -0
- package/dist/components/toc.js +107 -0
- package/dist/components/toggle-group.d.ts +80 -0
- package/dist/components/toggle-group.d.ts.map +1 -0
- package/dist/components/toggle-group.js +93 -0
- package/dist/components/toggle.d.ts +47 -0
- package/dist/components/toggle.d.ts.map +1 -0
- package/dist/components/toggle.js +41 -0
- package/dist/components/tooltip.d.ts +92 -0
- package/dist/components/tooltip.d.ts.map +1 -0
- package/dist/components/tooltip.js +147 -0
- package/dist/components/tour.d.ts +145 -0
- package/dist/components/tour.d.ts.map +1 -0
- package/dist/components/tour.js +133 -0
- package/dist/components/tree-view.d.ts +216 -0
- package/dist/components/tree-view.d.ts.map +1 -0
- package/dist/components/tree-view.js +293 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4 -0
- package/dist/patterns/confirm-dialog.d.ts +92 -0
- package/dist/patterns/confirm-dialog.d.ts.map +1 -0
- package/dist/patterns/confirm-dialog.js +92 -0
- package/dist/patterns/index.d.ts +3 -0
- package/dist/patterns/index.d.ts.map +1 -0
- package/dist/patterns/index.js +1 -0
- package/dist/utils/anatomy.d.ts +40 -0
- package/dist/utils/anatomy.d.ts.map +1 -0
- package/dist/utils/anatomy.js +41 -0
- package/dist/utils/aria-hidden.d.ts +12 -0
- package/dist/utils/aria-hidden.d.ts.map +1 -0
- package/dist/utils/aria-hidden.js +72 -0
- package/dist/utils/dismissable.d.ts +25 -0
- package/dist/utils/dismissable.d.ts.map +1 -0
- package/dist/utils/dismissable.js +65 -0
- package/dist/utils/dom.d.ts +8 -0
- package/dist/utils/dom.d.ts.map +1 -0
- package/dist/utils/dom.js +21 -0
- package/dist/utils/floating.d.ts +44 -0
- package/dist/utils/floating.d.ts.map +1 -0
- package/dist/utils/floating.js +44 -0
- package/dist/utils/focus-trap.d.ts +18 -0
- package/dist/utils/focus-trap.d.ts.map +1 -0
- package/dist/utils/focus-trap.js +85 -0
- package/dist/utils/focusables.d.ts +6 -0
- package/dist/utils/focusables.d.ts.map +1 -0
- package/dist/utils/focusables.js +65 -0
- package/dist/utils/index.d.ts +18 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +10 -0
- package/dist/utils/interact-outside.d.ts +26 -0
- package/dist/utils/interact-outside.d.ts.map +1 -0
- package/dist/utils/interact-outside.js +46 -0
- package/dist/utils/remove-scroll.d.ts +8 -0
- package/dist/utils/remove-scroll.d.ts.map +1 -0
- package/dist/utils/remove-scroll.js +37 -0
- package/dist/utils/tree-collection.d.ts +61 -0
- package/dist/utils/tree-collection.d.ts.map +1 -0
- package/dist/utils/tree-collection.js +137 -0
- package/dist/utils/typeahead.d.ts +49 -0
- package/dist/utils/typeahead.d.ts.map +1 -0
- package/dist/utils/typeahead.js +81 -0
- package/package.json +282 -0
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
import { typeaheadAccumulate, typeaheadMatch, isTypeaheadKey, TYPEAHEAD_TIMEOUT_MS, } from '../utils/typeahead';
|
|
2
|
+
export function init(opts = {}) {
|
|
3
|
+
return {
|
|
4
|
+
expanded: opts.expanded ?? [],
|
|
5
|
+
selected: opts.selected ?? [],
|
|
6
|
+
checked: opts.checked ?? [],
|
|
7
|
+
indeterminate: opts.indeterminate ?? [],
|
|
8
|
+
focused: null,
|
|
9
|
+
selectionMode: opts.selectionMode ?? 'single',
|
|
10
|
+
visibleItems: opts.visibleItems ?? [],
|
|
11
|
+
visibleLabels: opts.visibleLabels ?? [],
|
|
12
|
+
disabled: opts.disabled ?? false,
|
|
13
|
+
typeahead: '',
|
|
14
|
+
typeaheadExpiresAt: 0,
|
|
15
|
+
renaming: null,
|
|
16
|
+
renameDraft: '',
|
|
17
|
+
loading: [],
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
export function update(state, msg) {
|
|
21
|
+
if (state.disabled && msg.type !== 'setVisibleItems')
|
|
22
|
+
return [state, []];
|
|
23
|
+
switch (msg.type) {
|
|
24
|
+
case 'toggleBranch': {
|
|
25
|
+
const expanded = state.expanded.includes(msg.id)
|
|
26
|
+
? state.expanded.filter((id) => id !== msg.id)
|
|
27
|
+
: [...state.expanded, msg.id];
|
|
28
|
+
return [{ ...state, expanded }, []];
|
|
29
|
+
}
|
|
30
|
+
case 'expand':
|
|
31
|
+
if (state.expanded.includes(msg.id))
|
|
32
|
+
return [state, []];
|
|
33
|
+
return [{ ...state, expanded: [...state.expanded, msg.id] }, []];
|
|
34
|
+
case 'collapse':
|
|
35
|
+
if (!state.expanded.includes(msg.id))
|
|
36
|
+
return [state, []];
|
|
37
|
+
return [{ ...state, expanded: state.expanded.filter((id) => id !== msg.id) }, []];
|
|
38
|
+
case 'expandAll':
|
|
39
|
+
return [{ ...state, expanded: msg.ids }, []];
|
|
40
|
+
case 'collapseAll':
|
|
41
|
+
return [{ ...state, expanded: [] }, []];
|
|
42
|
+
case 'select': {
|
|
43
|
+
if (state.selectionMode === 'single') {
|
|
44
|
+
return [{ ...state, selected: [msg.id] }, []];
|
|
45
|
+
}
|
|
46
|
+
if (msg.additive) {
|
|
47
|
+
const isSelected = state.selected.includes(msg.id);
|
|
48
|
+
const selected = isSelected
|
|
49
|
+
? state.selected.filter((id) => id !== msg.id)
|
|
50
|
+
: [...state.selected, msg.id];
|
|
51
|
+
return [{ ...state, selected }, []];
|
|
52
|
+
}
|
|
53
|
+
return [{ ...state, selected: [msg.id] }, []];
|
|
54
|
+
}
|
|
55
|
+
case 'setSelected':
|
|
56
|
+
return [{ ...state, selected: msg.ids }, []];
|
|
57
|
+
case 'focus':
|
|
58
|
+
return [{ ...state, focused: msg.id }, []];
|
|
59
|
+
case 'focusNext': {
|
|
60
|
+
if (state.visibleItems.length === 0)
|
|
61
|
+
return [state, []];
|
|
62
|
+
const idx = state.focused ? state.visibleItems.indexOf(state.focused) : -1;
|
|
63
|
+
const next = state.visibleItems[Math.min(idx + 1, state.visibleItems.length - 1)];
|
|
64
|
+
return [{ ...state, focused: next ?? state.focused }, []];
|
|
65
|
+
}
|
|
66
|
+
case 'focusPrev': {
|
|
67
|
+
if (state.visibleItems.length === 0)
|
|
68
|
+
return [state, []];
|
|
69
|
+
const idx = state.focused
|
|
70
|
+
? state.visibleItems.indexOf(state.focused)
|
|
71
|
+
: state.visibleItems.length;
|
|
72
|
+
const prev = state.visibleItems[Math.max(0, idx - 1)];
|
|
73
|
+
return [{ ...state, focused: prev ?? state.focused }, []];
|
|
74
|
+
}
|
|
75
|
+
case 'focusFirst':
|
|
76
|
+
return [{ ...state, focused: state.visibleItems[0] ?? null }, []];
|
|
77
|
+
case 'focusLast':
|
|
78
|
+
return [{ ...state, focused: state.visibleItems[state.visibleItems.length - 1] ?? null }, []];
|
|
79
|
+
case 'setVisibleItems':
|
|
80
|
+
return [
|
|
81
|
+
{ ...state, visibleItems: msg.ids, visibleLabels: msg.labels ?? state.visibleLabels },
|
|
82
|
+
[],
|
|
83
|
+
];
|
|
84
|
+
case 'arrowLeftFrom': {
|
|
85
|
+
// If this is an expanded branch, collapse it and stay focused.
|
|
86
|
+
if (msg.isBranch && state.expanded.includes(msg.id)) {
|
|
87
|
+
return [{ ...state, expanded: state.expanded.filter((id) => id !== msg.id) }, []];
|
|
88
|
+
}
|
|
89
|
+
// Otherwise, move focus to parent if provided (WAI-ARIA).
|
|
90
|
+
if (msg.parentId !== null) {
|
|
91
|
+
return [{ ...state, focused: msg.parentId }, []];
|
|
92
|
+
}
|
|
93
|
+
return [state, []];
|
|
94
|
+
}
|
|
95
|
+
case 'arrowRightFrom': {
|
|
96
|
+
// If this branch is closed, expand it; otherwise focus the first
|
|
97
|
+
// visible child (the next item in depth-first visibleItems order).
|
|
98
|
+
if (!state.expanded.includes(msg.id)) {
|
|
99
|
+
return [{ ...state, expanded: [...state.expanded, msg.id] }, []];
|
|
100
|
+
}
|
|
101
|
+
const idx = state.visibleItems.indexOf(msg.id);
|
|
102
|
+
if (idx === -1 || idx === state.visibleItems.length - 1)
|
|
103
|
+
return [state, []];
|
|
104
|
+
const next = state.visibleItems[idx + 1];
|
|
105
|
+
return [{ ...state, focused: next }, []];
|
|
106
|
+
}
|
|
107
|
+
case 'typeahead': {
|
|
108
|
+
if (state.visibleItems.length === 0)
|
|
109
|
+
return [state, []];
|
|
110
|
+
const acc = typeaheadAccumulate(state.typeahead, msg.char, msg.now, state.typeaheadExpiresAt);
|
|
111
|
+
// Fall back to matching ids if labels weren't provided.
|
|
112
|
+
const labels = state.visibleLabels.length > 0 ? state.visibleLabels : state.visibleItems;
|
|
113
|
+
const disabledMask = new Array(labels.length).fill(false);
|
|
114
|
+
const startIdx = state.focused ? state.visibleItems.indexOf(state.focused) : null;
|
|
115
|
+
const matchIdx = typeaheadMatch(labels, disabledMask, acc, startIdx);
|
|
116
|
+
const focused = matchIdx === null ? state.focused : (state.visibleItems[matchIdx] ?? state.focused);
|
|
117
|
+
return [
|
|
118
|
+
{ ...state, typeahead: acc, typeaheadExpiresAt: msg.now + TYPEAHEAD_TIMEOUT_MS, focused },
|
|
119
|
+
[],
|
|
120
|
+
];
|
|
121
|
+
}
|
|
122
|
+
case 'toggleChecked': {
|
|
123
|
+
// Toggle the item's checked state, propagating to descendants if any.
|
|
124
|
+
// The caller passes `descendantIds` for branches; for leaves, pass
|
|
125
|
+
// an empty list or omit. Indeterminate flag is cleared on the id
|
|
126
|
+
// (a deliberate toggle is a definite state). The caller is
|
|
127
|
+
// responsible for recomputing `indeterminate` on ancestors via
|
|
128
|
+
// setIndeterminate after this message.
|
|
129
|
+
const desc = msg.descendantIds ?? [];
|
|
130
|
+
const all = [msg.id, ...desc];
|
|
131
|
+
const isChecked = state.checked.includes(msg.id);
|
|
132
|
+
const next = isChecked
|
|
133
|
+
? state.checked.filter((id) => !all.includes(id))
|
|
134
|
+
: Array.from(new Set([...state.checked, ...all]));
|
|
135
|
+
const indeterminate = state.indeterminate.filter((id) => !all.includes(id));
|
|
136
|
+
return [{ ...state, checked: next, indeterminate }, []];
|
|
137
|
+
}
|
|
138
|
+
case 'setChecked':
|
|
139
|
+
return [{ ...state, checked: msg.ids }, []];
|
|
140
|
+
case 'setIndeterminate':
|
|
141
|
+
return [{ ...state, indeterminate: msg.ids }, []];
|
|
142
|
+
case 'renameStart':
|
|
143
|
+
return [{ ...state, renaming: msg.id, renameDraft: msg.initial }, []];
|
|
144
|
+
case 'renameChange':
|
|
145
|
+
return [{ ...state, renameDraft: msg.value }, []];
|
|
146
|
+
case 'renameCommit':
|
|
147
|
+
case 'renameCancel':
|
|
148
|
+
return [{ ...state, renaming: null, renameDraft: '' }, []];
|
|
149
|
+
case 'loadingStart':
|
|
150
|
+
if (state.loading.includes(msg.id))
|
|
151
|
+
return [state, []];
|
|
152
|
+
return [{ ...state, loading: [...state.loading, msg.id] }, []];
|
|
153
|
+
case 'loadingEnd':
|
|
154
|
+
return [{ ...state, loading: state.loading.filter((id) => id !== msg.id) }, []];
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
export function isExpanded(state, id) {
|
|
158
|
+
return state.expanded.includes(id);
|
|
159
|
+
}
|
|
160
|
+
export function isSelected(state, id) {
|
|
161
|
+
return state.selected.includes(id);
|
|
162
|
+
}
|
|
163
|
+
export function isChecked(state, id) {
|
|
164
|
+
return state.checked.includes(id);
|
|
165
|
+
}
|
|
166
|
+
export function isIndeterminate(state, id) {
|
|
167
|
+
return state.indeterminate.includes(id);
|
|
168
|
+
}
|
|
169
|
+
export function isRenaming(state, id) {
|
|
170
|
+
return state.renaming === id;
|
|
171
|
+
}
|
|
172
|
+
export function isLoading(state, id) {
|
|
173
|
+
return state.loading.includes(id);
|
|
174
|
+
}
|
|
175
|
+
export function connect(get, send, opts) {
|
|
176
|
+
const itemId = (v) => `${opts.id}:item:${v}`;
|
|
177
|
+
const expandOnClick = opts.expandOnClick === true;
|
|
178
|
+
return {
|
|
179
|
+
root: {
|
|
180
|
+
role: 'tree',
|
|
181
|
+
'aria-multiselectable': (s) => (get(s).selectionMode === 'multiple' ? 'true' : undefined),
|
|
182
|
+
'aria-disabled': (s) => (get(s).disabled ? 'true' : undefined),
|
|
183
|
+
'data-scope': 'tree-view',
|
|
184
|
+
'data-part': 'root',
|
|
185
|
+
'data-disabled': (s) => (get(s).disabled ? '' : undefined),
|
|
186
|
+
},
|
|
187
|
+
item: (id, depth, isBranch, parentId = null) => ({
|
|
188
|
+
item: {
|
|
189
|
+
role: 'treeitem',
|
|
190
|
+
id: itemId(id),
|
|
191
|
+
'aria-expanded': (s) => (isBranch ? isExpanded(get(s), id) : undefined),
|
|
192
|
+
'aria-selected': (s) => get(s).selectionMode === 'single' ? isSelected(get(s), id) : undefined,
|
|
193
|
+
'aria-level': depth + 1,
|
|
194
|
+
'aria-busy': (s) => (isLoading(get(s), id) ? 'true' : undefined),
|
|
195
|
+
tabIndex: (s) => (get(s).focused === id ? 0 : -1),
|
|
196
|
+
'data-scope': 'tree-view',
|
|
197
|
+
'data-part': 'item',
|
|
198
|
+
'data-value': id,
|
|
199
|
+
'data-depth': String(depth),
|
|
200
|
+
'data-selected': (s) => (isSelected(get(s), id) ? '' : undefined),
|
|
201
|
+
'data-focused': (s) => (get(s).focused === id ? '' : undefined),
|
|
202
|
+
'data-loading': (s) => (isLoading(get(s), id) ? '' : undefined),
|
|
203
|
+
onClick: (e) => {
|
|
204
|
+
send({ type: 'select', id, additive: e.metaKey || e.ctrlKey });
|
|
205
|
+
if (expandOnClick && isBranch)
|
|
206
|
+
send({ type: 'toggleBranch', id });
|
|
207
|
+
},
|
|
208
|
+
onFocus: () => send({ type: 'focus', id }),
|
|
209
|
+
onKeyDown: (e) => {
|
|
210
|
+
switch (e.key) {
|
|
211
|
+
case 'ArrowDown':
|
|
212
|
+
e.preventDefault();
|
|
213
|
+
send({ type: 'focusNext' });
|
|
214
|
+
return;
|
|
215
|
+
case 'ArrowUp':
|
|
216
|
+
e.preventDefault();
|
|
217
|
+
send({ type: 'focusPrev' });
|
|
218
|
+
return;
|
|
219
|
+
case 'ArrowRight':
|
|
220
|
+
// WAI-ARIA: closed branch → expand (stay); open branch →
|
|
221
|
+
// focus first child. Leaf → nothing. The reducer decides
|
|
222
|
+
// based on current expanded state.
|
|
223
|
+
if (!isBranch)
|
|
224
|
+
return;
|
|
225
|
+
e.preventDefault();
|
|
226
|
+
send({ type: 'arrowRightFrom', id });
|
|
227
|
+
return;
|
|
228
|
+
case 'ArrowLeft':
|
|
229
|
+
// WAI-ARIA: open branch → collapse (stay); closed branch or
|
|
230
|
+
// leaf → focus parent (if known). Root end-nodes → nothing.
|
|
231
|
+
e.preventDefault();
|
|
232
|
+
send({ type: 'arrowLeftFrom', id, isBranch, parentId });
|
|
233
|
+
return;
|
|
234
|
+
case 'Home':
|
|
235
|
+
e.preventDefault();
|
|
236
|
+
send({ type: 'focusFirst' });
|
|
237
|
+
return;
|
|
238
|
+
case 'End':
|
|
239
|
+
e.preventDefault();
|
|
240
|
+
send({ type: 'focusLast' });
|
|
241
|
+
return;
|
|
242
|
+
case 'Enter':
|
|
243
|
+
case ' ':
|
|
244
|
+
e.preventDefault();
|
|
245
|
+
send({ type: 'select', id, additive: e.metaKey || e.ctrlKey });
|
|
246
|
+
if (isBranch)
|
|
247
|
+
send({ type: 'toggleBranch', id });
|
|
248
|
+
return;
|
|
249
|
+
default:
|
|
250
|
+
if (isTypeaheadKey(e)) {
|
|
251
|
+
send({ type: 'typeahead', char: e.key, now: Date.now() });
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
},
|
|
255
|
+
},
|
|
256
|
+
branchTrigger: {
|
|
257
|
+
'data-scope': 'tree-view',
|
|
258
|
+
'data-part': 'branch-trigger',
|
|
259
|
+
'data-state': (s) => (isExpanded(get(s), id) ? 'open' : 'closed'),
|
|
260
|
+
onClick: (e) => {
|
|
261
|
+
e.stopPropagation();
|
|
262
|
+
send({ type: 'toggleBranch', id });
|
|
263
|
+
},
|
|
264
|
+
},
|
|
265
|
+
checkbox: {
|
|
266
|
+
role: 'checkbox',
|
|
267
|
+
'aria-checked': (s) => {
|
|
268
|
+
if (isIndeterminate(get(s), id))
|
|
269
|
+
return 'mixed';
|
|
270
|
+
return isChecked(get(s), id) ? 'true' : 'false';
|
|
271
|
+
},
|
|
272
|
+
'data-scope': 'tree-view',
|
|
273
|
+
'data-part': 'checkbox',
|
|
274
|
+
'data-state': (s) => {
|
|
275
|
+
if (isIndeterminate(get(s), id))
|
|
276
|
+
return 'indeterminate';
|
|
277
|
+
return isChecked(get(s), id) ? 'checked' : 'unchecked';
|
|
278
|
+
},
|
|
279
|
+
},
|
|
280
|
+
}),
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
export const treeView = {
|
|
284
|
+
init,
|
|
285
|
+
update,
|
|
286
|
+
connect,
|
|
287
|
+
isExpanded,
|
|
288
|
+
isSelected,
|
|
289
|
+
isChecked,
|
|
290
|
+
isIndeterminate,
|
|
291
|
+
isRenaming,
|
|
292
|
+
isLoading,
|
|
293
|
+
};
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,cAAc,eAAe,CAAA;AAE7B,cAAc,oBAAoB,CAAA"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import type { Send, TransitionOptions } from '@llui/dom';
|
|
2
|
+
import { init as dialogInit, update as dialogUpdate } from '../components/dialog';
|
|
3
|
+
/**
|
|
4
|
+
* ConfirmDialog — a pre-wired dialog pattern for confirmations.
|
|
5
|
+
*
|
|
6
|
+
* Composes `dialog` with conventional content: title, description, cancel,
|
|
7
|
+
* confirm. Carries an opaque `tag` so the consumer's update handler can
|
|
8
|
+
* recognize which confirmation resolved.
|
|
9
|
+
*
|
|
10
|
+
* Usage in consumer's update:
|
|
11
|
+
*
|
|
12
|
+
* ```ts
|
|
13
|
+
* case 'confirm': {
|
|
14
|
+
* const [s, fx] = confirmDialog.update(state.confirm, msg.msg)
|
|
15
|
+
* // When the user clicks confirm, branch on the tag:
|
|
16
|
+
* if (msg.msg.type === 'confirm') {
|
|
17
|
+
* switch (state.confirm.tag) {
|
|
18
|
+
* case 'delete-user': return [{ ...state, confirm: s, users: ... }, fx]
|
|
19
|
+
* case 'logout': return [{ ...state, confirm: s }, [...fx, logoutEffect]]
|
|
20
|
+
* }
|
|
21
|
+
* }
|
|
22
|
+
* return [{ ...state, confirm: s }, fx]
|
|
23
|
+
* }
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
export interface ConfirmDialogState {
|
|
27
|
+
open: boolean;
|
|
28
|
+
tag: string;
|
|
29
|
+
title: string;
|
|
30
|
+
description: string;
|
|
31
|
+
confirmLabel: string;
|
|
32
|
+
cancelLabel: string;
|
|
33
|
+
destructive: boolean;
|
|
34
|
+
}
|
|
35
|
+
export type ConfirmDialogMsg = {
|
|
36
|
+
type: 'openWith';
|
|
37
|
+
tag: string;
|
|
38
|
+
title: string;
|
|
39
|
+
description?: string;
|
|
40
|
+
confirmLabel?: string;
|
|
41
|
+
cancelLabel?: string;
|
|
42
|
+
destructive?: boolean;
|
|
43
|
+
} | {
|
|
44
|
+
type: 'confirm';
|
|
45
|
+
} | {
|
|
46
|
+
type: 'cancel';
|
|
47
|
+
} | {
|
|
48
|
+
type: 'setOpen';
|
|
49
|
+
open: boolean;
|
|
50
|
+
};
|
|
51
|
+
export interface ConfirmDialogInit {
|
|
52
|
+
tag?: string;
|
|
53
|
+
title?: string;
|
|
54
|
+
description?: string;
|
|
55
|
+
confirmLabel?: string;
|
|
56
|
+
cancelLabel?: string;
|
|
57
|
+
destructive?: boolean;
|
|
58
|
+
}
|
|
59
|
+
export declare function init(opts?: ConfirmDialogInit): ConfirmDialogState;
|
|
60
|
+
export declare function update(state: ConfirmDialogState, msg: ConfirmDialogMsg): [ConfirmDialogState, never[]];
|
|
61
|
+
/**
|
|
62
|
+
* View the ConfirmDialog. Returns a `show`-wrapped tree that appears when
|
|
63
|
+
* the dialog is open, renders default content (title/description/buttons).
|
|
64
|
+
* Uses role="alertdialog" for destructive confirms.
|
|
65
|
+
*/
|
|
66
|
+
export interface ConfirmDialogViewOptions<S> {
|
|
67
|
+
get: (s: S) => ConfirmDialogState;
|
|
68
|
+
send: Send<ConfirmDialogMsg>;
|
|
69
|
+
id: string;
|
|
70
|
+
transition?: TransitionOptions;
|
|
71
|
+
/** Custom class for content root. */
|
|
72
|
+
contentClass?: string;
|
|
73
|
+
/** Custom class for destructive confirm button. */
|
|
74
|
+
destructiveClass?: string;
|
|
75
|
+
}
|
|
76
|
+
export declare function view<S>(opts: ConfirmDialogViewOptions<S>): Node[];
|
|
77
|
+
/** Helper to create an openWith message builder. */
|
|
78
|
+
export declare function openWith(tag: string, opts: {
|
|
79
|
+
title: string;
|
|
80
|
+
description?: string;
|
|
81
|
+
confirmLabel?: string;
|
|
82
|
+
cancelLabel?: string;
|
|
83
|
+
destructive?: boolean;
|
|
84
|
+
}): ConfirmDialogMsg;
|
|
85
|
+
export declare const confirmDialog: {
|
|
86
|
+
init: typeof init;
|
|
87
|
+
update: typeof update;
|
|
88
|
+
view: typeof view;
|
|
89
|
+
openWith: typeof openWith;
|
|
90
|
+
};
|
|
91
|
+
export { dialogInit, dialogUpdate };
|
|
92
|
+
//# sourceMappingURL=confirm-dialog.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"confirm-dialog.d.ts","sourceRoot":"","sources":["../../src/patterns/confirm-dialog.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,iBAAiB,EAAE,MAAM,WAAW,CAAA;AAExD,OAAO,EACL,IAAI,IAAI,UAAU,EAClB,MAAM,IAAI,YAAY,EAIvB,MAAM,sBAAsB,CAAA;AAE7B;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,OAAO,CAAA;IACb,GAAG,EAAE,MAAM,CAAA;IACX,KAAK,EAAE,MAAM,CAAA;IACb,WAAW,EAAE,MAAM,CAAA;IACnB,YAAY,EAAE,MAAM,CAAA;IACpB,WAAW,EAAE,MAAM,CAAA;IACnB,WAAW,EAAE,OAAO,CAAA;CACrB;AAED,MAAM,MAAM,gBAAgB,GACxB;IACE,IAAI,EAAE,UAAU,CAAA;IAChB,GAAG,EAAE,MAAM,CAAA;IACX,KAAK,EAAE,MAAM,CAAA;IACb,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,WAAW,CAAC,EAAE,OAAO,CAAA;CACtB,GACD;IAAE,IAAI,EAAE,SAAS,CAAA;CAAE,GACnB;IAAE,IAAI,EAAE,QAAQ,CAAA;CAAE,GAClB;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,IAAI,EAAE,OAAO,CAAA;CAAE,CAAA;AAEtC,MAAM,WAAW,iBAAiB;IAChC,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,WAAW,CAAC,EAAE,OAAO,CAAA;CACtB;AAED,wBAAgB,IAAI,CAAC,IAAI,GAAE,iBAAsB,GAAG,kBAAkB,CAUrE;AAED,wBAAgB,MAAM,CACpB,KAAK,EAAE,kBAAkB,EACzB,GAAG,EAAE,gBAAgB,GACpB,CAAC,kBAAkB,EAAE,KAAK,EAAE,CAAC,CAuB/B;AAED;;;;GAIG;AACH,MAAM,WAAW,wBAAwB,CAAC,CAAC;IACzC,GAAG,EAAE,CAAC,CAAC,EAAE,CAAC,KAAK,kBAAkB,CAAA;IACjC,IAAI,EAAE,IAAI,CAAC,gBAAgB,CAAC,CAAA;IAC5B,EAAE,EAAE,MAAM,CAAA;IACV,UAAU,CAAC,EAAE,iBAAiB,CAAA;IAC9B,qCAAqC;IACrC,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,mDAAmD;IACnD,gBAAgB,CAAC,EAAE,MAAM,CAAA;CAC1B;AAED,wBAAgB,IAAI,CAAC,CAAC,EAAE,IAAI,EAAE,wBAAwB,CAAC,CAAC,CAAC,GAAG,IAAI,EAAE,CAkDjE;AAED,oDAAoD;AACpD,wBAAgB,QAAQ,CACtB,GAAG,EAAE,MAAM,EACX,IAAI,EAAE;IACJ,KAAK,EAAE,MAAM,CAAA;IACb,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,WAAW,CAAC,EAAE,OAAO,CAAA;CACtB,GACA,gBAAgB,CAUlB;AAED,eAAO,MAAM,aAAa;;;;;CAAmC,CAAA;AAG7D,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,CAAA"}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { button, text, div, h2, p } from '@llui/dom';
|
|
2
|
+
import { init as dialogInit, update as dialogUpdate, connect as dialogConnect, overlay as dialogOverlay, } from '../components/dialog';
|
|
3
|
+
export function init(opts = {}) {
|
|
4
|
+
return {
|
|
5
|
+
open: false,
|
|
6
|
+
tag: opts.tag ?? '',
|
|
7
|
+
title: opts.title ?? '',
|
|
8
|
+
description: opts.description ?? '',
|
|
9
|
+
confirmLabel: opts.confirmLabel ?? 'Confirm',
|
|
10
|
+
cancelLabel: opts.cancelLabel ?? 'Cancel',
|
|
11
|
+
destructive: opts.destructive ?? false,
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
export function update(state, msg) {
|
|
15
|
+
switch (msg.type) {
|
|
16
|
+
case 'openWith':
|
|
17
|
+
return [
|
|
18
|
+
{
|
|
19
|
+
...state,
|
|
20
|
+
open: true,
|
|
21
|
+
tag: msg.tag,
|
|
22
|
+
title: msg.title,
|
|
23
|
+
description: msg.description ?? '',
|
|
24
|
+
confirmLabel: msg.confirmLabel ?? state.confirmLabel,
|
|
25
|
+
cancelLabel: msg.cancelLabel ?? state.cancelLabel,
|
|
26
|
+
destructive: msg.destructive ?? false,
|
|
27
|
+
},
|
|
28
|
+
[],
|
|
29
|
+
];
|
|
30
|
+
case 'confirm':
|
|
31
|
+
return [{ ...state, open: false }, []];
|
|
32
|
+
case 'cancel':
|
|
33
|
+
return [{ ...state, open: false }, []];
|
|
34
|
+
case 'setOpen':
|
|
35
|
+
return [{ ...state, open: msg.open }, []];
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
export function view(opts) {
|
|
39
|
+
// Build dialog parts — role='alertdialog' for proper modal semantics.
|
|
40
|
+
// Trigger + closeTrigger parts are unused (we provide our own buttons);
|
|
41
|
+
// their send is a no-op.
|
|
42
|
+
const parts = dialogConnect((s) => ({ open: opts.get(s).open }), () => {
|
|
43
|
+
/* unused — our buttons dispatch directly via opts.send */
|
|
44
|
+
}, { id: opts.id, role: 'alertdialog', closeLabel: 'Cancel' });
|
|
45
|
+
return dialogOverlay({
|
|
46
|
+
get: (s) => ({ open: opts.get(s).open }),
|
|
47
|
+
// Dismissable (Esc/outside-click) dispatches dialog.close, which we
|
|
48
|
+
// translate into confirm-dialog.cancel.
|
|
49
|
+
send: (m) => {
|
|
50
|
+
if (m.type === 'close')
|
|
51
|
+
opts.send({ type: 'cancel' });
|
|
52
|
+
},
|
|
53
|
+
parts,
|
|
54
|
+
content: () => [
|
|
55
|
+
div({ ...parts.content, class: opts.contentClass ?? 'confirm-dialog' }, [
|
|
56
|
+
h2({ ...parts.title }, [text((s) => opts.get(s).title)]),
|
|
57
|
+
p({ ...parts.description }, [text((s) => opts.get(s).description)]),
|
|
58
|
+
div({ class: 'confirm-dialog__actions' }, [
|
|
59
|
+
button({
|
|
60
|
+
type: 'button',
|
|
61
|
+
class: 'btn btn-secondary',
|
|
62
|
+
onClick: () => opts.send({ type: 'cancel' }),
|
|
63
|
+
}, [text((s) => opts.get(s).cancelLabel)]),
|
|
64
|
+
button({
|
|
65
|
+
type: 'button',
|
|
66
|
+
class: (s) => opts.get(s).destructive
|
|
67
|
+
? (opts.destructiveClass ?? 'btn btn-danger')
|
|
68
|
+
: 'btn btn-primary',
|
|
69
|
+
onClick: () => opts.send({ type: 'confirm' }),
|
|
70
|
+
}, [text((s) => opts.get(s).confirmLabel)]),
|
|
71
|
+
]),
|
|
72
|
+
]),
|
|
73
|
+
],
|
|
74
|
+
transition: opts.transition,
|
|
75
|
+
closeOnOutsideClick: false, // alertdialog default
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
/** Helper to create an openWith message builder. */
|
|
79
|
+
export function openWith(tag, opts) {
|
|
80
|
+
return {
|
|
81
|
+
type: 'openWith',
|
|
82
|
+
tag,
|
|
83
|
+
title: opts.title,
|
|
84
|
+
description: opts.description,
|
|
85
|
+
confirmLabel: opts.confirmLabel,
|
|
86
|
+
cancelLabel: opts.cancelLabel,
|
|
87
|
+
destructive: opts.destructive,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
export const confirmDialog = { init, update, view, openWith };
|
|
91
|
+
// Re-exported dialog primitives (available for advanced use cases)
|
|
92
|
+
export { dialogInit, dialogUpdate };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/patterns/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,aAAa,MAAM,kBAAkB,CAAA;AACjD,YAAY,EACV,kBAAkB,EAClB,gBAAgB,EAChB,iBAAiB,EACjB,wBAAwB,GACzB,MAAM,kBAAkB,CAAA"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * as confirmDialog from './confirm-dialog';
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Anatomy — describes a component's parts and assigns unique ids per instance.
|
|
3
|
+
*
|
|
4
|
+
* Each component (dialog, menu, tabs, ...) declares its parts once. Every
|
|
5
|
+
* mount creates a fresh scope with unique ids used for ARIA wiring
|
|
6
|
+
* (`aria-controls`, `aria-labelledby`) and `data-scope`/`data-part` for CSS.
|
|
7
|
+
*
|
|
8
|
+
* ```ts
|
|
9
|
+
* const DialogAnatomy = anatomy('dialog', [
|
|
10
|
+
* 'trigger', 'positioner', 'backdrop', 'content', 'title', 'description', 'closeTrigger',
|
|
11
|
+
* ] as const)
|
|
12
|
+
*
|
|
13
|
+
* // Per instance:
|
|
14
|
+
* const parts = DialogAnatomy.scope()
|
|
15
|
+
* parts.attrs('trigger') // { id: 'dialog-1:trigger', 'data-scope': 'dialog', 'data-part': 'trigger' }
|
|
16
|
+
* parts.idFor('content') // 'dialog-1:content'
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
/** Reset the internal id counter — tests only. */
|
|
20
|
+
export declare function resetAnatomyIdCounter(): void;
|
|
21
|
+
export interface AnatomyScope<P extends string> {
|
|
22
|
+
/** Instance id — unique across all anatomy scopes. */
|
|
23
|
+
readonly id: string;
|
|
24
|
+
/** Resolve the id for a specific part (for ARIA wiring). */
|
|
25
|
+
idFor(part: P): string;
|
|
26
|
+
/** Build the common data-attrs + id for a part. */
|
|
27
|
+
attrs(part: P): {
|
|
28
|
+
id: string;
|
|
29
|
+
'data-scope': string;
|
|
30
|
+
'data-part': P;
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
export interface Anatomy<P extends string> {
|
|
34
|
+
readonly name: string;
|
|
35
|
+
readonly parts: readonly P[];
|
|
36
|
+
/** Create a new scope instance. Pass an explicit id to force a value (SSR). */
|
|
37
|
+
scope(id?: string): AnatomyScope<P>;
|
|
38
|
+
}
|
|
39
|
+
export declare function anatomy<P extends string>(name: string, parts: readonly P[]): Anatomy<P>;
|
|
40
|
+
//# sourceMappingURL=anatomy.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"anatomy.d.ts","sourceRoot":"","sources":["../../src/utils/anatomy.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAIH,kDAAkD;AAClD,wBAAgB,qBAAqB,IAAI,IAAI,CAE5C;AAED,MAAM,WAAW,YAAY,CAAC,CAAC,SAAS,MAAM;IAC5C,sDAAsD;IACtD,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;IACnB,4DAA4D;IAC5D,KAAK,CAAC,IAAI,EAAE,CAAC,GAAG,MAAM,CAAA;IACtB,mDAAmD;IACnD,KAAK,CAAC,IAAI,EAAE,CAAC,GAAG;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,YAAY,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,CAAC,CAAA;KAAE,CAAA;CACrE;AAED,MAAM,WAAW,OAAO,CAAC,CAAC,SAAS,MAAM;IACvC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,KAAK,EAAE,SAAS,CAAC,EAAE,CAAA;IAC5B,+EAA+E;IAC/E,KAAK,CAAC,EAAE,CAAC,EAAE,MAAM,GAAG,YAAY,CAAC,CAAC,CAAC,CAAA;CACpC;AAED,wBAAgB,OAAO,CAAC,CAAC,SAAS,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,CAAC,EAAE,GAAG,OAAO,CAAC,CAAC,CAAC,CAiBvF"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Anatomy — describes a component's parts and assigns unique ids per instance.
|
|
3
|
+
*
|
|
4
|
+
* Each component (dialog, menu, tabs, ...) declares its parts once. Every
|
|
5
|
+
* mount creates a fresh scope with unique ids used for ARIA wiring
|
|
6
|
+
* (`aria-controls`, `aria-labelledby`) and `data-scope`/`data-part` for CSS.
|
|
7
|
+
*
|
|
8
|
+
* ```ts
|
|
9
|
+
* const DialogAnatomy = anatomy('dialog', [
|
|
10
|
+
* 'trigger', 'positioner', 'backdrop', 'content', 'title', 'description', 'closeTrigger',
|
|
11
|
+
* ] as const)
|
|
12
|
+
*
|
|
13
|
+
* // Per instance:
|
|
14
|
+
* const parts = DialogAnatomy.scope()
|
|
15
|
+
* parts.attrs('trigger') // { id: 'dialog-1:trigger', 'data-scope': 'dialog', 'data-part': 'trigger' }
|
|
16
|
+
* parts.idFor('content') // 'dialog-1:content'
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
let counter = 0;
|
|
20
|
+
/** Reset the internal id counter — tests only. */
|
|
21
|
+
export function resetAnatomyIdCounter() {
|
|
22
|
+
counter = 0;
|
|
23
|
+
}
|
|
24
|
+
export function anatomy(name, parts) {
|
|
25
|
+
return {
|
|
26
|
+
name,
|
|
27
|
+
parts,
|
|
28
|
+
scope(id) {
|
|
29
|
+
const scopeId = id ?? `${name}-${++counter}`;
|
|
30
|
+
return {
|
|
31
|
+
id: scopeId,
|
|
32
|
+
idFor: (part) => `${scopeId}:${part}`,
|
|
33
|
+
attrs: (part) => ({
|
|
34
|
+
id: `${scopeId}:${part}`,
|
|
35
|
+
'data-scope': name,
|
|
36
|
+
'data-part': part,
|
|
37
|
+
}),
|
|
38
|
+
};
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hide sibling subtrees from assistive tech while an overlay is open.
|
|
3
|
+
*
|
|
4
|
+
* Walks from `target` up to the document root, applying `aria-hidden="true"`
|
|
5
|
+
* and `inert` to every sibling at each level. Previous attribute values are
|
|
6
|
+
* recorded and restored on cleanup.
|
|
7
|
+
*
|
|
8
|
+
* Nested calls are supported — each layer only touches elements that haven't
|
|
9
|
+
* been claimed by a higher layer (tracked via a WeakMap reference count).
|
|
10
|
+
*/
|
|
11
|
+
export declare function setAriaHiddenOutside(target: Element): () => void;
|
|
12
|
+
//# sourceMappingURL=aria-hidden.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"aria-hidden.d.ts","sourceRoot":"","sources":["../../src/utils/aria-hidden.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAUH,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,OAAO,GAAG,MAAM,IAAI,CAoChE"}
|