@chromvoid/headless-ui 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +99 -0
- package/dist/a11y-contracts/index.d.ts +23 -0
- package/dist/a11y-contracts/index.js +1 -0
- package/dist/accordion/index.d.ts +78 -0
- package/dist/accordion/index.js +264 -0
- package/dist/adapters/index.d.ts +9 -0
- package/dist/adapters/index.js +1 -0
- package/dist/alert/index.d.ts +33 -0
- package/dist/alert/index.js +54 -0
- package/dist/alert-dialog/index.d.ts +69 -0
- package/dist/alert-dialog/index.js +94 -0
- package/dist/badge/index.d.ts +48 -0
- package/dist/badge/index.js +89 -0
- package/dist/breadcrumb/index.d.ts +55 -0
- package/dist/breadcrumb/index.js +77 -0
- package/dist/button/index.d.ts +46 -0
- package/dist/button/index.js +86 -0
- package/dist/callout/index.d.ts +41 -0
- package/dist/callout/index.js +63 -0
- package/dist/card/index.d.ts +54 -0
- package/dist/card/index.js +103 -0
- package/dist/carousel/index.d.ts +98 -0
- package/dist/carousel/index.js +243 -0
- package/dist/checkbox/index.d.ts +50 -0
- package/dist/checkbox/index.js +87 -0
- package/dist/combobox/index.d.ts +114 -0
- package/dist/combobox/index.js +431 -0
- package/dist/command-palette/index.d.ts +73 -0
- package/dist/command-palette/index.js +147 -0
- package/dist/context-menu/index.d.ts +111 -0
- package/dist/context-menu/index.js +372 -0
- package/dist/copy-button/index.d.ts +62 -0
- package/dist/copy-button/index.js +183 -0
- package/dist/core/index.d.ts +20 -0
- package/dist/core/index.js +2 -0
- package/dist/core/selection.d.ts +5 -0
- package/dist/core/selection.js +39 -0
- package/dist/core/value-range.d.ts +49 -0
- package/dist/core/value-range.js +134 -0
- package/dist/date-picker/index.d.ts +210 -0
- package/dist/date-picker/index.js +895 -0
- package/dist/dialog/index.d.ts +95 -0
- package/dist/dialog/index.js +153 -0
- package/dist/disclosure/index.d.ts +52 -0
- package/dist/disclosure/index.js +159 -0
- package/dist/drawer/index.d.ts +30 -0
- package/dist/drawer/index.js +39 -0
- package/dist/feed/index.d.ts +77 -0
- package/dist/feed/index.js +260 -0
- package/dist/grid/index.d.ts +103 -0
- package/dist/grid/index.js +415 -0
- package/dist/index.d.ts +51 -0
- package/dist/index.js +51 -0
- package/dist/input/index.d.ts +86 -0
- package/dist/input/index.js +156 -0
- package/dist/interactions/composite-navigation.d.ts +69 -0
- package/dist/interactions/composite-navigation.js +169 -0
- package/dist/interactions/index.d.ts +15 -0
- package/dist/interactions/index.js +4 -0
- package/dist/interactions/keyboard-intents.d.ts +16 -0
- package/dist/interactions/keyboard-intents.js +33 -0
- package/dist/interactions/overlay-focus.d.ts +40 -0
- package/dist/interactions/overlay-focus.js +93 -0
- package/dist/interactions/typeahead.d.ts +20 -0
- package/dist/interactions/typeahead.js +41 -0
- package/dist/landmarks/index.d.ts +39 -0
- package/dist/landmarks/index.js +58 -0
- package/dist/link/index.d.ts +34 -0
- package/dist/link/index.js +39 -0
- package/dist/listbox/index.d.ts +92 -0
- package/dist/listbox/index.js +337 -0
- package/dist/menu/index.d.ts +132 -0
- package/dist/menu/index.js +541 -0
- package/dist/menu-button/index.d.ts +71 -0
- package/dist/menu-button/index.js +121 -0
- package/dist/meter/index.d.ts +45 -0
- package/dist/meter/index.js +106 -0
- package/dist/number/index.d.ts +113 -0
- package/dist/number/index.js +252 -0
- package/dist/popover/index.d.ts +70 -0
- package/dist/popover/index.js +126 -0
- package/dist/progress/index.d.ts +49 -0
- package/dist/progress/index.js +79 -0
- package/dist/radio-group/index.d.ts +61 -0
- package/dist/radio-group/index.js +150 -0
- package/dist/select/index.d.ts +92 -0
- package/dist/select/index.js +239 -0
- package/dist/sidebar/index.d.ts +74 -0
- package/dist/sidebar/index.js +186 -0
- package/dist/slider/index.d.ts +61 -0
- package/dist/slider/index.js +150 -0
- package/dist/slider-multi-thumb/index.d.ts +70 -0
- package/dist/slider-multi-thumb/index.js +222 -0
- package/dist/spinbutton/index.d.ts +75 -0
- package/dist/spinbutton/index.js +214 -0
- package/dist/spinner/index.d.ts +1 -0
- package/dist/spinner/index.js +1 -0
- package/dist/spinner/spinner.d.ts +23 -0
- package/dist/spinner/spinner.js +25 -0
- package/dist/switch/index.d.ts +40 -0
- package/dist/switch/index.js +61 -0
- package/dist/table/index.d.ts +117 -0
- package/dist/table/index.js +377 -0
- package/dist/tabs/index.d.ts +63 -0
- package/dist/tabs/index.js +174 -0
- package/dist/textarea/index.d.ts +68 -0
- package/dist/textarea/index.js +137 -0
- package/dist/toast/index.d.ts +67 -0
- package/dist/toast/index.js +145 -0
- package/dist/toolbar/index.d.ts +59 -0
- package/dist/toolbar/index.js +139 -0
- package/dist/tooltip/index.d.ts +52 -0
- package/dist/tooltip/index.js +169 -0
- package/dist/treegrid/index.d.ts +101 -0
- package/dist/treegrid/index.js +463 -0
- package/dist/treeview/index.d.ts +68 -0
- package/dist/treeview/index.js +370 -0
- package/dist/window-splitter/index.d.ts +65 -0
- package/dist/window-splitter/index.js +204 -0
- package/package.json +92 -0
- package/specs/ADR-001-headless-architecture.md +461 -0
- package/specs/ADR-002-repo-release-model.md +108 -0
- package/specs/ADR-003-public-api-versioning.md +136 -0
- package/specs/ADR-004-focus-selection-policy.md +117 -0
- package/specs/IMPLEMENTATION-ROADMAP.md +237 -0
- package/specs/ISSUE-BACKLOG.md +681 -0
- package/specs/RELEASE-CANDIDATE.md +30 -0
- package/specs/components/accordion.md +130 -0
- package/specs/components/alert-dialog.md +72 -0
- package/specs/components/alert.md +65 -0
- package/specs/components/badge.md +220 -0
- package/specs/components/breadcrumb.md +74 -0
- package/specs/components/button.md +115 -0
- package/specs/components/callout.md +195 -0
- package/specs/components/card.md +280 -0
- package/specs/components/carousel.md +140 -0
- package/specs/components/checkbox.md +172 -0
- package/specs/components/combobox.md +423 -0
- package/specs/components/command-palette.md +92 -0
- package/specs/components/context-menu.md +556 -0
- package/specs/components/copy-button.md +293 -0
- package/specs/components/date-picker.md +400 -0
- package/specs/components/dialog.md +298 -0
- package/specs/components/disclosure.md +257 -0
- package/specs/components/drawer.md +353 -0
- package/specs/components/feed.md +265 -0
- package/specs/components/grid.md +186 -0
- package/specs/components/input.md +254 -0
- package/specs/components/landmarks.md +136 -0
- package/specs/components/link.md +134 -0
- package/specs/components/listbox.md +351 -0
- package/specs/components/menu-button.md +76 -0
- package/specs/components/menu.md +623 -0
- package/specs/components/meter.md +149 -0
- package/specs/components/number.md +393 -0
- package/specs/components/popover.md +252 -0
- package/specs/components/progress.md +188 -0
- package/specs/components/radio-group.md +151 -0
- package/specs/components/select.md +144 -0
- package/specs/components/sidebar.md +321 -0
- package/specs/components/slider-multi-thumb.md +78 -0
- package/specs/components/slider.md +84 -0
- package/specs/components/spinbutton.md +140 -0
- package/specs/components/spinner.md +132 -0
- package/specs/components/switch.md +175 -0
- package/specs/components/table.md +403 -0
- package/specs/components/tabs.md +265 -0
- package/specs/components/textarea.md +185 -0
- package/specs/components/toast.md +198 -0
- package/specs/components/toolbar.md +278 -0
- package/specs/components/tooltip.md +252 -0
- package/specs/components/treegrid.md +281 -0
- package/specs/components/treeview.md +91 -0
- package/specs/components/window-splitter.md +297 -0
- package/specs/ops/git-shard-sync.md +107 -0
- package/specs/ops/release-checklist.md +76 -0
- package/specs/release/GAP-TO-GREEN-ISSUES.md +88 -0
- package/specs/release/api-freeze-candidate.md +54 -0
- package/specs/release/changelog-automation.md +76 -0
- package/specs/release/changelog.generated.md +53 -0
- package/specs/release/changelog.patch.generated.md +46 -0
- package/specs/release/consumer-integration.md +53 -0
- package/specs/release/migration-notes-pre-v1.md +40 -0
- package/specs/release/mvp-changelog.md +57 -0
- package/specs/release/release-notes-template.md +61 -0
- package/specs/release/release-rehearsal.md +113 -0
- package/specs/release/semver-deprecation-dry-run.md +89 -0
- package/specs/release/shard-release-drill-report.md +50 -0
- package/specs/release/shard-release-follow-ups.md +31 -0
- package/specs/signals.md +208 -0
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { action, atom } from '@reatom/core';
|
|
2
|
+
const toPositiveInteger = (value) => {
|
|
3
|
+
if (typeof value !== 'number' || !Number.isFinite(value) || value <= 0) {
|
|
4
|
+
return undefined;
|
|
5
|
+
}
|
|
6
|
+
return Math.floor(value);
|
|
7
|
+
};
|
|
8
|
+
const toNonNegativeInteger = (value) => {
|
|
9
|
+
if (value === undefined) {
|
|
10
|
+
return undefined;
|
|
11
|
+
}
|
|
12
|
+
if (!Number.isFinite(value) || value < 0) {
|
|
13
|
+
return undefined;
|
|
14
|
+
}
|
|
15
|
+
return Math.floor(value);
|
|
16
|
+
};
|
|
17
|
+
const normalizeResize = (value) => value === 'none' ? 'none' : 'vertical';
|
|
18
|
+
export function createTextarea(options = {}) {
|
|
19
|
+
const idBase = options.idBase ?? 'textarea';
|
|
20
|
+
const valueAtom = atom(options.value ?? '', `${idBase}.value`);
|
|
21
|
+
const disabledAtom = atom(options.disabled ?? false, `${idBase}.disabled`);
|
|
22
|
+
const readonlyAtom = atom(options.readonly ?? false, `${idBase}.readonly`);
|
|
23
|
+
const requiredAtom = atom(options.required ?? false, `${idBase}.required`);
|
|
24
|
+
const placeholderAtom = atom(options.placeholder ?? '', `${idBase}.placeholder`);
|
|
25
|
+
const rowsAtom = atom(toPositiveInteger(options.rows) ?? 4, `${idBase}.rows`);
|
|
26
|
+
const colsAtom = atom(toPositiveInteger(options.cols) ?? 20, `${idBase}.cols`);
|
|
27
|
+
const minLengthAtom = atom(toNonNegativeInteger(options.minLength), `${idBase}.minLength`);
|
|
28
|
+
const maxLengthAtom = atom(toNonNegativeInteger(options.maxLength), `${idBase}.maxLength`);
|
|
29
|
+
const resizeAtom = atom(normalizeResize(options.resize), `${idBase}.resize`);
|
|
30
|
+
const focusedAtom = atom(false, `${idBase}.focused`);
|
|
31
|
+
const filled = () => valueAtom().length > 0;
|
|
32
|
+
const setValue = action((value) => {
|
|
33
|
+
valueAtom.set(value);
|
|
34
|
+
}, `${idBase}.setValue`);
|
|
35
|
+
const setDisabled = action((disabled) => {
|
|
36
|
+
disabledAtom.set(disabled);
|
|
37
|
+
}, `${idBase}.setDisabled`);
|
|
38
|
+
const setReadonly = action((readonly) => {
|
|
39
|
+
readonlyAtom.set(readonly);
|
|
40
|
+
}, `${idBase}.setReadonly`);
|
|
41
|
+
const setRequired = action((required) => {
|
|
42
|
+
requiredAtom.set(required);
|
|
43
|
+
}, `${idBase}.setRequired`);
|
|
44
|
+
const setPlaceholder = action((placeholder) => {
|
|
45
|
+
placeholderAtom.set(placeholder);
|
|
46
|
+
}, `${idBase}.setPlaceholder`);
|
|
47
|
+
const setRows = action((rows) => {
|
|
48
|
+
const normalized = toPositiveInteger(rows);
|
|
49
|
+
if (normalized === undefined) {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
rowsAtom.set(normalized);
|
|
53
|
+
}, `${idBase}.setRows`);
|
|
54
|
+
const setCols = action((cols) => {
|
|
55
|
+
const normalized = toPositiveInteger(cols);
|
|
56
|
+
if (normalized === undefined) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
colsAtom.set(normalized);
|
|
60
|
+
}, `${idBase}.setCols`);
|
|
61
|
+
const setMinLength = action((minLength) => {
|
|
62
|
+
minLengthAtom.set(toNonNegativeInteger(minLength));
|
|
63
|
+
}, `${idBase}.setMinLength`);
|
|
64
|
+
const setMaxLength = action((maxLength) => {
|
|
65
|
+
maxLengthAtom.set(toNonNegativeInteger(maxLength));
|
|
66
|
+
}, `${idBase}.setMaxLength`);
|
|
67
|
+
const setResize = action((resize) => {
|
|
68
|
+
resizeAtom.set(normalizeResize(resize));
|
|
69
|
+
}, `${idBase}.setResize`);
|
|
70
|
+
const setFocused = action((focused) => {
|
|
71
|
+
focusedAtom.set(focused);
|
|
72
|
+
}, `${idBase}.setFocused`);
|
|
73
|
+
const handleInput = action((value) => {
|
|
74
|
+
if (disabledAtom() || readonlyAtom())
|
|
75
|
+
return;
|
|
76
|
+
valueAtom.set(value);
|
|
77
|
+
options.onInput?.(value);
|
|
78
|
+
}, `${idBase}.handleInput`);
|
|
79
|
+
const contracts = {
|
|
80
|
+
getTextareaProps() {
|
|
81
|
+
const isDisabled = disabledAtom();
|
|
82
|
+
const isReadonly = readonlyAtom();
|
|
83
|
+
const isRequired = requiredAtom();
|
|
84
|
+
const currentPlaceholder = placeholderAtom();
|
|
85
|
+
const minLength = minLengthAtom();
|
|
86
|
+
const maxLength = maxLengthAtom();
|
|
87
|
+
return {
|
|
88
|
+
id: `${idBase}-textarea`,
|
|
89
|
+
'aria-disabled': isDisabled ? 'true' : undefined,
|
|
90
|
+
'aria-readonly': isReadonly ? 'true' : undefined,
|
|
91
|
+
'aria-required': isRequired ? 'true' : undefined,
|
|
92
|
+
placeholder: currentPlaceholder || undefined,
|
|
93
|
+
disabled: isDisabled || undefined,
|
|
94
|
+
readonly: isReadonly || undefined,
|
|
95
|
+
required: isRequired || undefined,
|
|
96
|
+
tabindex: isDisabled ? '-1' : '0',
|
|
97
|
+
rows: rowsAtom(),
|
|
98
|
+
cols: colsAtom(),
|
|
99
|
+
minlength: minLength,
|
|
100
|
+
maxlength: maxLength,
|
|
101
|
+
};
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
const state = {
|
|
105
|
+
value: valueAtom,
|
|
106
|
+
disabled: disabledAtom,
|
|
107
|
+
readonly: readonlyAtom,
|
|
108
|
+
required: requiredAtom,
|
|
109
|
+
placeholder: placeholderAtom,
|
|
110
|
+
rows: rowsAtom,
|
|
111
|
+
cols: colsAtom,
|
|
112
|
+
minLength: minLengthAtom,
|
|
113
|
+
maxLength: maxLengthAtom,
|
|
114
|
+
resize: resizeAtom,
|
|
115
|
+
focused: focusedAtom,
|
|
116
|
+
filled,
|
|
117
|
+
};
|
|
118
|
+
const actions = {
|
|
119
|
+
setValue,
|
|
120
|
+
setDisabled,
|
|
121
|
+
setReadonly,
|
|
122
|
+
setRequired,
|
|
123
|
+
setPlaceholder,
|
|
124
|
+
setRows,
|
|
125
|
+
setCols,
|
|
126
|
+
setMinLength,
|
|
127
|
+
setMaxLength,
|
|
128
|
+
setResize,
|
|
129
|
+
setFocused,
|
|
130
|
+
handleInput,
|
|
131
|
+
};
|
|
132
|
+
return {
|
|
133
|
+
state,
|
|
134
|
+
actions,
|
|
135
|
+
contracts,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { type Atom, type Computed } from '@reatom/core';
|
|
2
|
+
export type ToastLevel = 'info' | 'success' | 'warning' | 'error' | 'loading';
|
|
3
|
+
export interface ToastAction {
|
|
4
|
+
label: string;
|
|
5
|
+
onClick?: () => void;
|
|
6
|
+
}
|
|
7
|
+
export interface ToastItem {
|
|
8
|
+
id: string;
|
|
9
|
+
message: string;
|
|
10
|
+
title?: string;
|
|
11
|
+
level?: ToastLevel;
|
|
12
|
+
durationMs?: number;
|
|
13
|
+
closable?: boolean;
|
|
14
|
+
icon?: string;
|
|
15
|
+
progress?: boolean;
|
|
16
|
+
actions?: readonly ToastAction[];
|
|
17
|
+
}
|
|
18
|
+
export interface CreateToastOptions {
|
|
19
|
+
idBase?: string;
|
|
20
|
+
initialItems?: readonly ToastItem[];
|
|
21
|
+
maxVisible?: number;
|
|
22
|
+
defaultDurationMs?: number;
|
|
23
|
+
ariaLive?: 'polite' | 'assertive';
|
|
24
|
+
}
|
|
25
|
+
export interface ToastState {
|
|
26
|
+
items: Atom<ToastItem[]>;
|
|
27
|
+
visibleItems: Computed<ToastItem[]>;
|
|
28
|
+
isPaused: Atom<boolean>;
|
|
29
|
+
}
|
|
30
|
+
export interface ToastActions {
|
|
31
|
+
push(item: Omit<ToastItem, 'id'> & {
|
|
32
|
+
id?: string;
|
|
33
|
+
}): string;
|
|
34
|
+
dismiss(id: string): void;
|
|
35
|
+
clear(): void;
|
|
36
|
+
pause(): void;
|
|
37
|
+
resume(): void;
|
|
38
|
+
}
|
|
39
|
+
export interface ToastRegionProps {
|
|
40
|
+
id: string;
|
|
41
|
+
role: 'region';
|
|
42
|
+
'aria-live': 'polite' | 'assertive';
|
|
43
|
+
'aria-atomic': 'false';
|
|
44
|
+
}
|
|
45
|
+
export interface ToastItemProps {
|
|
46
|
+
id: string;
|
|
47
|
+
role: 'status' | 'alert';
|
|
48
|
+
'data-level': ToastLevel;
|
|
49
|
+
}
|
|
50
|
+
export interface ToastDismissButtonProps {
|
|
51
|
+
id: string;
|
|
52
|
+
role: 'button';
|
|
53
|
+
tabindex: '0';
|
|
54
|
+
'aria-label': string;
|
|
55
|
+
onClick: () => void;
|
|
56
|
+
}
|
|
57
|
+
export interface ToastContracts {
|
|
58
|
+
getRegionProps(): ToastRegionProps;
|
|
59
|
+
getToastProps(id: string): ToastItemProps;
|
|
60
|
+
getDismissButtonProps(id: string): ToastDismissButtonProps;
|
|
61
|
+
}
|
|
62
|
+
export interface ToastModel {
|
|
63
|
+
readonly state: ToastState;
|
|
64
|
+
readonly actions: ToastActions;
|
|
65
|
+
readonly contracts: ToastContracts;
|
|
66
|
+
}
|
|
67
|
+
export declare function createToast(options?: CreateToastOptions): ToastModel;
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { action, atom, computed } from '@reatom/core';
|
|
2
|
+
export function createToast(options = {}) {
|
|
3
|
+
const idBase = options.idBase ?? 'toast';
|
|
4
|
+
const maxVisible = Math.max(options.maxVisible ?? 3, 1);
|
|
5
|
+
const defaultDurationMs = Math.max(options.defaultDurationMs ?? 5000, 0);
|
|
6
|
+
const ariaLive = options.ariaLive ?? 'polite';
|
|
7
|
+
const itemsAtom = atom([...(options.initialItems ?? [])], `${idBase}.items`);
|
|
8
|
+
const isPausedAtom = atom(false, `${idBase}.isPaused`);
|
|
9
|
+
const visibleItemsAtom = computed(() => itemsAtom().slice(0, maxVisible), `${idBase}.visibleItems`);
|
|
10
|
+
const timers = new Map();
|
|
11
|
+
const remainingMsById = new Map();
|
|
12
|
+
const startedAtById = new Map();
|
|
13
|
+
let nonce = 0;
|
|
14
|
+
const stopTimer = (id) => {
|
|
15
|
+
const timer = timers.get(id);
|
|
16
|
+
if (timer == null)
|
|
17
|
+
return;
|
|
18
|
+
clearTimeout(timer);
|
|
19
|
+
timers.delete(id);
|
|
20
|
+
};
|
|
21
|
+
const clearTracking = (id) => {
|
|
22
|
+
stopTimer(id);
|
|
23
|
+
remainingMsById.delete(id);
|
|
24
|
+
startedAtById.delete(id);
|
|
25
|
+
};
|
|
26
|
+
const dismiss = action((id) => {
|
|
27
|
+
clearTracking(id);
|
|
28
|
+
itemsAtom.set(itemsAtom().filter((item) => item.id !== id));
|
|
29
|
+
}, `${idBase}.dismiss`);
|
|
30
|
+
const scheduleAutoDismiss = (id, durationMs) => {
|
|
31
|
+
if (durationMs <= 0)
|
|
32
|
+
return;
|
|
33
|
+
if (isPausedAtom()) {
|
|
34
|
+
remainingMsById.set(id, durationMs);
|
|
35
|
+
startedAtById.delete(id);
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
stopTimer(id);
|
|
39
|
+
remainingMsById.set(id, durationMs);
|
|
40
|
+
startedAtById.set(id, Date.now());
|
|
41
|
+
const timer = setTimeout(() => {
|
|
42
|
+
clearTracking(id);
|
|
43
|
+
dismiss(id);
|
|
44
|
+
}, durationMs);
|
|
45
|
+
timers.set(id, timer);
|
|
46
|
+
};
|
|
47
|
+
const push = action((item) => {
|
|
48
|
+
const id = item.id ?? `${idBase}-${++nonce}`;
|
|
49
|
+
const next = {
|
|
50
|
+
id,
|
|
51
|
+
message: item.message,
|
|
52
|
+
title: item.title,
|
|
53
|
+
level: item.level ?? 'info',
|
|
54
|
+
durationMs: item.durationMs ?? defaultDurationMs,
|
|
55
|
+
closable: item.closable ?? true,
|
|
56
|
+
icon: item.icon,
|
|
57
|
+
progress: item.progress,
|
|
58
|
+
actions: item.actions,
|
|
59
|
+
};
|
|
60
|
+
itemsAtom.set([next, ...itemsAtom()]);
|
|
61
|
+
scheduleAutoDismiss(id, next.durationMs ?? defaultDurationMs);
|
|
62
|
+
return id;
|
|
63
|
+
}, `${idBase}.push`);
|
|
64
|
+
const clear = action(() => {
|
|
65
|
+
for (const item of itemsAtom()) {
|
|
66
|
+
clearTracking(item.id);
|
|
67
|
+
}
|
|
68
|
+
itemsAtom.set([]);
|
|
69
|
+
}, `${idBase}.clear`);
|
|
70
|
+
const pause = action(() => {
|
|
71
|
+
if (isPausedAtom())
|
|
72
|
+
return;
|
|
73
|
+
isPausedAtom.set(true);
|
|
74
|
+
const now = Date.now();
|
|
75
|
+
for (const id of timers.keys()) {
|
|
76
|
+
const startedAt = startedAtById.get(id) ?? now;
|
|
77
|
+
const scheduledMs = remainingMsById.get(id) ?? 0;
|
|
78
|
+
const elapsedMs = Math.max(now - startedAt, 0);
|
|
79
|
+
const remainingMs = Math.max(scheduledMs - elapsedMs, 0);
|
|
80
|
+
remainingMsById.set(id, remainingMs);
|
|
81
|
+
startedAtById.delete(id);
|
|
82
|
+
stopTimer(id);
|
|
83
|
+
}
|
|
84
|
+
}, `${idBase}.pause`);
|
|
85
|
+
const resume = action(() => {
|
|
86
|
+
if (!isPausedAtom())
|
|
87
|
+
return;
|
|
88
|
+
isPausedAtom.set(false);
|
|
89
|
+
for (const item of itemsAtom()) {
|
|
90
|
+
const durationMs = remainingMsById.get(item.id) ?? item.durationMs ?? defaultDurationMs;
|
|
91
|
+
scheduleAutoDismiss(item.id, durationMs);
|
|
92
|
+
}
|
|
93
|
+
}, `${idBase}.resume`);
|
|
94
|
+
const actions = {
|
|
95
|
+
push,
|
|
96
|
+
dismiss,
|
|
97
|
+
clear,
|
|
98
|
+
pause,
|
|
99
|
+
resume,
|
|
100
|
+
};
|
|
101
|
+
const contracts = {
|
|
102
|
+
getRegionProps() {
|
|
103
|
+
return {
|
|
104
|
+
id: `${idBase}-region`,
|
|
105
|
+
role: 'region',
|
|
106
|
+
'aria-live': ariaLive,
|
|
107
|
+
'aria-atomic': 'false',
|
|
108
|
+
};
|
|
109
|
+
},
|
|
110
|
+
getToastProps(id) {
|
|
111
|
+
const toast = itemsAtom().find((item) => item.id === id);
|
|
112
|
+
if (!toast) {
|
|
113
|
+
throw new Error(`Unknown toast id: ${id}`);
|
|
114
|
+
}
|
|
115
|
+
const level = toast.level ?? 'info';
|
|
116
|
+
return {
|
|
117
|
+
id: `${idBase}-item-${id}`,
|
|
118
|
+
role: level === 'error' || level === 'warning' ? 'alert' : 'status',
|
|
119
|
+
'data-level': level,
|
|
120
|
+
};
|
|
121
|
+
},
|
|
122
|
+
getDismissButtonProps(id) {
|
|
123
|
+
return {
|
|
124
|
+
id: `${idBase}-dismiss-${id}`,
|
|
125
|
+
role: 'button',
|
|
126
|
+
tabindex: '0',
|
|
127
|
+
'aria-label': 'Dismiss notification',
|
|
128
|
+
onClick: () => dismiss(id),
|
|
129
|
+
};
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
const state = {
|
|
133
|
+
items: itemsAtom,
|
|
134
|
+
visibleItems: visibleItemsAtom,
|
|
135
|
+
isPaused: isPausedAtom,
|
|
136
|
+
};
|
|
137
|
+
for (const item of itemsAtom()) {
|
|
138
|
+
scheduleAutoDismiss(item.id, item.durationMs ?? defaultDurationMs);
|
|
139
|
+
}
|
|
140
|
+
return {
|
|
141
|
+
state,
|
|
142
|
+
actions,
|
|
143
|
+
contracts,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { type Atom } from '@reatom/core';
|
|
2
|
+
import { type CompositeNavigationOrientation } from '../interactions/composite-navigation.js';
|
|
3
|
+
export interface ToolbarItem {
|
|
4
|
+
id: string;
|
|
5
|
+
disabled?: boolean;
|
|
6
|
+
separator?: boolean;
|
|
7
|
+
}
|
|
8
|
+
export interface CreateToolbarOptions {
|
|
9
|
+
items: readonly ToolbarItem[];
|
|
10
|
+
idBase?: string;
|
|
11
|
+
orientation?: CompositeNavigationOrientation;
|
|
12
|
+
wrap?: boolean;
|
|
13
|
+
ariaLabel?: string;
|
|
14
|
+
initialActiveId?: string | null;
|
|
15
|
+
}
|
|
16
|
+
export interface ToolbarState {
|
|
17
|
+
activeId: Atom<string | null>;
|
|
18
|
+
lastActiveId: Atom<string | null>;
|
|
19
|
+
orientation: CompositeNavigationOrientation;
|
|
20
|
+
}
|
|
21
|
+
export interface ToolbarActions {
|
|
22
|
+
setActive(id: string): void;
|
|
23
|
+
moveNext(): void;
|
|
24
|
+
movePrev(): void;
|
|
25
|
+
moveFirst(): void;
|
|
26
|
+
moveLast(): void;
|
|
27
|
+
handleKeyDown(event: Pick<KeyboardEvent, 'key'>): void;
|
|
28
|
+
handleToolbarFocus(): void;
|
|
29
|
+
handleToolbarBlur(): void;
|
|
30
|
+
}
|
|
31
|
+
export interface ToolbarRootProps {
|
|
32
|
+
id: string;
|
|
33
|
+
role: 'toolbar';
|
|
34
|
+
'aria-orientation': CompositeNavigationOrientation;
|
|
35
|
+
'aria-label'?: string;
|
|
36
|
+
}
|
|
37
|
+
export interface ToolbarItemProps {
|
|
38
|
+
id: string;
|
|
39
|
+
tabindex: '0' | '-1';
|
|
40
|
+
'aria-disabled'?: 'true';
|
|
41
|
+
'data-active': 'true' | 'false';
|
|
42
|
+
onFocus: () => void;
|
|
43
|
+
}
|
|
44
|
+
export interface ToolbarSeparatorProps {
|
|
45
|
+
id: string;
|
|
46
|
+
role: 'separator';
|
|
47
|
+
'aria-orientation': 'vertical' | 'horizontal';
|
|
48
|
+
}
|
|
49
|
+
export interface ToolbarContracts {
|
|
50
|
+
getRootProps(): ToolbarRootProps;
|
|
51
|
+
getItemProps(id: string): ToolbarItemProps;
|
|
52
|
+
getSeparatorProps(id: string): ToolbarSeparatorProps;
|
|
53
|
+
}
|
|
54
|
+
export interface ToolbarModel {
|
|
55
|
+
readonly state: ToolbarState;
|
|
56
|
+
readonly actions: ToolbarActions;
|
|
57
|
+
readonly contracts: ToolbarContracts;
|
|
58
|
+
}
|
|
59
|
+
export declare function createToolbar(options: CreateToolbarOptions): ToolbarModel;
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { action, atom } from '@reatom/core';
|
|
2
|
+
import { createCompositeNavigation, } from '../interactions/composite-navigation.js';
|
|
3
|
+
export function createToolbar(options) {
|
|
4
|
+
const idBase = options.idBase ?? 'toolbar';
|
|
5
|
+
const orientation = options.orientation ?? 'horizontal';
|
|
6
|
+
const allItems = options.items;
|
|
7
|
+
const navigableItems = allItems.filter((item) => !item.separator);
|
|
8
|
+
const resolveInitialActiveId = () => {
|
|
9
|
+
const enabledNavigable = navigableItems.filter((item) => !item.disabled);
|
|
10
|
+
if (options.initialActiveId != null &&
|
|
11
|
+
enabledNavigable.some((item) => item.id === options.initialActiveId)) {
|
|
12
|
+
return options.initialActiveId;
|
|
13
|
+
}
|
|
14
|
+
return enabledNavigable[0]?.id ?? null;
|
|
15
|
+
};
|
|
16
|
+
const navigation = createCompositeNavigation({
|
|
17
|
+
idBase: `${idBase}.nav`,
|
|
18
|
+
items: navigableItems,
|
|
19
|
+
orientation,
|
|
20
|
+
focusStrategy: 'roving-tabindex',
|
|
21
|
+
wrapMode: options.wrap === false ? 'clamp' : 'wrap',
|
|
22
|
+
initialActiveId: resolveInitialActiveId(),
|
|
23
|
+
});
|
|
24
|
+
const lastActiveIdAtom = atom(null, `${idBase}.lastActiveId`);
|
|
25
|
+
const setActive = action((id) => {
|
|
26
|
+
const item = allItems.find((i) => i.id === id);
|
|
27
|
+
if (!item || item.separator)
|
|
28
|
+
return;
|
|
29
|
+
navigation.actions.setActive(id);
|
|
30
|
+
}, `${idBase}.setActive`);
|
|
31
|
+
const moveNext = action(() => {
|
|
32
|
+
navigation.actions.moveNext();
|
|
33
|
+
}, `${idBase}.moveNext`);
|
|
34
|
+
const movePrev = action(() => {
|
|
35
|
+
navigation.actions.movePrev();
|
|
36
|
+
}, `${idBase}.movePrev`);
|
|
37
|
+
const moveFirst = action(() => {
|
|
38
|
+
navigation.actions.moveFirst();
|
|
39
|
+
}, `${idBase}.moveFirst`);
|
|
40
|
+
const moveLast = action(() => {
|
|
41
|
+
navigation.actions.moveLast();
|
|
42
|
+
}, `${idBase}.moveLast`);
|
|
43
|
+
const handleKeyDown = action((event) => {
|
|
44
|
+
switch (event.key) {
|
|
45
|
+
case 'Home':
|
|
46
|
+
moveFirst();
|
|
47
|
+
return;
|
|
48
|
+
case 'End':
|
|
49
|
+
moveLast();
|
|
50
|
+
return;
|
|
51
|
+
case 'ArrowRight':
|
|
52
|
+
if (orientation === 'horizontal') {
|
|
53
|
+
moveNext();
|
|
54
|
+
}
|
|
55
|
+
return;
|
|
56
|
+
case 'ArrowLeft':
|
|
57
|
+
if (orientation === 'horizontal') {
|
|
58
|
+
movePrev();
|
|
59
|
+
}
|
|
60
|
+
return;
|
|
61
|
+
case 'ArrowDown':
|
|
62
|
+
if (orientation === 'vertical') {
|
|
63
|
+
moveNext();
|
|
64
|
+
}
|
|
65
|
+
return;
|
|
66
|
+
case 'ArrowUp':
|
|
67
|
+
if (orientation === 'vertical') {
|
|
68
|
+
movePrev();
|
|
69
|
+
}
|
|
70
|
+
return;
|
|
71
|
+
default:
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
}, `${idBase}.handleKeyDown`);
|
|
75
|
+
const handleToolbarBlur = action(() => {
|
|
76
|
+
lastActiveIdAtom.set(navigation.state.activeId());
|
|
77
|
+
}, `${idBase}.handleToolbarBlur`);
|
|
78
|
+
const handleToolbarFocus = action(() => {
|
|
79
|
+
const lastId = lastActiveIdAtom();
|
|
80
|
+
if (lastId == null)
|
|
81
|
+
return;
|
|
82
|
+
const item = navigableItems.find((i) => i.id === lastId);
|
|
83
|
+
if (!item || item.disabled)
|
|
84
|
+
return;
|
|
85
|
+
navigation.actions.setActive(lastId);
|
|
86
|
+
}, `${idBase}.handleToolbarFocus`);
|
|
87
|
+
const actions = {
|
|
88
|
+
setActive,
|
|
89
|
+
moveNext,
|
|
90
|
+
movePrev,
|
|
91
|
+
moveFirst,
|
|
92
|
+
moveLast,
|
|
93
|
+
handleKeyDown,
|
|
94
|
+
handleToolbarFocus,
|
|
95
|
+
handleToolbarBlur,
|
|
96
|
+
};
|
|
97
|
+
const contracts = {
|
|
98
|
+
getRootProps() {
|
|
99
|
+
return {
|
|
100
|
+
id: `${idBase}-root`,
|
|
101
|
+
role: 'toolbar',
|
|
102
|
+
'aria-orientation': orientation,
|
|
103
|
+
'aria-label': options.ariaLabel,
|
|
104
|
+
};
|
|
105
|
+
},
|
|
106
|
+
getItemProps(id) {
|
|
107
|
+
const item = navigation.contracts.getItemFocusProps(id);
|
|
108
|
+
return {
|
|
109
|
+
...item,
|
|
110
|
+
onFocus: () => setActive(id),
|
|
111
|
+
};
|
|
112
|
+
},
|
|
113
|
+
getSeparatorProps(id) {
|
|
114
|
+
const item = allItems.find((i) => i.id === id);
|
|
115
|
+
if (!item) {
|
|
116
|
+
throw new Error(`Unknown toolbar item id: ${id}`);
|
|
117
|
+
}
|
|
118
|
+
if (!item.separator) {
|
|
119
|
+
throw new Error(`Item "${id}" is not a separator`);
|
|
120
|
+
}
|
|
121
|
+
const perpendicularOrientation = orientation === 'horizontal' ? 'vertical' : 'horizontal';
|
|
122
|
+
return {
|
|
123
|
+
id: `${idBase}-separator-${id}`,
|
|
124
|
+
role: 'separator',
|
|
125
|
+
'aria-orientation': perpendicularOrientation,
|
|
126
|
+
};
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
const state = {
|
|
130
|
+
activeId: navigation.state.activeId,
|
|
131
|
+
lastActiveId: lastActiveIdAtom,
|
|
132
|
+
orientation,
|
|
133
|
+
};
|
|
134
|
+
return {
|
|
135
|
+
state,
|
|
136
|
+
actions,
|
|
137
|
+
contracts,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { type Atom } from '@reatom/core';
|
|
2
|
+
export interface CreateTooltipOptions {
|
|
3
|
+
idBase?: string;
|
|
4
|
+
initialOpen?: boolean;
|
|
5
|
+
isDisabled?: boolean;
|
|
6
|
+
showDelay?: number;
|
|
7
|
+
hideDelay?: number;
|
|
8
|
+
trigger?: string;
|
|
9
|
+
}
|
|
10
|
+
export interface TooltipState {
|
|
11
|
+
isOpen: Atom<boolean>;
|
|
12
|
+
isDisabled: Atom<boolean>;
|
|
13
|
+
}
|
|
14
|
+
export interface TooltipActions {
|
|
15
|
+
open(): void;
|
|
16
|
+
close(): void;
|
|
17
|
+
show(): void;
|
|
18
|
+
hide(): void;
|
|
19
|
+
setDisabled(value: boolean): void;
|
|
20
|
+
handleKeyDown(event: Pick<KeyboardEvent, 'key'>): void;
|
|
21
|
+
handlePointerEnter(): void;
|
|
22
|
+
handlePointerLeave(): void;
|
|
23
|
+
handleFocus(): void;
|
|
24
|
+
handleBlur(): void;
|
|
25
|
+
handleClick(): void;
|
|
26
|
+
}
|
|
27
|
+
export interface TooltipTriggerProps {
|
|
28
|
+
id: string;
|
|
29
|
+
'aria-describedby'?: string;
|
|
30
|
+
onPointerEnter?: () => void;
|
|
31
|
+
onPointerLeave?: () => void;
|
|
32
|
+
onFocus?: () => void;
|
|
33
|
+
onBlur?: () => void;
|
|
34
|
+
onClick?: () => void;
|
|
35
|
+
onKeyDown: (event: Pick<KeyboardEvent, 'key'>) => void;
|
|
36
|
+
}
|
|
37
|
+
export interface TooltipContentProps {
|
|
38
|
+
id: string;
|
|
39
|
+
role: 'tooltip';
|
|
40
|
+
tabindex: '-1';
|
|
41
|
+
hidden: boolean;
|
|
42
|
+
}
|
|
43
|
+
export interface TooltipContracts {
|
|
44
|
+
getTriggerProps(): TooltipTriggerProps;
|
|
45
|
+
getTooltipProps(): TooltipContentProps;
|
|
46
|
+
}
|
|
47
|
+
export interface TooltipModel {
|
|
48
|
+
readonly state: TooltipState;
|
|
49
|
+
readonly actions: TooltipActions;
|
|
50
|
+
readonly contracts: TooltipContracts;
|
|
51
|
+
}
|
|
52
|
+
export declare function createTooltip(options?: CreateTooltipOptions): TooltipModel;
|