@tangible/ui 0.0.1 → 0.0.2
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/components/Card/Card.d.ts +1 -0
- package/components/Card/Card.js +17 -20
- package/components/Checkbox/Checkbox.d.ts +9 -0
- package/components/Checkbox/Checkbox.js +92 -0
- package/components/Checkbox/index.d.ts +2 -0
- package/components/Checkbox/index.js +1 -0
- package/components/Checkbox/types.d.ts +10 -0
- package/components/Checkbox/types.js +1 -0
- package/components/Chip/Chip.d.ts +4 -1
- package/components/Chip/Chip.js +32 -7
- package/components/ChipGroup/ChipGroup.d.ts +5 -0
- package/components/ChipGroup/ChipGroup.js +68 -0
- package/components/ChipGroup/ChipGroupContext.d.ts +3 -0
- package/components/ChipGroup/ChipGroupContext.js +5 -0
- package/components/ChipGroup/index.d.ts +3 -0
- package/components/ChipGroup/index.js +2 -0
- package/components/ChipGroup/types.d.ts +36 -0
- package/components/ChipGroup/types.js +1 -0
- package/components/Chips/Chips.d.ts +2 -0
- package/components/Chips/Chips.js +1 -1
- package/components/Combobox/Combobox.d.ts +33 -0
- package/components/Combobox/Combobox.js +466 -0
- package/components/Combobox/ComboboxContext.d.ts +8 -0
- package/components/Combobox/ComboboxContext.js +36 -0
- package/components/Combobox/index.d.ts +2 -0
- package/components/Combobox/index.js +1 -0
- package/components/Combobox/types.d.ts +204 -0
- package/components/Combobox/types.js +1 -0
- package/components/Dropdown/Dropdown.js +2 -1
- package/components/Field/Field.d.ts +39 -0
- package/components/Field/Field.js +92 -0
- package/components/Field/FieldContext.d.ts +16 -0
- package/components/Field/FieldContext.js +10 -0
- package/components/Field/index.d.ts +2 -0
- package/components/Field/index.js +1 -0
- package/components/Modal/Modal.d.ts +4 -4
- package/components/Modal/Modal.js +14 -12
- package/components/MultiSelect/MultiSelect.d.ts +39 -0
- package/components/MultiSelect/MultiSelect.js +623 -0
- package/components/MultiSelect/MultiSelectContext.d.ts +20 -0
- package/components/MultiSelect/MultiSelectContext.js +56 -0
- package/components/MultiSelect/index.d.ts +2 -0
- package/components/MultiSelect/index.js +1 -0
- package/components/MultiSelect/types.d.ts +218 -0
- package/components/MultiSelect/types.js +3 -0
- package/components/Notice/Notice.d.ts +1 -1
- package/components/Notice/Notice.js +1 -1
- package/components/Progress/Progress.js +1 -1
- package/components/Progress/types.d.ts +7 -7
- package/components/Radio/Radio.d.ts +2 -0
- package/components/Radio/Radio.js +50 -0
- package/components/Radio/RadioGroup.d.ts +2 -0
- package/components/Radio/RadioGroup.js +54 -0
- package/components/Radio/RadioGroupContext.d.ts +3 -0
- package/components/Radio/RadioGroupContext.js +9 -0
- package/components/Radio/index.d.ts +8 -0
- package/components/Radio/index.js +6 -0
- package/components/Radio/types.d.ts +32 -0
- package/components/Radio/types.js +1 -0
- package/components/Rating/Rating.d.ts +5 -5
- package/components/Rating/Rating.js +2 -2
- package/components/SegmentedControl/SegmentedControl.js +20 -104
- package/components/SegmentedControl/types.d.ts +4 -8
- package/components/Select/Select.d.ts +39 -0
- package/components/Select/Select.js +497 -0
- package/components/Select/SelectContext.d.ts +20 -0
- package/components/Select/SelectContext.js +56 -0
- package/components/Select/index.d.ts +3 -0
- package/components/Select/index.js +1 -0
- package/components/Select/types.d.ts +216 -0
- package/components/Select/types.js +11 -0
- package/components/Sidebar/Sidebar.js +12 -12
- package/components/Sidebar/types.d.ts +5 -5
- package/components/StepIndicator/StepIndicator.js +1 -1
- package/components/StepList/StepList.js +2 -2
- package/components/StepList/types.d.ts +4 -4
- package/components/Switch/Switch.d.ts +9 -0
- package/components/Switch/Switch.js +91 -0
- package/components/Switch/index.d.ts +2 -0
- package/components/Switch/index.js +1 -0
- package/components/Switch/types.d.ts +11 -0
- package/components/Switch/types.js +1 -0
- package/components/TextInput/TextInput.d.ts +8 -0
- package/components/TextInput/TextInput.js +25 -0
- package/components/TextInput/index.d.ts +2 -0
- package/components/TextInput/index.js +1 -0
- package/components/TextInput/types.d.ts +32 -0
- package/components/TextInput/types.js +1 -0
- package/components/Textarea/Textarea.d.ts +6 -0
- package/components/Textarea/Textarea.js +49 -0
- package/components/Textarea/index.d.ts +2 -0
- package/components/Textarea/index.js +1 -0
- package/components/Textarea/types.d.ts +25 -0
- package/components/Textarea/types.js +1 -0
- package/components/index.d.ts +20 -0
- package/components/index.js +10 -0
- package/icons/icons.svg +1 -0
- package/icons/manifest.json +8 -0
- package/icons/registry.d.ts +2 -0
- package/icons/registry.js +1 -0
- package/icons/system/index.d.ts +2 -0
- package/icons/system/index.js +11 -0
- package/package.json +1 -1
- package/styles/all.css +1 -1
- package/styles/all.expanded.css +1187 -96
- package/styles/all.expanded.unlayered.css +1187 -96
- package/styles/all.unlayered.css +1 -1
- package/styles/components/_bundle.scss +20 -0
- package/styles/components/input/index.scss +5 -20
- package/styles/index.scss +16 -0
- package/styles/system/_control.scss +34 -0
- package/styles/system/_tokens.scss +8 -0
- package/styles/system/index.scss +2 -1
- package/styles/utilities/_index.scss +50 -0
- package/tui-manifest.json +632 -61
- package/utils/compose-events.d.ts +15 -0
- package/utils/compose-events.js +27 -0
- package/utils/hash.d.ts +15 -0
- package/utils/hash.js +32 -0
- package/utils/index.d.ts +3 -0
- package/utils/index.js +6 -0
- package/utils/is-dev.d.ts +5 -0
- package/utils/is-dev.js +7 -0
- package/utils/use-controllable-state.d.ts +19 -0
- package/utils/use-controllable-state.js +59 -0
- package/utils/use-roving-group.d.ts +33 -0
- package/utils/use-roving-group.js +123 -0
- package/utils/value-key.d.ts +16 -0
- package/utils/value-key.js +14 -0
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compose two event handlers into one.
|
|
3
|
+
* Internal handler runs first, then user handler (if not defaultPrevented).
|
|
4
|
+
*
|
|
5
|
+
* Use this instead of relying on spread order when combining handlers
|
|
6
|
+
* from different sources (e.g., Floating UI props + our handlers).
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* const props = getReferenceProps({
|
|
10
|
+
* onPointerDown: composeEventHandlers(ourHandler, floatingHandler),
|
|
11
|
+
* });
|
|
12
|
+
*/
|
|
13
|
+
export declare function composeEventHandlers<E extends {
|
|
14
|
+
defaultPrevented?: boolean;
|
|
15
|
+
}>(internalHandler: ((event: E) => void) | undefined, userHandler: ((event: E) => void) | undefined): ((event: E) => void) | undefined;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compose two event handlers into one.
|
|
3
|
+
* Internal handler runs first, then user handler (if not defaultPrevented).
|
|
4
|
+
*
|
|
5
|
+
* Use this instead of relying on spread order when combining handlers
|
|
6
|
+
* from different sources (e.g., Floating UI props + our handlers).
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* const props = getReferenceProps({
|
|
10
|
+
* onPointerDown: composeEventHandlers(ourHandler, floatingHandler),
|
|
11
|
+
* });
|
|
12
|
+
*/
|
|
13
|
+
export function composeEventHandlers(internalHandler, userHandler) {
|
|
14
|
+
if (!internalHandler && !userHandler)
|
|
15
|
+
return undefined;
|
|
16
|
+
if (!internalHandler)
|
|
17
|
+
return userHandler;
|
|
18
|
+
if (!userHandler)
|
|
19
|
+
return internalHandler;
|
|
20
|
+
return (event) => {
|
|
21
|
+
internalHandler(event);
|
|
22
|
+
// Only call user handler if internal didn't prevent default
|
|
23
|
+
if (!event.defaultPrevented) {
|
|
24
|
+
userHandler(event);
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
}
|
package/utils/hash.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FNV-1a 32-bit hash function.
|
|
3
|
+
* Fast, deterministic, low collision risk for typical string inputs.
|
|
4
|
+
* Used for generating stable DOM IDs from arbitrary values.
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Generate a short, stable hash string from any value.
|
|
8
|
+
* Returns base36-encoded FNV-1a hash (6-7 chars).
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* hashForId('apple') // 'k7x2m1'
|
|
12
|
+
* hashForId('foo/bar') // 'a3b4c5'
|
|
13
|
+
* hashForId('foo:bar') // 'd6e7f8' (different from foo/bar)
|
|
14
|
+
*/
|
|
15
|
+
export declare function hashForId(value: string | number): string;
|
package/utils/hash.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FNV-1a 32-bit hash function.
|
|
3
|
+
* Fast, deterministic, low collision risk for typical string inputs.
|
|
4
|
+
* Used for generating stable DOM IDs from arbitrary values.
|
|
5
|
+
*/
|
|
6
|
+
const FNV_OFFSET_BASIS = 2166136261;
|
|
7
|
+
const FNV_PRIME = 16777619;
|
|
8
|
+
/**
|
|
9
|
+
* Compute FNV-1a 32-bit hash of a string.
|
|
10
|
+
*/
|
|
11
|
+
function fnv1a32(str) {
|
|
12
|
+
let hash = FNV_OFFSET_BASIS;
|
|
13
|
+
for (let i = 0; i < str.length; i++) {
|
|
14
|
+
hash ^= str.charCodeAt(i);
|
|
15
|
+
hash = Math.imul(hash, FNV_PRIME);
|
|
16
|
+
}
|
|
17
|
+
// Convert to unsigned 32-bit
|
|
18
|
+
return hash >>> 0;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Generate a short, stable hash string from any value.
|
|
22
|
+
* Returns base36-encoded FNV-1a hash (6-7 chars).
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* hashForId('apple') // 'k7x2m1'
|
|
26
|
+
* hashForId('foo/bar') // 'a3b4c5'
|
|
27
|
+
* hashForId('foo:bar') // 'd6e7f8' (different from foo/bar)
|
|
28
|
+
*/
|
|
29
|
+
export function hashForId(value) {
|
|
30
|
+
const str = typeof value === 'string' ? value : String(value);
|
|
31
|
+
return fnv1a32(str).toString(36);
|
|
32
|
+
}
|
package/utils/index.d.ts
CHANGED
|
@@ -8,3 +8,6 @@ export { mergeProps } from './merge-props';
|
|
|
8
8
|
export { cx } from './cx';
|
|
9
9
|
export { getContrastTone, getContrastForeground } from './color/contrast';
|
|
10
10
|
export type { ContrastTone } from './color/contrast';
|
|
11
|
+
export { hashForId } from './hash';
|
|
12
|
+
export { composeEventHandlers } from './compose-events';
|
|
13
|
+
export { useControllableState } from './use-controllable-state';
|
package/utils/index.js
CHANGED
|
@@ -14,3 +14,9 @@ export { mergeProps } from './merge-props.js';
|
|
|
14
14
|
export { cx } from './cx.js';
|
|
15
15
|
// Contrast utilities (for user-selected colors)
|
|
16
16
|
export { getContrastTone, getContrastForeground } from './color/contrast.js';
|
|
17
|
+
// Hash utilities (for stable DOM IDs)
|
|
18
|
+
export { hashForId } from './hash.js';
|
|
19
|
+
// Event composition
|
|
20
|
+
export { composeEventHandlers } from './compose-events.js';
|
|
21
|
+
// Controllable state hook
|
|
22
|
+
export { useControllableState } from './use-controllable-state.js';
|
package/utils/is-dev.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared controlled/uncontrolled state hook.
|
|
3
|
+
*
|
|
4
|
+
* - When `value` is provided (not `undefined`): controlled mode. Setter only fires `onChange`.
|
|
5
|
+
* - When `value` is `undefined`: uncontrolled mode. Setter updates internal state AND fires `onChange`.
|
|
6
|
+
* - `isControlled` override: for components where `undefined` is a valid controlled value
|
|
7
|
+
* (e.g. ChipGroup single-select deselection). Pass `true` to force controlled mode
|
|
8
|
+
* even when value is `undefined`.
|
|
9
|
+
* - Supports functional updates: `setValue(prev => next)`.
|
|
10
|
+
* - `defaultValue` seeds the internal state (callers decide their own fallback).
|
|
11
|
+
* - The returned setter is stable across renders.
|
|
12
|
+
*/
|
|
13
|
+
export declare function useControllableState<T>({ value, defaultValue, onChange, isControlled: isControlledOverride, }: {
|
|
14
|
+
value?: T;
|
|
15
|
+
defaultValue?: T;
|
|
16
|
+
onChange?: (value: T) => void;
|
|
17
|
+
/** Force controlled mode even when value is undefined. */
|
|
18
|
+
isControlled?: boolean;
|
|
19
|
+
}): [T | undefined, (next: T | ((prev: T | undefined) => T)) => void];
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { useState, useCallback, useRef, useEffect } from 'react';
|
|
2
|
+
import { isDev as checkIsDev } from './is-dev.js';
|
|
3
|
+
/**
|
|
4
|
+
* Shared controlled/uncontrolled state hook.
|
|
5
|
+
*
|
|
6
|
+
* - When `value` is provided (not `undefined`): controlled mode. Setter only fires `onChange`.
|
|
7
|
+
* - When `value` is `undefined`: uncontrolled mode. Setter updates internal state AND fires `onChange`.
|
|
8
|
+
* - `isControlled` override: for components where `undefined` is a valid controlled value
|
|
9
|
+
* (e.g. ChipGroup single-select deselection). Pass `true` to force controlled mode
|
|
10
|
+
* even when value is `undefined`.
|
|
11
|
+
* - Supports functional updates: `setValue(prev => next)`.
|
|
12
|
+
* - `defaultValue` seeds the internal state (callers decide their own fallback).
|
|
13
|
+
* - The returned setter is stable across renders.
|
|
14
|
+
*/
|
|
15
|
+
export function useControllableState({ value, defaultValue, onChange, isControlled: isControlledOverride, }) {
|
|
16
|
+
const [internalValue, setInternalValue] = useState(defaultValue);
|
|
17
|
+
// Keep refs so the setter callback stays stable
|
|
18
|
+
const onChangeRef = useRef(onChange);
|
|
19
|
+
onChangeRef.current = onChange;
|
|
20
|
+
const internalRef = useRef(internalValue);
|
|
21
|
+
internalRef.current = internalValue;
|
|
22
|
+
const isControlled = isControlledOverride ?? value !== undefined;
|
|
23
|
+
const currentValue = isControlled ? value : internalValue;
|
|
24
|
+
// Ref for stable setter — tracks controlled state
|
|
25
|
+
const isControlledRef = useRef(isControlled);
|
|
26
|
+
isControlledRef.current = isControlled;
|
|
27
|
+
const controlledValueRef = useRef(value);
|
|
28
|
+
controlledValueRef.current = value;
|
|
29
|
+
// Dev-only: warn if component switches between controlled and uncontrolled
|
|
30
|
+
const wasControlledRef = useRef(isControlled);
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
if (checkIsDev()) {
|
|
33
|
+
if (wasControlledRef.current && !isControlled) {
|
|
34
|
+
console.warn('useControllableState: Switching from controlled to uncontrolled. This is likely a bug. Decide between using a controlled or uncontrolled value for the lifetime of the component.');
|
|
35
|
+
}
|
|
36
|
+
if (!wasControlledRef.current && isControlled) {
|
|
37
|
+
console.warn('useControllableState: Switching from uncontrolled to controlled. This is likely a bug. Decide between using a controlled or uncontrolled value for the lifetime of the component.');
|
|
38
|
+
}
|
|
39
|
+
wasControlledRef.current = isControlled;
|
|
40
|
+
}
|
|
41
|
+
}, [isControlled]);
|
|
42
|
+
const setValue = useCallback((next) => {
|
|
43
|
+
// Resolve functional updates using the appropriate source of truth
|
|
44
|
+
const prev = isControlledRef.current
|
|
45
|
+
? controlledValueRef.current
|
|
46
|
+
: internalRef.current;
|
|
47
|
+
const resolvedNext = typeof next === 'function'
|
|
48
|
+
? next(prev)
|
|
49
|
+
: next;
|
|
50
|
+
if (!isControlledRef.current) {
|
|
51
|
+
// Uncontrolled — update internal state
|
|
52
|
+
setInternalValue(resolvedNext);
|
|
53
|
+
internalRef.current = resolvedNext;
|
|
54
|
+
}
|
|
55
|
+
onChangeRef.current?.(resolvedNext);
|
|
56
|
+
}, [] // Stable — reads everything from refs
|
|
57
|
+
);
|
|
58
|
+
return [currentValue, setValue];
|
|
59
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { OptionValue } from './value-key';
|
|
2
|
+
export type RovingItemRecord = {
|
|
3
|
+
value: OptionValue;
|
|
4
|
+
element: HTMLElement;
|
|
5
|
+
disabled: boolean;
|
|
6
|
+
/** Internal — assigned by registry on first mount */
|
|
7
|
+
mountIndex: number;
|
|
8
|
+
};
|
|
9
|
+
type UseRovingGroupOptions = {
|
|
10
|
+
selectedValue: OptionValue | undefined;
|
|
11
|
+
onSelect: (value: OptionValue) => void;
|
|
12
|
+
/** Group-level disabled. Default false. */
|
|
13
|
+
disabled?: boolean;
|
|
14
|
+
/** Wrap around at boundaries. Default true. */
|
|
15
|
+
loop?: boolean;
|
|
16
|
+
/** Layout orientation. Default 'horizontal'. */
|
|
17
|
+
orientation?: 'horizontal' | 'vertical';
|
|
18
|
+
/**
|
|
19
|
+
* Whether only orientation-matching arrow keys navigate.
|
|
20
|
+
*
|
|
21
|
+
* - `false` (default): All 4 arrows work — WAI-ARIA APG Radio Group pattern.
|
|
22
|
+
* - `true`: Only orientation-matching arrows work — Tabs/Toolbar pattern.
|
|
23
|
+
*/
|
|
24
|
+
orientationKeyboard?: boolean;
|
|
25
|
+
};
|
|
26
|
+
export declare function useRovingGroup({ selectedValue, onSelect, disabled, loop, orientation, orientationKeyboard, }: UseRovingGroupOptions): {
|
|
27
|
+
registerItem: (record: Omit<RovingItemRecord, "mountIndex">) => void;
|
|
28
|
+
unregisterItem: (value: OptionValue) => void;
|
|
29
|
+
getOrderedItems: () => readonly RovingItemRecord[];
|
|
30
|
+
focusableValue: OptionValue | undefined;
|
|
31
|
+
handleKeyDown: (event: React.KeyboardEvent) => void;
|
|
32
|
+
};
|
|
33
|
+
export {};
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { useCallback, useMemo, useRef, useState } from 'react';
|
|
2
|
+
import { toKey } from './value-key.js';
|
|
3
|
+
export function useRovingGroup({ selectedValue, onSelect, disabled = false, loop = true, orientation = 'horizontal', orientationKeyboard = false, }) {
|
|
4
|
+
const [registryVersion, setRegistryVersion] = useState(0);
|
|
5
|
+
const itemsRef = useRef(new Map());
|
|
6
|
+
const mountCounterRef = useRef(0);
|
|
7
|
+
// ── Item registration ──────────────────────────────────────────────────
|
|
8
|
+
const registerItem = useCallback((record) => {
|
|
9
|
+
const key = toKey(record.value);
|
|
10
|
+
const existing = itemsRef.current.get(key);
|
|
11
|
+
itemsRef.current.set(key, {
|
|
12
|
+
...record,
|
|
13
|
+
mountIndex: existing?.mountIndex ?? mountCounterRef.current++,
|
|
14
|
+
});
|
|
15
|
+
setRegistryVersion((v) => v + 1);
|
|
16
|
+
}, []);
|
|
17
|
+
const unregisterItem = useCallback((value) => {
|
|
18
|
+
itemsRef.current.delete(toKey(value));
|
|
19
|
+
setRegistryVersion((v) => v + 1);
|
|
20
|
+
}, []);
|
|
21
|
+
// ── DOM-order sort ─────────────────────────────────────────────────────
|
|
22
|
+
const getOrderedItems = useCallback(() => {
|
|
23
|
+
const items = [...itemsRef.current.values()];
|
|
24
|
+
return items.sort((a, b) => {
|
|
25
|
+
const position = a.element.compareDocumentPosition(b.element);
|
|
26
|
+
if (position & Node.DOCUMENT_POSITION_FOLLOWING)
|
|
27
|
+
return -1;
|
|
28
|
+
if (position & Node.DOCUMENT_POSITION_PRECEDING)
|
|
29
|
+
return 1;
|
|
30
|
+
return a.mountIndex - b.mountIndex;
|
|
31
|
+
});
|
|
32
|
+
}, []);
|
|
33
|
+
// ── Focusable value (which item gets tabIndex={0}) ─────────────────────
|
|
34
|
+
const focusableValue = useMemo(() => {
|
|
35
|
+
const items = getOrderedItems();
|
|
36
|
+
const enabledItems = items.filter((item) => !item.disabled && !disabled);
|
|
37
|
+
if (enabledItems.length === 0)
|
|
38
|
+
return undefined;
|
|
39
|
+
// Selected + enabled item gets focus
|
|
40
|
+
if (selectedValue !== undefined) {
|
|
41
|
+
const selectedItem = items.find((item) => toKey(item.value) === toKey(selectedValue));
|
|
42
|
+
if (selectedItem && !selectedItem.disabled && !disabled) {
|
|
43
|
+
return selectedValue;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
// Otherwise first enabled item
|
|
47
|
+
return enabledItems[0].value;
|
|
48
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
49
|
+
}, [selectedValue, disabled, getOrderedItems, registryVersion]);
|
|
50
|
+
// ── Keyboard navigation ────────────────────────────────────────────────
|
|
51
|
+
const handleKeyDown = useCallback((event) => {
|
|
52
|
+
const items = getOrderedItems().filter((item) => !item.disabled && !disabled);
|
|
53
|
+
if (items.length === 0)
|
|
54
|
+
return;
|
|
55
|
+
let currentIndex = selectedValue !== undefined
|
|
56
|
+
? items.findIndex((item) => toKey(item.value) === toKey(selectedValue))
|
|
57
|
+
: -1;
|
|
58
|
+
if (currentIndex === -1)
|
|
59
|
+
currentIndex = 0;
|
|
60
|
+
// Determine which keys map to next/prev
|
|
61
|
+
let nextKeys;
|
|
62
|
+
let prevKeys;
|
|
63
|
+
if (orientationKeyboard) {
|
|
64
|
+
// Only orientation-matching arrows (Tabs/Toolbar pattern)
|
|
65
|
+
nextKeys =
|
|
66
|
+
orientation === 'horizontal' ? ['ArrowRight'] : ['ArrowDown'];
|
|
67
|
+
prevKeys =
|
|
68
|
+
orientation === 'horizontal' ? ['ArrowLeft'] : ['ArrowUp'];
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
// All 4 arrows (Radio Group pattern)
|
|
72
|
+
nextKeys = ['ArrowRight', 'ArrowDown'];
|
|
73
|
+
prevKeys = ['ArrowLeft', 'ArrowUp'];
|
|
74
|
+
}
|
|
75
|
+
let targetIndex = null;
|
|
76
|
+
if (nextKeys.includes(event.key)) {
|
|
77
|
+
event.preventDefault();
|
|
78
|
+
if (loop) {
|
|
79
|
+
targetIndex = (currentIndex + 1) % items.length;
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
targetIndex = Math.min(currentIndex + 1, items.length - 1);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
else if (prevKeys.includes(event.key)) {
|
|
86
|
+
event.preventDefault();
|
|
87
|
+
if (loop) {
|
|
88
|
+
targetIndex = (currentIndex - 1 + items.length) % items.length;
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
targetIndex = Math.max(currentIndex - 1, 0);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
else if (event.key === 'Home') {
|
|
95
|
+
event.preventDefault();
|
|
96
|
+
targetIndex = 0;
|
|
97
|
+
}
|
|
98
|
+
else if (event.key === 'End') {
|
|
99
|
+
event.preventDefault();
|
|
100
|
+
targetIndex = items.length - 1;
|
|
101
|
+
}
|
|
102
|
+
if (targetIndex !== null && targetIndex !== currentIndex) {
|
|
103
|
+
const targetItem = items[targetIndex];
|
|
104
|
+
targetItem.element.focus();
|
|
105
|
+
onSelect(targetItem.value);
|
|
106
|
+
}
|
|
107
|
+
}, [
|
|
108
|
+
getOrderedItems,
|
|
109
|
+
disabled,
|
|
110
|
+
selectedValue,
|
|
111
|
+
orientationKeyboard,
|
|
112
|
+
orientation,
|
|
113
|
+
loop,
|
|
114
|
+
onSelect,
|
|
115
|
+
]);
|
|
116
|
+
return {
|
|
117
|
+
registerItem,
|
|
118
|
+
unregisterItem,
|
|
119
|
+
getOrderedItems,
|
|
120
|
+
focusableValue,
|
|
121
|
+
handleKeyDown,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared value-key utility for selection components.
|
|
3
|
+
*
|
|
4
|
+
* All components that track selection by value (Select, MultiSelect, Combobox,
|
|
5
|
+
* Radio, ChipGroup) use this to normalise string | number values into string
|
|
6
|
+
* keys for Map/Set lookups and equality comparisons.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Value type accepted by selection components.
|
|
10
|
+
*/
|
|
11
|
+
export type OptionValue = string | number;
|
|
12
|
+
/**
|
|
13
|
+
* Normalise a value to a string key for Map/Set lookups.
|
|
14
|
+
* Prefixes with type indicator to avoid collisions between 1 (number) and "1" (string).
|
|
15
|
+
*/
|
|
16
|
+
export declare function toKey(value: OptionValue): string;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared value-key utility for selection components.
|
|
3
|
+
*
|
|
4
|
+
* All components that track selection by value (Select, MultiSelect, Combobox,
|
|
5
|
+
* Radio, ChipGroup) use this to normalise string | number values into string
|
|
6
|
+
* keys for Map/Set lookups and equality comparisons.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Normalise a value to a string key for Map/Set lookups.
|
|
10
|
+
* Prefixes with type indicator to avoid collisions between 1 (number) and "1" (string).
|
|
11
|
+
*/
|
|
12
|
+
export function toKey(value) {
|
|
13
|
+
return typeof value === 'number' ? `n:${value}` : `s:${value}`;
|
|
14
|
+
}
|