@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,183 @@
|
|
|
1
|
+
import { action, atom, computed } from '@reatom/core';
|
|
2
|
+
const clampDuration = (v) => Math.max(0, v);
|
|
3
|
+
const isSpaceKey = (key) => key === ' ' || key === 'Spacebar';
|
|
4
|
+
const STATUS_TO_ICON = {
|
|
5
|
+
idle: 'copy',
|
|
6
|
+
success: 'success',
|
|
7
|
+
error: 'error',
|
|
8
|
+
};
|
|
9
|
+
export function createCopyButton(options = {}) {
|
|
10
|
+
const clipboard = options.clipboard ?? navigator.clipboard;
|
|
11
|
+
const statusAtom = atom('idle', 'copyButton.status');
|
|
12
|
+
const isDisabledAtom = atom(options.isDisabled ?? false, 'copyButton.isDisabled');
|
|
13
|
+
const isCopyingAtom = atom(false, 'copyButton.isCopying');
|
|
14
|
+
const feedbackDurationAtom = atom(clampDuration(options.feedbackDuration ?? 1500), 'copyButton.feedbackDuration');
|
|
15
|
+
const valueBoxAtom = atom({ ref: options.value ?? '' }, 'copyButton.value');
|
|
16
|
+
const valueAccessor = () => valueBoxAtom().ref;
|
|
17
|
+
valueAccessor.set = (v) => valueBoxAtom.set({ ref: v });
|
|
18
|
+
const valueAtom = valueAccessor;
|
|
19
|
+
const isIdleAtom = computed(() => statusAtom() === 'idle', 'copyButton.isIdle');
|
|
20
|
+
const isSuccessAtom = computed(() => statusAtom() === 'success', 'copyButton.isSuccess');
|
|
21
|
+
const isErrorAtom = computed(() => statusAtom() === 'error', 'copyButton.isError');
|
|
22
|
+
const isUnavailableAtom = computed(() => isDisabledAtom() || isCopyingAtom(), 'copyButton.isUnavailable');
|
|
23
|
+
let revertTimer = null;
|
|
24
|
+
let copyVersion = 0;
|
|
25
|
+
const clearRevertTimer = () => {
|
|
26
|
+
if (revertTimer !== null) {
|
|
27
|
+
clearTimeout(revertTimer);
|
|
28
|
+
revertTimer = null;
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
const scheduleRevert = (version) => {
|
|
32
|
+
clearRevertTimer();
|
|
33
|
+
revertTimer = setTimeout(() => {
|
|
34
|
+
revertTimer = null;
|
|
35
|
+
if (copyVersion === version) {
|
|
36
|
+
statusAtom.set('idle');
|
|
37
|
+
}
|
|
38
|
+
}, feedbackDurationAtom());
|
|
39
|
+
};
|
|
40
|
+
const copy = async () => {
|
|
41
|
+
if (isUnavailableAtom())
|
|
42
|
+
return;
|
|
43
|
+
const version = ++copyVersion;
|
|
44
|
+
isCopyingAtom.set(true);
|
|
45
|
+
clearRevertTimer();
|
|
46
|
+
let resolvedValue;
|
|
47
|
+
try {
|
|
48
|
+
const currentValue = valueAtom();
|
|
49
|
+
if (typeof currentValue === 'function') {
|
|
50
|
+
resolvedValue = await currentValue();
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
resolvedValue = currentValue;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
catch (err) {
|
|
57
|
+
isCopyingAtom.set(false);
|
|
58
|
+
if (copyVersion !== version)
|
|
59
|
+
return;
|
|
60
|
+
statusAtom.set('error');
|
|
61
|
+
options.onError?.(err);
|
|
62
|
+
scheduleRevert(version);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
try {
|
|
66
|
+
await clipboard.writeText(resolvedValue);
|
|
67
|
+
isCopyingAtom.set(false);
|
|
68
|
+
if (copyVersion !== version)
|
|
69
|
+
return;
|
|
70
|
+
statusAtom.set('success');
|
|
71
|
+
options.onCopy?.(resolvedValue);
|
|
72
|
+
}
|
|
73
|
+
catch (err) {
|
|
74
|
+
isCopyingAtom.set(false);
|
|
75
|
+
if (copyVersion !== version)
|
|
76
|
+
return;
|
|
77
|
+
statusAtom.set('error');
|
|
78
|
+
options.onError?.(err);
|
|
79
|
+
}
|
|
80
|
+
scheduleRevert(version);
|
|
81
|
+
};
|
|
82
|
+
const setDisabled = action((v) => {
|
|
83
|
+
isDisabledAtom.set(v);
|
|
84
|
+
}, 'copyButton.setDisabled');
|
|
85
|
+
const setFeedbackDuration = action((v) => {
|
|
86
|
+
feedbackDurationAtom.set(clampDuration(v));
|
|
87
|
+
}, 'copyButton.setFeedbackDuration');
|
|
88
|
+
const setValue = action((v) => {
|
|
89
|
+
valueAtom.set(v);
|
|
90
|
+
}, 'copyButton.setValue');
|
|
91
|
+
const reset = action(() => {
|
|
92
|
+
++copyVersion;
|
|
93
|
+
clearRevertTimer();
|
|
94
|
+
isCopyingAtom.set(false);
|
|
95
|
+
statusAtom.set('idle');
|
|
96
|
+
}, 'copyButton.reset');
|
|
97
|
+
const getButtonProps = () => {
|
|
98
|
+
const unavailable = isUnavailableAtom();
|
|
99
|
+
const status = statusAtom();
|
|
100
|
+
let ariaLabel;
|
|
101
|
+
if (options.ariaLabel != null) {
|
|
102
|
+
if (status === 'idle') {
|
|
103
|
+
ariaLabel = options.ariaLabel;
|
|
104
|
+
}
|
|
105
|
+
else if (status === 'success') {
|
|
106
|
+
ariaLabel = 'Copied';
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
ariaLabel = 'Copy failed';
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
const props = {
|
|
113
|
+
role: 'button',
|
|
114
|
+
tabindex: unavailable ? '-1' : '0',
|
|
115
|
+
'aria-disabled': unavailable ? 'true' : 'false',
|
|
116
|
+
onClick: (_e) => {
|
|
117
|
+
copy();
|
|
118
|
+
},
|
|
119
|
+
onKeyDown: (e) => {
|
|
120
|
+
if (isUnavailableAtom()) {
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
if (e.key === 'Enter') {
|
|
124
|
+
copy();
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
if (isSpaceKey(e.key)) {
|
|
128
|
+
e.preventDefault();
|
|
129
|
+
}
|
|
130
|
+
},
|
|
131
|
+
onKeyUp: (e) => {
|
|
132
|
+
if (isUnavailableAtom())
|
|
133
|
+
return;
|
|
134
|
+
if (isSpaceKey(e.key)) {
|
|
135
|
+
copy();
|
|
136
|
+
}
|
|
137
|
+
},
|
|
138
|
+
};
|
|
139
|
+
if (ariaLabel !== undefined) {
|
|
140
|
+
props['aria-label'] = ariaLabel;
|
|
141
|
+
}
|
|
142
|
+
return props;
|
|
143
|
+
};
|
|
144
|
+
const getStatusProps = () => ({
|
|
145
|
+
role: 'status',
|
|
146
|
+
'aria-live': 'polite',
|
|
147
|
+
'aria-atomic': 'true',
|
|
148
|
+
});
|
|
149
|
+
const getIconContainerProps = (which) => {
|
|
150
|
+
const activeIcon = STATUS_TO_ICON[statusAtom()];
|
|
151
|
+
const props = {
|
|
152
|
+
'aria-hidden': 'true',
|
|
153
|
+
};
|
|
154
|
+
if (which !== activeIcon) {
|
|
155
|
+
props.hidden = true;
|
|
156
|
+
}
|
|
157
|
+
return props;
|
|
158
|
+
};
|
|
159
|
+
const state = {
|
|
160
|
+
status: statusAtom,
|
|
161
|
+
isDisabled: isDisabledAtom,
|
|
162
|
+
isCopying: isCopyingAtom,
|
|
163
|
+
feedbackDuration: feedbackDurationAtom,
|
|
164
|
+
value: valueAtom,
|
|
165
|
+
isIdle: isIdleAtom,
|
|
166
|
+
isSuccess: isSuccessAtom,
|
|
167
|
+
isError: isErrorAtom,
|
|
168
|
+
isUnavailable: isUnavailableAtom,
|
|
169
|
+
};
|
|
170
|
+
const actions = {
|
|
171
|
+
copy,
|
|
172
|
+
setDisabled,
|
|
173
|
+
setFeedbackDuration,
|
|
174
|
+
setValue,
|
|
175
|
+
reset,
|
|
176
|
+
};
|
|
177
|
+
const contracts = {
|
|
178
|
+
getButtonProps,
|
|
179
|
+
getStatusProps,
|
|
180
|
+
getIconContainerProps,
|
|
181
|
+
};
|
|
182
|
+
return { state, actions, contracts };
|
|
183
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { Atom } from '@reatom/core';
|
|
2
|
+
export type HeadlessId = string;
|
|
3
|
+
export interface HeadlessState {
|
|
4
|
+
activeId: Atom<HeadlessId | null>;
|
|
5
|
+
selectedIds: Atom<HeadlessId[]>;
|
|
6
|
+
isOpen: Atom<boolean>;
|
|
7
|
+
}
|
|
8
|
+
export interface HeadlessApi {
|
|
9
|
+
open(): void;
|
|
10
|
+
close(): void;
|
|
11
|
+
setActive(id: HeadlessId | null): void;
|
|
12
|
+
toggleSelected(id: HeadlessId): void;
|
|
13
|
+
clearSelected(): void;
|
|
14
|
+
}
|
|
15
|
+
export interface HeadlessModel {
|
|
16
|
+
readonly state: HeadlessState;
|
|
17
|
+
readonly api: HeadlessApi;
|
|
18
|
+
}
|
|
19
|
+
export * from './selection.js';
|
|
20
|
+
export * from './value-range.js';
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export type SelectionMode = 'single' | 'multiple';
|
|
2
|
+
export declare const normalizeSelection: (ids: readonly string[], allowedIds: ReadonlySet<string>, mode: SelectionMode) => string[];
|
|
3
|
+
export declare const toggleSelection: (selectedIds: readonly string[], id: string, mode: SelectionMode, allowedIds: ReadonlySet<string>) => string[];
|
|
4
|
+
export declare const selectOnly: (id: string, allowedIds: ReadonlySet<string>) => string[];
|
|
5
|
+
export declare const selectRangeByOrder: (orderedIds: readonly string[], fromId: string, toId: string) => string[];
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export const normalizeSelection = (ids, allowedIds, mode) => {
|
|
2
|
+
const filtered = ids.filter((id) => allowedIds.has(id));
|
|
3
|
+
if (mode === 'single') {
|
|
4
|
+
return filtered.slice(0, 1);
|
|
5
|
+
}
|
|
6
|
+
return [...new Set(filtered)];
|
|
7
|
+
};
|
|
8
|
+
export const toggleSelection = (selectedIds, id, mode, allowedIds) => {
|
|
9
|
+
if (!allowedIds.has(id)) {
|
|
10
|
+
return [...selectedIds];
|
|
11
|
+
}
|
|
12
|
+
if (mode === 'single') {
|
|
13
|
+
return [id];
|
|
14
|
+
}
|
|
15
|
+
const selected = new Set(selectedIds);
|
|
16
|
+
if (selected.has(id)) {
|
|
17
|
+
selected.delete(id);
|
|
18
|
+
}
|
|
19
|
+
else {
|
|
20
|
+
selected.add(id);
|
|
21
|
+
}
|
|
22
|
+
return [...selected];
|
|
23
|
+
};
|
|
24
|
+
export const selectOnly = (id, allowedIds) => {
|
|
25
|
+
if (!allowedIds.has(id)) {
|
|
26
|
+
return [];
|
|
27
|
+
}
|
|
28
|
+
return [id];
|
|
29
|
+
};
|
|
30
|
+
export const selectRangeByOrder = (orderedIds, fromId, toId) => {
|
|
31
|
+
const fromIndex = orderedIds.indexOf(fromId);
|
|
32
|
+
const toIndex = orderedIds.indexOf(toId);
|
|
33
|
+
if (fromIndex < 0 || toIndex < 0) {
|
|
34
|
+
return [];
|
|
35
|
+
}
|
|
36
|
+
const start = Math.min(fromIndex, toIndex);
|
|
37
|
+
const end = Math.max(fromIndex, toIndex);
|
|
38
|
+
return orderedIds.slice(start, end + 1);
|
|
39
|
+
};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { type Atom, type Computed } from '@reatom/core';
|
|
2
|
+
export interface ValueRangeInput {
|
|
3
|
+
min: number;
|
|
4
|
+
max: number;
|
|
5
|
+
step?: number;
|
|
6
|
+
largeStep?: number;
|
|
7
|
+
}
|
|
8
|
+
export interface ValueRangeConfig {
|
|
9
|
+
min: number;
|
|
10
|
+
max: number;
|
|
11
|
+
step: number;
|
|
12
|
+
largeStep: number;
|
|
13
|
+
}
|
|
14
|
+
export interface CreateValueRangeOptions extends ValueRangeInput {
|
|
15
|
+
idBase?: string;
|
|
16
|
+
initialValue?: number;
|
|
17
|
+
}
|
|
18
|
+
export interface ValueRangeState {
|
|
19
|
+
value: Atom<number>;
|
|
20
|
+
min: Atom<number>;
|
|
21
|
+
max: Atom<number>;
|
|
22
|
+
step: Atom<number>;
|
|
23
|
+
largeStep: Atom<number>;
|
|
24
|
+
percentage: Computed<number>;
|
|
25
|
+
}
|
|
26
|
+
export interface ValueRangeActions {
|
|
27
|
+
setValue(value: number): void;
|
|
28
|
+
increment(): void;
|
|
29
|
+
decrement(): void;
|
|
30
|
+
incrementLarge(): void;
|
|
31
|
+
decrementLarge(): void;
|
|
32
|
+
setFirst(): void;
|
|
33
|
+
setLast(): void;
|
|
34
|
+
}
|
|
35
|
+
export interface ValueRangeModel {
|
|
36
|
+
readonly state: ValueRangeState;
|
|
37
|
+
readonly actions: ValueRangeActions;
|
|
38
|
+
}
|
|
39
|
+
export declare const normalizeValueRange: ({ min, max, step, largeStep }: ValueRangeInput) => ValueRangeConfig;
|
|
40
|
+
export declare const clampValue: (value: number, min: number, max: number) => number;
|
|
41
|
+
export declare const snapValueToStep: (value: number, range: ValueRangeConfig) => number;
|
|
42
|
+
export declare const constrainValueToRange: (value: number, range: ValueRangeConfig) => number;
|
|
43
|
+
export declare const calculateValueRangePercentage: (value: number, min: number, max: number) => number;
|
|
44
|
+
export declare const incrementValue: (value: number, range: ValueRangeConfig) => number;
|
|
45
|
+
export declare const decrementValue: (value: number, range: ValueRangeConfig) => number;
|
|
46
|
+
export declare const incrementValueLarge: (value: number, range: ValueRangeConfig) => number;
|
|
47
|
+
export declare const decrementValueLarge: (value: number, range: ValueRangeConfig) => number;
|
|
48
|
+
export declare const valuesEqualWithinStep: (left: number, right: number) => boolean;
|
|
49
|
+
export declare function createValueRange(options: CreateValueRangeOptions): ValueRangeModel;
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { action, atom, computed } from '@reatom/core';
|
|
2
|
+
const EPSILON = 1e-9;
|
|
3
|
+
const decimalPlaces = (value) => {
|
|
4
|
+
const text = value.toString().toLowerCase();
|
|
5
|
+
const scientific = text.match(/e-(\d+)$/);
|
|
6
|
+
if (scientific?.[1]) {
|
|
7
|
+
return Number.parseInt(scientific[1], 10);
|
|
8
|
+
}
|
|
9
|
+
const decimalIndex = text.indexOf('.');
|
|
10
|
+
if (decimalIndex < 0)
|
|
11
|
+
return 0;
|
|
12
|
+
return text.length - decimalIndex - 1;
|
|
13
|
+
};
|
|
14
|
+
const normalizePrecision = (value, precision) => Number(value.toFixed(Math.min(Math.max(precision, 0), 12)));
|
|
15
|
+
const sanitizeStep = (value, fallback) => {
|
|
16
|
+
if (value == null || !Number.isFinite(value) || value <= 0) {
|
|
17
|
+
return fallback;
|
|
18
|
+
}
|
|
19
|
+
return value;
|
|
20
|
+
};
|
|
21
|
+
export const normalizeValueRange = ({ min, max, step, largeStep }) => {
|
|
22
|
+
const normalizedMin = Math.min(min, max);
|
|
23
|
+
const normalizedMax = Math.max(min, max);
|
|
24
|
+
const normalizedStep = sanitizeStep(step, 1);
|
|
25
|
+
const range = normalizedMax - normalizedMin;
|
|
26
|
+
const defaultLargeStep = Math.max(normalizedStep, range / 10);
|
|
27
|
+
return {
|
|
28
|
+
min: normalizedMin,
|
|
29
|
+
max: normalizedMax,
|
|
30
|
+
step: normalizedStep,
|
|
31
|
+
largeStep: sanitizeStep(largeStep, defaultLargeStep),
|
|
32
|
+
};
|
|
33
|
+
};
|
|
34
|
+
export const clampValue = (value, min, max) => {
|
|
35
|
+
const normalizedMin = Math.min(min, max);
|
|
36
|
+
const normalizedMax = Math.max(min, max);
|
|
37
|
+
if (value < normalizedMin)
|
|
38
|
+
return normalizedMin;
|
|
39
|
+
if (value > normalizedMax)
|
|
40
|
+
return normalizedMax;
|
|
41
|
+
return value;
|
|
42
|
+
};
|
|
43
|
+
export const snapValueToStep = (value, range) => {
|
|
44
|
+
const clamped = clampValue(value, range.min, range.max);
|
|
45
|
+
const offset = (clamped - range.min) / range.step;
|
|
46
|
+
const snapped = range.min + Math.round(offset) * range.step;
|
|
47
|
+
const precision = Math.max(decimalPlaces(range.min), decimalPlaces(range.step));
|
|
48
|
+
return clampValue(normalizePrecision(snapped, precision + 2), range.min, range.max);
|
|
49
|
+
};
|
|
50
|
+
export const constrainValueToRange = (value, range) => snapValueToStep(value, range);
|
|
51
|
+
export const calculateValueRangePercentage = (value, min, max) => {
|
|
52
|
+
const normalizedMin = Math.min(min, max);
|
|
53
|
+
const normalizedMax = Math.max(min, max);
|
|
54
|
+
const span = normalizedMax - normalizedMin;
|
|
55
|
+
if (span <= EPSILON) {
|
|
56
|
+
return 0;
|
|
57
|
+
}
|
|
58
|
+
const clamped = clampValue(value, normalizedMin, normalizedMax);
|
|
59
|
+
return normalizePrecision(((clamped - normalizedMin) / span) * 100, 4);
|
|
60
|
+
};
|
|
61
|
+
export const incrementValue = (value, range) => {
|
|
62
|
+
const next = snapValueToStep(value, range) + range.step;
|
|
63
|
+
return snapValueToStep(next, range);
|
|
64
|
+
};
|
|
65
|
+
export const decrementValue = (value, range) => {
|
|
66
|
+
const next = snapValueToStep(value, range) - range.step;
|
|
67
|
+
return snapValueToStep(next, range);
|
|
68
|
+
};
|
|
69
|
+
export const incrementValueLarge = (value, range) => {
|
|
70
|
+
const next = snapValueToStep(value, range) + range.largeStep;
|
|
71
|
+
return snapValueToStep(next, range);
|
|
72
|
+
};
|
|
73
|
+
export const decrementValueLarge = (value, range) => {
|
|
74
|
+
const next = snapValueToStep(value, range) - range.largeStep;
|
|
75
|
+
return snapValueToStep(next, range);
|
|
76
|
+
};
|
|
77
|
+
export const valuesEqualWithinStep = (left, right) => Math.abs(left - right) <= EPSILON;
|
|
78
|
+
export function createValueRange(options) {
|
|
79
|
+
const idBase = options.idBase ?? 'value-range';
|
|
80
|
+
const initialRange = normalizeValueRange(options);
|
|
81
|
+
const minAtom = atom(initialRange.min, `${idBase}.min`);
|
|
82
|
+
const maxAtom = atom(initialRange.max, `${idBase}.max`);
|
|
83
|
+
const stepAtom = atom(initialRange.step, `${idBase}.step`);
|
|
84
|
+
const largeStepAtom = atom(initialRange.largeStep, `${idBase}.largeStep`);
|
|
85
|
+
const getRange = () => ({
|
|
86
|
+
min: minAtom(),
|
|
87
|
+
max: maxAtom(),
|
|
88
|
+
step: stepAtom(),
|
|
89
|
+
largeStep: largeStepAtom(),
|
|
90
|
+
});
|
|
91
|
+
const valueAtom = atom(constrainValueToRange(options.initialValue ?? initialRange.min, getRange()), `${idBase}.value`);
|
|
92
|
+
const percentageAtom = computed(() => calculateValueRangePercentage(valueAtom(), minAtom(), maxAtom()), `${idBase}.percentage`);
|
|
93
|
+
const setValue = action((value) => {
|
|
94
|
+
valueAtom.set(constrainValueToRange(value, getRange()));
|
|
95
|
+
}, `${idBase}.setValue`);
|
|
96
|
+
const increment = action(() => {
|
|
97
|
+
valueAtom.set(incrementValue(valueAtom(), getRange()));
|
|
98
|
+
}, `${idBase}.increment`);
|
|
99
|
+
const decrement = action(() => {
|
|
100
|
+
valueAtom.set(decrementValue(valueAtom(), getRange()));
|
|
101
|
+
}, `${idBase}.decrement`);
|
|
102
|
+
const incrementLarge = action(() => {
|
|
103
|
+
valueAtom.set(incrementValueLarge(valueAtom(), getRange()));
|
|
104
|
+
}, `${idBase}.incrementLarge`);
|
|
105
|
+
const decrementLarge = action(() => {
|
|
106
|
+
valueAtom.set(decrementValueLarge(valueAtom(), getRange()));
|
|
107
|
+
}, `${idBase}.decrementLarge`);
|
|
108
|
+
const setFirst = action(() => {
|
|
109
|
+
valueAtom.set(minAtom());
|
|
110
|
+
}, `${idBase}.setFirst`);
|
|
111
|
+
const setLast = action(() => {
|
|
112
|
+
valueAtom.set(maxAtom());
|
|
113
|
+
}, `${idBase}.setLast`);
|
|
114
|
+
const actions = {
|
|
115
|
+
setValue,
|
|
116
|
+
increment,
|
|
117
|
+
decrement,
|
|
118
|
+
incrementLarge,
|
|
119
|
+
decrementLarge,
|
|
120
|
+
setFirst,
|
|
121
|
+
setLast,
|
|
122
|
+
};
|
|
123
|
+
return {
|
|
124
|
+
state: {
|
|
125
|
+
value: valueAtom,
|
|
126
|
+
min: minAtom,
|
|
127
|
+
max: maxAtom,
|
|
128
|
+
step: stepAtom,
|
|
129
|
+
largeStep: largeStepAtom,
|
|
130
|
+
percentage: percentageAtom,
|
|
131
|
+
},
|
|
132
|
+
actions,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { type Atom, type Computed } from '@reatom/core';
|
|
2
|
+
export type DatePickerTimeZone = 'local' | 'utc';
|
|
3
|
+
export interface ParsedDateTime {
|
|
4
|
+
date: string;
|
|
5
|
+
time: string;
|
|
6
|
+
full: string;
|
|
7
|
+
}
|
|
8
|
+
export interface CalendarDay {
|
|
9
|
+
date: string;
|
|
10
|
+
month: 'prev' | 'current' | 'next';
|
|
11
|
+
inRange: boolean;
|
|
12
|
+
isToday: boolean;
|
|
13
|
+
disabled: boolean;
|
|
14
|
+
}
|
|
15
|
+
export interface CreateDatePickerOptions {
|
|
16
|
+
idBase?: string;
|
|
17
|
+
value?: string | null;
|
|
18
|
+
required?: boolean;
|
|
19
|
+
disabled?: boolean;
|
|
20
|
+
readonly?: boolean;
|
|
21
|
+
placeholder?: string;
|
|
22
|
+
locale?: string;
|
|
23
|
+
timeZone?: DatePickerTimeZone;
|
|
24
|
+
min?: string | null;
|
|
25
|
+
max?: string | null;
|
|
26
|
+
minuteStep?: number;
|
|
27
|
+
hourCycle?: 12 | 24;
|
|
28
|
+
closeOnEscape?: boolean;
|
|
29
|
+
ariaLabel?: string;
|
|
30
|
+
parseDateTime?: (value: string, locale: string) => ParsedDateTime | null;
|
|
31
|
+
formatDateTime?: (value: ParsedDateTime, locale: string) => string;
|
|
32
|
+
onInput?: (value: string) => void;
|
|
33
|
+
onCommit?: (value: string | null) => void;
|
|
34
|
+
onClear?: () => void;
|
|
35
|
+
}
|
|
36
|
+
export type DatePickerKeyboardEventLike = Pick<KeyboardEvent, 'key' | 'shiftKey' | 'ctrlKey' | 'metaKey' | 'altKey' | 'preventDefault'>;
|
|
37
|
+
export interface DatePickerState {
|
|
38
|
+
inputValue: Atom<string>;
|
|
39
|
+
isOpen: Atom<boolean>;
|
|
40
|
+
focusedDate: Atom<string | null>;
|
|
41
|
+
committedDate: Atom<string | null>;
|
|
42
|
+
committedTime: Atom<string | null>;
|
|
43
|
+
draftDate: Atom<string | null>;
|
|
44
|
+
draftTime: Atom<string | null>;
|
|
45
|
+
displayedYear: Atom<number>;
|
|
46
|
+
displayedMonth: Atom<number>;
|
|
47
|
+
isInputFocused: Atom<boolean>;
|
|
48
|
+
isCalendarFocused: Atom<boolean>;
|
|
49
|
+
disabled: Atom<boolean>;
|
|
50
|
+
readonly: Atom<boolean>;
|
|
51
|
+
required: Atom<boolean>;
|
|
52
|
+
placeholder: Atom<string>;
|
|
53
|
+
locale: Atom<string>;
|
|
54
|
+
timeZone: Atom<DatePickerTimeZone>;
|
|
55
|
+
min: Atom<string | null>;
|
|
56
|
+
max: Atom<string | null>;
|
|
57
|
+
minuteStep: Atom<number>;
|
|
58
|
+
hourCycle: Atom<12 | 24>;
|
|
59
|
+
isDualCommit: Computed<boolean>;
|
|
60
|
+
hasCommittedSelection: Computed<boolean>;
|
|
61
|
+
hasDraftSelection: Computed<boolean>;
|
|
62
|
+
committedValue: Computed<string | null>;
|
|
63
|
+
draftValue: Computed<string | null>;
|
|
64
|
+
parsedValue: Computed<ParsedDateTime | null>;
|
|
65
|
+
canCommitInput: Computed<boolean>;
|
|
66
|
+
inputInvalid: Computed<boolean>;
|
|
67
|
+
visibleDays: Computed<CalendarDay[]>;
|
|
68
|
+
today: Computed<string>;
|
|
69
|
+
selectedCellId: Computed<string | null>;
|
|
70
|
+
}
|
|
71
|
+
export interface DatePickerActions {
|
|
72
|
+
open(): void;
|
|
73
|
+
close(): void;
|
|
74
|
+
toggle(): void;
|
|
75
|
+
setInputValue(value: string): void;
|
|
76
|
+
commitInput(): void;
|
|
77
|
+
clear(): void;
|
|
78
|
+
setDisabled(value: boolean): void;
|
|
79
|
+
setReadonly(value: boolean): void;
|
|
80
|
+
setRequired(value: boolean): void;
|
|
81
|
+
setPlaceholder(value: string): void;
|
|
82
|
+
setLocale(value: string): void;
|
|
83
|
+
setTimeZone(value: DatePickerTimeZone): void;
|
|
84
|
+
setMin(value: string | null): void;
|
|
85
|
+
setMax(value: string | null): void;
|
|
86
|
+
setMinuteStep(value: number): void;
|
|
87
|
+
setHourCycle(value: 12 | 24): void;
|
|
88
|
+
setDisplayedMonth(year: number, month: number): void;
|
|
89
|
+
moveMonth(offset: -1 | 1): void;
|
|
90
|
+
moveYear(offset: -1 | 1): void;
|
|
91
|
+
setFocusedDate(date: string | null): void;
|
|
92
|
+
moveFocusPreviousDay(): void;
|
|
93
|
+
moveFocusNextDay(): void;
|
|
94
|
+
moveFocusPreviousWeek(): void;
|
|
95
|
+
moveFocusNextWeek(): void;
|
|
96
|
+
selectDraftDate(date: string): void;
|
|
97
|
+
setDraftTime(time: string): void;
|
|
98
|
+
jumpToNow(): void;
|
|
99
|
+
commitDraft(): void;
|
|
100
|
+
cancelDraft(): void;
|
|
101
|
+
handleInputKeyDown(event: DatePickerKeyboardEventLike): void;
|
|
102
|
+
handleDialogKeyDown(event: DatePickerKeyboardEventLike): void;
|
|
103
|
+
handleCalendarKeyDown(event: DatePickerKeyboardEventLike): void;
|
|
104
|
+
handleTimeKeyDown(event: DatePickerKeyboardEventLike): void;
|
|
105
|
+
handleOutsidePointer(): void;
|
|
106
|
+
}
|
|
107
|
+
export interface DatePickerInputProps {
|
|
108
|
+
id: string;
|
|
109
|
+
role: 'combobox';
|
|
110
|
+
tabindex: '0';
|
|
111
|
+
autocomplete: 'off';
|
|
112
|
+
disabled: boolean;
|
|
113
|
+
readonly?: true;
|
|
114
|
+
required?: true;
|
|
115
|
+
value: string;
|
|
116
|
+
placeholder: string;
|
|
117
|
+
'aria-haspopup': 'dialog';
|
|
118
|
+
'aria-expanded': 'true' | 'false';
|
|
119
|
+
'aria-controls': string;
|
|
120
|
+
'aria-activedescendant'?: string;
|
|
121
|
+
'aria-invalid'?: 'true';
|
|
122
|
+
'aria-label'?: string;
|
|
123
|
+
onInput: (value: string) => void;
|
|
124
|
+
onKeyDown: (event: DatePickerKeyboardEventLike) => void;
|
|
125
|
+
onFocus: () => void;
|
|
126
|
+
onBlur: () => void;
|
|
127
|
+
}
|
|
128
|
+
export interface DatePickerDialogProps {
|
|
129
|
+
id: string;
|
|
130
|
+
role: 'dialog';
|
|
131
|
+
tabindex: '-1';
|
|
132
|
+
hidden: boolean;
|
|
133
|
+
'aria-modal': 'true';
|
|
134
|
+
'aria-label': string;
|
|
135
|
+
onKeyDown: (event: DatePickerKeyboardEventLike) => void;
|
|
136
|
+
onPointerDownOutside: () => void;
|
|
137
|
+
}
|
|
138
|
+
export interface DatePickerCalendarGridProps {
|
|
139
|
+
id: string;
|
|
140
|
+
role: 'grid';
|
|
141
|
+
tabindex: '-1';
|
|
142
|
+
'aria-label': string;
|
|
143
|
+
onKeyDown: (event: DatePickerKeyboardEventLike) => void;
|
|
144
|
+
}
|
|
145
|
+
export interface DatePickerCalendarDayProps {
|
|
146
|
+
id: string;
|
|
147
|
+
role: 'gridcell';
|
|
148
|
+
tabindex: '0' | '-1';
|
|
149
|
+
'aria-selected': 'true' | 'false';
|
|
150
|
+
'aria-disabled'?: 'true';
|
|
151
|
+
'aria-current'?: 'date';
|
|
152
|
+
'data-date': string;
|
|
153
|
+
onClick: () => void;
|
|
154
|
+
onMouseEnter: () => void;
|
|
155
|
+
}
|
|
156
|
+
export interface DatePickerMonthNavButtonProps {
|
|
157
|
+
id: string;
|
|
158
|
+
role: 'button';
|
|
159
|
+
tabindex: '0';
|
|
160
|
+
'aria-label': 'Previous month' | 'Next month';
|
|
161
|
+
onClick: () => void;
|
|
162
|
+
}
|
|
163
|
+
export interface DatePickerYearNavButtonProps {
|
|
164
|
+
id: string;
|
|
165
|
+
role: 'button';
|
|
166
|
+
tabindex: '0';
|
|
167
|
+
'aria-label': 'Previous year' | 'Next year';
|
|
168
|
+
onClick: () => void;
|
|
169
|
+
}
|
|
170
|
+
export interface DatePickerTimeSegmentProps {
|
|
171
|
+
id: string;
|
|
172
|
+
type: 'text';
|
|
173
|
+
inputmode: 'numeric';
|
|
174
|
+
'aria-label': string;
|
|
175
|
+
value: string;
|
|
176
|
+
minlength: '2';
|
|
177
|
+
maxlength: '2';
|
|
178
|
+
disabled: boolean;
|
|
179
|
+
readonly: boolean;
|
|
180
|
+
onInput: (value: string) => void;
|
|
181
|
+
onKeyDown: (event: DatePickerKeyboardEventLike) => void;
|
|
182
|
+
}
|
|
183
|
+
export interface DatePickerButtonProps {
|
|
184
|
+
id: string;
|
|
185
|
+
role: 'button';
|
|
186
|
+
tabindex: '0';
|
|
187
|
+
'aria-label': string;
|
|
188
|
+
disabled: boolean;
|
|
189
|
+
onClick: () => void;
|
|
190
|
+
}
|
|
191
|
+
export interface DatePickerContracts {
|
|
192
|
+
getInputProps(): DatePickerInputProps;
|
|
193
|
+
getDialogProps(): DatePickerDialogProps;
|
|
194
|
+
getCalendarGridProps(): DatePickerCalendarGridProps;
|
|
195
|
+
getCalendarDayProps(date: string): DatePickerCalendarDayProps;
|
|
196
|
+
getMonthNavButtonProps(direction: 'prev' | 'next'): DatePickerMonthNavButtonProps;
|
|
197
|
+
getYearNavButtonProps(direction: 'prev' | 'next'): DatePickerYearNavButtonProps;
|
|
198
|
+
getHourInputProps(): DatePickerTimeSegmentProps;
|
|
199
|
+
getMinuteInputProps(): DatePickerTimeSegmentProps;
|
|
200
|
+
getApplyButtonProps(): DatePickerButtonProps;
|
|
201
|
+
getCancelButtonProps(): DatePickerButtonProps;
|
|
202
|
+
getClearButtonProps(): DatePickerButtonProps;
|
|
203
|
+
getVisibleDays(): readonly CalendarDay[];
|
|
204
|
+
}
|
|
205
|
+
export interface DatePickerModel {
|
|
206
|
+
readonly state: DatePickerState;
|
|
207
|
+
readonly actions: DatePickerActions;
|
|
208
|
+
readonly contracts: DatePickerContracts;
|
|
209
|
+
}
|
|
210
|
+
export declare function createDatePicker(options?: CreateDatePickerOptions): DatePickerModel;
|