@inkra/react 0.3.0 → 0.4.3
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/README.md +15 -3
- package/dist/Button.js +3 -2
- package/dist/PencilIcon.d.ts +10 -0
- package/dist/PencilIcon.js +83 -39
- package/dist/PencilLine.js +9 -1
- package/dist/PencilText.js +15 -9
- package/dist/accordion/Accordion.js +65 -5
- package/dist/alert-dialog/index.js +14 -4
- package/dist/audio-player/index.d.ts +2 -0
- package/dist/audio-player/index.js +126 -31
- package/dist/calendar/index.d.ts +4 -1
- package/dist/calendar/index.js +31 -12
- package/dist/carousel/index.js +13 -6
- package/dist/choice-grid/index.d.ts +6 -1
- package/dist/choice-grid/index.js +61 -33
- package/dist/color-picker/GradientEditor.d.ts +39 -0
- package/dist/color-picker/GradientEditor.js +63 -0
- package/dist/color-picker/gradient.d.ts +25 -0
- package/dist/color-picker/gradient.js +60 -0
- package/dist/color-picker/index.d.ts +27 -7
- package/dist/color-picker/index.js +134 -32
- package/dist/color-swatch/index.d.ts +10 -0
- package/dist/color-swatch/index.js +9 -0
- package/dist/command/index.js +27 -4
- package/dist/content/index.d.ts +5 -0
- package/dist/content/index.js +12 -3
- package/dist/context-menu/index.js +1 -1
- package/dist/data-table/index.d.ts +7 -1
- package/dist/data-table/index.js +64 -16
- package/dist/data-table/model.d.ts +2 -0
- package/dist/data-table/model.js +7 -0
- package/dist/date-picker/index.d.ts +2 -0
- package/dist/date-picker/index.js +10 -9
- package/dist/day-agenda/index.d.ts +6 -1
- package/dist/day-agenda/index.js +46 -21
- package/dist/day-agenda/layout.js +65 -9
- package/dist/display/ProgressRing.js +7 -3
- package/dist/display/index.d.ts +4 -1
- package/dist/display/index.js +3 -2
- package/dist/dockable-panel/index.js +5 -5
- package/dist/drawer/index.js +4 -3
- package/dist/editable/index.d.ts +24 -2
- package/dist/editable/index.js +142 -37
- package/dist/empty/illustrations.d.ts +8 -0
- package/dist/empty/illustrations.js +78 -0
- package/dist/empty/index.d.ts +6 -1
- package/dist/empty/index.js +11 -3
- package/dist/flip/index.js +28 -8
- package/dist/form/Field.js +11 -10
- package/dist/form/Textarea.d.ts +3 -1
- package/dist/form/Textarea.js +6 -4
- package/dist/hotkey/index.d.ts +1 -0
- package/dist/hotkey/index.js +6 -4
- package/dist/hover-card/index.js +1 -1
- package/dist/image-cropper/index.js +4 -2
- package/dist/image-viewer/VideoResource.d.ts +21 -0
- package/dist/image-viewer/VideoResource.js +194 -0
- package/dist/image-viewer/index.d.ts +14 -0
- package/dist/image-viewer/index.js +118 -65
- package/dist/image-viewer/lucideGeometry.d.ts +3 -0
- package/dist/image-viewer/lucideGeometry.js +19 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/input-number/index.js +9 -4
- package/dist/internal/cache.d.ts +4 -0
- package/dist/internal/cache.js +18 -1
- package/dist/internal/iconGeometry.d.ts +2 -0
- package/dist/internal/iconGeometry.js +32 -0
- package/dist/internal/renderTokens.d.ts +4 -0
- package/dist/internal/renderTokens.js +4 -0
- package/dist/internal/stroke.d.ts +7 -0
- package/dist/internal/stroke.js +63 -7
- package/dist/internal/surfaceBudget.d.ts +20 -0
- package/dist/internal/surfaceBudget.js +69 -0
- package/dist/internal/surfaceRenderer.js +87 -28
- package/dist/internal/surfaceVisibility.d.ts +3 -0
- package/dist/internal/surfaceVisibility.js +60 -0
- package/dist/internal/textRenderer.d.ts +2 -0
- package/dist/internal/textRenderer.js +3 -3
- package/dist/internal/usePencilSurface.js +17 -1
- package/dist/internal/wash.js +4 -3
- package/dist/journey-map/terrain.js +10 -5
- package/dist/layout/index.js +1 -1
- package/dist/log/index.js +29 -22
- package/dist/markdown/CodeBlock.d.ts +19 -0
- package/dist/markdown/CodeBlock.js +86 -0
- package/dist/markdown/Deletion.d.ts +2 -0
- package/dist/markdown/Deletion.js +69 -0
- package/dist/markdown/icons.d.ts +14 -0
- package/dist/markdown/icons.js +16 -0
- package/dist/markdown/index.d.ts +17 -0
- package/dist/markdown/index.js +175 -0
- package/dist/node-graph/index.d.ts +2 -0
- package/dist/node-graph/index.js +49 -25
- package/dist/number-ticker/precision.js +9 -2
- package/dist/overflow-list/index.js +5 -2
- package/dist/overlay/Overlay.js +1 -1
- package/dist/overlay/Popover.js +3 -3
- package/dist/overlay/themeBridge.js +14 -2
- package/dist/pagination/index.d.ts +10 -1
- package/dist/pagination/index.js +6 -4
- package/dist/pan-zoom/index.d.ts +2 -0
- package/dist/pan-zoom/index.js +7 -6
- package/dist/pencil.d.ts +1 -0
- package/dist/pencil.js +4 -4
- package/dist/radar/index.d.ts +2 -0
- package/dist/radar/index.js +40 -20
- package/dist/rating/index.d.ts +4 -0
- package/dist/rating/index.js +128 -24
- package/dist/resizable/index.js +1 -1
- package/dist/scroll-area/index.d.ts +1 -1
- package/dist/scroll-area/index.js +5 -2
- package/dist/select/MultipleSelect.js +5 -4
- package/dist/select/SearchableSelect.js +61 -6
- package/dist/select/Select.d.ts +1 -0
- package/dist/select/Select.js +3 -1
- package/dist/select/StepperSelect.d.ts +29 -0
- package/dist/select/StepperSelect.js +119 -0
- package/dist/select/index.d.ts +2 -0
- package/dist/select/index.js +1 -0
- package/dist/select/stepperModel.d.ts +7 -0
- package/dist/select/stepperModel.js +29 -0
- package/dist/selection/Checkbox.js +4 -3
- package/dist/selection/RadioGroup.js +29 -13
- package/dist/selection/Switch.js +4 -3
- package/dist/selection/useNativeChecked.d.ts +2 -1
- package/dist/selection/useNativeChecked.js +26 -8
- package/dist/slider/index.js +50 -6
- package/dist/sortable/index.js +6 -2
- package/dist/sparkline/index.js +17 -2
- package/dist/sparkline/model.js +11 -4
- package/dist/styles/accordion.css +228 -0
- package/dist/styles/alert-dialog.css +139 -0
- package/dist/styles/audio-player.css +22 -0
- package/dist/styles/base.css +638 -0
- package/dist/styles/breadcrumb.css +7 -0
- package/dist/styles/calendar.css +62 -0
- package/dist/styles/carousel.css +173 -0
- package/dist/styles/choice-grid.css +226 -0
- package/dist/styles/color-picker.css +281 -0
- package/dist/styles/color-swatch.css +47 -0
- package/dist/styles/command.css +52 -0
- package/dist/styles/content.css +79 -0
- package/dist/styles/context-menu.css +35 -0
- package/dist/styles/data-table.css +31 -0
- package/dist/styles/date-picker.css +107 -0
- package/dist/styles/day-agenda.css +30 -0
- package/dist/styles/display.css +304 -0
- package/dist/styles/dockable-panel.css +110 -0
- package/dist/styles/drawer.css +133 -0
- package/dist/styles/dropdown-menu.css +68 -0
- package/dist/styles/editable.css +145 -0
- package/dist/styles/empty.css +108 -0
- package/dist/styles/flip.css +101 -0
- package/dist/styles/form.css +634 -0
- package/dist/styles/heatmap.css +256 -0
- package/dist/styles/hotkey.css +189 -0
- package/dist/styles/hover-card.css +27 -0
- package/dist/styles/image-cropper.css +205 -0
- package/dist/styles/image-viewer.css +422 -0
- package/dist/styles/input-number.css +21 -0
- package/dist/styles/journey-map.css +19 -0
- package/dist/styles/keypad.css +92 -0
- package/dist/styles/layout.css +72 -0
- package/dist/styles/leaflet.css +661 -0
- package/dist/styles/log.css +203 -0
- package/dist/styles/markdown.css +379 -0
- package/dist/styles/node-graph.css +142 -0
- package/dist/styles/number-ticker.css +83 -0
- package/dist/styles/overflow-list.css +51 -0
- package/dist/styles/overlay.css +284 -0
- package/dist/styles/pagination.css +21 -0
- package/dist/styles/pan-zoom.css +136 -0
- package/dist/styles/radar.css +1 -0
- package/dist/styles/rating.css +121 -0
- package/dist/styles/resizable.css +108 -0
- package/dist/styles/scroll-area.css +122 -0
- package/dist/styles/select.css +557 -0
- package/dist/styles/selection.css +264 -0
- package/dist/styles/slider.css +145 -0
- package/dist/styles/slot.css +109 -0
- package/dist/styles/sortable.css +159 -0
- package/dist/styles/sparkline.css +24 -0
- package/dist/styles/steps.css +27 -0
- package/dist/styles/tabs.css +79 -0
- package/dist/styles/tag-input.css +213 -0
- package/dist/styles/time-picker.css +66 -0
- package/dist/styles/timeline.css +24 -0
- package/dist/styles/toast.css +160 -0
- package/dist/styles/toggle-group.css +107 -0
- package/dist/styles/tooltip.css +78 -0
- package/dist/styles/transfer.css +148 -0
- package/dist/styles/tree-select.css +131 -0
- package/dist/styles/tree.css +59 -0
- package/dist/styles/typewriter.css +62 -0
- package/dist/styles/upload.css +60 -0
- package/dist/styles/virtual-list.css +112 -0
- package/dist/styles.css +2094 -421
- package/dist/tabs/index.d.ts +9 -0
- package/dist/tabs/index.js +66 -16
- package/dist/tabs/useOverflow.d.ts +20 -0
- package/dist/tabs/useOverflow.js +145 -0
- package/dist/tag-input/index.d.ts +7 -2
- package/dist/tag-input/index.js +70 -26
- package/dist/time-picker/index.js +38 -20
- package/dist/toast/index.d.ts +19 -11
- package/dist/toast/index.js +48 -8
- package/dist/toast/manager.d.ts +42 -0
- package/dist/toast/manager.js +85 -0
- package/dist/toast/useToastNavigation.d.ts +3 -0
- package/dist/toast/useToastNavigation.js +75 -0
- package/dist/tokens.css +4 -0
- package/dist/tooltip/Tooltip.js +3 -3
- package/dist/transfer/index.d.ts +5 -0
- package/dist/transfer/index.js +93 -29
- package/dist/transfer/model.d.ts +4 -0
- package/dist/transfer/model.js +37 -11
- package/dist/tree/index.d.ts +5 -1
- package/dist/tree/index.js +5 -5
- package/dist/tree-select/index.js +67 -30
- package/dist/tree-select/model.d.ts +11 -5
- package/dist/tree-select/model.js +111 -83
- package/dist/typewriter/index.js +10 -10
- package/dist/upload/index.js +30 -16
- package/dist/virtual-list/index.js +7 -5
- package/package.json +413 -3
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Combobox } from '@base-ui/react/combobox';
|
|
3
3
|
import { DirectionProvider } from '@base-ui/react/direction-provider';
|
|
4
|
-
import { forwardRef, useId, useMemo, useRef } from 'react';
|
|
4
|
+
import { forwardRef, useCallback, useEffect, useId, useMemo, useRef, useState } from 'react';
|
|
5
5
|
import { Button } from '../Button.js';
|
|
6
6
|
import { Input } from '../form/Input.js';
|
|
7
|
+
import { InputAction } from '../form/InputAction.js';
|
|
7
8
|
import { mergeAriaIds, useFieldContext } from '../form/context.js';
|
|
8
9
|
import { PencilIcon } from '../PencilIcon.js';
|
|
9
10
|
import { PencilSurface } from '../PencilSurface.js';
|
|
@@ -16,6 +17,7 @@ const clearPaths = [{ d: 'M6 6l12 12M18 6L6 18' }];
|
|
|
16
17
|
const SearchableControl = forwardRef(function SearchableControl(props, forwardedRef) {
|
|
17
18
|
const { items, searchPlacement = 'trigger', placeholder = 'Select an option', searchPlaceholder = 'Search options', emptyContent = 'No matching options', clearable = true, clearLabel = 'Clear selection', openLabel = 'Show options', size = 'md', multiple = false, value, defaultValue, onValueChange: _onValueChange, name, form, dir, disabled, readOnly, required, invalid, id, className, children: _children, open, defaultOpen, onOpenChange, side = 'bottom', align = 'start', sideOffset = 8, collisionPadding = 8, 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, 'aria-describedby': ariaDescribedBy, ...rest } = props;
|
|
18
19
|
const indicatorSize = size === 'sm' ? 24 : size === 'lg' ? 32 : 28;
|
|
20
|
+
const actionSize = size === 'sm' ? 'icon-sm' : size === 'lg' ? 'icon-lg' : 'icon';
|
|
19
21
|
const field = useFieldContext();
|
|
20
22
|
const generatedId = useId();
|
|
21
23
|
const controlId = field?.controlId ?? id ?? generatedId;
|
|
@@ -24,26 +26,79 @@ const SearchableControl = forwardRef(function SearchableControl(props, forwarded
|
|
|
24
26
|
const resolvedInvalid = Boolean(invalid || field?.invalid);
|
|
25
27
|
const describedBy = mergeAriaIds(ariaDescribedBy, field?.descriptionId, field?.errorId);
|
|
26
28
|
const searchRef = useRef(null);
|
|
29
|
+
const triggerRef = useRef(null);
|
|
30
|
+
const rootRef = useRef(null);
|
|
31
|
+
const defaultValueRef = useRef(defaultValue);
|
|
32
|
+
const actionsRef = useRef(null);
|
|
27
33
|
const portalRef = usePortalTriggerRef(forwardedRef);
|
|
34
|
+
const [resetVersion, setResetVersion] = useState(0);
|
|
35
|
+
const captureRoot = useCallback((node) => {
|
|
36
|
+
rootRef.current = node;
|
|
37
|
+
portalRef(node);
|
|
38
|
+
}, [portalRef]);
|
|
28
39
|
const lookup = useMemo(() => new Map(items.map(item => [item.value, item])), [items]);
|
|
29
40
|
const values = useMemo(() => items.map(item => item.value), [items]);
|
|
30
41
|
const { contains } = Combobox.useFilter({ sensitivity: 'base' });
|
|
42
|
+
const itemToStringLabel = useCallback((item) => lookup.get(item)?.label ?? item, [lookup]);
|
|
43
|
+
const filter = useCallback((item, query) => {
|
|
44
|
+
const option = lookup.get(item);
|
|
45
|
+
return contains(`${option?.label ?? item} ${option?.keywords ?? ''}`, query);
|
|
46
|
+
}, [contains, lookup]);
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
const root = rootRef.current;
|
|
49
|
+
if (!root)
|
|
50
|
+
return undefined;
|
|
51
|
+
const ownerDocument = root.ownerDocument;
|
|
52
|
+
const formElement = form
|
|
53
|
+
? ownerDocument.getElementById(form)
|
|
54
|
+
: root.closest('form');
|
|
55
|
+
if (!formElement)
|
|
56
|
+
return undefined;
|
|
57
|
+
let timer;
|
|
58
|
+
const reset = (event) => {
|
|
59
|
+
clearTimeout(timer);
|
|
60
|
+
timer = setTimeout(() => {
|
|
61
|
+
if (event.defaultPrevented)
|
|
62
|
+
return;
|
|
63
|
+
actionsRef.current?.close();
|
|
64
|
+
setResetVersion(version => version + 1);
|
|
65
|
+
}, 0);
|
|
66
|
+
};
|
|
67
|
+
formElement.addEventListener('reset', reset);
|
|
68
|
+
return () => {
|
|
69
|
+
clearTimeout(timer);
|
|
70
|
+
formElement.removeEventListener('reset', reset);
|
|
71
|
+
};
|
|
72
|
+
}, [form]);
|
|
31
73
|
const summary = (selected) => {
|
|
32
74
|
const selectedValues = Array.isArray(selected) ? selected : selected == null ? [] : [selected];
|
|
33
75
|
return selectedValues.map(item => lookup.get(item)?.label ?? item).join(', ');
|
|
34
76
|
};
|
|
35
|
-
const search = _jsx(Combobox.Input, { id: searchPlacement === 'trigger' ? controlId : `${controlId}-search`, "aria-label": searchPlacement === 'trigger' ? ariaLabel : searchPlaceholder, "aria-labelledby": searchPlacement === 'trigger' ? ariaLabelledBy : undefined, "aria-describedby": describedBy, "aria-invalid": resolvedInvalid || undefined, ref: searchRef, placeholder: searchPlacement === 'trigger' ? placeholder : searchPlaceholder, render: _jsx(Input, {
|
|
36
|
-
|
|
77
|
+
const search = _jsx(Combobox.Input, { id: searchPlacement === 'trigger' ? controlId : `${controlId}-search`, "aria-label": searchPlacement === 'trigger' ? ariaLabel : searchPlaceholder, "aria-labelledby": searchPlacement === 'trigger' ? ariaLabelledBy : undefined, "aria-describedby": describedBy, "aria-invalid": resolvedInvalid || undefined, ref: searchRef, placeholder: searchPlacement === 'trigger' ? placeholder : searchPlaceholder, render: _jsx(Input, { classNames: searchPlacement === 'trigger' ? {
|
|
78
|
+
root: 'inkra-search-select__input',
|
|
79
|
+
control: 'inkra-search-select__input-control',
|
|
80
|
+
} : undefined, size: size, variant: searchPlacement === 'trigger' ? 'ghost' : 'underline', startIcon: _jsx(PencilIcon, { paths: searchPaths, size: 22, sourceSize: 24 }) }) });
|
|
81
|
+
const clear = clearable ? _jsx(InputAction, { disabled: resolvedReadOnly || resolvedDisabled, seed: `search-select-clear-${controlId}`, children: _jsx(Combobox.Clear, { "aria-label": clearLabel, className: "inkra-search-select__clear", disabled: resolvedReadOnly || resolvedDisabled, keepMounted: true, onMouseDown: event => {
|
|
82
|
+
event.preventDefault();
|
|
83
|
+
event.stopPropagation();
|
|
84
|
+
}, onClick: event => {
|
|
85
|
+
event.stopPropagation();
|
|
86
|
+
setTimeout(() => {
|
|
87
|
+
actionsRef.current?.close();
|
|
88
|
+
(searchPlacement === 'trigger' ? searchRef.current : triggerRef.current)?.focus({ preventScroll: true });
|
|
89
|
+
}, 0);
|
|
90
|
+
}, render: (renderProps, state) => _jsx(Button, { ...renderProps, "aria-hidden": !state.visible || undefined, tabIndex: state.visible ? 0 : -1, variant: "ghost", size: actionSize }), children: _jsx(PencilIcon, { paths: clearPaths, size: 24, sourceSize: 24 }) }) }) : null;
|
|
91
|
+
return _jsx(DirectionProvider, { direction: dir, children: _jsxs(Combobox.Root, { actionsRef: actionsRef, items: values, multiple: multiple, value: value, defaultValue: defaultValueRef.current, disabled: resolvedDisabled, readOnly: resolvedReadOnly, required: Boolean(required || field?.required), name: name, form: form, autoHighlight: true, open: resolvedReadOnly || resolvedDisabled ? false : open, defaultOpen: resetVersion === 0 ? defaultOpen : false, onOpenChange: (next, details) => { if (resolvedReadOnly || resolvedDisabled)
|
|
37
92
|
details.cancel();
|
|
38
93
|
else
|
|
39
|
-
onOpenChange?.(next); }, itemToStringLabel:
|
|
94
|
+
onOpenChange?.(next); }, itemToStringLabel: itemToStringLabel, filter: filter, onValueChange: next => {
|
|
40
95
|
if (resolvedDisabled || resolvedReadOnly)
|
|
41
96
|
return;
|
|
42
97
|
if (props.multiple)
|
|
43
98
|
props.onValueChange?.(Array.isArray(next) ? next : []);
|
|
44
99
|
else
|
|
45
100
|
props.onValueChange?.(typeof next === 'string' ? next : null);
|
|
46
|
-
}, children: [_jsxs("div", { ...rest, ref:
|
|
101
|
+
}, children: [_jsxs("div", { ...rest, ref: captureRoot, dir: dir, className: ['inkra-search-select', className].filter(Boolean).join(' '), "data-disabled": resolvedDisabled || undefined, "data-placement": searchPlacement, "data-invalid": resolvedInvalid || undefined, "data-readonly": resolvedReadOnly || undefined, "data-size": size, children: [_jsx(Combobox.InputGroup, { render: _jsx(PencilSurface, { className: "inkra-search-select__anchor inkra-search-select__control", radius: 8, seed: `search-select-control-${controlId}`, variant: resolvedInvalid ? 'danger' : 'outline' }), children: searchPlacement === 'trigger' ? _jsxs(_Fragment, { children: [search, _jsx(InputAction, { disabled: resolvedReadOnly || resolvedDisabled, seed: `search-select-open-${controlId}`, children: _jsx(Combobox.Trigger, { "aria-label": openLabel, className: "inkra-search-select__open", disabled: resolvedReadOnly || resolvedDisabled, render: _jsx(Button, { variant: "ghost", size: actionSize }), children: _jsx(PencilIcon, { paths: chevronPaths, size: indicatorSize, sourceSize: 24 }) }) }), clear] }) : _jsxs(_Fragment, { children: [_jsxs(Combobox.Trigger, { id: controlId, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, "aria-describedby": describedBy, "aria-invalid": resolvedInvalid || undefined, "aria-readonly": resolvedReadOnly || undefined, className: "inkra-search-select__trigger", ref: triggerRef, render: _jsx(Button, { variant: "ghost", fullWidth: true, size: size, align: "start" }), children: [_jsx(Combobox.Value, { children: (selected) => _jsx(PencilText, { ellipsis: true, children: summary(selected) || placeholder }) }), _jsx(PencilIcon, { paths: chevronPaths, size: indicatorSize, sourceSize: 24 })] }), clear] }) }), multiple && searchPlacement === 'trigger' && _jsx(Combobox.Value, { children: (selected) => selected.length ? _jsx(PencilText, { className: "inkra-search-select__summary", children: summary(selected) }) : null })] }), _jsx(Combobox.Portal, { children: _jsx(PortalThemeBridge, { children: _jsx(Combobox.Positioner, { side: side, align: align, sideOffset: sideOffset, collisionPadding: collisionPadding, className: "inkra-select__positioner inkra-search-select__positioner", children: _jsx(Combobox.Popup, { className: "inkra-search-select__popup", initialFocus: searchPlacement === 'popup' ? searchRef : false, children: _jsxs(PencilSurface, { variant: "paper", radius: 8, children: [searchPlacement === 'popup' && _jsx("div", { className: "inkra-search-select__search", children: search }), _jsx(Combobox.Empty, { className: "inkra-search-select__empty", children: typeof emptyContent === 'string' ? _jsx(PencilText, { children: emptyContent }) : emptyContent }), _jsx(Combobox.List, { className: "inkra-select__viewport inkra-search-select__list", children: (item) => _jsx(Combobox.Item, { value: item, disabled: lookup.get(item)?.disabled, className: "inkra-select__item", children: _jsxs(PencilSurface, { className: "inkra-select__item-surface", variant: "selection", seed: `search-option-${item}`, children: [_jsx(PencilText, { children: lookup.get(item)?.label ?? item }), _jsx(Combobox.ItemIndicator, { className: "inkra-select__item-indicator", children: _jsx(PencilIcon, { paths: checkPaths, size: 22, sourceSize: 24 }) })] }) }, item) })] }) }) }) }) })] }, resetVersion) });
|
|
47
102
|
});
|
|
48
103
|
export const SearchableSelect = forwardRef(function SearchableSelect(props, ref) {
|
|
49
104
|
return _jsx(PortalThemeProvider, { children: _jsx(SearchableControl, { ...props, ref: ref }) });
|
package/dist/select/Select.d.ts
CHANGED
|
@@ -87,6 +87,7 @@ export interface SelectClearProps extends ButtonHTMLAttributes<HTMLButtonElement
|
|
|
87
87
|
export declare const SelectClear: import("react").ForwardRefExoticComponent<SelectClearProps & import("react").RefAttributes<HTMLButtonElement>>;
|
|
88
88
|
export declare const Select: Readonly<{
|
|
89
89
|
Searchable: import("react").ForwardRefExoticComponent<import("./SearchableSelect.js").SearchableSelectProps & import("react").RefAttributes<HTMLDivElement>>;
|
|
90
|
+
Stepper: import("react").ForwardRefExoticComponent<import("./StepperSelect.js").SelectStepperProps & import("react").RefAttributes<HTMLButtonElement>>;
|
|
90
91
|
Root: typeof SelectRoot;
|
|
91
92
|
Trigger: import("react").ForwardRefExoticComponent<SelectTriggerProps & import("react").RefAttributes<HTMLButtonElement>>;
|
|
92
93
|
Value: import("react").ForwardRefExoticComponent<SelectValueProps & import("react").RefAttributes<HTMLSpanElement>>;
|
package/dist/select/Select.js
CHANGED
|
@@ -3,6 +3,7 @@ import * as SelectPrimitive from '@radix-ui/react-select';
|
|
|
3
3
|
import { createContext, forwardRef, useCallback, useContext, useEffect, useId, useMemo, useRef, useState, } from 'react';
|
|
4
4
|
import { Button } from '../Button.js';
|
|
5
5
|
import { SearchableSelect } from './SearchableSelect.js';
|
|
6
|
+
import { SelectStepper } from './StepperSelect.js';
|
|
6
7
|
import { MultipleSelect, MultipleSelectContext, MultipleSelectRoot } from './MultipleSelect.js';
|
|
7
8
|
import { mergeAriaIds, useFieldContext } from '../form/context.js';
|
|
8
9
|
import { PencilIcon } from '../PencilIcon.js';
|
|
@@ -185,8 +186,9 @@ export const SelectClear = forwardRef(function SelectClear({ children, className
|
|
|
185
186
|
state.triggerRef.current?.focus({ preventScroll: true });
|
|
186
187
|
}, children: children ?? 'Clear' });
|
|
187
188
|
});
|
|
188
|
-
export const Select = Object.freeze({
|
|
189
|
+
export const Select = /* @__PURE__ */ Object.freeze({
|
|
189
190
|
Searchable: SearchableSelect,
|
|
191
|
+
Stepper: SelectStepper,
|
|
190
192
|
Root: SelectRoot,
|
|
191
193
|
Trigger: SelectTrigger,
|
|
192
194
|
Value: SelectValue,
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { AriaAttributes, ButtonHTMLAttributes } from 'react';
|
|
2
|
+
import type { FieldControlSize } from '../form/types.js';
|
|
3
|
+
export interface SelectStepperItem {
|
|
4
|
+
value: string;
|
|
5
|
+
label: string;
|
|
6
|
+
disabled?: boolean;
|
|
7
|
+
}
|
|
8
|
+
export interface SelectStepperProps extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, 'children' | 'defaultValue' | 'dir' | 'form' | 'name' | 'onChange' | 'readOnly' | 'required' | 'size' | 'value'> {
|
|
9
|
+
items: readonly SelectStepperItem[];
|
|
10
|
+
value?: string | null;
|
|
11
|
+
defaultValue?: string | null;
|
|
12
|
+
onValueChange?: (value: string) => void;
|
|
13
|
+
loop?: boolean;
|
|
14
|
+
placeholder?: string;
|
|
15
|
+
previousLabel?: string;
|
|
16
|
+
nextLabel?: string;
|
|
17
|
+
size?: FieldControlSize;
|
|
18
|
+
fullWidth?: boolean;
|
|
19
|
+
name?: string;
|
|
20
|
+
form?: string;
|
|
21
|
+
dir?: 'ltr' | 'rtl';
|
|
22
|
+
disabled?: boolean;
|
|
23
|
+
readOnly?: boolean;
|
|
24
|
+
required?: boolean;
|
|
25
|
+
invalid?: boolean;
|
|
26
|
+
'aria-errormessage'?: string;
|
|
27
|
+
'aria-invalid'?: AriaAttributes['aria-invalid'];
|
|
28
|
+
}
|
|
29
|
+
export declare const SelectStepper: import("react").ForwardRefExoticComponent<SelectStepperProps & import("react").RefAttributes<HTMLButtonElement>>;
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { forwardRef, useEffect, useId, useRef, useState } from 'react';
|
|
3
|
+
import { Button } from '../Button.js';
|
|
4
|
+
import { InputAction } from '../form/InputAction.js';
|
|
5
|
+
import { useResolvedControlProps } from '../form/controlProps.js';
|
|
6
|
+
import { PencilIcon } from '../PencilIcon.js';
|
|
7
|
+
import { PencilSurface } from '../PencilSurface.js';
|
|
8
|
+
import { PencilText } from '../PencilText.js';
|
|
9
|
+
import { findAdjacentStepperItem, findEdgeStepperItem, resolveStepperNativeValue } from './stepperModel.js';
|
|
10
|
+
const previousPaths = [{ d: 'M15 18l-6-6 6-6' }];
|
|
11
|
+
const nextPaths = [{ d: 'M9 18l6-6-6-6' }];
|
|
12
|
+
export const SelectStepper = forwardRef(function SelectStepper({ 'aria-describedby': ariaDescribedBy, 'aria-errormessage': ariaErrorMessage, 'aria-invalid': ariaInvalid, className, defaultValue = null, dir, disabled, form, fullWidth = true, id, invalid, items, loop = false, name, nextLabel = 'Select next option', onKeyDown, onValueChange, placeholder = 'Select an option', previousLabel = 'Select previous option', readOnly, required, size = 'md', value, ...props }, forwardedRef) {
|
|
13
|
+
const generatedId = useId();
|
|
14
|
+
const controlRef = useRef(null);
|
|
15
|
+
const selectRef = useRef(null);
|
|
16
|
+
const itemsRef = useRef(items);
|
|
17
|
+
const defaultValueRef = useRef(defaultValue);
|
|
18
|
+
const latestValueRef = useRef(value);
|
|
19
|
+
const controlled = value !== undefined;
|
|
20
|
+
const [uncontrolledValue, setUncontrolledValue] = useState(defaultValue);
|
|
21
|
+
const currentValue = controlled ? value ?? null : uncontrolledValue;
|
|
22
|
+
const state = useResolvedControlProps({
|
|
23
|
+
id,
|
|
24
|
+
required,
|
|
25
|
+
disabled,
|
|
26
|
+
readOnly,
|
|
27
|
+
invalid,
|
|
28
|
+
'aria-describedby': ariaDescribedBy,
|
|
29
|
+
'aria-errormessage': ariaErrorMessage,
|
|
30
|
+
'aria-invalid': ariaInvalid,
|
|
31
|
+
});
|
|
32
|
+
const controlId = state.id ?? generatedId;
|
|
33
|
+
const currentItemIndex = items.findIndex(item => item.value === currentValue);
|
|
34
|
+
const currentItem = currentItemIndex < 0 ? undefined : items[currentItemIndex];
|
|
35
|
+
const iconSize = size === 'sm' ? 18 : size === 'lg' ? 26 : 22;
|
|
36
|
+
const buttonSize = size === 'sm' ? 'icon-sm' : size === 'lg' ? 'icon-lg' : 'icon';
|
|
37
|
+
defaultValueRef.current = defaultValue;
|
|
38
|
+
itemsRef.current = items;
|
|
39
|
+
latestValueRef.current = value;
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
const select = selectRef.current;
|
|
42
|
+
const formElement = select?.form;
|
|
43
|
+
if (!select || !formElement)
|
|
44
|
+
return undefined;
|
|
45
|
+
let resetTimer;
|
|
46
|
+
const reset = (event) => {
|
|
47
|
+
clearTimeout(resetTimer);
|
|
48
|
+
resetTimer = setTimeout(() => {
|
|
49
|
+
if (event.defaultPrevented)
|
|
50
|
+
return;
|
|
51
|
+
if (controlled) {
|
|
52
|
+
select.value = resolveStepperNativeValue(itemsRef.current, latestValueRef.current);
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
const restored = defaultValueRef.current;
|
|
56
|
+
// Native reset may restore the first option before React can render. Write the
|
|
57
|
+
// effective default even when setState bails out because state already matches.
|
|
58
|
+
select.value = resolveStepperNativeValue(itemsRef.current, restored);
|
|
59
|
+
setUncontrolledValue(restored);
|
|
60
|
+
}
|
|
61
|
+
}, 0);
|
|
62
|
+
};
|
|
63
|
+
formElement.addEventListener('reset', reset);
|
|
64
|
+
return () => {
|
|
65
|
+
clearTimeout(resetTimer);
|
|
66
|
+
formElement.removeEventListener('reset', reset);
|
|
67
|
+
};
|
|
68
|
+
}, [controlled, form]);
|
|
69
|
+
function adjacentItem(direction) {
|
|
70
|
+
return findAdjacentStepperItem(items, currentValue, direction, loop);
|
|
71
|
+
}
|
|
72
|
+
function change(next) {
|
|
73
|
+
if (!next || state.disabled || state.readOnly)
|
|
74
|
+
return;
|
|
75
|
+
if (!controlled)
|
|
76
|
+
setUncontrolledValue(next.value);
|
|
77
|
+
onValueChange?.(next.value);
|
|
78
|
+
controlRef.current?.focus({ preventScroll: true });
|
|
79
|
+
}
|
|
80
|
+
function handleKeyDown(event) {
|
|
81
|
+
onKeyDown?.(event);
|
|
82
|
+
if (event.defaultPrevented || state.disabled || state.readOnly)
|
|
83
|
+
return;
|
|
84
|
+
const rtl = dir === 'rtl' || (dir == null && getComputedStyle(event.currentTarget).direction === 'rtl');
|
|
85
|
+
let next;
|
|
86
|
+
if (event.key === 'ArrowUp')
|
|
87
|
+
next = adjacentItem(-1);
|
|
88
|
+
else if (event.key === 'ArrowDown')
|
|
89
|
+
next = adjacentItem(1);
|
|
90
|
+
else if (event.key === 'ArrowLeft')
|
|
91
|
+
next = adjacentItem(rtl ? 1 : -1);
|
|
92
|
+
else if (event.key === 'ArrowRight')
|
|
93
|
+
next = adjacentItem(rtl ? -1 : 1);
|
|
94
|
+
else if (event.key === 'Home')
|
|
95
|
+
next = findEdgeStepperItem(items, 'first');
|
|
96
|
+
else if (event.key === 'End')
|
|
97
|
+
next = findEdgeStepperItem(items, 'last');
|
|
98
|
+
else
|
|
99
|
+
return;
|
|
100
|
+
event.preventDefault();
|
|
101
|
+
change(next);
|
|
102
|
+
}
|
|
103
|
+
const previous = adjacentItem(-1);
|
|
104
|
+
const next = adjacentItem(1);
|
|
105
|
+
const interactionDisabled = state.disabled || state.readOnly;
|
|
106
|
+
const visibleLabel = currentItem?.label ?? placeholder;
|
|
107
|
+
const nativeValue = resolveStepperNativeValue(items, currentValue);
|
|
108
|
+
const seed = `inkra-select-stepper-${controlId}`;
|
|
109
|
+
return (_jsx("span", { className: ['inkra-field-control-hit', 'inkra-input-root', 'inkra-select-stepper', className].filter(Boolean).join(' '), "data-disabled": state.disabled || undefined, "data-full-width": fullWidth, "data-invalid": state.invalid || undefined, "data-readonly": state.readOnly || undefined, "data-size": size, dir: dir, children: _jsxs(PencilSurface, { as: "div", className: "inkra-field-control inkra-field-control--input inkra-select-stepper__control", clipContent: true, seed: seed, variant: state.invalid ? 'danger' : 'outline', children: [_jsx(InputAction, { disabled: interactionDisabled || previous == null, position: "start", seed: `${seed}-previous`, children: _jsx(Button, { "aria-label": previousLabel, className: "inkra-select-stepper__button inkra-select-stepper__button--previous", disabled: interactionDisabled || previous == null, onClick: () => change(previous), size: buttonSize, type: "button", variant: "ghost", children: _jsx(PencilIcon, { paths: previousPaths, size: iconSize, sourceSize: 24 }) }) }), _jsx("button", { ...props, "aria-describedby": state.describedBy, "aria-errormessage": state.errorMessage, "aria-invalid": state.ariaInvalid, "aria-readonly": state.readOnly || undefined, "aria-required": state.required || undefined, "aria-valuemax": items.length ? items.length - 1 : undefined, "aria-valuemin": items.length ? 0 : undefined, "aria-valuenow": currentItemIndex < 0 ? undefined : currentItemIndex, "aria-valuetext": visibleLabel, className: "inkra-select-stepper__value", "data-placeholder": currentItem == null || undefined, disabled: state.disabled, id: controlId, onKeyDown: handleKeyDown, ref: node => {
|
|
110
|
+
controlRef.current = node;
|
|
111
|
+
if (typeof forwardedRef === 'function')
|
|
112
|
+
forwardedRef(node);
|
|
113
|
+
else if (forwardedRef)
|
|
114
|
+
forwardedRef.current = node;
|
|
115
|
+
}, role: "spinbutton", type: "button", children: _jsx(PencilText, { ellipsis: true, seed: `${seed}-value`, children: visibleLabel }) }), _jsx(InputAction, { disabled: interactionDisabled || next == null, seed: `${seed}-next`, children: _jsx(Button, { "aria-label": nextLabel, className: "inkra-select-stepper__button inkra-select-stepper__button--next", disabled: interactionDisabled || next == null, onClick: () => change(next), size: buttonSize, type: "button", variant: "ghost", children: _jsx(PencilIcon, { paths: nextPaths, size: iconSize, sourceSize: 24 }) }) }), _jsxs("select", { "aria-hidden": "true", className: "inkra-select-stepper__native", disabled: state.disabled, form: form, name: name, onChange: () => undefined, onInvalid: event => {
|
|
116
|
+
event.preventDefault();
|
|
117
|
+
controlRef.current?.focus({ preventScroll: true });
|
|
118
|
+
}, ref: selectRef, required: state.required && !state.readOnly, tabIndex: -1, value: nativeValue, children: [_jsx("option", { value: "" }), items.map(item => _jsx("option", { disabled: item.disabled, value: item.value, children: item.label }, item.value))] })] }) }));
|
|
119
|
+
});
|
package/dist/select/index.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
export { Select, SelectClear, SelectContent, SelectGroup, SelectItem, SelectItemIndicator, SelectItemText, SelectLabel, SelectPortal, SelectRoot, SelectScrollDownButton, SelectScrollUpButton, SelectSeparator, SelectTrigger, SelectValue, SelectViewport, } from './Select.js';
|
|
2
2
|
export { SearchableSelect } from './SearchableSelect.js';
|
|
3
3
|
export type { SearchableSelectProps, SearchableSelectOption } from './SearchableSelect.js';
|
|
4
|
+
export { SelectStepper } from './StepperSelect.js';
|
|
5
|
+
export type { SelectStepperItem, SelectStepperProps } from './StepperSelect.js';
|
|
4
6
|
export type { SelectClearProps, SelectContentProps, SelectGroupProps, SelectItemIndicatorProps, SelectItemProps, SelectItemTextProps, SelectLabelProps, SelectPortalProps, SelectRootProps, SelectSingleRootProps, SelectMultipleRootProps, SelectOption, SelectScrollDownButtonProps, SelectScrollUpButtonProps, SelectSeparatorProps, SelectSize, SelectTriggerProps, SelectValueProps, SelectViewportProps, } from './Select.js';
|
package/dist/select/index.js
CHANGED
|
@@ -1,2 +1,3 @@
|
|
|
1
1
|
export { Select, SelectClear, SelectContent, SelectGroup, SelectItem, SelectItemIndicator, SelectItemText, SelectLabel, SelectPortal, SelectRoot, SelectScrollDownButton, SelectScrollUpButton, SelectSeparator, SelectTrigger, SelectValue, SelectViewport, } from './Select.js';
|
|
2
2
|
export { SearchableSelect } from './SearchableSelect.js';
|
|
3
|
+
export { SelectStepper } from './StepperSelect.js';
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export interface StepperModelItem {
|
|
2
|
+
value: string;
|
|
3
|
+
disabled?: boolean;
|
|
4
|
+
}
|
|
5
|
+
export declare function findAdjacentStepperItem<Item extends StepperModelItem>(items: readonly Item[], currentValue: string | null, direction: -1 | 1, loop: boolean): Item | undefined;
|
|
6
|
+
export declare function findEdgeStepperItem<Item extends StepperModelItem>(items: readonly Item[], edge: 'first' | 'last'): Item | undefined;
|
|
7
|
+
export declare function resolveStepperNativeValue(items: readonly StepperModelItem[], value: string | null | undefined): string;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export function findAdjacentStepperItem(items, currentValue, direction, loop) {
|
|
2
|
+
if (items.length === 0)
|
|
3
|
+
return undefined;
|
|
4
|
+
const currentIndex = items.findIndex(item => item.value === currentValue);
|
|
5
|
+
if (currentIndex < 0) {
|
|
6
|
+
const ordered = direction < 0 ? [...items].reverse() : items;
|
|
7
|
+
return ordered.find(item => !item.disabled);
|
|
8
|
+
}
|
|
9
|
+
for (let offset = 1; offset <= items.length; offset += 1) {
|
|
10
|
+
let index = currentIndex + direction * offset;
|
|
11
|
+
if (loop)
|
|
12
|
+
index = (index + items.length) % items.length;
|
|
13
|
+
else if (index < 0 || index >= items.length)
|
|
14
|
+
return undefined;
|
|
15
|
+
if (index === currentIndex)
|
|
16
|
+
return undefined;
|
|
17
|
+
const item = items[index];
|
|
18
|
+
if (!item.disabled)
|
|
19
|
+
return item;
|
|
20
|
+
}
|
|
21
|
+
return undefined;
|
|
22
|
+
}
|
|
23
|
+
export function findEdgeStepperItem(items, edge) {
|
|
24
|
+
const ordered = edge === 'last' ? [...items].reverse() : items;
|
|
25
|
+
return ordered.find(item => !item.disabled);
|
|
26
|
+
}
|
|
27
|
+
export function resolveStepperNativeValue(items, value) {
|
|
28
|
+
return value != null && items.some(item => item.value === value) ? value : '';
|
|
29
|
+
}
|
|
@@ -10,7 +10,7 @@ function ids(...values) {
|
|
|
10
10
|
const value = values.filter(Boolean).join(' ');
|
|
11
11
|
return value || undefined;
|
|
12
12
|
}
|
|
13
|
-
export const Checkbox = forwardRef(function Checkbox({ 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, 'aria-describedby': ariaDescribedBy, checked, className, defaultChecked, description, disabled = false, id, indeterminate = false, label, onChange, onCheckedChange, seed, ...props }, forwardedRef) {
|
|
13
|
+
export const Checkbox = forwardRef(function Checkbox({ 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, 'aria-describedby': ariaDescribedBy, checked, className, defaultChecked, description, disabled = false, form, id, indeterminate = false, label, onChange, onCheckedChange, seed, ...props }, forwardedRef) {
|
|
14
14
|
const generatedId = useId();
|
|
15
15
|
const inputId = id ?? `inkra-checkbox-${generatedId}`;
|
|
16
16
|
const labelId = `${inputId}-label`;
|
|
@@ -22,16 +22,17 @@ export const Checkbox = forwardRef(function Checkbox({ 'aria-label': ariaLabel,
|
|
|
22
22
|
const { controlled, syncUncontrolled, visualChecked } = useNativeChecked({
|
|
23
23
|
checked,
|
|
24
24
|
defaultChecked,
|
|
25
|
+
form,
|
|
25
26
|
indeterminate,
|
|
26
27
|
inputRef,
|
|
27
28
|
});
|
|
28
29
|
const selected = visualChecked || indeterminate;
|
|
29
|
-
return (_jsxs("label", { className: ['inkra-selection-control', 'inkra-checkbox', className].filter(Boolean).join(' '), "data-checked": visualChecked || undefined, "data-disabled": disabled || undefined, "data-indeterminate": indeterminate || undefined, htmlFor: inputId, children: [_jsx("input", { ...props, "aria-checked": indeterminate ? 'mixed' : undefined, "aria-describedby": ids(ariaDescribedBy, descriptionId), "aria-label": ariaLabel, "aria-labelledby": resolvedLabelledBy, checked: controlled ? checked : undefined, className: "inkra-selection-control__input", defaultChecked: controlled ? undefined : defaultChecked, disabled: disabled, id: inputId, onChange: (event) => {
|
|
30
|
+
return (_jsxs("label", { className: ['inkra-selection-control', 'inkra-checkbox', className].filter(Boolean).join(' '), "data-checked": visualChecked || undefined, "data-disabled": disabled || undefined, "data-indeterminate": indeterminate || undefined, htmlFor: inputId, children: [_jsx("input", { ...props, "aria-checked": indeterminate ? 'mixed' : undefined, "aria-describedby": ids(ariaDescribedBy, descriptionId), "aria-label": ariaLabel, "aria-labelledby": resolvedLabelledBy, checked: controlled ? checked : undefined, className: "inkra-selection-control__input", defaultChecked: controlled ? undefined : defaultChecked, disabled: disabled, form: form, id: inputId, onChange: (event) => {
|
|
30
31
|
const nextChecked = event.currentTarget.checked;
|
|
31
32
|
event.currentTarget.indeterminate = indeterminate;
|
|
32
33
|
if (!controlled)
|
|
33
34
|
syncUncontrolled(nextChecked);
|
|
34
35
|
onChange?.(event);
|
|
35
36
|
onCheckedChange?.(nextChecked);
|
|
36
|
-
}, ref: inputRef, type: "checkbox" }),
|
|
37
|
+
}, ref: inputRef, type: "checkbox" }), _jsxs(PencilSurface, { "aria-hidden": "true", as: "span", className: "inkra-selection-control__mark inkra-checkbox__mark", radius: 3, seed: `${resolvedSeed}-mark`, variant: selected ? 'quiet' : 'outline', children: [_jsx(PencilSurface, { as: "span", "aria-hidden": "true", className: "inkra-selection-control__focus-ring", seed: `${resolvedSeed}-focus`, variant: "outline" }), selected ? (_jsx(PencilIcon, { className: "inkra-selection-control__glyph", paths: indeterminate ? mixedPaths : checkPaths, seed: `${resolvedSeed}-${indeterminate ? 'mixed' : 'check'}`, size: 18, sourceSize: 24 })) : null] }), _jsxs("span", { className: "inkra-selection-control__copy", children: [typeof label === 'string' ? (_jsx(PencilText, { as: "span", className: "inkra-selection-control__label", id: labelId, seed: `${resolvedSeed}-label`, children: label })) : _jsx("span", { className: "inkra-selection-control__label", id: labelId, children: label }), description == null ? null : (_jsx("span", { className: "inkra-selection-control__description", id: descriptionId, children: description }))] })] }));
|
|
37
38
|
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
-
import { createContext, forwardRef, useContext, useEffect, useId, useImperativeHandle, useRef, useState, } from 'react';
|
|
2
|
+
import { createContext, forwardRef, useContext, useEffect, useId, useImperativeHandle, useMemo, useRef, useState, } from 'react';
|
|
3
3
|
import { PencilIcon } from '../PencilIcon.js';
|
|
4
4
|
import { PencilSurface } from '../PencilSurface.js';
|
|
5
5
|
import { PencilText } from '../PencilText.js';
|
|
@@ -22,29 +22,45 @@ export const RadioGroup = forwardRef(function RadioGroup({ 'aria-label': ariaLab
|
|
|
22
22
|
const descriptionId = description == null ? undefined : `inkra-radio-group-${generatedId}-description`;
|
|
23
23
|
const rootRef = useRef(null);
|
|
24
24
|
const initialDefaultValueRef = useRef(defaultValue);
|
|
25
|
+
const currentValueRef = useRef(value);
|
|
25
26
|
useImperativeHandle(forwardedRef, () => rootRef.current);
|
|
26
27
|
const controlled = value !== undefined;
|
|
27
28
|
const [uncontrolledValue, setUncontrolledValue] = useState(initialDefaultValueRef.current);
|
|
28
29
|
const currentValue = controlled ? value : uncontrolledValue;
|
|
30
|
+
currentValueRef.current = value;
|
|
29
31
|
useEffect(() => {
|
|
30
32
|
const root = rootRef.current;
|
|
31
33
|
const ownerForm = root?.form;
|
|
32
|
-
if (!root || !ownerForm
|
|
34
|
+
if (!root || !ownerForm)
|
|
33
35
|
return undefined;
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
36
|
+
let timer;
|
|
37
|
+
const handleReset = (event) => {
|
|
38
|
+
clearTimeout(timer);
|
|
39
|
+
timer = setTimeout(() => {
|
|
40
|
+
if (event.defaultPrevented)
|
|
41
|
+
return;
|
|
42
|
+
const restored = controlled ? currentValueRef.current : initialDefaultValueRef.current;
|
|
43
|
+
root.querySelectorAll(`input[type="radio"][data-inkra-radio-group="${generatedId}"]`).forEach(input => {
|
|
44
|
+
if (input.form !== ownerForm || input.name !== name)
|
|
45
|
+
return;
|
|
46
|
+
input.checked = input.value === restored;
|
|
47
|
+
});
|
|
48
|
+
if (!controlled)
|
|
49
|
+
setUncontrolledValue(restored);
|
|
50
|
+
}, 0);
|
|
39
51
|
};
|
|
40
52
|
ownerForm.addEventListener('reset', handleReset);
|
|
41
|
-
return () =>
|
|
42
|
-
|
|
43
|
-
|
|
53
|
+
return () => {
|
|
54
|
+
clearTimeout(timer);
|
|
55
|
+
ownerForm.removeEventListener('reset', handleReset);
|
|
56
|
+
};
|
|
57
|
+
}, [controlled, form, generatedId, name]);
|
|
58
|
+
const context = useMemo(() => ({
|
|
44
59
|
controlled,
|
|
45
60
|
defaultValue: initialDefaultValueRef.current,
|
|
46
61
|
disabled,
|
|
47
62
|
form,
|
|
63
|
+
groupId: generatedId,
|
|
48
64
|
name,
|
|
49
65
|
onItemChange(event) {
|
|
50
66
|
if (!event.currentTarget.checked)
|
|
@@ -56,7 +72,7 @@ export const RadioGroup = forwardRef(function RadioGroup({ 'aria-label': ariaLab
|
|
|
56
72
|
required,
|
|
57
73
|
variant,
|
|
58
74
|
value: currentValue,
|
|
59
|
-
};
|
|
75
|
+
}), [controlled, disabled, form, name, onValueChange, required, variant, currentValue]);
|
|
60
76
|
return (_jsx(RadioGroupContext.Provider, { value: context, children: _jsxs("fieldset", { ...props, "aria-describedby": ids(ariaDescribedBy, descriptionId), "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, className: ['inkra-radio-group', className].filter(Boolean).join(' '), "data-orientation": orientation, "data-variant": variant, disabled: disabled, form: form, ref: rootRef, children: [label == null ? null : typeof label === 'string' ? (_jsx(PencilText, { as: "legend", className: "inkra-radio-group__label", seed: `inkra-radio-group-${generatedId}-label`, children: label })) : _jsx("legend", { className: "inkra-radio-group__label", children: label }), description == null ? null : (_jsx("span", { className: "inkra-radio-group__description", id: descriptionId, children: description })), variant === 'segmented' ? (_jsx(PencilSurface, { className: "inkra-radio-group__frame", radius: 8, seed: `inkra-radio-group-${generatedId}-frame`, variant: "outline", children: _jsx("div", { className: "inkra-radio-group__items", "data-orientation": orientation, children: children }) })) : _jsx("div", { className: "inkra-radio-group__items", "data-orientation": orientation, children: children })] }) }));
|
|
61
77
|
});
|
|
62
78
|
export const RadioGroupItem = forwardRef(function RadioGroupItem({ 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, 'aria-describedby': ariaDescribedBy, className, description, disabled = false, form, icon, id, label, onChange, required = false, seed, value, ...props }, ref) {
|
|
@@ -75,8 +91,8 @@ export const RadioGroupItem = forwardRef(function RadioGroupItem({ 'aria-label':
|
|
|
75
91
|
const resolvedSeed = seed ?? inputId;
|
|
76
92
|
const visibleLabel = typeof label === 'string' ? (_jsx(PencilText, { as: "span", className: "inkra-selection-control__label", id: labelId, seed: `${resolvedSeed}-label`, children: label })) : _jsx("span", { className: "inkra-selection-control__label", id: labelId, children: label });
|
|
77
93
|
const visibleDescription = description == null ? null : (_jsx("span", { className: "inkra-selection-control__description", id: descriptionId, children: description }));
|
|
78
|
-
return (_jsxs("label", { className: ['inkra-selection-control', 'inkra-radio-group__item', className].filter(Boolean).join(' '), "data-checked": selected || undefined, "data-disabled": itemDisabled || undefined, htmlFor: inputId, children: [_jsx("input", { ...props, "aria-describedby": ids(ariaDescribedBy, descriptionId), "aria-label": ariaLabel, "aria-labelledby": resolvedLabelledBy, checked: context.controlled ? selected : undefined, className: "inkra-selection-control__input", defaultChecked: context.controlled ? undefined : context.defaultValue === value, disabled: itemDisabled, form: form ?? context.form, id: inputId, name: context.name, onChange: (event) => {
|
|
94
|
+
return (_jsxs("label", { className: ['inkra-selection-control', 'inkra-radio-group__item', className].filter(Boolean).join(' '), "data-checked": selected || undefined, "data-disabled": itemDisabled || undefined, htmlFor: inputId, children: [_jsx("input", { ...props, "aria-describedby": ids(ariaDescribedBy, descriptionId), "aria-label": ariaLabel, "aria-labelledby": resolvedLabelledBy, checked: context.controlled ? selected : undefined, className: "inkra-selection-control__input", defaultChecked: context.controlled ? undefined : context.defaultValue === value, disabled: itemDisabled, "data-inkra-radio-group": context.groupId, form: form ?? context.form, id: inputId, name: context.name, onChange: (event) => {
|
|
79
95
|
onChange?.(event);
|
|
80
96
|
context.onItemChange(event);
|
|
81
|
-
}, ref: ref, required: required || context.required, type: "radio", value: value }), context.variant === 'segmented' ? (_jsxs("span", { className: "inkra-radio-group__segment", children: [_jsx(PencilSurface, { "aria-hidden": "true", as: "span", className: "inkra-radio-segment__surface inkra-radio-segment__surface--selection", ref: selectionRef, seed: `${resolvedSeed}-segment-selection`, variant: "selection" }), _jsxs("span", { className: "inkra-radio-segment__content", children: [icon == null ? null : _jsx("span", { "aria-hidden": "true", className: "inkra-radio-group__icon", children: icon }), _jsxs("span", { className: "inkra-selection-control__copy", children: [visibleLabel, visibleDescription] })] })] })) : (_jsxs(_Fragment, { children: [
|
|
97
|
+
}, ref: ref, required: required || context.required, type: "radio", value: value }), context.variant === 'segmented' ? (_jsxs("span", { className: "inkra-radio-group__segment", children: [_jsx(PencilSurface, { as: "span", "aria-hidden": "true", className: "inkra-selection-control__focus-ring inkra-selection-control__focus-ring--inset", seed: `${resolvedSeed}-focus`, variant: "outline" }), _jsx(PencilSurface, { "aria-hidden": "true", as: "span", className: "inkra-radio-segment__surface inkra-radio-segment__surface--selection", ref: selectionRef, seed: `${resolvedSeed}-segment-selection`, variant: "selection" }), _jsxs("span", { className: "inkra-radio-segment__content", children: [icon == null ? null : _jsx("span", { "aria-hidden": "true", className: "inkra-radio-group__icon", children: icon }), _jsxs("span", { className: "inkra-selection-control__copy", children: [visibleLabel, visibleDescription] })] })] })) : (_jsxs(_Fragment, { children: [_jsxs("span", { "aria-hidden": "true", className: "inkra-selection-control__mark inkra-radio-group__mark", children: [_jsx(PencilSurface, { as: "span", "aria-hidden": "true", className: "inkra-selection-control__focus-ring", seed: `${resolvedSeed}-focus`, variant: "outline" }), _jsx(PencilIcon, { className: "inkra-selection-control__glyph", paths: selected ? selectedPaths : ringPaths, seed: `${resolvedSeed}-${selected ? 'selected' : 'ring'}`, size: 24, sourceSize: 24 })] }), icon == null ? null : _jsx("span", { "aria-hidden": "true", className: "inkra-radio-group__icon", children: icon }), _jsxs("span", { className: "inkra-selection-control__copy", children: [visibleLabel, visibleDescription] })] }))] }));
|
|
82
98
|
});
|
package/dist/selection/Switch.js
CHANGED
|
@@ -12,7 +12,7 @@ function ids(...values) {
|
|
|
12
12
|
const value = values.filter(Boolean).join(' ');
|
|
13
13
|
return value || undefined;
|
|
14
14
|
}
|
|
15
|
-
export const Switch = forwardRef(function Switch({ 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, 'aria-describedby': ariaDescribedBy, checked, className, defaultChecked, description, disabled = false, id, label, onChange, onCheckedChange, seed, ...props }, forwardedRef) {
|
|
15
|
+
export const Switch = forwardRef(function Switch({ 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, 'aria-describedby': ariaDescribedBy, checked, className, defaultChecked, description, disabled = false, form, id, label, onChange, onCheckedChange, seed, ...props }, forwardedRef) {
|
|
16
16
|
const generatedId = useId();
|
|
17
17
|
const inputId = id ?? `inkra-switch-${generatedId}`;
|
|
18
18
|
const labelId = `${inputId}-label`;
|
|
@@ -24,13 +24,14 @@ export const Switch = forwardRef(function Switch({ 'aria-label': ariaLabel, 'ari
|
|
|
24
24
|
const { controlled, syncUncontrolled, visualChecked } = useNativeChecked({
|
|
25
25
|
checked,
|
|
26
26
|
defaultChecked,
|
|
27
|
+
form,
|
|
27
28
|
inputRef,
|
|
28
29
|
});
|
|
29
|
-
return (_jsxs("label", { className: ['inkra-selection-control', 'inkra-switch', className].filter(Boolean).join(' '), "data-checked": visualChecked || undefined, "data-disabled": disabled || undefined, htmlFor: inputId, children: [_jsx("input", { ...props, "aria-describedby": ids(ariaDescribedBy, descriptionId), "aria-label": ariaLabel, "aria-labelledby": resolvedLabelledBy, checked: controlled ? checked : undefined, className: "inkra-selection-control__input", defaultChecked: controlled ? undefined : defaultChecked, disabled: disabled, id: inputId, onChange: (event) => {
|
|
30
|
+
return (_jsxs("label", { className: ['inkra-selection-control', 'inkra-switch', className].filter(Boolean).join(' '), "data-checked": visualChecked || undefined, "data-disabled": disabled || undefined, htmlFor: inputId, children: [_jsx("input", { ...props, "aria-describedby": ids(ariaDescribedBy, descriptionId), "aria-label": ariaLabel, "aria-labelledby": resolvedLabelledBy, checked: controlled ? checked : undefined, className: "inkra-selection-control__input", defaultChecked: controlled ? undefined : defaultChecked, disabled: disabled, form: form, id: inputId, onChange: (event) => {
|
|
30
31
|
const nextChecked = event.currentTarget.checked;
|
|
31
32
|
if (!controlled)
|
|
32
33
|
syncUncontrolled(nextChecked);
|
|
33
34
|
onChange?.(event);
|
|
34
35
|
onCheckedChange?.(nextChecked);
|
|
35
|
-
}, ref: inputRef, role: "switch", type: "checkbox" }),
|
|
36
|
+
}, ref: inputRef, role: "switch", type: "checkbox" }), _jsxs(PencilSurface, { "aria-hidden": "true", as: "span", className: "inkra-switch__track", radius: 8, seed: `${resolvedSeed}-track`, variant: visualChecked ? 'quiet' : 'outline', children: [_jsx(PencilSurface, { as: "span", "aria-hidden": "true", className: "inkra-selection-control__focus-ring", seed: `${resolvedSeed}-focus`, variant: "outline" }), _jsx(PencilIcon, { className: "inkra-switch__thumb", paths: thumbPaths, seed: `${resolvedSeed}-thumb`, size: 24, sourceSize: 40 })] }), _jsxs("span", { className: "inkra-selection-control__copy", children: [typeof label === 'string' ? (_jsx(PencilText, { as: "span", className: "inkra-selection-control__label", id: labelId, seed: `${resolvedSeed}-label`, children: label })) : _jsx("span", { className: "inkra-selection-control__label", id: labelId, children: label }), description == null ? null : (_jsx("span", { className: "inkra-selection-control__description", id: descriptionId, children: description }))] })] }));
|
|
36
37
|
});
|
|
@@ -2,10 +2,11 @@ import type { RefObject } from 'react';
|
|
|
2
2
|
interface NativeCheckedOptions {
|
|
3
3
|
checked?: boolean;
|
|
4
4
|
defaultChecked?: boolean;
|
|
5
|
+
form?: string;
|
|
5
6
|
inputRef: RefObject<HTMLInputElement | null>;
|
|
6
7
|
indeterminate?: boolean;
|
|
7
8
|
}
|
|
8
|
-
export declare function useNativeChecked({ checked, defaultChecked, inputRef, indeterminate, }: NativeCheckedOptions): {
|
|
9
|
+
export declare function useNativeChecked({ checked, defaultChecked, form, inputRef, indeterminate, }: NativeCheckedOptions): {
|
|
9
10
|
controlled: boolean;
|
|
10
11
|
visualChecked: boolean;
|
|
11
12
|
syncUncontrolled: import("react").Dispatch<import("react").SetStateAction<boolean>>;
|
|
@@ -1,8 +1,13 @@
|
|
|
1
|
-
import { useEffect, useState } from 'react';
|
|
1
|
+
import { useEffect, useRef, useState } from 'react';
|
|
2
2
|
import { useBrowserLayoutEffect } from '../internal/useBrowserLayoutEffect.js';
|
|
3
|
-
export function useNativeChecked({ checked, defaultChecked, inputRef, indeterminate = false, }) {
|
|
3
|
+
export function useNativeChecked({ checked, defaultChecked, form, inputRef, indeterminate = false, }) {
|
|
4
4
|
const controlled = checked !== undefined;
|
|
5
|
-
const
|
|
5
|
+
const defaultCheckedRef = useRef(Boolean(defaultChecked));
|
|
6
|
+
const checkedRef = useRef(checked);
|
|
7
|
+
const indeterminateRef = useRef(indeterminate);
|
|
8
|
+
const [uncontrolledChecked, setUncontrolledChecked] = useState(defaultCheckedRef.current);
|
|
9
|
+
checkedRef.current = checked;
|
|
10
|
+
indeterminateRef.current = indeterminate;
|
|
6
11
|
useBrowserLayoutEffect(() => {
|
|
7
12
|
const input = inputRef.current;
|
|
8
13
|
if (input)
|
|
@@ -11,14 +16,27 @@ export function useNativeChecked({ checked, defaultChecked, inputRef, indetermin
|
|
|
11
16
|
useEffect(() => {
|
|
12
17
|
const input = inputRef.current;
|
|
13
18
|
const form = input?.form;
|
|
14
|
-
if (!input || !form
|
|
19
|
+
if (!input || !form)
|
|
15
20
|
return undefined;
|
|
16
|
-
|
|
17
|
-
|
|
21
|
+
let timer;
|
|
22
|
+
const handleReset = (event) => {
|
|
23
|
+
clearTimeout(timer);
|
|
24
|
+
timer = setTimeout(() => {
|
|
25
|
+
if (event.defaultPrevented)
|
|
26
|
+
return;
|
|
27
|
+
const restored = controlled ? Boolean(checkedRef.current) : defaultCheckedRef.current;
|
|
28
|
+
input.checked = restored;
|
|
29
|
+
input.indeterminate = indeterminateRef.current;
|
|
30
|
+
if (!controlled)
|
|
31
|
+
setUncontrolledChecked(restored);
|
|
32
|
+
}, 0);
|
|
18
33
|
};
|
|
19
34
|
form.addEventListener('reset', handleReset);
|
|
20
|
-
return () =>
|
|
21
|
-
|
|
35
|
+
return () => {
|
|
36
|
+
clearTimeout(timer);
|
|
37
|
+
form.removeEventListener('reset', handleReset);
|
|
38
|
+
};
|
|
39
|
+
}, [controlled, form, inputRef]);
|
|
22
40
|
return {
|
|
23
41
|
controlled,
|
|
24
42
|
visualChecked: controlled ? Boolean(checked) : uncontrolledChecked,
|