@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
|
@@ -11,6 +11,7 @@ type CommonProps = {
|
|
|
11
11
|
};
|
|
12
12
|
export type CardRootProps<TAs extends RootAs = 'article'> = CommonProps & {
|
|
13
13
|
as?: TAs;
|
|
14
|
+
/** @deprecated Use Card.Link for interactive cards. onClick without Card.Link has no keyboard/screen-reader semantics. */
|
|
14
15
|
onClick?: React.MouseEventHandler<HTMLElement>;
|
|
15
16
|
children?: React.ReactNode;
|
|
16
17
|
};
|
package/components/Card/Card.js
CHANGED
|
@@ -1,29 +1,26 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import React, { forwardRef,
|
|
2
|
+
import React, { forwardRef, useRef, useEffect } from 'react';
|
|
3
3
|
import { cx } from '../../utils/cx.js';
|
|
4
|
+
import { isDev } from '../../utils/is-dev.js';
|
|
4
5
|
export const Card = forwardRef(function Card({ as = 'article', inline, elevated, interactive, disabled, className, style, onClick, children, ...rest }, ref) {
|
|
5
6
|
const Tag = as;
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
7
|
+
const hasCardLinkChild = React.Children.toArray(children).some((child) => {
|
|
8
|
+
if (!React.isValidElement(child))
|
|
9
|
+
return false;
|
|
10
|
+
const type = child.type;
|
|
11
|
+
return child.type === CardLink || type.displayName === 'Card.Link';
|
|
12
|
+
});
|
|
13
|
+
// Dev warning: suggest Card.Link for interactive cards
|
|
14
|
+
const warnedRef = useRef(false);
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
if (isDev() && !warnedRef.current && (interactive || onClick) && !hasCardLinkChild) {
|
|
17
|
+
warnedRef.current = true;
|
|
18
|
+
console.warn('[TUI Card] Interactive cards should use <Card.Link> for accessible click targets. ' +
|
|
19
|
+
'`interactive` and `onClick` provide visual hover styles but no keyboard/screen-reader semantics.');
|
|
15
20
|
}
|
|
16
|
-
}, [
|
|
21
|
+
}, [interactive, onClick, hasCardLinkChild]);
|
|
17
22
|
const classes = cx('tui-card', inline && 'is-layout-inline', elevated && 'is-style-elevated', interactive && 'has-interaction', className);
|
|
18
|
-
|
|
19
|
-
const buttonProps = isClickable
|
|
20
|
-
? {
|
|
21
|
-
role: 'button',
|
|
22
|
-
tabIndex: 0,
|
|
23
|
-
onKeyDown: handleKeyDown,
|
|
24
|
-
}
|
|
25
|
-
: {};
|
|
26
|
-
return (_jsx(Tag, { ref: ref, className: classes, style: style, "aria-disabled": disabled || undefined, onClick: disabled ? undefined : onClick, ...buttonProps, ...rest, children: _jsx("div", { className: "tui-card__inner", children: children }) }));
|
|
23
|
+
return (_jsx(Tag, { ref: ref, className: classes, style: style, "aria-disabled": disabled || undefined, onClick: disabled ? undefined : onClick, ...rest, children: _jsx("div", { className: "tui-card__inner", children: children }) }));
|
|
27
24
|
});
|
|
28
25
|
function CardHead({ className, children, ...rest }) {
|
|
29
26
|
return (_jsx("div", { className: cx('tui-card__head', className), ...rest, children: children }));
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export declare const Checkbox: import("react").ForwardRefExoticComponent<Omit<import("react").InputHTMLAttributes<HTMLInputElement>, "role" | "type" | "defaultChecked" | "onChange" | "checked"> & {
|
|
2
|
+
checked?: boolean;
|
|
3
|
+
defaultChecked?: boolean;
|
|
4
|
+
onCheckedChange?: (checked: boolean) => void;
|
|
5
|
+
indeterminate?: boolean;
|
|
6
|
+
label?: import("react").ReactNode;
|
|
7
|
+
disabled?: boolean;
|
|
8
|
+
className?: string;
|
|
9
|
+
} & import("react").RefAttributes<HTMLInputElement>>;
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { forwardRef, useEffect, useRef } from 'react';
|
|
3
|
+
import { cx } from '../../utils/cx.js';
|
|
4
|
+
import { composeRefs } from '../../utils/compose-refs.js';
|
|
5
|
+
import { useControllableState } from '../../utils/use-controllable-state.js';
|
|
6
|
+
import { isDev } from '../../utils/is-dev.js';
|
|
7
|
+
// =============================================================================
|
|
8
|
+
// Checkbox Component
|
|
9
|
+
// =============================================================================
|
|
10
|
+
//
|
|
11
|
+
// Native <input type="checkbox"> with accent-color, reusing tui-inline-choice CSS.
|
|
12
|
+
//
|
|
13
|
+
// Bare (no label): returns <input> directly for Field.Control cloneElement.
|
|
14
|
+
// With label: wraps in <label class="tui-inline-choice">.
|
|
15
|
+
//
|
|
16
|
+
// CSS token API (inherited from input styles):
|
|
17
|
+
// --tui-input-accent Accent color for checked state
|
|
18
|
+
//
|
|
19
|
+
// =============================================================================
|
|
20
|
+
// Props that should route to the <input>, not the wrapper label
|
|
21
|
+
const INPUT_PROPS = new Set([
|
|
22
|
+
'id',
|
|
23
|
+
'name',
|
|
24
|
+
'value',
|
|
25
|
+
'aria-describedby',
|
|
26
|
+
'aria-invalid',
|
|
27
|
+
'aria-required',
|
|
28
|
+
'aria-label',
|
|
29
|
+
'aria-labelledby',
|
|
30
|
+
'form',
|
|
31
|
+
'required',
|
|
32
|
+
'tabIndex',
|
|
33
|
+
'autoFocus',
|
|
34
|
+
'onFocus',
|
|
35
|
+
'onBlur',
|
|
36
|
+
]);
|
|
37
|
+
export const Checkbox = forwardRef(function Checkbox({ checked: controlledChecked, defaultChecked = false, onCheckedChange, indeterminate = false, label, disabled, className, ...rest }, externalRef) {
|
|
38
|
+
const internalRef = useRef(null);
|
|
39
|
+
const [checked, setChecked] = useControllableState({
|
|
40
|
+
value: controlledChecked,
|
|
41
|
+
defaultValue: defaultChecked,
|
|
42
|
+
onChange: onCheckedChange,
|
|
43
|
+
});
|
|
44
|
+
// Sync indeterminate DOM property (not available as HTML attribute)
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
if (internalRef.current) {
|
|
47
|
+
internalRef.current.indeterminate = indeterminate;
|
|
48
|
+
}
|
|
49
|
+
}, [indeterminate]);
|
|
50
|
+
// Dev-only: warn if bare checkbox has no accessible name (fire once)
|
|
51
|
+
const hasWarnedRef = useRef(false);
|
|
52
|
+
useEffect(() => {
|
|
53
|
+
if (hasWarnedRef.current)
|
|
54
|
+
return;
|
|
55
|
+
if (isDev() && !label) {
|
|
56
|
+
const ariaLabel = rest['aria-label'];
|
|
57
|
+
const ariaLabelledBy = rest['aria-labelledby'];
|
|
58
|
+
// Check explicit ARIA props, then fall back to native <label> association
|
|
59
|
+
// (e.g. Field.Label htmlFor wiring via Field.Control id injection)
|
|
60
|
+
const hasName = (typeof ariaLabel === 'string' && ariaLabel.trim() !== '') ||
|
|
61
|
+
(typeof ariaLabelledBy === 'string' && ariaLabelledBy.trim() !== '') ||
|
|
62
|
+
(internalRef.current?.labels && internalRef.current.labels.length > 0);
|
|
63
|
+
if (!hasName) {
|
|
64
|
+
console.warn('Checkbox: Missing accessible name. Provide label prop, aria-label, or aria-labelledby.');
|
|
65
|
+
hasWarnedRef.current = true;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
69
|
+
}, []);
|
|
70
|
+
const handleChange = () => {
|
|
71
|
+
// Clicking an indeterminate checkbox produces checked=true (native behaviour)
|
|
72
|
+
setChecked((prev) => !prev);
|
|
73
|
+
};
|
|
74
|
+
const isChecked = checked ?? false;
|
|
75
|
+
// Bare: no label — Field.Control can inject id/aria-* directly
|
|
76
|
+
if (!label) {
|
|
77
|
+
return (_jsx("input", { ref: composeRefs(internalRef, externalRef), type: "checkbox", checked: isChecked, disabled: disabled, className: className, "data-indeterminate": indeterminate || undefined, "aria-checked": indeterminate ? 'mixed' : undefined, onChange: handleChange, ...rest }));
|
|
78
|
+
}
|
|
79
|
+
// Split rest props: some go on input, some on wrapper
|
|
80
|
+
const inputProps = {};
|
|
81
|
+
const wrapperProps = {};
|
|
82
|
+
for (const [key, val] of Object.entries(rest)) {
|
|
83
|
+
if (INPUT_PROPS.has(key)) {
|
|
84
|
+
inputProps[key] = val;
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
wrapperProps[key] = val;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
// With label: wrap in tui-inline-choice
|
|
91
|
+
return (_jsxs("label", { className: cx('tui-inline-choice', disabled && 'is-disabled', className), ...wrapperProps, children: [_jsx("input", { ref: composeRefs(internalRef, externalRef), type: "checkbox", checked: isChecked, disabled: disabled, "data-indeterminate": indeterminate || undefined, "aria-checked": indeterminate ? 'mixed' : undefined, onChange: handleChange, ...inputProps }), _jsx("span", { children: label })] }));
|
|
92
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { Checkbox } from './Checkbox.js';
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { InputHTMLAttributes, ReactNode } from 'react';
|
|
2
|
+
export type CheckboxProps = Omit<InputHTMLAttributes<HTMLInputElement>, 'type' | 'checked' | 'defaultChecked' | 'onChange' | 'role'> & {
|
|
3
|
+
checked?: boolean;
|
|
4
|
+
defaultChecked?: boolean;
|
|
5
|
+
onCheckedChange?: (checked: boolean) => void;
|
|
6
|
+
indeterminate?: boolean;
|
|
7
|
+
label?: ReactNode;
|
|
8
|
+
disabled?: boolean;
|
|
9
|
+
className?: string;
|
|
10
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
|
+
import type { OptionValue } from '../../utils/value-key';
|
|
2
3
|
import type { SizeCompact, Theme as ThemeFull } from '../../types';
|
|
3
4
|
type Size = SizeCompact;
|
|
4
5
|
type Theme = ThemeFull;
|
|
@@ -19,6 +20,8 @@ export type ChipProps = {
|
|
|
19
20
|
leftIcon?: React.ReactNode;
|
|
20
21
|
rightIcon?: React.ReactNode;
|
|
21
22
|
onClick?: React.MouseEventHandler<HTMLElement>;
|
|
23
|
+
/** When inside ChipGroup, identifies this chip for selection tracking */
|
|
24
|
+
value?: OptionValue;
|
|
22
25
|
} & Omit<React.HTMLAttributes<HTMLSpanElement>, 'onClick'>;
|
|
23
|
-
export declare function Chip({ as, href, target, rel, children, size, theme, variant, selected, disabled, interactive, className, leftIcon, rightIcon, onClick, ...rest }: ChipProps): import("react/jsx-runtime").JSX.Element;
|
|
26
|
+
export declare function Chip({ as, href, target, rel, children, size, theme, variant, selected: selectedProp, disabled: disabledProp, interactive, className, leftIcon, rightIcon, onClick: onClickProp, value, ...rest }: ChipProps): import("react/jsx-runtime").JSX.Element;
|
|
24
27
|
export {};
|
package/components/Chip/Chip.js
CHANGED
|
@@ -1,11 +1,31 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import * as React from 'react';
|
|
3
|
-
import { useCallback } from 'react';
|
|
3
|
+
import { useCallback, useEffect } from 'react';
|
|
4
4
|
import { cx } from '../../utils/cx.js';
|
|
5
|
-
|
|
5
|
+
import { toKey } from '../../utils/value-key.js';
|
|
6
|
+
import { isDev } from '../../utils/is-dev.js';
|
|
7
|
+
import { useChipGroupContext } from '../ChipGroup/ChipGroupContext.js';
|
|
8
|
+
export function Chip({ as = 'span', href, target, rel, children, size = 'md', theme = 'secondary', variant = 'default', selected: selectedProp, disabled: disabledProp, interactive, className, leftIcon, rightIcon, onClick: onClickProp, value, ...rest }) {
|
|
9
|
+
const groupContext = useChipGroupContext();
|
|
10
|
+
// Dev warning: inside ChipGroup without value
|
|
11
|
+
useEffect(() => {
|
|
12
|
+
if (isDev() && groupContext && value === undefined) {
|
|
13
|
+
console.warn('Chip: Inside a ChipGroup but missing `value` prop. This chip will not be managed by the group.');
|
|
14
|
+
}
|
|
15
|
+
}, [groupContext, value]);
|
|
16
|
+
// Derive from group context when applicable
|
|
17
|
+
const isManaged = groupContext !== null && value !== undefined;
|
|
18
|
+
const selected = isManaged ? groupContext.selectedValues.has(toKey(value)) : selectedProp;
|
|
19
|
+
const disabled = (isManaged ? groupContext.disabled : false) || disabledProp;
|
|
20
|
+
const managedClick = useCallback(() => {
|
|
21
|
+
if (groupContext && value !== undefined) {
|
|
22
|
+
groupContext.onSelect(value);
|
|
23
|
+
}
|
|
24
|
+
}, [groupContext, value]);
|
|
25
|
+
const onClick = isManaged ? managedClick : onClickProp;
|
|
6
26
|
const Tag = (as === 'a' ? 'a' : as);
|
|
7
27
|
// Determine if this chip is clickable (needs button semantics)
|
|
8
|
-
const isClickable = (interactive || onClick) && !disabled;
|
|
28
|
+
const isClickable = (interactive || onClick || isManaged) && !disabled;
|
|
9
29
|
// Keyboard handler for interactive chips
|
|
10
30
|
const handleKeyDown = useCallback((e) => {
|
|
11
31
|
if (!isClickable || !onClick)
|
|
@@ -15,7 +35,7 @@ export function Chip({ as = 'span', href, target, rel, children, size = 'md', th
|
|
|
15
35
|
onClick(e);
|
|
16
36
|
}
|
|
17
37
|
}, [isClickable, onClick]);
|
|
18
|
-
const classes = cx('tui-chip', size && `is-size-${size}`, theme && `is-theme-${theme}`, variant !== 'default' && `is-style-${variant}`, selected && 'is-selected', interactive && 'is-interactive', className);
|
|
38
|
+
const classes = cx('tui-chip', size && `is-size-${size}`, theme && `is-theme-${theme}`, variant !== 'default' && `is-style-${variant}`, selected && 'is-selected', (interactive || isManaged) && 'is-interactive', className);
|
|
19
39
|
const anchorProps = as === 'a'
|
|
20
40
|
? {
|
|
21
41
|
href: disabled ? undefined : href ?? '#',
|
|
@@ -25,12 +45,17 @@ export function Chip({ as = 'span', href, target, rel, children, size = 'md', th
|
|
|
25
45
|
tabIndex: disabled ? -1 : undefined,
|
|
26
46
|
}
|
|
27
47
|
: { 'aria-disabled': disabled || undefined };
|
|
28
|
-
// Button semantics for
|
|
29
|
-
|
|
48
|
+
// Button semantics for clickable chips.
|
|
49
|
+
// Non-anchor clickable chips always need role="button".
|
|
50
|
+
// Managed anchor chips also get role="button" — toggle semantics
|
|
51
|
+
// take priority over link semantics inside a ChipGroup.
|
|
52
|
+
const needsButtonRole = isClickable && (as !== 'a' || isManaged);
|
|
53
|
+
const buttonProps = needsButtonRole
|
|
30
54
|
? {
|
|
31
55
|
role: 'button',
|
|
32
|
-
tabIndex: 0,
|
|
56
|
+
tabIndex: as !== 'a' ? 0 : undefined, // anchors are natively focusable
|
|
33
57
|
onKeyDown: handleKeyDown,
|
|
58
|
+
'aria-pressed': isManaged ? (selected ?? false) : undefined,
|
|
34
59
|
}
|
|
35
60
|
: {};
|
|
36
61
|
return (_jsxs(Tag, { className: classes, ...anchorProps, ...buttonProps, onClick: disabled ? undefined : onClick, ...rest, children: [leftIcon && leftIcon, _jsx("span", { className: "tui-chip__text", children: children }), rightIcon && rightIcon] }));
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useCallback, useEffect, useMemo } from 'react';
|
|
3
|
+
import { cx } from '../../utils/cx.js';
|
|
4
|
+
import { useControllableState } from '../../utils/use-controllable-state.js';
|
|
5
|
+
import { toKey } from '../../utils/value-key.js';
|
|
6
|
+
import { ChipGroupContext } from './ChipGroupContext.js';
|
|
7
|
+
import { isDev } from '../../utils/is-dev.js';
|
|
8
|
+
// =============================================================================
|
|
9
|
+
// ChipGroup Component
|
|
10
|
+
// =============================================================================
|
|
11
|
+
//
|
|
12
|
+
// Selection group for Chip components. Provides context to managed Chips.
|
|
13
|
+
// Chips with a `value` prop derive selected/onClick from context.
|
|
14
|
+
//
|
|
15
|
+
// Single mode: one selection at a time (toggleable — clicking selected deselects)
|
|
16
|
+
// Multiple mode: toggle individual chips on/off
|
|
17
|
+
//
|
|
18
|
+
// =============================================================================
|
|
19
|
+
export function ChipGroup(props) {
|
|
20
|
+
const { multiple = false, disabled = false, density = 'sm', direction = 'inline', alignment, 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, className, children, } = props;
|
|
21
|
+
// --- Single mode ---
|
|
22
|
+
// isControlled override: 'value' in props detects explicit value={undefined}
|
|
23
|
+
// (deselection) vs prop not passed at all (uncontrolled).
|
|
24
|
+
const [singleValue, setSingleValue] = useControllableState({
|
|
25
|
+
value: !multiple ? props.value : undefined,
|
|
26
|
+
defaultValue: !multiple ? props.defaultValue : undefined,
|
|
27
|
+
onChange: !multiple ? props.onValueChange : undefined,
|
|
28
|
+
isControlled: !multiple && 'value' in props,
|
|
29
|
+
});
|
|
30
|
+
// --- Multiple mode ---
|
|
31
|
+
const [multiValue, setMultiValue] = useControllableState({
|
|
32
|
+
value: multiple ? props.value : undefined,
|
|
33
|
+
defaultValue: multiple ? (props.defaultValue ?? []) : undefined,
|
|
34
|
+
onChange: multiple ? props.onValueChange : undefined,
|
|
35
|
+
isControlled: multiple && 'value' in props,
|
|
36
|
+
});
|
|
37
|
+
// Build Set of toKey'd values for efficient lookup in Chip
|
|
38
|
+
const selectedValues = useMemo(() => {
|
|
39
|
+
if (multiple) {
|
|
40
|
+
return new Set((multiValue ?? []).map(toKey));
|
|
41
|
+
}
|
|
42
|
+
return singleValue !== undefined ? new Set([toKey(singleValue)]) : new Set();
|
|
43
|
+
}, [multiple, singleValue, multiValue]);
|
|
44
|
+
const onSelect = useCallback((value) => {
|
|
45
|
+
if (multiple) {
|
|
46
|
+
setMultiValue((prev) => {
|
|
47
|
+
const current = prev ?? [];
|
|
48
|
+
const key = toKey(value);
|
|
49
|
+
return current.some((v) => toKey(v) === key)
|
|
50
|
+
? current.filter((v) => toKey(v) !== key)
|
|
51
|
+
: [...current, value];
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
// Toggle: clicking selected chip deselects
|
|
56
|
+
setSingleValue((prev) => prev !== undefined && toKey(prev) === toKey(value) ? undefined : value);
|
|
57
|
+
}
|
|
58
|
+
}, [multiple, setSingleValue, setMultiValue]);
|
|
59
|
+
const contextValue = useMemo(() => ({ selectedValues, multiple, disabled, onSelect }), [selectedValues, multiple, disabled, onSelect]);
|
|
60
|
+
// Dev-only: warn if group has no accessible name
|
|
61
|
+
useEffect(() => {
|
|
62
|
+
if (isDev() && !ariaLabel && !ariaLabelledBy) {
|
|
63
|
+
console.warn('ChipGroup: Missing accessible name. Provide aria-label or aria-labelledby.');
|
|
64
|
+
}
|
|
65
|
+
}, [ariaLabel, ariaLabelledBy]);
|
|
66
|
+
return (_jsx(ChipGroupContext.Provider, { value: contextValue, children: _jsx("div", { role: "group", "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, className: cx('tui-chip-group', `is-density-${density}`, direction === 'stack' && 'is-direction-stack', alignment && `is-align-${alignment}`, className), children: children }) }));
|
|
67
|
+
}
|
|
68
|
+
ChipGroup.displayName = 'ChipGroup';
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import type { OptionValue } from '../../utils/value-key';
|
|
3
|
+
type ChipGroupBaseProps = {
|
|
4
|
+
disabled?: boolean;
|
|
5
|
+
/** Gap between chips. Default 'sm'. */
|
|
6
|
+
density?: 'xs' | 'sm' | 'md';
|
|
7
|
+
/** Layout direction. Default 'inline' (row wrap). */
|
|
8
|
+
direction?: 'inline' | 'stack';
|
|
9
|
+
/** Alignment along main axis. */
|
|
10
|
+
alignment?: 'start' | 'center' | 'end';
|
|
11
|
+
'aria-label'?: string;
|
|
12
|
+
'aria-labelledby'?: string;
|
|
13
|
+
className?: string;
|
|
14
|
+
children: ReactNode;
|
|
15
|
+
};
|
|
16
|
+
export type ChipGroupSingleProps = ChipGroupBaseProps & {
|
|
17
|
+
multiple?: false;
|
|
18
|
+
value?: OptionValue;
|
|
19
|
+
defaultValue?: OptionValue;
|
|
20
|
+
onValueChange?: (value: OptionValue | undefined) => void;
|
|
21
|
+
};
|
|
22
|
+
export type ChipGroupMultipleProps = ChipGroupBaseProps & {
|
|
23
|
+
multiple: true;
|
|
24
|
+
value?: OptionValue[];
|
|
25
|
+
defaultValue?: OptionValue[];
|
|
26
|
+
onValueChange?: (value: OptionValue[]) => void;
|
|
27
|
+
};
|
|
28
|
+
export type ChipGroupProps = ChipGroupSingleProps | ChipGroupMultipleProps;
|
|
29
|
+
export type ChipGroupContextValue = {
|
|
30
|
+
/** Set of toKey'd values for efficient lookup */
|
|
31
|
+
selectedValues: Set<string>;
|
|
32
|
+
multiple: boolean;
|
|
33
|
+
disabled: boolean;
|
|
34
|
+
onSelect: (value: OptionValue) => void;
|
|
35
|
+
};
|
|
36
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -4,6 +4,8 @@ type Size = SizeCompact;
|
|
|
4
4
|
type Theme = ThemeFull;
|
|
5
5
|
type Variant = 'default' | 'outline' | 'ghost' | 'solid' | 'flush';
|
|
6
6
|
export type ChipOption = {
|
|
7
|
+
/** Stable identifier used as React key. Falls back to href, then label string. */
|
|
8
|
+
id?: string;
|
|
7
9
|
label: React.ReactNode;
|
|
8
10
|
as?: 'a' | 'span' | 'div';
|
|
9
11
|
href?: string;
|
|
@@ -17,5 +17,5 @@ export function Chips({ name, options, density = 'sm', direction = 'inline', ali
|
|
|
17
17
|
alignItems: !isInline && alignment ? alignment : undefined,
|
|
18
18
|
marginBlockEnd: 'var(--tui-spacing-sm)',
|
|
19
19
|
};
|
|
20
|
-
return (_jsx("div", { role: role, className: className, "data-chips-name": name, style: style, children: options.map((o) => (_jsx(Chip, { as: o.as ?? (o.href ? 'a' : 'span'), href: o.href, target: o.target, rel: o.rel, size: o.size, theme: o.theme, variant: o.variant, disabled: o.disabled, className: o.className, leftIcon: o.leftIcon, rightIcon: o.rightIcon, children: o.label }))) }));
|
|
20
|
+
return (_jsx("div", { role: role, className: className, "data-chips-name": name, style: style, children: options.map((o, index) => (_jsx(Chip, { as: o.as ?? (o.href ? 'a' : 'span'), href: o.href, target: o.target, rel: o.rel, size: o.size, theme: o.theme, variant: o.variant, disabled: o.disabled, className: o.className, leftIcon: o.leftIcon, rightIcon: o.rightIcon, children: o.label }, o.id ?? o.href ?? (typeof o.label === 'string' ? o.label : `chip-${index}`)))) }));
|
|
21
21
|
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { ComboboxProps, ComboboxContentProps, ComboboxOptionProps, ComboboxGroupProps, ComboboxLabelProps } from './types';
|
|
2
|
+
declare function ComboboxRoot({ value: controlledValue, defaultValue, onValueChange, inputValue: controlledInputValue, onInputChange, open: controlledOpen, defaultOpen, onOpenChange, disabled, placeholder, size, openOnFocus, filterMode, onQueryChange, 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, inputClassName, children, }: ComboboxProps): import("react/jsx-runtime").JSX.Element;
|
|
3
|
+
declare namespace ComboboxRoot {
|
|
4
|
+
var displayName: string;
|
|
5
|
+
}
|
|
6
|
+
declare function ComboboxContentComponent({ className, children }: ComboboxContentProps): import("react/jsx-runtime").JSX.Element;
|
|
7
|
+
declare namespace ComboboxContentComponent {
|
|
8
|
+
var displayName: string;
|
|
9
|
+
}
|
|
10
|
+
declare function ComboboxOptionComponent({ value: optionValue, disabled, textValue: explicitTextValue, className, children, }: ComboboxOptionProps): import("react/jsx-runtime").JSX.Element;
|
|
11
|
+
declare namespace ComboboxOptionComponent {
|
|
12
|
+
var displayName: string;
|
|
13
|
+
}
|
|
14
|
+
declare function ComboboxGroupComponent({ className, children }: ComboboxGroupProps): import("react/jsx-runtime").JSX.Element;
|
|
15
|
+
declare namespace ComboboxGroupComponent {
|
|
16
|
+
var displayName: string;
|
|
17
|
+
}
|
|
18
|
+
declare function ComboboxLabelComponent({ className, children }: ComboboxLabelProps): import("react/jsx-runtime").JSX.Element;
|
|
19
|
+
declare namespace ComboboxLabelComponent {
|
|
20
|
+
var displayName: string;
|
|
21
|
+
}
|
|
22
|
+
type ComboboxCompound = typeof ComboboxRoot & {
|
|
23
|
+
Content: typeof ComboboxContentComponent;
|
|
24
|
+
Option: typeof ComboboxOptionComponent;
|
|
25
|
+
Group: typeof ComboboxGroupComponent;
|
|
26
|
+
Label: typeof ComboboxLabelComponent;
|
|
27
|
+
};
|
|
28
|
+
export declare const Combobox: ComboboxCompound;
|
|
29
|
+
export declare const ComboboxContent: typeof ComboboxContentComponent;
|
|
30
|
+
export declare const ComboboxOption: typeof ComboboxOptionComponent;
|
|
31
|
+
export declare const ComboboxGroup: typeof ComboboxGroupComponent;
|
|
32
|
+
export declare const ComboboxLabel: typeof ComboboxLabelComponent;
|
|
33
|
+
export { useComboboxContext as useCombobox } from './ComboboxContext';
|