@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.
Files changed (191) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +99 -0
  3. package/dist/a11y-contracts/index.d.ts +23 -0
  4. package/dist/a11y-contracts/index.js +1 -0
  5. package/dist/accordion/index.d.ts +78 -0
  6. package/dist/accordion/index.js +264 -0
  7. package/dist/adapters/index.d.ts +9 -0
  8. package/dist/adapters/index.js +1 -0
  9. package/dist/alert/index.d.ts +33 -0
  10. package/dist/alert/index.js +54 -0
  11. package/dist/alert-dialog/index.d.ts +69 -0
  12. package/dist/alert-dialog/index.js +94 -0
  13. package/dist/badge/index.d.ts +48 -0
  14. package/dist/badge/index.js +89 -0
  15. package/dist/breadcrumb/index.d.ts +55 -0
  16. package/dist/breadcrumb/index.js +77 -0
  17. package/dist/button/index.d.ts +46 -0
  18. package/dist/button/index.js +86 -0
  19. package/dist/callout/index.d.ts +41 -0
  20. package/dist/callout/index.js +63 -0
  21. package/dist/card/index.d.ts +54 -0
  22. package/dist/card/index.js +103 -0
  23. package/dist/carousel/index.d.ts +98 -0
  24. package/dist/carousel/index.js +243 -0
  25. package/dist/checkbox/index.d.ts +50 -0
  26. package/dist/checkbox/index.js +87 -0
  27. package/dist/combobox/index.d.ts +114 -0
  28. package/dist/combobox/index.js +431 -0
  29. package/dist/command-palette/index.d.ts +73 -0
  30. package/dist/command-palette/index.js +147 -0
  31. package/dist/context-menu/index.d.ts +111 -0
  32. package/dist/context-menu/index.js +372 -0
  33. package/dist/copy-button/index.d.ts +62 -0
  34. package/dist/copy-button/index.js +183 -0
  35. package/dist/core/index.d.ts +20 -0
  36. package/dist/core/index.js +2 -0
  37. package/dist/core/selection.d.ts +5 -0
  38. package/dist/core/selection.js +39 -0
  39. package/dist/core/value-range.d.ts +49 -0
  40. package/dist/core/value-range.js +134 -0
  41. package/dist/date-picker/index.d.ts +210 -0
  42. package/dist/date-picker/index.js +895 -0
  43. package/dist/dialog/index.d.ts +95 -0
  44. package/dist/dialog/index.js +153 -0
  45. package/dist/disclosure/index.d.ts +52 -0
  46. package/dist/disclosure/index.js +159 -0
  47. package/dist/drawer/index.d.ts +30 -0
  48. package/dist/drawer/index.js +39 -0
  49. package/dist/feed/index.d.ts +77 -0
  50. package/dist/feed/index.js +260 -0
  51. package/dist/grid/index.d.ts +103 -0
  52. package/dist/grid/index.js +415 -0
  53. package/dist/index.d.ts +51 -0
  54. package/dist/index.js +51 -0
  55. package/dist/input/index.d.ts +86 -0
  56. package/dist/input/index.js +156 -0
  57. package/dist/interactions/composite-navigation.d.ts +69 -0
  58. package/dist/interactions/composite-navigation.js +169 -0
  59. package/dist/interactions/index.d.ts +15 -0
  60. package/dist/interactions/index.js +4 -0
  61. package/dist/interactions/keyboard-intents.d.ts +16 -0
  62. package/dist/interactions/keyboard-intents.js +33 -0
  63. package/dist/interactions/overlay-focus.d.ts +40 -0
  64. package/dist/interactions/overlay-focus.js +93 -0
  65. package/dist/interactions/typeahead.d.ts +20 -0
  66. package/dist/interactions/typeahead.js +41 -0
  67. package/dist/landmarks/index.d.ts +39 -0
  68. package/dist/landmarks/index.js +58 -0
  69. package/dist/link/index.d.ts +34 -0
  70. package/dist/link/index.js +39 -0
  71. package/dist/listbox/index.d.ts +92 -0
  72. package/dist/listbox/index.js +337 -0
  73. package/dist/menu/index.d.ts +132 -0
  74. package/dist/menu/index.js +541 -0
  75. package/dist/menu-button/index.d.ts +71 -0
  76. package/dist/menu-button/index.js +121 -0
  77. package/dist/meter/index.d.ts +45 -0
  78. package/dist/meter/index.js +106 -0
  79. package/dist/number/index.d.ts +113 -0
  80. package/dist/number/index.js +252 -0
  81. package/dist/popover/index.d.ts +70 -0
  82. package/dist/popover/index.js +126 -0
  83. package/dist/progress/index.d.ts +49 -0
  84. package/dist/progress/index.js +79 -0
  85. package/dist/radio-group/index.d.ts +61 -0
  86. package/dist/radio-group/index.js +150 -0
  87. package/dist/select/index.d.ts +92 -0
  88. package/dist/select/index.js +239 -0
  89. package/dist/sidebar/index.d.ts +74 -0
  90. package/dist/sidebar/index.js +186 -0
  91. package/dist/slider/index.d.ts +61 -0
  92. package/dist/slider/index.js +150 -0
  93. package/dist/slider-multi-thumb/index.d.ts +70 -0
  94. package/dist/slider-multi-thumb/index.js +222 -0
  95. package/dist/spinbutton/index.d.ts +75 -0
  96. package/dist/spinbutton/index.js +214 -0
  97. package/dist/spinner/index.d.ts +1 -0
  98. package/dist/spinner/index.js +1 -0
  99. package/dist/spinner/spinner.d.ts +23 -0
  100. package/dist/spinner/spinner.js +25 -0
  101. package/dist/switch/index.d.ts +40 -0
  102. package/dist/switch/index.js +61 -0
  103. package/dist/table/index.d.ts +117 -0
  104. package/dist/table/index.js +377 -0
  105. package/dist/tabs/index.d.ts +63 -0
  106. package/dist/tabs/index.js +174 -0
  107. package/dist/textarea/index.d.ts +68 -0
  108. package/dist/textarea/index.js +137 -0
  109. package/dist/toast/index.d.ts +67 -0
  110. package/dist/toast/index.js +145 -0
  111. package/dist/toolbar/index.d.ts +59 -0
  112. package/dist/toolbar/index.js +139 -0
  113. package/dist/tooltip/index.d.ts +52 -0
  114. package/dist/tooltip/index.js +169 -0
  115. package/dist/treegrid/index.d.ts +101 -0
  116. package/dist/treegrid/index.js +463 -0
  117. package/dist/treeview/index.d.ts +68 -0
  118. package/dist/treeview/index.js +370 -0
  119. package/dist/window-splitter/index.d.ts +65 -0
  120. package/dist/window-splitter/index.js +204 -0
  121. package/package.json +92 -0
  122. package/specs/ADR-001-headless-architecture.md +461 -0
  123. package/specs/ADR-002-repo-release-model.md +108 -0
  124. package/specs/ADR-003-public-api-versioning.md +136 -0
  125. package/specs/ADR-004-focus-selection-policy.md +117 -0
  126. package/specs/IMPLEMENTATION-ROADMAP.md +237 -0
  127. package/specs/ISSUE-BACKLOG.md +681 -0
  128. package/specs/RELEASE-CANDIDATE.md +30 -0
  129. package/specs/components/accordion.md +130 -0
  130. package/specs/components/alert-dialog.md +72 -0
  131. package/specs/components/alert.md +65 -0
  132. package/specs/components/badge.md +220 -0
  133. package/specs/components/breadcrumb.md +74 -0
  134. package/specs/components/button.md +115 -0
  135. package/specs/components/callout.md +195 -0
  136. package/specs/components/card.md +280 -0
  137. package/specs/components/carousel.md +140 -0
  138. package/specs/components/checkbox.md +172 -0
  139. package/specs/components/combobox.md +423 -0
  140. package/specs/components/command-palette.md +92 -0
  141. package/specs/components/context-menu.md +556 -0
  142. package/specs/components/copy-button.md +293 -0
  143. package/specs/components/date-picker.md +400 -0
  144. package/specs/components/dialog.md +298 -0
  145. package/specs/components/disclosure.md +257 -0
  146. package/specs/components/drawer.md +353 -0
  147. package/specs/components/feed.md +265 -0
  148. package/specs/components/grid.md +186 -0
  149. package/specs/components/input.md +254 -0
  150. package/specs/components/landmarks.md +136 -0
  151. package/specs/components/link.md +134 -0
  152. package/specs/components/listbox.md +351 -0
  153. package/specs/components/menu-button.md +76 -0
  154. package/specs/components/menu.md +623 -0
  155. package/specs/components/meter.md +149 -0
  156. package/specs/components/number.md +393 -0
  157. package/specs/components/popover.md +252 -0
  158. package/specs/components/progress.md +188 -0
  159. package/specs/components/radio-group.md +151 -0
  160. package/specs/components/select.md +144 -0
  161. package/specs/components/sidebar.md +321 -0
  162. package/specs/components/slider-multi-thumb.md +78 -0
  163. package/specs/components/slider.md +84 -0
  164. package/specs/components/spinbutton.md +140 -0
  165. package/specs/components/spinner.md +132 -0
  166. package/specs/components/switch.md +175 -0
  167. package/specs/components/table.md +403 -0
  168. package/specs/components/tabs.md +265 -0
  169. package/specs/components/textarea.md +185 -0
  170. package/specs/components/toast.md +198 -0
  171. package/specs/components/toolbar.md +278 -0
  172. package/specs/components/tooltip.md +252 -0
  173. package/specs/components/treegrid.md +281 -0
  174. package/specs/components/treeview.md +91 -0
  175. package/specs/components/window-splitter.md +297 -0
  176. package/specs/ops/git-shard-sync.md +107 -0
  177. package/specs/ops/release-checklist.md +76 -0
  178. package/specs/release/GAP-TO-GREEN-ISSUES.md +88 -0
  179. package/specs/release/api-freeze-candidate.md +54 -0
  180. package/specs/release/changelog-automation.md +76 -0
  181. package/specs/release/changelog.generated.md +53 -0
  182. package/specs/release/changelog.patch.generated.md +46 -0
  183. package/specs/release/consumer-integration.md +53 -0
  184. package/specs/release/migration-notes-pre-v1.md +40 -0
  185. package/specs/release/mvp-changelog.md +57 -0
  186. package/specs/release/release-notes-template.md +61 -0
  187. package/specs/release/release-rehearsal.md +113 -0
  188. package/specs/release/semver-deprecation-dry-run.md +89 -0
  189. package/specs/release/shard-release-drill-report.md +50 -0
  190. package/specs/release/shard-release-follow-ups.md +31 -0
  191. package/specs/signals.md +208 -0
