@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,111 @@
1
+ import { type Atom } from '@reatom/core';
2
+ export type ContextMenuOpenSource = 'pointer' | 'keyboard' | 'programmatic';
3
+ export type ContextMenuItemType = 'item' | 'separator' | 'group-label' | 'checkbox' | 'radio' | 'submenu';
4
+ export interface ContextMenuItem {
5
+ id: string;
6
+ label?: string;
7
+ disabled?: boolean;
8
+ type?: ContextMenuItemType;
9
+ checked?: boolean;
10
+ group?: string;
11
+ children?: readonly ContextMenuItem[];
12
+ }
13
+ export interface CreateContextMenuOptions {
14
+ items: readonly ContextMenuItem[];
15
+ idBase?: string;
16
+ ariaLabel?: string;
17
+ closeOnSelect?: boolean;
18
+ closeOnOutsidePointer?: boolean;
19
+ longPressDuration?: number;
20
+ }
21
+ export interface ContextMenuState {
22
+ isOpen: Atom<boolean>;
23
+ activeId: Atom<string | null>;
24
+ anchorX: Atom<number>;
25
+ anchorY: Atom<number>;
26
+ openedBy: Atom<ContextMenuOpenSource | null>;
27
+ restoreTargetId: Atom<string | null>;
28
+ checkedIds: Atom<ReadonlySet<string>>;
29
+ openSubmenuId: Atom<string | null>;
30
+ submenuActiveId: Atom<string | null>;
31
+ }
32
+ export interface ContextMenuKeyboardEventLike {
33
+ key: string;
34
+ shiftKey?: boolean;
35
+ ctrlKey?: boolean;
36
+ metaKey?: boolean;
37
+ altKey?: boolean;
38
+ }
39
+ export interface ContextMenuActions {
40
+ openAt(x: number, y: number, source?: ContextMenuOpenSource): void;
41
+ close(): void;
42
+ select(id: string): void;
43
+ handleTargetKeyDown(event: ContextMenuKeyboardEventLike): void;
44
+ handleKeyDown(event: ContextMenuKeyboardEventLike): void;
45
+ handleOutsidePointer(): void;
46
+ handleTouchStart(point: {
47
+ clientX: number;
48
+ clientY: number;
49
+ }): void;
50
+ handleTouchMove(): void;
51
+ handleTouchEnd(): void;
52
+ }
53
+ export interface ContextMenuTargetProps {
54
+ id: string;
55
+ onContextMenu: (event: {
56
+ clientX: number;
57
+ clientY: number;
58
+ preventDefault?: () => void;
59
+ }) => void;
60
+ onKeyDown: (event: ContextMenuKeyboardEventLike) => void;
61
+ }
62
+ export interface ContextMenuProps {
63
+ id: string;
64
+ role: 'menu';
65
+ tabindex: '-1';
66
+ hidden: boolean;
67
+ 'aria-label'?: string;
68
+ 'data-anchor-x': string;
69
+ 'data-anchor-y': string;
70
+ onKeyDown: (event: ContextMenuKeyboardEventLike) => void;
71
+ }
72
+ export interface ContextMenuItemProps {
73
+ id: string;
74
+ role: 'menuitem' | 'menuitemcheckbox' | 'menuitemradio';
75
+ tabindex: '-1';
76
+ 'aria-disabled'?: 'true';
77
+ 'data-active': 'true' | 'false';
78
+ 'aria-checked'?: 'true' | 'false';
79
+ 'aria-haspopup'?: 'menu';
80
+ 'aria-expanded'?: 'true' | 'false';
81
+ onClick: () => void;
82
+ }
83
+ export interface ContextMenuSeparatorProps {
84
+ id: string;
85
+ role: 'separator';
86
+ }
87
+ export interface ContextMenuGroupLabelProps {
88
+ id: string;
89
+ role: 'presentation';
90
+ 'aria-label'?: string;
91
+ }
92
+ export interface ContextMenuSubmenuProps {
93
+ id: string;
94
+ role: 'menu';
95
+ tabindex: '-1';
96
+ hidden: boolean;
97
+ }
98
+ export interface ContextMenuContracts {
99
+ getTargetProps(): ContextMenuTargetProps;
100
+ getMenuProps(): ContextMenuProps;
101
+ getItemProps(id: string): ContextMenuItemProps;
102
+ getSeparatorProps(id: string): ContextMenuSeparatorProps;
103
+ getGroupLabelProps(id: string): ContextMenuGroupLabelProps;
104
+ getSubmenuProps(id: string): ContextMenuSubmenuProps;
105
+ }
106
+ export interface ContextMenuModel {
107
+ readonly state: ContextMenuState;
108
+ readonly actions: ContextMenuActions;
109
+ readonly contracts: ContextMenuContracts;
110
+ }
111
+ export declare function createContextMenu(options: CreateContextMenuOptions): ContextMenuModel;
@@ -0,0 +1,372 @@
1
+ import { action, atom } from '@reatom/core';
2
+ import { isTypeaheadEvent, advanceTypeaheadState, findTypeaheadMatch, createInitialTypeaheadState, normalizeTypeaheadText, } from '../interactions/typeahead.js';
3
+ import { createMenu } from '../menu/index.js';
4
+ const isActionableItem = (item) => item.type == null ||
5
+ item.type === 'item' ||
6
+ item.type === 'checkbox' ||
7
+ item.type === 'radio' ||
8
+ item.type === 'submenu';
9
+ const buildItemMap = (items) => {
10
+ const map = new Map();
11
+ for (const item of items) {
12
+ map.set(item.id, item);
13
+ if (item.type === 'submenu' && item.children) {
14
+ for (const child of item.children) {
15
+ map.set(child.id, child);
16
+ }
17
+ }
18
+ }
19
+ return map;
20
+ };
21
+ const TYPEAHEAD_TIMEOUT = 500;
22
+ export function createContextMenu(options) {
23
+ const idBase = options.idBase ?? 'context-menu';
24
+ const longPressDuration = options.longPressDuration ?? 500;
25
+ const allItemMap = buildItemMap(options.items);
26
+ const actionableItems = options.items
27
+ .filter(isActionableItem)
28
+ .map((item) => ({ id: item.id, label: item.label, disabled: item.disabled }));
29
+ const anchorXAtom = atom(0, `${idBase}.anchorX`);
30
+ const anchorYAtom = atom(0, `${idBase}.anchorY`);
31
+ const openedByAtom = atom(null, `${idBase}.openedBy`);
32
+ const restoreTargetIdAtom = atom(null, `${idBase}.restoreTargetId`);
33
+ const initialCheckedIds = new Set();
34
+ for (const item of options.items) {
35
+ if ((item.type === 'checkbox' || item.type === 'radio') && item.checked) {
36
+ initialCheckedIds.add(item.id);
37
+ }
38
+ }
39
+ const checkedIdsAtom = atom(initialCheckedIds, `${idBase}.checkedIds`);
40
+ const openSubmenuIdAtom = atom(null, `${idBase}.openSubmenuId`);
41
+ const submenuActiveIdAtom = atom(null, `${idBase}.submenuActiveId`);
42
+ const submenuItems = new Map();
43
+ for (const item of options.items) {
44
+ if (item.type === 'submenu' && item.children) {
45
+ const enabledIds = item.children.filter((c) => !c.disabled).map((c) => c.id);
46
+ submenuItems.set(item.id, { enabledIds, allChildren: item.children });
47
+ }
48
+ }
49
+ let typeaheadState = createInitialTypeaheadState();
50
+ const typeaheadItems = actionableItems
51
+ .filter((item) => !item.disabled && item.label != null)
52
+ .map((item) => ({ id: item.id, text: normalizeTypeaheadText(item.label) }));
53
+ let longPressTimer = null;
54
+ const menu = createMenu({
55
+ idBase,
56
+ items: actionableItems,
57
+ ariaLabel: options.ariaLabel,
58
+ closeOnSelect: options.closeOnSelect,
59
+ });
60
+ const clearLongPressTimer = () => {
61
+ if (longPressTimer != null) {
62
+ clearTimeout(longPressTimer);
63
+ longPressTimer = null;
64
+ }
65
+ };
66
+ const openAt = action((x, y, source = 'programmatic') => {
67
+ anchorXAtom.set(x);
68
+ anchorYAtom.set(y);
69
+ openedByAtom.set(source);
70
+ restoreTargetIdAtom.set(null);
71
+ openSubmenuIdAtom.set(null);
72
+ submenuActiveIdAtom.set(null);
73
+ menu.actions.open(source);
74
+ }, `${idBase}.openAt`);
75
+ const close = action(() => {
76
+ menu.actions.close();
77
+ menu.actions.setActive(null);
78
+ openedByAtom.set(null);
79
+ restoreTargetIdAtom.set(`${idBase}-target`);
80
+ openSubmenuIdAtom.set(null);
81
+ submenuActiveIdAtom.set(null);
82
+ }, `${idBase}.close`);
83
+ const closeSubmenu = () => {
84
+ openSubmenuIdAtom.set(null);
85
+ submenuActiveIdAtom.set(null);
86
+ };
87
+ const handleCheckableSelect = (id) => {
88
+ const item = allItemMap.get(id);
89
+ if (!item)
90
+ return false;
91
+ if (item.type === 'checkbox') {
92
+ const current = new Set(checkedIdsAtom());
93
+ if (current.has(id)) {
94
+ current.delete(id);
95
+ }
96
+ else {
97
+ current.add(id);
98
+ }
99
+ checkedIdsAtom.set(current);
100
+ return true;
101
+ }
102
+ if (item.type === 'radio' && item.group) {
103
+ const current = new Set(checkedIdsAtom());
104
+ for (const groupItem of options.items) {
105
+ if (groupItem.type === 'radio' && groupItem.group === item.group) {
106
+ current.delete(groupItem.id);
107
+ }
108
+ }
109
+ current.add(id);
110
+ checkedIdsAtom.set(current);
111
+ return true;
112
+ }
113
+ return false;
114
+ };
115
+ const select = action((id) => {
116
+ const item = allItemMap.get(id);
117
+ if (!item || item.disabled)
118
+ return;
119
+ if (item.type === 'separator' || item.type === 'group-label')
120
+ return;
121
+ handleCheckableSelect(id);
122
+ const isSubmenuChild = !actionableItems.some((ai) => ai.id === id);
123
+ if (isSubmenuChild) {
124
+ const closeOnSelect = options.closeOnSelect ?? true;
125
+ if (closeOnSelect) {
126
+ close();
127
+ }
128
+ return;
129
+ }
130
+ const wasOpen = menu.state.isOpen();
131
+ menu.actions.select(id);
132
+ if (wasOpen && !menu.state.isOpen()) {
133
+ openedByAtom.set(null);
134
+ restoreTargetIdAtom.set(`${idBase}-target`);
135
+ openSubmenuIdAtom.set(null);
136
+ submenuActiveIdAtom.set(null);
137
+ }
138
+ }, `${idBase}.select`);
139
+ const handleTargetKeyDown = action((event) => {
140
+ const isContextKey = event.key === 'ContextMenu';
141
+ const isShiftF10 = event.key === 'F10' && event.shiftKey === true;
142
+ if (isContextKey || isShiftF10) {
143
+ openAt(anchorXAtom(), anchorYAtom(), 'keyboard');
144
+ }
145
+ }, `${idBase}.handleTargetKeyDown`);
146
+ const handleSubmenuKeyDown = (event) => {
147
+ const openSubId = openSubmenuIdAtom();
148
+ if (openSubId == null)
149
+ return false;
150
+ const subInfo = submenuItems.get(openSubId);
151
+ if (!subInfo)
152
+ return false;
153
+ if (event.key === 'Escape' || event.key === 'ArrowLeft') {
154
+ closeSubmenu();
155
+ return true;
156
+ }
157
+ if (event.key === 'ArrowDown') {
158
+ const activeId = submenuActiveIdAtom();
159
+ const idx = activeId != null ? subInfo.enabledIds.indexOf(activeId) : -1;
160
+ const nextIdx = (idx + 1) % subInfo.enabledIds.length;
161
+ submenuActiveIdAtom.set(subInfo.enabledIds[nextIdx] ?? null);
162
+ return true;
163
+ }
164
+ if (event.key === 'ArrowUp') {
165
+ const activeId = submenuActiveIdAtom();
166
+ const idx = activeId != null ? subInfo.enabledIds.indexOf(activeId) : 0;
167
+ const nextIdx = (idx - 1 + subInfo.enabledIds.length) % subInfo.enabledIds.length;
168
+ submenuActiveIdAtom.set(subInfo.enabledIds[nextIdx] ?? null);
169
+ return true;
170
+ }
171
+ if (event.key === 'Home') {
172
+ submenuActiveIdAtom.set(subInfo.enabledIds[0] ?? null);
173
+ return true;
174
+ }
175
+ if (event.key === 'End') {
176
+ submenuActiveIdAtom.set(subInfo.enabledIds[subInfo.enabledIds.length - 1] ?? null);
177
+ return true;
178
+ }
179
+ if (event.key === 'Enter' || event.key === ' ') {
180
+ const activeId = submenuActiveIdAtom();
181
+ if (activeId != null) {
182
+ select(activeId);
183
+ }
184
+ return true;
185
+ }
186
+ return false;
187
+ };
188
+ const handleKeyDown = action((event) => {
189
+ if (!menu.state.isOpen())
190
+ return;
191
+ const keyEvent = {
192
+ key: event.key,
193
+ shiftKey: event.shiftKey ?? false,
194
+ ctrlKey: event.ctrlKey ?? false,
195
+ metaKey: event.metaKey ?? false,
196
+ altKey: event.altKey ?? false,
197
+ };
198
+ if (handleSubmenuKeyDown(event))
199
+ return;
200
+ if (event.key === 'Escape' || event.key === 'Tab') {
201
+ close();
202
+ return;
203
+ }
204
+ if (event.key === 'ArrowRight') {
205
+ const activeId = menu.state.activeId();
206
+ if (activeId != null && submenuItems.has(activeId)) {
207
+ const subInfo = submenuItems.get(activeId);
208
+ openSubmenuIdAtom.set(activeId);
209
+ submenuActiveIdAtom.set(subInfo.enabledIds[0] ?? null);
210
+ return;
211
+ }
212
+ }
213
+ if (isTypeaheadEvent(keyEvent)) {
214
+ const { query, next } = advanceTypeaheadState(typeaheadState, event.key, Date.now(), TYPEAHEAD_TIMEOUT);
215
+ typeaheadState = next;
216
+ const currentActiveId = menu.state.activeId();
217
+ const startIndex = currentActiveId != null ? typeaheadItems.findIndex((item) => item.id === currentActiveId) : 0;
218
+ const matchId = findTypeaheadMatch(query, typeaheadItems, startIndex >= 0 ? startIndex : 0);
219
+ if (matchId != null) {
220
+ menu.actions.setActive(matchId);
221
+ }
222
+ return;
223
+ }
224
+ menu.actions.handleMenuKeyDown(keyEvent);
225
+ }, `${idBase}.handleKeyDown`);
226
+ const handleOutsidePointer = action(() => {
227
+ if (options.closeOnOutsidePointer === false)
228
+ return;
229
+ if (!menu.state.isOpen())
230
+ return;
231
+ close();
232
+ }, `${idBase}.handleOutsidePointer`);
233
+ const handleTouchStart = action((point) => {
234
+ clearLongPressTimer();
235
+ longPressTimer = setTimeout(() => {
236
+ openAt(point.clientX, point.clientY, 'pointer');
237
+ longPressTimer = null;
238
+ }, longPressDuration);
239
+ }, `${idBase}.handleTouchStart`);
240
+ const handleTouchMove = action(() => {
241
+ clearLongPressTimer();
242
+ }, `${idBase}.handleTouchMove`);
243
+ const handleTouchEnd = action(() => {
244
+ clearLongPressTimer();
245
+ }, `${idBase}.handleTouchEnd`);
246
+ const actions = {
247
+ openAt,
248
+ close,
249
+ select,
250
+ handleTargetKeyDown,
251
+ handleKeyDown,
252
+ handleOutsidePointer,
253
+ handleTouchStart,
254
+ handleTouchMove,
255
+ handleTouchEnd,
256
+ };
257
+ const contracts = {
258
+ getTargetProps() {
259
+ return {
260
+ id: `${idBase}-target`,
261
+ onContextMenu: (event) => {
262
+ event.preventDefault?.();
263
+ openAt(event.clientX, event.clientY, 'pointer');
264
+ },
265
+ onKeyDown: handleTargetKeyDown,
266
+ };
267
+ },
268
+ getMenuProps() {
269
+ const menuProps = menu.contracts.getMenuProps();
270
+ return {
271
+ ...menuProps,
272
+ hidden: !menu.state.isOpen(),
273
+ 'data-anchor-x': String(anchorXAtom()),
274
+ 'data-anchor-y': String(anchorYAtom()),
275
+ onKeyDown: handleKeyDown,
276
+ };
277
+ },
278
+ getItemProps(id) {
279
+ const contextItem = allItemMap.get(id);
280
+ if (!contextItem) {
281
+ throw new Error(`Unknown context-menu item id: ${id}`);
282
+ }
283
+ const itemType = contextItem.type ?? 'item';
284
+ if (itemType === 'submenu') {
285
+ const baseItem = menu.contracts.getItemProps(id);
286
+ const isSubOpen = openSubmenuIdAtom() === id;
287
+ return {
288
+ ...baseItem,
289
+ role: 'menuitem',
290
+ 'aria-haspopup': 'menu',
291
+ 'aria-expanded': isSubOpen ? 'true' : 'false',
292
+ onClick: () => select(id),
293
+ };
294
+ }
295
+ if (itemType === 'checkbox') {
296
+ const baseItem = menu.contracts.getItemProps(id);
297
+ const isChecked = checkedIdsAtom().has(id);
298
+ return {
299
+ ...baseItem,
300
+ role: 'menuitemcheckbox',
301
+ 'aria-checked': isChecked ? 'true' : 'false',
302
+ onClick: () => select(id),
303
+ };
304
+ }
305
+ if (itemType === 'radio') {
306
+ const baseItem = menu.contracts.getItemProps(id);
307
+ const isChecked = checkedIdsAtom().has(id);
308
+ return {
309
+ ...baseItem,
310
+ role: 'menuitemradio',
311
+ 'aria-checked': isChecked ? 'true' : 'false',
312
+ onClick: () => select(id),
313
+ };
314
+ }
315
+ const isInMenu = actionableItems.some((ai) => ai.id === id);
316
+ if (!isInMenu) {
317
+ const subActiveId = submenuActiveIdAtom();
318
+ return {
319
+ id: `${idBase}-item-${id}`,
320
+ role: 'menuitem',
321
+ tabindex: '-1',
322
+ 'aria-disabled': contextItem.disabled ? 'true' : undefined,
323
+ 'data-active': (subActiveId === id ? 'true' : 'false'),
324
+ onClick: () => select(id),
325
+ };
326
+ }
327
+ const item = menu.contracts.getItemProps(id);
328
+ return {
329
+ ...item,
330
+ onClick: () => select(id),
331
+ };
332
+ },
333
+ getSeparatorProps(id) {
334
+ return {
335
+ id: `${idBase}-separator-${id}`,
336
+ role: 'separator',
337
+ };
338
+ },
339
+ getGroupLabelProps(id) {
340
+ const item = allItemMap.get(id);
341
+ return {
342
+ id: `${idBase}-group-${id}`,
343
+ role: 'presentation',
344
+ 'aria-label': item?.label,
345
+ };
346
+ },
347
+ getSubmenuProps(id) {
348
+ return {
349
+ id: `${idBase}-submenu-${id}`,
350
+ role: 'menu',
351
+ tabindex: '-1',
352
+ hidden: openSubmenuIdAtom() !== id,
353
+ };
354
+ },
355
+ };
356
+ const state = {
357
+ isOpen: menu.state.isOpen,
358
+ activeId: menu.state.activeId,
359
+ anchorX: anchorXAtom,
360
+ anchorY: anchorYAtom,
361
+ openedBy: openedByAtom,
362
+ restoreTargetId: restoreTargetIdAtom,
363
+ checkedIds: checkedIdsAtom,
364
+ openSubmenuId: openSubmenuIdAtom,
365
+ submenuActiveId: submenuActiveIdAtom,
366
+ };
367
+ return {
368
+ state,
369
+ actions,
370
+ contracts,
371
+ };
372
+ }
@@ -0,0 +1,62 @@
1
+ import { type Atom, type Computed } from '@reatom/core';
2
+ export type CopyButtonStatus = 'idle' | 'success' | 'error';
3
+ export type CopyButtonValue = string | (() => Promise<string>);
4
+ export interface ClipboardAdapter {
5
+ writeText(text: string): Promise<void>;
6
+ }
7
+ export interface CreateCopyButtonOptions {
8
+ value?: CopyButtonValue;
9
+ feedbackDuration?: number;
10
+ isDisabled?: boolean;
11
+ ariaLabel?: string;
12
+ onCopy?: (value: string) => void;
13
+ onError?: (error: unknown) => void;
14
+ clipboard?: ClipboardAdapter;
15
+ }
16
+ export interface CopyButtonState {
17
+ status: Atom<CopyButtonStatus>;
18
+ isDisabled: Atom<boolean>;
19
+ isCopying: Atom<boolean>;
20
+ feedbackDuration: Atom<number>;
21
+ value: Atom<CopyButtonValue>;
22
+ isIdle: Computed<boolean>;
23
+ isSuccess: Computed<boolean>;
24
+ isError: Computed<boolean>;
25
+ isUnavailable: Computed<boolean>;
26
+ }
27
+ export interface CopyButtonActions {
28
+ copy(): Promise<void>;
29
+ setDisabled(v: boolean): void;
30
+ setFeedbackDuration(v: number): void;
31
+ setValue(v: CopyButtonValue): void;
32
+ reset(): void;
33
+ }
34
+ export interface CopyButtonProps {
35
+ role: 'button';
36
+ tabindex: '0' | '-1';
37
+ 'aria-disabled': 'true' | 'false';
38
+ 'aria-label'?: string;
39
+ onClick: (e: Event) => void;
40
+ onKeyDown: (e: KeyboardEvent) => void;
41
+ onKeyUp: (e: KeyboardEvent) => void;
42
+ }
43
+ export interface CopyStatusProps {
44
+ role: 'status';
45
+ 'aria-live': 'polite';
46
+ 'aria-atomic': 'true';
47
+ }
48
+ export interface CopyIconProps {
49
+ 'aria-hidden': 'true';
50
+ hidden?: boolean;
51
+ }
52
+ export interface CopyButtonContracts {
53
+ getButtonProps(): CopyButtonProps;
54
+ getStatusProps(): CopyStatusProps;
55
+ getIconContainerProps(which: 'copy' | 'success' | 'error'): CopyIconProps;
56
+ }
57
+ export interface CopyButtonModel {
58
+ readonly state: CopyButtonState;
59
+ readonly actions: CopyButtonActions;
60
+ readonly contracts: CopyButtonContracts;
61
+ }
62
+ export declare function createCopyButton(options?: CreateCopyButtonOptions): CopyButtonModel;