@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,103 @@
|
|
|
1
|
+
import { action, atom } from '@reatom/core';
|
|
2
|
+
const isSpaceKey = (key) => key === ' ' || key === 'Spacebar';
|
|
3
|
+
export function createCard(options = {}) {
|
|
4
|
+
const idBase = options.idBase ?? 'card';
|
|
5
|
+
const isExpandableAtom = atom(options.isExpandable ?? false, `${idBase}.isExpandable`);
|
|
6
|
+
const isExpandedAtom = atom(options.isExpanded ?? false, `${idBase}.isExpanded`);
|
|
7
|
+
const isDisabledAtom = atom(options.isDisabled ?? false, `${idBase}.isDisabled`);
|
|
8
|
+
const notifyExpandedChange = (next) => {
|
|
9
|
+
if (isExpandedAtom() === next)
|
|
10
|
+
return;
|
|
11
|
+
isExpandedAtom.set(next);
|
|
12
|
+
options.onExpandedChange?.(next);
|
|
13
|
+
};
|
|
14
|
+
const toggle = action(() => {
|
|
15
|
+
if (!isExpandableAtom() || isDisabledAtom())
|
|
16
|
+
return;
|
|
17
|
+
notifyExpandedChange(!isExpandedAtom());
|
|
18
|
+
}, `${idBase}.toggle`);
|
|
19
|
+
const expand = action(() => {
|
|
20
|
+
if (!isExpandableAtom() || isDisabledAtom())
|
|
21
|
+
return;
|
|
22
|
+
notifyExpandedChange(true);
|
|
23
|
+
}, `${idBase}.expand`);
|
|
24
|
+
const collapse = action(() => {
|
|
25
|
+
if (!isExpandableAtom() || isDisabledAtom())
|
|
26
|
+
return;
|
|
27
|
+
notifyExpandedChange(false);
|
|
28
|
+
}, `${idBase}.collapse`);
|
|
29
|
+
const setDisabled = action((value) => {
|
|
30
|
+
isDisabledAtom.set(value);
|
|
31
|
+
}, `${idBase}.setDisabled`);
|
|
32
|
+
const handleClick = action(() => {
|
|
33
|
+
toggle();
|
|
34
|
+
}, `${idBase}.handleClick`);
|
|
35
|
+
const handleKeyDown = action((event) => {
|
|
36
|
+
if (!isExpandableAtom() || isDisabledAtom())
|
|
37
|
+
return;
|
|
38
|
+
if (event.key === 'Enter' || isSpaceKey(event.key)) {
|
|
39
|
+
event.preventDefault?.();
|
|
40
|
+
toggle();
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
if (event.key === 'ArrowDown' || event.key === 'ArrowRight') {
|
|
44
|
+
event.preventDefault?.();
|
|
45
|
+
expand();
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
if (event.key === 'ArrowUp' || event.key === 'ArrowLeft') {
|
|
49
|
+
event.preventDefault?.();
|
|
50
|
+
collapse();
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
}, `${idBase}.handleKeyDown`);
|
|
54
|
+
const triggerId = `${idBase}-trigger`;
|
|
55
|
+
const contentId = `${idBase}-content`;
|
|
56
|
+
const actions = {
|
|
57
|
+
toggle,
|
|
58
|
+
expand,
|
|
59
|
+
collapse,
|
|
60
|
+
setDisabled,
|
|
61
|
+
handleClick,
|
|
62
|
+
handleKeyDown,
|
|
63
|
+
};
|
|
64
|
+
const contracts = {
|
|
65
|
+
getCardProps() {
|
|
66
|
+
return {};
|
|
67
|
+
},
|
|
68
|
+
getTriggerProps() {
|
|
69
|
+
if (!isExpandableAtom())
|
|
70
|
+
return {};
|
|
71
|
+
return {
|
|
72
|
+
id: triggerId,
|
|
73
|
+
role: 'button',
|
|
74
|
+
tabindex: isDisabledAtom() ? '-1' : '0',
|
|
75
|
+
'aria-expanded': isExpandedAtom() ? 'true' : 'false',
|
|
76
|
+
'aria-controls': contentId,
|
|
77
|
+
'aria-disabled': isDisabledAtom() ? 'true' : undefined,
|
|
78
|
+
onClick: handleClick,
|
|
79
|
+
onKeyDown: handleKeyDown,
|
|
80
|
+
};
|
|
81
|
+
},
|
|
82
|
+
getContentProps() {
|
|
83
|
+
if (!isExpandableAtom())
|
|
84
|
+
return {};
|
|
85
|
+
return {
|
|
86
|
+
id: contentId,
|
|
87
|
+
role: 'region',
|
|
88
|
+
'aria-labelledby': triggerId,
|
|
89
|
+
hidden: !isExpandedAtom(),
|
|
90
|
+
};
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
const state = {
|
|
94
|
+
isExpandable: isExpandableAtom,
|
|
95
|
+
isExpanded: isExpandedAtom,
|
|
96
|
+
isDisabled: isDisabledAtom,
|
|
97
|
+
};
|
|
98
|
+
return {
|
|
99
|
+
state,
|
|
100
|
+
actions,
|
|
101
|
+
contracts,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { type Atom, type Computed } from '@reatom/core';
|
|
2
|
+
export interface CarouselSlide {
|
|
3
|
+
id: string;
|
|
4
|
+
label?: string;
|
|
5
|
+
disabled?: boolean;
|
|
6
|
+
}
|
|
7
|
+
export interface CarouselKeyboardEventLike {
|
|
8
|
+
key: string;
|
|
9
|
+
}
|
|
10
|
+
export interface CreateCarouselOptions {
|
|
11
|
+
slides: readonly CarouselSlide[];
|
|
12
|
+
idBase?: string;
|
|
13
|
+
ariaLabel?: string;
|
|
14
|
+
ariaLabelledBy?: string;
|
|
15
|
+
autoplay?: boolean;
|
|
16
|
+
autoplayIntervalMs?: number;
|
|
17
|
+
visibleSlides?: number;
|
|
18
|
+
initialActiveSlideIndex?: number;
|
|
19
|
+
initialPaused?: boolean;
|
|
20
|
+
}
|
|
21
|
+
export interface CarouselState {
|
|
22
|
+
activeSlideIndex: Atom<number>;
|
|
23
|
+
isPaused: Computed<boolean>;
|
|
24
|
+
slideCount: Computed<number>;
|
|
25
|
+
visibleSlideIndices: Computed<number[]>;
|
|
26
|
+
}
|
|
27
|
+
export interface CarouselActions {
|
|
28
|
+
moveNext(): void;
|
|
29
|
+
movePrev(): void;
|
|
30
|
+
moveTo(index: number): void;
|
|
31
|
+
play(): void;
|
|
32
|
+
pause(): void;
|
|
33
|
+
togglePlay(): void;
|
|
34
|
+
handleKeyDown(event: CarouselKeyboardEventLike): void;
|
|
35
|
+
handleFocusIn(): void;
|
|
36
|
+
handleFocusOut(): void;
|
|
37
|
+
handlePointerEnter(): void;
|
|
38
|
+
handlePointerLeave(): void;
|
|
39
|
+
}
|
|
40
|
+
export interface CarouselRootProps {
|
|
41
|
+
id: string;
|
|
42
|
+
role: 'region';
|
|
43
|
+
'aria-roledescription': 'carousel';
|
|
44
|
+
'aria-label'?: string;
|
|
45
|
+
'aria-labelledby'?: string;
|
|
46
|
+
'aria-live': 'off' | 'polite';
|
|
47
|
+
onFocusIn: () => void;
|
|
48
|
+
onFocusOut: () => void;
|
|
49
|
+
onPointerEnter: () => void;
|
|
50
|
+
onPointerLeave: () => void;
|
|
51
|
+
}
|
|
52
|
+
export interface CarouselSlideGroupProps {
|
|
53
|
+
id: string;
|
|
54
|
+
role: 'group';
|
|
55
|
+
'aria-label'?: string;
|
|
56
|
+
}
|
|
57
|
+
export interface CarouselSlideProps {
|
|
58
|
+
id: string;
|
|
59
|
+
role: 'group';
|
|
60
|
+
'aria-roledescription': 'slide';
|
|
61
|
+
'aria-label': string;
|
|
62
|
+
'aria-hidden': 'true' | 'false';
|
|
63
|
+
'data-active': 'true' | 'false';
|
|
64
|
+
}
|
|
65
|
+
export interface CarouselControlButtonProps {
|
|
66
|
+
id: string;
|
|
67
|
+
role: 'button';
|
|
68
|
+
tabindex: '0';
|
|
69
|
+
'aria-controls': string;
|
|
70
|
+
'aria-label': string;
|
|
71
|
+
onClick: () => void;
|
|
72
|
+
}
|
|
73
|
+
export type CarouselPlayPauseButtonProps = CarouselControlButtonProps;
|
|
74
|
+
export interface CarouselIndicatorProps {
|
|
75
|
+
id: string;
|
|
76
|
+
role: 'button';
|
|
77
|
+
tabindex: '0';
|
|
78
|
+
'aria-controls': string;
|
|
79
|
+
'aria-label': string;
|
|
80
|
+
'aria-current'?: 'true';
|
|
81
|
+
'data-active': 'true' | 'false';
|
|
82
|
+
onClick: () => void;
|
|
83
|
+
}
|
|
84
|
+
export interface CarouselContracts {
|
|
85
|
+
getRootProps(): CarouselRootProps;
|
|
86
|
+
getSlideGroupProps(): CarouselSlideGroupProps;
|
|
87
|
+
getSlideProps(index: number): CarouselSlideProps;
|
|
88
|
+
getNextButtonProps(): CarouselControlButtonProps;
|
|
89
|
+
getPrevButtonProps(): CarouselControlButtonProps;
|
|
90
|
+
getPlayPauseButtonProps(): CarouselPlayPauseButtonProps;
|
|
91
|
+
getIndicatorProps(index: number): CarouselIndicatorProps;
|
|
92
|
+
}
|
|
93
|
+
export interface CarouselModel {
|
|
94
|
+
readonly state: CarouselState;
|
|
95
|
+
readonly actions: CarouselActions;
|
|
96
|
+
readonly contracts: CarouselContracts;
|
|
97
|
+
}
|
|
98
|
+
export declare function createCarousel(options: CreateCarouselOptions): CarouselModel;
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import { action, atom, computed } from '@reatom/core';
|
|
2
|
+
const clampIndex = (value, count) => {
|
|
3
|
+
if (count <= 0)
|
|
4
|
+
return 0;
|
|
5
|
+
return Math.min(Math.max(value, 0), count - 1);
|
|
6
|
+
};
|
|
7
|
+
export function createCarousel(options) {
|
|
8
|
+
const idBase = options.idBase ?? 'carousel';
|
|
9
|
+
const autoplayEnabled = options.autoplay ?? false;
|
|
10
|
+
const autoplayIntervalMs = Math.max(options.autoplayIntervalMs ?? 5000, 1);
|
|
11
|
+
const visibleSlides = Math.max(options.visibleSlides ?? 1, 1);
|
|
12
|
+
const slides = [...options.slides];
|
|
13
|
+
const slideCountAtom = computed(() => slides.length, `${idBase}.slideCount`);
|
|
14
|
+
const initialActiveSlideIndex = clampIndex(options.initialActiveSlideIndex ?? 0, slides.length);
|
|
15
|
+
const activeSlideIndexAtom = atom(initialActiveSlideIndex, `${idBase}.activeSlideIndex`);
|
|
16
|
+
const isPointerInsideAtom = atom(false, `${idBase}.isPointerInside`);
|
|
17
|
+
const isFocusWithinAtom = atom(false, `${idBase}.isFocusWithin`);
|
|
18
|
+
const userPausedAtom = atom(options.initialPaused ?? false, `${idBase}.userPaused`);
|
|
19
|
+
const liveModeAtom = atom(autoplayEnabled ? 'off' : 'polite', `${idBase}.liveMode`);
|
|
20
|
+
const isPausedAtom = computed(() => autoplayEnabled && (userPausedAtom() || isPointerInsideAtom() || isFocusWithinAtom()), `${idBase}.isPaused`);
|
|
21
|
+
const visibleSlideIndicesAtom = computed(() => {
|
|
22
|
+
const count = slideCountAtom();
|
|
23
|
+
if (count === 0)
|
|
24
|
+
return [];
|
|
25
|
+
const start = clampIndex(activeSlideIndexAtom(), count);
|
|
26
|
+
const length = Math.min(visibleSlides, count);
|
|
27
|
+
return Array.from({ length }, (_, offset) => (start + offset) % count);
|
|
28
|
+
}, `${idBase}.visibleSlideIndices`);
|
|
29
|
+
let rotationTimer = null;
|
|
30
|
+
const clearRotationTimer = () => {
|
|
31
|
+
if (rotationTimer == null)
|
|
32
|
+
return;
|
|
33
|
+
clearTimeout(rotationTimer);
|
|
34
|
+
rotationTimer = null;
|
|
35
|
+
};
|
|
36
|
+
const isAutoplayRunning = () => autoplayEnabled && !isPausedAtom() && slideCountAtom() > 1;
|
|
37
|
+
const setActiveByIndex = (index, source) => {
|
|
38
|
+
const count = slideCountAtom();
|
|
39
|
+
if (count <= 0) {
|
|
40
|
+
activeSlideIndexAtom.set(0);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
const target = ((index % count) + count) % count;
|
|
44
|
+
activeSlideIndexAtom.set(target);
|
|
45
|
+
if (!autoplayEnabled) {
|
|
46
|
+
liveModeAtom.set('polite');
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
if (source === 'manual') {
|
|
50
|
+
liveModeAtom.set('polite');
|
|
51
|
+
}
|
|
52
|
+
if (source === 'auto') {
|
|
53
|
+
liveModeAtom.set('off');
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
const syncAutoplay = () => {
|
|
57
|
+
clearRotationTimer();
|
|
58
|
+
if (!isAutoplayRunning()) {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
rotationTimer = setTimeout(() => {
|
|
62
|
+
rotationTimer = null;
|
|
63
|
+
setActiveByIndex(activeSlideIndexAtom() + 1, 'auto');
|
|
64
|
+
syncAutoplay();
|
|
65
|
+
}, autoplayIntervalMs);
|
|
66
|
+
};
|
|
67
|
+
const moveNext = action(() => {
|
|
68
|
+
setActiveByIndex(activeSlideIndexAtom() + 1, 'manual');
|
|
69
|
+
syncAutoplay();
|
|
70
|
+
}, `${idBase}.moveNext`);
|
|
71
|
+
const movePrev = action(() => {
|
|
72
|
+
setActiveByIndex(activeSlideIndexAtom() - 1, 'manual');
|
|
73
|
+
syncAutoplay();
|
|
74
|
+
}, `${idBase}.movePrev`);
|
|
75
|
+
const moveTo = action((index) => {
|
|
76
|
+
setActiveByIndex(index, 'manual');
|
|
77
|
+
syncAutoplay();
|
|
78
|
+
}, `${idBase}.moveTo`);
|
|
79
|
+
const play = action(() => {
|
|
80
|
+
userPausedAtom.set(false);
|
|
81
|
+
syncAutoplay();
|
|
82
|
+
}, `${idBase}.play`);
|
|
83
|
+
const pause = action(() => {
|
|
84
|
+
userPausedAtom.set(true);
|
|
85
|
+
clearRotationTimer();
|
|
86
|
+
}, `${idBase}.pause`);
|
|
87
|
+
const togglePlay = action(() => {
|
|
88
|
+
if (userPausedAtom()) {
|
|
89
|
+
play();
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
pause();
|
|
93
|
+
}
|
|
94
|
+
}, `${idBase}.togglePlay`);
|
|
95
|
+
const handleFocusIn = action(() => {
|
|
96
|
+
isFocusWithinAtom.set(true);
|
|
97
|
+
clearRotationTimer();
|
|
98
|
+
}, `${idBase}.handleFocusIn`);
|
|
99
|
+
const handleFocusOut = action(() => {
|
|
100
|
+
isFocusWithinAtom.set(false);
|
|
101
|
+
syncAutoplay();
|
|
102
|
+
}, `${idBase}.handleFocusOut`);
|
|
103
|
+
const handlePointerEnter = action(() => {
|
|
104
|
+
isPointerInsideAtom.set(true);
|
|
105
|
+
clearRotationTimer();
|
|
106
|
+
}, `${idBase}.handlePointerEnter`);
|
|
107
|
+
const handlePointerLeave = action(() => {
|
|
108
|
+
isPointerInsideAtom.set(false);
|
|
109
|
+
syncAutoplay();
|
|
110
|
+
}, `${idBase}.handlePointerLeave`);
|
|
111
|
+
const handleKeyDown = action((event) => {
|
|
112
|
+
switch (event.key) {
|
|
113
|
+
case 'ArrowRight':
|
|
114
|
+
moveNext();
|
|
115
|
+
return;
|
|
116
|
+
case 'ArrowLeft':
|
|
117
|
+
movePrev();
|
|
118
|
+
return;
|
|
119
|
+
case 'Home':
|
|
120
|
+
moveTo(0);
|
|
121
|
+
return;
|
|
122
|
+
case 'End':
|
|
123
|
+
moveTo(slideCountAtom() - 1);
|
|
124
|
+
return;
|
|
125
|
+
default:
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
}, `${idBase}.handleKeyDown`);
|
|
129
|
+
const rootId = `${idBase}-root`;
|
|
130
|
+
const slideGroupId = `${idBase}-slides`;
|
|
131
|
+
const slideId = (index) => `${idBase}-slide-${index}`;
|
|
132
|
+
const actions = {
|
|
133
|
+
moveNext,
|
|
134
|
+
movePrev,
|
|
135
|
+
moveTo,
|
|
136
|
+
play,
|
|
137
|
+
pause,
|
|
138
|
+
togglePlay,
|
|
139
|
+
handleKeyDown,
|
|
140
|
+
handleFocusIn,
|
|
141
|
+
handleFocusOut,
|
|
142
|
+
handlePointerEnter,
|
|
143
|
+
handlePointerLeave,
|
|
144
|
+
};
|
|
145
|
+
const contracts = {
|
|
146
|
+
getRootProps() {
|
|
147
|
+
return {
|
|
148
|
+
id: rootId,
|
|
149
|
+
role: 'region',
|
|
150
|
+
'aria-roledescription': 'carousel',
|
|
151
|
+
'aria-label': options.ariaLabel,
|
|
152
|
+
'aria-labelledby': options.ariaLabelledBy,
|
|
153
|
+
'aria-live': liveModeAtom(),
|
|
154
|
+
onFocusIn: handleFocusIn,
|
|
155
|
+
onFocusOut: handleFocusOut,
|
|
156
|
+
onPointerEnter: handlePointerEnter,
|
|
157
|
+
onPointerLeave: handlePointerLeave,
|
|
158
|
+
};
|
|
159
|
+
},
|
|
160
|
+
getSlideGroupProps() {
|
|
161
|
+
return {
|
|
162
|
+
id: slideGroupId,
|
|
163
|
+
role: 'group',
|
|
164
|
+
'aria-label': 'Slides',
|
|
165
|
+
};
|
|
166
|
+
},
|
|
167
|
+
getSlideProps(index) {
|
|
168
|
+
const count = slideCountAtom();
|
|
169
|
+
if (index < 0 || index >= count) {
|
|
170
|
+
throw new Error(`Unknown carousel slide index: ${index}`);
|
|
171
|
+
}
|
|
172
|
+
const visible = visibleSlideIndicesAtom().includes(index);
|
|
173
|
+
return {
|
|
174
|
+
id: slideId(index),
|
|
175
|
+
role: 'group',
|
|
176
|
+
'aria-roledescription': 'slide',
|
|
177
|
+
'aria-label': slides[index]?.label ?? `${index + 1} of ${count}`,
|
|
178
|
+
'aria-hidden': visible ? 'false' : 'true',
|
|
179
|
+
'data-active': index === activeSlideIndexAtom() ? 'true' : 'false',
|
|
180
|
+
};
|
|
181
|
+
},
|
|
182
|
+
getNextButtonProps() {
|
|
183
|
+
return {
|
|
184
|
+
id: `${idBase}-next`,
|
|
185
|
+
role: 'button',
|
|
186
|
+
tabindex: '0',
|
|
187
|
+
'aria-controls': slideGroupId,
|
|
188
|
+
'aria-label': 'Next slide',
|
|
189
|
+
onClick: moveNext,
|
|
190
|
+
};
|
|
191
|
+
},
|
|
192
|
+
getPrevButtonProps() {
|
|
193
|
+
return {
|
|
194
|
+
id: `${idBase}-prev`,
|
|
195
|
+
role: 'button',
|
|
196
|
+
tabindex: '0',
|
|
197
|
+
'aria-controls': slideGroupId,
|
|
198
|
+
'aria-label': 'Previous slide',
|
|
199
|
+
onClick: movePrev,
|
|
200
|
+
};
|
|
201
|
+
},
|
|
202
|
+
getPlayPauseButtonProps() {
|
|
203
|
+
const paused = userPausedAtom();
|
|
204
|
+
return {
|
|
205
|
+
id: `${idBase}-play-pause`,
|
|
206
|
+
role: 'button',
|
|
207
|
+
tabindex: '0',
|
|
208
|
+
'aria-controls': slideGroupId,
|
|
209
|
+
'aria-label': paused ? 'Start slide rotation' : 'Stop slide rotation',
|
|
210
|
+
onClick: togglePlay,
|
|
211
|
+
};
|
|
212
|
+
},
|
|
213
|
+
getIndicatorProps(index) {
|
|
214
|
+
const count = slideCountAtom();
|
|
215
|
+
if (index < 0 || index >= count) {
|
|
216
|
+
throw new Error(`Unknown carousel indicator index: ${index}`);
|
|
217
|
+
}
|
|
218
|
+
const isActive = activeSlideIndexAtom() === index;
|
|
219
|
+
return {
|
|
220
|
+
id: `${idBase}-indicator-${index}`,
|
|
221
|
+
role: 'button',
|
|
222
|
+
tabindex: '0',
|
|
223
|
+
'aria-controls': slideId(index),
|
|
224
|
+
'aria-label': `Go to slide ${index + 1}`,
|
|
225
|
+
'aria-current': isActive ? 'true' : undefined,
|
|
226
|
+
'data-active': isActive ? 'true' : 'false',
|
|
227
|
+
onClick: () => moveTo(index),
|
|
228
|
+
};
|
|
229
|
+
},
|
|
230
|
+
};
|
|
231
|
+
const state = {
|
|
232
|
+
activeSlideIndex: activeSlideIndexAtom,
|
|
233
|
+
isPaused: isPausedAtom,
|
|
234
|
+
slideCount: slideCountAtom,
|
|
235
|
+
visibleSlideIndices: visibleSlideIndicesAtom,
|
|
236
|
+
};
|
|
237
|
+
syncAutoplay();
|
|
238
|
+
return {
|
|
239
|
+
state,
|
|
240
|
+
actions,
|
|
241
|
+
contracts,
|
|
242
|
+
};
|
|
243
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { type Atom } from '@reatom/core';
|
|
2
|
+
export type CheckboxValue = boolean | 'mixed';
|
|
3
|
+
export interface CreateCheckboxOptions {
|
|
4
|
+
idBase?: string;
|
|
5
|
+
checked?: CheckboxValue;
|
|
6
|
+
isDisabled?: boolean;
|
|
7
|
+
isReadOnly?: boolean;
|
|
8
|
+
allowMixed?: boolean;
|
|
9
|
+
ariaLabelledBy?: string;
|
|
10
|
+
ariaDescribedBy?: string;
|
|
11
|
+
onCheckedChange?: (value: CheckboxValue) => void;
|
|
12
|
+
}
|
|
13
|
+
export interface CheckboxState {
|
|
14
|
+
checked: Atom<CheckboxValue>;
|
|
15
|
+
isDisabled: Atom<boolean>;
|
|
16
|
+
isReadOnly: Atom<boolean>;
|
|
17
|
+
}
|
|
18
|
+
export interface CheckboxActions {
|
|
19
|
+
setChecked(value: CheckboxValue): void;
|
|
20
|
+
setDisabled(value: boolean): void;
|
|
21
|
+
setReadOnly(value: boolean): void;
|
|
22
|
+
toggle(): void;
|
|
23
|
+
handleClick(): void;
|
|
24
|
+
handleKeyDown(event: Pick<KeyboardEvent, 'key'> & {
|
|
25
|
+
preventDefault?: () => void;
|
|
26
|
+
}): void;
|
|
27
|
+
}
|
|
28
|
+
export interface CheckboxProps {
|
|
29
|
+
id: string;
|
|
30
|
+
role: 'checkbox';
|
|
31
|
+
tabindex: '0' | '-1';
|
|
32
|
+
'aria-checked': 'true' | 'false' | 'mixed';
|
|
33
|
+
'aria-disabled'?: 'true';
|
|
34
|
+
'aria-readonly'?: 'true';
|
|
35
|
+
'aria-labelledby'?: string;
|
|
36
|
+
'aria-describedby'?: string;
|
|
37
|
+
onClick: () => void;
|
|
38
|
+
onKeyDown: (event: Pick<KeyboardEvent, 'key'> & {
|
|
39
|
+
preventDefault?: () => void;
|
|
40
|
+
}) => void;
|
|
41
|
+
}
|
|
42
|
+
export interface CheckboxContracts {
|
|
43
|
+
getCheckboxProps(): CheckboxProps;
|
|
44
|
+
}
|
|
45
|
+
export interface CheckboxModel {
|
|
46
|
+
readonly state: CheckboxState;
|
|
47
|
+
readonly actions: CheckboxActions;
|
|
48
|
+
readonly contracts: CheckboxContracts;
|
|
49
|
+
}
|
|
50
|
+
export declare function createCheckbox(options?: CreateCheckboxOptions): CheckboxModel;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { action, atom } from '@reatom/core';
|
|
2
|
+
const normalizeCheckboxValue = (value, allowMixed) => {
|
|
3
|
+
if (value === 'mixed' && !allowMixed) {
|
|
4
|
+
return false;
|
|
5
|
+
}
|
|
6
|
+
return value;
|
|
7
|
+
};
|
|
8
|
+
const nextCheckboxValue = (value) => {
|
|
9
|
+
if (value === 'mixed')
|
|
10
|
+
return true;
|
|
11
|
+
return !value;
|
|
12
|
+
};
|
|
13
|
+
const toAriaChecked = (value) => {
|
|
14
|
+
if (value === 'mixed')
|
|
15
|
+
return 'mixed';
|
|
16
|
+
return value ? 'true' : 'false';
|
|
17
|
+
};
|
|
18
|
+
const isSpaceKey = (key) => key === ' ' || key === 'Spacebar';
|
|
19
|
+
export function createCheckbox(options = {}) {
|
|
20
|
+
const idBase = options.idBase ?? 'checkbox';
|
|
21
|
+
const allowMixed = options.allowMixed ?? options.checked === 'mixed';
|
|
22
|
+
const checkedAtom = atom(normalizeCheckboxValue(options.checked ?? false, allowMixed), `${idBase}.checked`);
|
|
23
|
+
const isDisabledAtom = atom(options.isDisabled ?? false, `${idBase}.isDisabled`);
|
|
24
|
+
const isReadOnlyAtom = atom(options.isReadOnly ?? false, `${idBase}.isReadOnly`);
|
|
25
|
+
const canMutate = () => !isDisabledAtom() && !isReadOnlyAtom();
|
|
26
|
+
const setChecked = action((value) => {
|
|
27
|
+
const normalized = normalizeCheckboxValue(value, allowMixed);
|
|
28
|
+
checkedAtom.set(normalized);
|
|
29
|
+
options.onCheckedChange?.(normalized);
|
|
30
|
+
}, `${idBase}.setChecked`);
|
|
31
|
+
const setDisabled = action((value) => {
|
|
32
|
+
isDisabledAtom.set(value);
|
|
33
|
+
}, `${idBase}.setDisabled`);
|
|
34
|
+
const setReadOnly = action((value) => {
|
|
35
|
+
isReadOnlyAtom.set(value);
|
|
36
|
+
}, `${idBase}.setReadOnly`);
|
|
37
|
+
const toggle = action(() => {
|
|
38
|
+
if (!canMutate())
|
|
39
|
+
return;
|
|
40
|
+
setChecked(nextCheckboxValue(checkedAtom()));
|
|
41
|
+
}, `${idBase}.toggle`);
|
|
42
|
+
const handleClick = action(() => {
|
|
43
|
+
toggle();
|
|
44
|
+
}, `${idBase}.handleClick`);
|
|
45
|
+
const handleKeyDown = action((event) => {
|
|
46
|
+
if (isSpaceKey(event.key)) {
|
|
47
|
+
if (!canMutate())
|
|
48
|
+
return;
|
|
49
|
+
event.preventDefault?.();
|
|
50
|
+
toggle();
|
|
51
|
+
}
|
|
52
|
+
}, `${idBase}.handleKeyDown`);
|
|
53
|
+
const actions = {
|
|
54
|
+
setChecked,
|
|
55
|
+
setDisabled,
|
|
56
|
+
setReadOnly,
|
|
57
|
+
toggle,
|
|
58
|
+
handleClick,
|
|
59
|
+
handleKeyDown,
|
|
60
|
+
};
|
|
61
|
+
const contracts = {
|
|
62
|
+
getCheckboxProps() {
|
|
63
|
+
return {
|
|
64
|
+
id: `${idBase}-root`,
|
|
65
|
+
role: 'checkbox',
|
|
66
|
+
tabindex: isDisabledAtom() ? '-1' : '0',
|
|
67
|
+
'aria-checked': toAriaChecked(checkedAtom()),
|
|
68
|
+
'aria-disabled': isDisabledAtom() ? 'true' : undefined,
|
|
69
|
+
'aria-readonly': isReadOnlyAtom() ? 'true' : undefined,
|
|
70
|
+
'aria-labelledby': options.ariaLabelledBy,
|
|
71
|
+
'aria-describedby': options.ariaDescribedBy,
|
|
72
|
+
onClick: handleClick,
|
|
73
|
+
onKeyDown: handleKeyDown,
|
|
74
|
+
};
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
const state = {
|
|
78
|
+
checked: checkedAtom,
|
|
79
|
+
isDisabled: isDisabledAtom,
|
|
80
|
+
isReadOnly: isReadOnlyAtom,
|
|
81
|
+
};
|
|
82
|
+
return {
|
|
83
|
+
state,
|
|
84
|
+
actions,
|
|
85
|
+
contracts,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { type Atom, type Computed } from '@reatom/core';
|
|
2
|
+
export interface ComboboxOption {
|
|
3
|
+
id: string;
|
|
4
|
+
label: string;
|
|
5
|
+
disabled?: boolean;
|
|
6
|
+
}
|
|
7
|
+
export interface ComboboxOptionGroup {
|
|
8
|
+
id: string;
|
|
9
|
+
label: string;
|
|
10
|
+
options: ComboboxOption[];
|
|
11
|
+
}
|
|
12
|
+
export interface ComboboxVisibleGroup {
|
|
13
|
+
id: string;
|
|
14
|
+
label: string;
|
|
15
|
+
options: readonly ComboboxOption[];
|
|
16
|
+
}
|
|
17
|
+
export type ComboboxMatchMode = 'includes' | 'startsWith';
|
|
18
|
+
export type ComboboxType = 'editable' | 'select-only';
|
|
19
|
+
export interface CreateComboboxOptions {
|
|
20
|
+
options: readonly (ComboboxOption | ComboboxOptionGroup)[];
|
|
21
|
+
type?: ComboboxType;
|
|
22
|
+
multiple?: boolean;
|
|
23
|
+
clearable?: boolean;
|
|
24
|
+
closeOnSelect?: boolean;
|
|
25
|
+
matchMode?: ComboboxMatchMode;
|
|
26
|
+
filter?: (option: ComboboxOption, inputValue: string) => boolean;
|
|
27
|
+
typeahead?: boolean | {
|
|
28
|
+
enabled?: boolean;
|
|
29
|
+
timeoutMs?: number;
|
|
30
|
+
};
|
|
31
|
+
idBase?: string;
|
|
32
|
+
ariaLabel?: string;
|
|
33
|
+
initialInputValue?: string;
|
|
34
|
+
initialSelectedId?: string | null;
|
|
35
|
+
initialSelectedIds?: string[];
|
|
36
|
+
initialOpen?: boolean;
|
|
37
|
+
}
|
|
38
|
+
export interface ComboboxState {
|
|
39
|
+
inputValue: Atom<string>;
|
|
40
|
+
isOpen: Atom<boolean>;
|
|
41
|
+
activeId: Atom<string | null>;
|
|
42
|
+
selectedId: Atom<string | null>;
|
|
43
|
+
selectedIds: Atom<string[]>;
|
|
44
|
+
hasSelection: Computed<boolean>;
|
|
45
|
+
type: Computed<ComboboxType>;
|
|
46
|
+
multiple: Computed<boolean>;
|
|
47
|
+
}
|
|
48
|
+
export interface ComboboxInputProps {
|
|
49
|
+
id: string;
|
|
50
|
+
role: 'combobox';
|
|
51
|
+
tabindex: '0';
|
|
52
|
+
'aria-haspopup': 'listbox';
|
|
53
|
+
'aria-expanded': 'true' | 'false';
|
|
54
|
+
'aria-controls': string;
|
|
55
|
+
'aria-autocomplete'?: 'list';
|
|
56
|
+
'aria-activedescendant'?: string;
|
|
57
|
+
'aria-label'?: string;
|
|
58
|
+
}
|
|
59
|
+
export interface ComboboxListboxProps {
|
|
60
|
+
id: string;
|
|
61
|
+
role: 'listbox';
|
|
62
|
+
tabindex: '-1';
|
|
63
|
+
'aria-label'?: string;
|
|
64
|
+
'aria-multiselectable'?: 'true';
|
|
65
|
+
}
|
|
66
|
+
export interface ComboboxOptionProps {
|
|
67
|
+
id: string;
|
|
68
|
+
role: 'option';
|
|
69
|
+
tabindex: '-1';
|
|
70
|
+
'aria-selected': 'true' | 'false';
|
|
71
|
+
'aria-disabled'?: 'true';
|
|
72
|
+
'data-active': 'true' | 'false';
|
|
73
|
+
}
|
|
74
|
+
export interface ComboboxGroupProps {
|
|
75
|
+
id: string;
|
|
76
|
+
role: 'group';
|
|
77
|
+
'aria-labelledby': string;
|
|
78
|
+
}
|
|
79
|
+
export interface ComboboxGroupLabelProps {
|
|
80
|
+
id: string;
|
|
81
|
+
role: 'presentation';
|
|
82
|
+
}
|
|
83
|
+
export interface ComboboxActions {
|
|
84
|
+
open(): void;
|
|
85
|
+
close(): void;
|
|
86
|
+
setInputValue(value: string): void;
|
|
87
|
+
setActive(id: string | null): void;
|
|
88
|
+
moveNext(): void;
|
|
89
|
+
movePrev(): void;
|
|
90
|
+
moveFirst(): void;
|
|
91
|
+
moveLast(): void;
|
|
92
|
+
commitActive(): void;
|
|
93
|
+
select(id: string): void;
|
|
94
|
+
toggleOption(id: string): void;
|
|
95
|
+
removeSelected(id: string): void;
|
|
96
|
+
clearSelection(): void;
|
|
97
|
+
clear(): void;
|
|
98
|
+
handleKeyDown(event: Pick<KeyboardEvent, 'key' | 'shiftKey' | 'ctrlKey' | 'metaKey' | 'altKey'>): void;
|
|
99
|
+
}
|
|
100
|
+
export interface ComboboxContracts {
|
|
101
|
+
getVisibleOptions(): readonly (ComboboxOption | ComboboxVisibleGroup)[];
|
|
102
|
+
getFlatVisibleOptions(): readonly ComboboxOption[];
|
|
103
|
+
getInputProps(): ComboboxInputProps;
|
|
104
|
+
getListboxProps(): ComboboxListboxProps;
|
|
105
|
+
getOptionProps(id: string): ComboboxOptionProps;
|
|
106
|
+
getGroupProps(groupId: string): ComboboxGroupProps;
|
|
107
|
+
getGroupLabelProps(groupId: string): ComboboxGroupLabelProps;
|
|
108
|
+
}
|
|
109
|
+
export interface ComboboxModel {
|
|
110
|
+
readonly state: ComboboxState;
|
|
111
|
+
readonly actions: ComboboxActions;
|
|
112
|
+
readonly contracts: ComboboxContracts;
|
|
113
|
+
}
|
|
114
|
+
export declare function createCombobox(options: CreateComboboxOptions): ComboboxModel;
|