@@ -0,0 +1,252 @@
1
+ import { action, atom } from '@reatom/core';
2
+ import { createSpinbutton } from '../spinbutton/index.js';
3
+ export function createNumber(options = {}) {
4
+ const idBase = options.idBase ?? 'number';
5
+ const spinbutton = createSpinbutton({
6
+ idBase,
7
+ value: options.value,
8
+ min: options.min,
9
+ max: options.max,
10
+ step: options.step,
11
+ largeStep: options.largeStep,
12
+ isDisabled: options.disabled,
13
+ isReadOnly: options.readonly,
14
+ ariaLabel: options.ariaLabel,
15
+ ariaLabelledBy: options.ariaLabelledBy,
16
+ ariaDescribedBy: options.ariaDescribedBy,
17
+ formatValueText: options.formatValueText,
18
+ onValueChange: options.onValueChange,
19
+ });
20
+ const focusedAtom = atom(false, `${idBase}.focused`);
21
+ const clearableAtom = atom(options.clearable ?? false, `${idBase}.clearable`);
22
+ const stepperAtom = atom(options.stepper ?? false, `${idBase}.stepper`);
23
+ const draftTextAtom = atom(null, `${idBase}.draftText`);
24
+ const placeholderAtom = atom(options.placeholder ?? '', `${idBase}.placeholder`);
25
+ const requiredAtom = atom(options.required ?? false, `${idBase}.required`);
26
+ const rawDefault = options.defaultValue ?? options.min ?? 0;
27
+ const defaultValueAtom = atom(rawDefault, `${idBase}.defaultValue`);
28
+ const filled = () => spinbutton.state.value() !== defaultValueAtom();
29
+ const showClearButton = () => clearableAtom() && filled() && !spinbutton.state.isDisabled() && !spinbutton.state.isReadOnly();
30
+ const forceSetValue = (value) => {
31
+ const wasDisabled = spinbutton.state.isDisabled();
32
+ const wasReadOnly = spinbutton.state.isReadOnly();
33
+ if (wasDisabled)
34
+ spinbutton.actions.setDisabled(false);
35
+ if (wasReadOnly)
36
+ spinbutton.actions.setReadOnly(false);
37
+ spinbutton.actions.setValue(value);
38
+ if (wasDisabled)
39
+ spinbutton.actions.setDisabled(true);
40
+ if (wasReadOnly)
41
+ spinbutton.actions.setReadOnly(true);
42
+ };
43
+ const setValue = action((value) => {
44
+ forceSetValue(value);
45
+ draftTextAtom.set(null);
46
+ }, `${idBase}.setValue`);
47
+ const increment = action(() => {
48
+ spinbutton.actions.increment();
49
+ draftTextAtom.set(null);
50
+ }, `${idBase}.increment`);
51
+ const decrement = action(() => {
52
+ spinbutton.actions.decrement();
53
+ draftTextAtom.set(null);
54
+ }, `${idBase}.decrement`);
55
+ const incrementLarge = action(() => {
56
+ spinbutton.actions.incrementLarge();
57
+ draftTextAtom.set(null);
58
+ }, `${idBase}.incrementLarge`);
59
+ const decrementLarge = action(() => {
60
+ spinbutton.actions.decrementLarge();
61
+ draftTextAtom.set(null);
62
+ }, `${idBase}.decrementLarge`);
63
+ const setFirst = action(() => {
64
+ spinbutton.actions.setFirst();
65
+ draftTextAtom.set(null);
66
+ }, `${idBase}.setFirst`);
67
+ const setLast = action(() => {
68
+ spinbutton.actions.setLast();
69
+ draftTextAtom.set(null);
70
+ }, `${idBase}.setLast`);
71
+ const setDisabled = action((v) => {
72
+ spinbutton.actions.setDisabled(v);
73
+ }, `${idBase}.setDisabled`);
74
+ const setReadOnly = action((v) => {
75
+ spinbutton.actions.setReadOnly(v);
76
+ }, `${idBase}.setReadOnly`);
77
+ const setRequired = action((v) => {
78
+ requiredAtom.set(v);
79
+ }, `${idBase}.setRequired`);
80
+ const setClearable = action((v) => {
81
+ clearableAtom.set(v);
82
+ }, `${idBase}.setClearable`);
83
+ const setStepper = action((v) => {
84
+ stepperAtom.set(v);
85
+ }, `${idBase}.setStepper`);
86
+ const setPlaceholder = action((v) => {
87
+ placeholderAtom.set(v);
88
+ }, `${idBase}.setPlaceholder`);
89
+ const setDraftText = action((v) => {
90
+ draftTextAtom.set(v);
91
+ }, `${idBase}.setDraftText`);
92
+ const clear = action(() => {
93
+ if (spinbutton.state.isDisabled() || spinbutton.state.isReadOnly())
94
+ return;
95
+ forceSetValue(defaultValueAtom());
96
+ draftTextAtom.set(null);
97
+ options.onClear?.();
98
+ }, `${idBase}.clear`);
99
+ const commitDraft = action(() => {
100
+ const draft = draftTextAtom();
101
+ if (draft === null)
102
+ return;
103
+ if (draft.trim() === '') {
104
+ clear();
105
+ return;
106
+ }
107
+ const parsed = parseFloat(draft);
108
+ if (Number.isFinite(parsed)) {
109
+ forceSetValue(parsed);
110
+ }
111
+ draftTextAtom.set(null);
112
+ }, `${idBase}.commitDraft`);
113
+ const handleInput = action((text) => {
114
+ if (spinbutton.state.isDisabled() || spinbutton.state.isReadOnly())
115
+ return;
116
+ draftTextAtom.set(text);
117
+ }, `${idBase}.handleInput`);
118
+ const setFocused = action((v) => {
119
+ focusedAtom.set(v);
120
+ if (!v) {
121
+ commitDraft();
122
+ }
123
+ }, `${idBase}.setFocused`);
124
+ const handleKeyDown = action((event) => {
125
+ if (event.key === 'Escape') {
126
+ if (clearableAtom() && filled() && !spinbutton.state.isDisabled() && !spinbutton.state.isReadOnly()) {
127
+ clear();
128
+ event.preventDefault?.();
129
+ }
130
+ return;
131
+ }
132
+ if (event.key === 'Enter') {
133
+ commitDraft();
134
+ event.preventDefault?.();
135
+ return;
136
+ }
137
+ spinbutton.actions.handleKeyDown(event);
138
+ draftTextAtom.set(null);
139
+ }, `${idBase}.handleKeyDown`);
140
+ const contracts = {
141
+ getInputProps() {
142
+ const value = spinbutton.state.value();
143
+ const isDisabled = spinbutton.state.isDisabled();
144
+ const isReadOnly = spinbutton.state.isReadOnly();
145
+ const isRequired = requiredAtom();
146
+ const currentPlaceholder = placeholderAtom();
147
+ const min = spinbutton.state.min();
148
+ const max = spinbutton.state.max();
149
+ return {
150
+ id: `${idBase}-input`,
151
+ role: 'spinbutton',
152
+ tabindex: isDisabled ? '-1' : '0',
153
+ inputmode: 'decimal',
154
+ 'aria-valuenow': String(value),
155
+ 'aria-valuemin': min != null ? String(min) : undefined,
156
+ 'aria-valuemax': max != null ? String(max) : undefined,
157
+ 'aria-valuetext': options.formatValueText?.(value),
158
+ 'aria-disabled': isDisabled ? 'true' : undefined,
159
+ 'aria-readonly': isReadOnly ? 'true' : undefined,
160
+ 'aria-required': isRequired ? 'true' : undefined,
161
+ 'aria-label': options.ariaLabel,
162
+ 'aria-labelledby': options.ariaLabelledBy,
163
+ 'aria-describedby': options.ariaDescribedBy,
164
+ placeholder: currentPlaceholder || undefined,
165
+ autocomplete: 'off',
166
+ };
167
+ },
168
+ getIncrementButtonProps() {
169
+ const sbProps = spinbutton.contracts.getIncrementButtonProps();
170
+ const isHidden = !stepperAtom();
171
+ return {
172
+ id: sbProps.id,
173
+ tabindex: sbProps.tabindex,
174
+ 'aria-label': sbProps['aria-label'],
175
+ 'aria-disabled': sbProps['aria-disabled'],
176
+ hidden: isHidden ? true : undefined,
177
+ 'aria-hidden': isHidden ? 'true' : undefined,
178
+ onClick: increment,
179
+ };
180
+ },
181
+ getDecrementButtonProps() {
182
+ const sbProps = spinbutton.contracts.getDecrementButtonProps();
183
+ const isHidden = !stepperAtom();
184
+ return {
185
+ id: sbProps.id,
186
+ tabindex: sbProps.tabindex,
187
+ 'aria-label': sbProps['aria-label'],
188
+ 'aria-disabled': sbProps['aria-disabled'],
189
+ hidden: isHidden ? true : undefined,
190
+ 'aria-hidden': isHidden ? 'true' : undefined,
191
+ onClick: decrement,
192
+ };
193
+ },
194
+ getClearButtonProps() {
195
+ const isVisible = showClearButton();
196
+ return {
197
+ role: 'button',
198
+ 'aria-label': 'Clear value',
199
+ tabindex: '-1',
200
+ hidden: isVisible ? undefined : true,
201
+ 'aria-hidden': isVisible ? undefined : 'true',
202
+ onClick: clear,
203
+ };
204
+ },
205
+ };
206
+ const state = {
207
+ value: spinbutton.state.value,
208
+ min: spinbutton.state.min,
209
+ max: spinbutton.state.max,
210
+ step: spinbutton.state.step,
211
+ largeStep: spinbutton.state.largeStep,
212
+ isDisabled: spinbutton.state.isDisabled,
213
+ isReadOnly: spinbutton.state.isReadOnly,
214
+ hasMin: () => spinbutton.state.hasMin(),
215
+ hasMax: () => spinbutton.state.hasMax(),
216
+ focused: focusedAtom,
217
+ filled,
218
+ clearable: clearableAtom,
219
+ showClearButton,
220
+ stepper: stepperAtom,
221
+ draftText: draftTextAtom,
222
+ placeholder: placeholderAtom,
223
+ required: requiredAtom,
224
+ defaultValue: defaultValueAtom,
225
+ };
226
+ const actions = {
227
+ setValue,
228
+ increment,
229
+ decrement,
230
+ incrementLarge,
231
+ decrementLarge,
232
+ setFirst,
233
+ setLast,
234
+ handleKeyDown,
235
+ setDisabled,
236
+ setReadOnly,
237
+ setRequired,
238
+ setClearable,
239
+ setStepper,
240
+ setFocused,
241
+ setPlaceholder,
242
+ setDraftText,
243
+ commitDraft,
244
+ clear,
245
+ handleInput,
246
+ };
247
+ return {
248
+ state,
249
+ actions,
250
+ contracts,
251
+ };
252
+ }
@@ -0,0 +1,70 @@
1
+ import { type Atom, type Computed } from '@reatom/core';
2
+ import { type OverlayDismissIntent, type OverlayOpenSource } from '../interactions/overlay-focus.js';
3
+ export type PopoverOpenSource = OverlayOpenSource;
4
+ export type PopoverDismissIntent = OverlayDismissIntent;
5
+ export interface CreatePopoverOptions {
6
+ idBase?: string;
7
+ initialOpen?: boolean;
8
+ initialTriggerId?: string | null;
9
+ ariaLabel?: string;
10
+ ariaLabelledBy?: string;
11
+ closeOnEscape?: boolean;
12
+ closeOnOutsidePointer?: boolean;
13
+ closeOnOutsideFocus?: boolean;
14
+ useNativePopover?: boolean;
15
+ }
16
+ export interface PopoverState {
17
+ isOpen: Atom<boolean>;
18
+ triggerId: Atom<string | null>;
19
+ openedBy: Atom<PopoverOpenSource | null>;
20
+ restoreTargetId: Atom<string | null>;
21
+ lastDismissIntent: Atom<PopoverDismissIntent | null>;
22
+ isInteractive: Computed<boolean>;
23
+ useNativePopover: Atom<boolean>;
24
+ }
25
+ export interface PopoverActions {
26
+ setTriggerId(id: string | null): void;
27
+ open(source?: PopoverOpenSource): void;
28
+ close(intent?: PopoverDismissIntent): void;
29
+ toggle(source?: PopoverOpenSource): void;
30
+ handleTriggerKeyDown(event: Pick<KeyboardEvent, 'key'>): void;
31
+ handleContentKeyDown(event: Pick<KeyboardEvent, 'key'>): void;
32
+ handleOutsidePointer(): void;
33
+ handleOutsideFocus(): void;
34
+ handleNativeToggle(newState: 'open' | 'closed'): void;
35
+ }
36
+ export interface PopoverTriggerProps {
37
+ id: string;
38
+ role: 'button';
39
+ tabindex: '0';
40
+ 'aria-haspopup': 'dialog';
41
+ 'aria-expanded': 'true' | 'false';
42
+ 'aria-controls': string;
43
+ popovertarget?: string;
44
+ popovertargetaction?: 'toggle';
45
+ onClick: () => void;
46
+ onKeyDown: (event: Pick<KeyboardEvent, 'key'>) => void;
47
+ }
48
+ export interface PopoverContentProps {
49
+ id: string;
50
+ role: 'dialog';
51
+ tabindex: '-1';
52
+ hidden?: boolean;
53
+ popover?: 'manual';
54
+ 'aria-modal': 'false';
55
+ 'aria-label'?: string;
56
+ 'aria-labelledby'?: string;
57
+ onKeyDown: (event: Pick<KeyboardEvent, 'key'>) => void;
58
+ onPointerDownOutside: () => void;
59
+ onFocusOutside: () => void;
60
+ }
61
+ export interface PopoverContracts {
62
+ getTriggerProps(): PopoverTriggerProps;
63
+ getContentProps(): PopoverContentProps;
64
+ }
65
+ export interface PopoverModel {
66
+ readonly state: PopoverState;
67
+ readonly actions: PopoverActions;
68
+ readonly contracts: PopoverContracts;
69
+ }
70
+ export declare function createPopover(options?: CreatePopoverOptions): PopoverModel;
@@ -0,0 +1,126 @@
1
+ import { action, atom, computed } from '@reatom/core';
2
+ import { createOverlayFocus, } from '../interactions/overlay-focus.js';
3
+ const isSpaceKey = (key) => key === ' ' || key === 'Spacebar';
4
+ export function createPopover(options = {}) {
5
+ const idBase = options.idBase ?? 'popover';
6
+ const overlay = createOverlayFocus({
7
+ idBase: `${idBase}.overlay`,
8
+ initialOpen: options.initialOpen,
9
+ initialTriggerId: options.initialTriggerId ?? `${idBase}-trigger`,
10
+ trapFocus: false,
11
+ restoreFocus: true,
12
+ });
13
+ const useNativePopoverAtom = atom(options.useNativePopover ?? false, `${idBase}.useNativePopover`);
14
+ const isInteractiveAtom = computed(() => overlay.state.isOpen(), `${idBase}.isInteractive`);
15
+ const open = action((source = 'programmatic') => {
16
+ overlay.actions.open(source, overlay.state.triggerId() ?? `${idBase}-trigger`);
17
+ }, `${idBase}.open`);
18
+ const close = action((intent = 'programmatic') => {
19
+ overlay.actions.close(intent);
20
+ }, `${idBase}.close`);
21
+ const toggle = action((source = 'programmatic') => {
22
+ if (overlay.state.isOpen()) {
23
+ close();
24
+ return;
25
+ }
26
+ open(source);
27
+ }, `${idBase}.toggle`);
28
+ const setTriggerId = action((id) => {
29
+ overlay.actions.setTrigger(id);
30
+ }, `${idBase}.setTriggerId`);
31
+ const handleTriggerKeyDown = action((event) => {
32
+ if (event.key === 'Enter' || isSpaceKey(event.key) || event.key === 'ArrowDown') {
33
+ toggle('keyboard');
34
+ }
35
+ }, `${idBase}.handleTriggerKeyDown`);
36
+ const handleContentKeyDown = action((event) => {
37
+ if (options.closeOnEscape === false)
38
+ return;
39
+ overlay.actions.handleKeyDown(event);
40
+ }, `${idBase}.handleContentKeyDown`);
41
+ const handleOutsidePointer = action(() => {
42
+ if (options.closeOnOutsidePointer === false)
43
+ return;
44
+ overlay.actions.handleOutsidePointer();
45
+ }, `${idBase}.handleOutsidePointer`);
46
+ const handleOutsideFocus = action(() => {
47
+ if (options.closeOnOutsideFocus === false)
48
+ return;
49
+ overlay.actions.handleOutsideFocus();
50
+ }, `${idBase}.handleOutsideFocus`);
51
+ const handleNativeToggle = action((newState) => {
52
+ const isCurrentlyOpen = overlay.state.isOpen();
53
+ if (newState === 'closed' && isCurrentlyOpen) {
54
+ close();
55
+ }
56
+ else if (newState === 'open' && !isCurrentlyOpen) {
57
+ open('programmatic');
58
+ }
59
+ }, `${idBase}.handleNativeToggle`);
60
+ const actions = {
61
+ setTriggerId,
62
+ open,
63
+ close,
64
+ toggle,
65
+ handleTriggerKeyDown,
66
+ handleContentKeyDown,
67
+ handleOutsidePointer,
68
+ handleOutsideFocus,
69
+ handleNativeToggle,
70
+ };
71
+ const contracts = {
72
+ getTriggerProps() {
73
+ const isNative = useNativePopoverAtom();
74
+ const props = {
75
+ id: overlay.state.triggerId() ?? `${idBase}-trigger`,
76
+ role: 'button',
77
+ tabindex: '0',
78
+ 'aria-haspopup': 'dialog',
79
+ 'aria-expanded': overlay.state.isOpen() ? 'true' : 'false',
80
+ 'aria-controls': `${idBase}-content`,
81
+ onClick: () => toggle('pointer'),
82
+ onKeyDown: handleTriggerKeyDown,
83
+ };
84
+ if (isNative) {
85
+ props.popovertarget = `${idBase}-content`;
86
+ props.popovertargetaction = 'toggle';
87
+ }
88
+ return props;
89
+ },
90
+ getContentProps() {
91
+ const isNative = useNativePopoverAtom();
92
+ const props = {
93
+ id: `${idBase}-content`,
94
+ role: 'dialog',
95
+ tabindex: '-1',
96
+ 'aria-modal': 'false',
97
+ 'aria-label': options.ariaLabel,
98
+ 'aria-labelledby': options.ariaLabelledBy,
99
+ onKeyDown: handleContentKeyDown,
100
+ onPointerDownOutside: handleOutsidePointer,
101
+ onFocusOutside: handleOutsideFocus,
102
+ };
103
+ if (isNative) {
104
+ props.popover = 'manual';
105
+ }
106
+ else {
107
+ props.hidden = !overlay.state.isOpen();
108
+ }
109
+ return props;
110
+ },
111
+ };
112
+ const state = {
113
+ isOpen: overlay.state.isOpen,
114
+ triggerId: overlay.state.triggerId,
115
+ openedBy: overlay.state.openedBy,
116
+ restoreTargetId: overlay.state.restoreTargetId,
117
+ lastDismissIntent: overlay.state.lastDismissIntent,
118
+ isInteractive: isInteractiveAtom,
119
+ useNativePopover: useNativePopoverAtom,
120
+ };
121
+ return {
122
+ state,
123
+ actions,
124
+ contracts,
125
+ };
126
+ }
@@ -0,0 +1,49 @@
1
+ import { type Atom, type Computed } from '@reatom/core';
2
+ export interface CreateProgressOptions {
3
+ idBase?: string;
4
+ value?: number;
5
+ min?: number;
6
+ max?: number;
7
+ step?: number;
8
+ isIndeterminate?: boolean;
9
+ valueText?: string;
10
+ ariaLabel?: string;
11
+ ariaLabelledBy?: string;
12
+ ariaDescribedBy?: string;
13
+ formatValueText?: (value: number) => string;
14
+ onValueChange?: (value: number) => void;
15
+ }
16
+ export interface ProgressState {
17
+ value: Atom<number>;
18
+ min: Atom<number>;
19
+ max: Atom<number>;
20
+ percentage: Computed<number>;
21
+ isIndeterminate: Atom<boolean>;
22
+ isComplete: Computed<boolean>;
23
+ }
24
+ export interface ProgressActions {
25
+ setValue(value: number): void;
26
+ increment(): void;
27
+ decrement(): void;
28
+ setIndeterminate(value: boolean): void;
29
+ }
30
+ export interface ProgressProps {
31
+ id: string;
32
+ role: 'progressbar';
33
+ 'aria-valuenow'?: string;
34
+ 'aria-valuemin'?: string;
35
+ 'aria-valuemax'?: string;
36
+ 'aria-valuetext'?: string;
37
+ 'aria-label'?: string;
38
+ 'aria-labelledby'?: string;
39
+ 'aria-describedby'?: string;
40
+ }
41
+ export interface ProgressContracts {
42
+ getProgressProps(): ProgressProps;
43
+ }
44
+ export interface ProgressModel {
45
+ readonly state: ProgressState;
46
+ readonly actions: ProgressActions;
47
+ readonly contracts: ProgressContracts;
48
+ }
49
+ export declare function createProgress(options?: CreateProgressOptions): ProgressModel;
@@ -0,0 +1,79 @@
1
+ import { action, atom, computed } from '@reatom/core';
2
+ import { createValueRange } from '../core/value-range.js';
3
+ export function createProgress(options = {}) {
4
+ const idBase = options.idBase ?? 'progress';
5
+ const isIndeterminateAtom = atom(options.isIndeterminate ?? false, `${idBase}.isIndeterminate`);
6
+ const range = createValueRange({
7
+ idBase: `${idBase}.range`,
8
+ min: options.min ?? 0,
9
+ max: options.max ?? 100,
10
+ step: options.step,
11
+ initialValue: options.value,
12
+ });
13
+ const commitValue = (update) => {
14
+ const previous = range.state.value();
15
+ update();
16
+ const next = range.state.value();
17
+ if (previous !== next) {
18
+ options.onValueChange?.(next);
19
+ }
20
+ };
21
+ const setValue = action((value) => {
22
+ commitValue(() => {
23
+ range.actions.setValue(value);
24
+ });
25
+ }, `${idBase}.setValue`);
26
+ const increment = action(() => {
27
+ commitValue(() => {
28
+ range.actions.increment();
29
+ });
30
+ }, `${idBase}.increment`);
31
+ const decrement = action(() => {
32
+ commitValue(() => {
33
+ range.actions.decrement();
34
+ });
35
+ }, `${idBase}.decrement`);
36
+ const setIndeterminate = action((value) => {
37
+ isIndeterminateAtom.set(value);
38
+ }, `${idBase}.setIndeterminate`);
39
+ const isCompleteAtom = computed(() => !isIndeterminateAtom() && range.state.value() >= range.state.max(), `${idBase}.isComplete`);
40
+ const actions = {
41
+ setValue,
42
+ increment,
43
+ decrement,
44
+ setIndeterminate,
45
+ };
46
+ const contracts = {
47
+ getProgressProps() {
48
+ const value = range.state.value();
49
+ const indeterminate = isIndeterminateAtom();
50
+ const percentageText = `${Math.round(range.state.percentage())}%`;
51
+ return {
52
+ id: `${idBase}-root`,
53
+ role: 'progressbar',
54
+ 'aria-valuenow': indeterminate ? undefined : String(value),
55
+ 'aria-valuemin': indeterminate ? undefined : String(range.state.min()),
56
+ 'aria-valuemax': indeterminate ? undefined : String(range.state.max()),
57
+ 'aria-valuetext': indeterminate
58
+ ? undefined
59
+ : (options.valueText ?? options.formatValueText?.(value) ?? percentageText),
60
+ 'aria-label': options.ariaLabel,
61
+ 'aria-labelledby': options.ariaLabelledBy,
62
+ 'aria-describedby': options.ariaDescribedBy,
63
+ };
64
+ },
65
+ };
66
+ const state = {
67
+ value: range.state.value,
68
+ min: range.state.min,
69
+ max: range.state.max,
70
+ percentage: range.state.percentage,
71
+ isIndeterminate: isIndeterminateAtom,
72
+ isComplete: isCompleteAtom,
73
+ };
74
+ return {
75
+ state,
76
+ actions,
77
+ contracts,
78
+ };
79
+ }
@@ -0,0 +1,61 @@
1
+ import { type Atom } from '@reatom/core';
2
+ import { type CompositeNavigationOrientation } from '../interactions/composite-navigation.js';
3
+ export interface RadioGroupItem {
4
+ id: string;
5
+ disabled?: boolean;
6
+ describedBy?: string;
7
+ }
8
+ export interface CreateRadioGroupOptions {
9
+ items: readonly RadioGroupItem[];
10
+ idBase?: string;
11
+ orientation?: CompositeNavigationOrientation;
12
+ isDisabled?: boolean;
13
+ ariaLabel?: string;
14
+ ariaLabelledBy?: string;
15
+ initialValue?: string | null;
16
+ initialActiveId?: string | null;
17
+ }
18
+ export interface RadioGroupState {
19
+ value: Atom<string | null>;
20
+ activeId: Atom<string | null>;
21
+ isDisabled: Atom<boolean>;
22
+ orientation: CompositeNavigationOrientation;
23
+ }
24
+ export interface RadioGroupActions {
25
+ setDisabled(value: boolean): void;
26
+ select(id: string): void;
27
+ moveNext(): void;
28
+ movePrev(): void;
29
+ moveFirst(): void;
30
+ moveLast(): void;
31
+ handleKeyDown(event: Pick<KeyboardEvent, 'key'>): void;
32
+ }
33
+ export interface RadioGroupRootProps {
34
+ role: 'radiogroup';
35
+ 'aria-label'?: string;
36
+ 'aria-labelledby'?: string;
37
+ 'aria-disabled'?: 'true';
38
+ 'aria-orientation': CompositeNavigationOrientation;
39
+ onKeyDown: (event: Pick<KeyboardEvent, 'key'>) => void;
40
+ }
41
+ export interface RadioProps {
42
+ id: string;
43
+ role: 'radio';
44
+ tabindex: '0' | '-1';
45
+ 'aria-checked': 'true' | 'false';
46
+ 'aria-disabled'?: 'true';
47
+ 'aria-describedby'?: string;
48
+ 'data-active': 'true' | 'false';
49
+ onClick: () => void;
50
+ onKeyDown: (event: Pick<KeyboardEvent, 'key'>) => void;
51
+ }
52
+ export interface RadioGroupContracts {
53
+ getRootProps(): RadioGroupRootProps;
54
+ getRadioProps(id: string): RadioProps;
55
+ }
56
+ export interface RadioGroupModel {
57
+ readonly state: RadioGroupState;
58
+ readonly actions: RadioGroupActions;
59
+ readonly contracts: RadioGroupContracts;
60
+ }
61
+ export declare function createRadioGroup(options: CreateRadioGroupOptions): RadioGroupModel;