@tangible/ui 0.0.1 → 0.0.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/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/MoveHandle/MoveHandle.d.ts +2 -0
- package/components/MoveHandle/MoveHandle.js +84 -0
- package/components/MoveHandle/index.d.ts +2 -0
- package/components/MoveHandle/index.js +1 -0
- package/components/MoveHandle/types.d.ts +43 -0
- package/components/MoveHandle/types.js +1 -0
- 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 +22 -0
- package/components/index.js +11 -0
- package/icons/icons.svg +2 -0
- package/icons/manifest.json +16 -0
- package/icons/registry.d.ts +4 -0
- package/icons/registry.js +2 -0
- package/icons/system/index.d.ts +4 -0
- package/icons/system/index.js +22 -0
- package/package.json +1 -1
- package/styles/all.css +1 -1
- package/styles/all.expanded.css +1838 -136
- package/styles/all.expanded.unlayered.css +1838 -136
- package/styles/all.unlayered.css +1 -1
- package/styles/components/_bundle.scss +22 -0
- package/styles/components/input/index.scss +5 -20
- package/styles/index.scss +21 -0
- package/styles/system/_control.scss +49 -0
- package/styles/system/_tokens.scss +124 -2
- package/styles/system/index.scss +2 -1
- package/styles/utilities/_index.scss +50 -0
- package/tui-manifest.json +907 -112
- 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,216 @@
|
|
|
1
|
+
import type { Placement, FloatingContext, ReferenceType } from '@floating-ui/react';
|
|
2
|
+
import type { RefObject, CSSProperties, MutableRefObject } from 'react';
|
|
3
|
+
import type { SizeStandard } from '../../types/sizes';
|
|
4
|
+
import type { OptionValue } from '../../utils/value-key';
|
|
5
|
+
/**
|
|
6
|
+
* Placement sides for the dropdown content.
|
|
7
|
+
*/
|
|
8
|
+
export type Side = 'top' | 'bottom';
|
|
9
|
+
/**
|
|
10
|
+
* Alignment relative to trigger.
|
|
11
|
+
*/
|
|
12
|
+
export type Align = 'start' | 'center' | 'end';
|
|
13
|
+
export type SelectProps = {
|
|
14
|
+
/**
|
|
15
|
+
* Control size.
|
|
16
|
+
* @default 'md'
|
|
17
|
+
*/
|
|
18
|
+
size?: SizeStandard;
|
|
19
|
+
/**
|
|
20
|
+
* Controlled selected value.
|
|
21
|
+
*/
|
|
22
|
+
value?: OptionValue;
|
|
23
|
+
/**
|
|
24
|
+
* Default value for uncontrolled usage.
|
|
25
|
+
*/
|
|
26
|
+
defaultValue?: OptionValue;
|
|
27
|
+
/**
|
|
28
|
+
* Callback when selection changes.
|
|
29
|
+
* Called with undefined when selection is cleared.
|
|
30
|
+
*/
|
|
31
|
+
onValueChange?: (value: OptionValue | undefined) => void;
|
|
32
|
+
/**
|
|
33
|
+
* Controlled open state.
|
|
34
|
+
*/
|
|
35
|
+
open?: boolean;
|
|
36
|
+
/**
|
|
37
|
+
* Default open state for uncontrolled usage.
|
|
38
|
+
* @default false
|
|
39
|
+
*/
|
|
40
|
+
defaultOpen?: boolean;
|
|
41
|
+
/**
|
|
42
|
+
* Callback when open state changes.
|
|
43
|
+
*/
|
|
44
|
+
onOpenChange?: (open: boolean) => void;
|
|
45
|
+
/**
|
|
46
|
+
* Whether the entire select is disabled.
|
|
47
|
+
* @default false
|
|
48
|
+
*/
|
|
49
|
+
disabled?: boolean;
|
|
50
|
+
/**
|
|
51
|
+
* Placeholder text when no value is selected.
|
|
52
|
+
*/
|
|
53
|
+
placeholder?: string;
|
|
54
|
+
/**
|
|
55
|
+
* Preferred side of the trigger to place the listbox.
|
|
56
|
+
* @default 'bottom'
|
|
57
|
+
*/
|
|
58
|
+
side?: Side;
|
|
59
|
+
/**
|
|
60
|
+
* Alignment along the side.
|
|
61
|
+
* @default 'start'
|
|
62
|
+
*/
|
|
63
|
+
align?: Align;
|
|
64
|
+
/**
|
|
65
|
+
* Distance from the trigger in pixels.
|
|
66
|
+
* @default 4
|
|
67
|
+
*/
|
|
68
|
+
sideOffset?: number;
|
|
69
|
+
/**
|
|
70
|
+
* Whether the selection can be cleared with a clear button.
|
|
71
|
+
* @default false
|
|
72
|
+
*/
|
|
73
|
+
clearable?: boolean;
|
|
74
|
+
/**
|
|
75
|
+
* Accessible name for the select.
|
|
76
|
+
*/
|
|
77
|
+
'aria-label'?: string;
|
|
78
|
+
/**
|
|
79
|
+
* ID of element that labels this select.
|
|
80
|
+
*/
|
|
81
|
+
'aria-labelledby'?: string;
|
|
82
|
+
/**
|
|
83
|
+
* ID of element(s) that describe this select.
|
|
84
|
+
*/
|
|
85
|
+
'aria-describedby'?: string;
|
|
86
|
+
/**
|
|
87
|
+
* Optional trigger ID override.
|
|
88
|
+
* Useful for Field integration via htmlFor.
|
|
89
|
+
*/
|
|
90
|
+
id?: string;
|
|
91
|
+
children: React.ReactNode;
|
|
92
|
+
};
|
|
93
|
+
export type SelectTriggerProps = {
|
|
94
|
+
/**
|
|
95
|
+
* When true, merges props onto the child element instead of wrapping.
|
|
96
|
+
* Child must be a single React element that accepts ref and event handlers.
|
|
97
|
+
* @default false
|
|
98
|
+
*/
|
|
99
|
+
asChild?: boolean;
|
|
100
|
+
/**
|
|
101
|
+
* Additional CSS class names.
|
|
102
|
+
*/
|
|
103
|
+
className?: string;
|
|
104
|
+
children?: React.ReactNode;
|
|
105
|
+
};
|
|
106
|
+
export type SelectContentProps = {
|
|
107
|
+
/**
|
|
108
|
+
* Additional CSS class names.
|
|
109
|
+
*/
|
|
110
|
+
className?: string;
|
|
111
|
+
children: React.ReactNode;
|
|
112
|
+
};
|
|
113
|
+
export type SelectOptionProps = {
|
|
114
|
+
/**
|
|
115
|
+
* The value for this option. Required and must be unique.
|
|
116
|
+
* Can be string or number.
|
|
117
|
+
*/
|
|
118
|
+
value: OptionValue;
|
|
119
|
+
/**
|
|
120
|
+
* Whether this option is disabled.
|
|
121
|
+
* @default false
|
|
122
|
+
*/
|
|
123
|
+
disabled?: boolean;
|
|
124
|
+
/**
|
|
125
|
+
* Text value for typeahead. Required when children is not a string.
|
|
126
|
+
* If children is a string, defaults to that.
|
|
127
|
+
*/
|
|
128
|
+
textValue?: string;
|
|
129
|
+
/**
|
|
130
|
+
* Additional CSS class names.
|
|
131
|
+
*/
|
|
132
|
+
className?: string;
|
|
133
|
+
children: React.ReactNode;
|
|
134
|
+
};
|
|
135
|
+
export type SelectGroupProps = {
|
|
136
|
+
/**
|
|
137
|
+
* Additional CSS class names.
|
|
138
|
+
*/
|
|
139
|
+
className?: string;
|
|
140
|
+
children: React.ReactNode;
|
|
141
|
+
};
|
|
142
|
+
export type SelectLabelProps = {
|
|
143
|
+
/**
|
|
144
|
+
* Additional CSS class names.
|
|
145
|
+
*/
|
|
146
|
+
className?: string;
|
|
147
|
+
children: React.ReactNode;
|
|
148
|
+
};
|
|
149
|
+
/**
|
|
150
|
+
* Registration record for an option.
|
|
151
|
+
*/
|
|
152
|
+
export type RegisteredOption = {
|
|
153
|
+
value: OptionValue;
|
|
154
|
+
ref: RefObject<HTMLElement | null>;
|
|
155
|
+
disabled: boolean;
|
|
156
|
+
textValue: string;
|
|
157
|
+
};
|
|
158
|
+
/**
|
|
159
|
+
* Stable context: config, IDs, refs, and stable callbacks.
|
|
160
|
+
* Rarely changes — safe to subscribe without rerender concerns.
|
|
161
|
+
*/
|
|
162
|
+
export type SelectActionsContextValue = {
|
|
163
|
+
disabled: boolean;
|
|
164
|
+
placeholder: string;
|
|
165
|
+
clearable: boolean;
|
|
166
|
+
size: SizeStandard;
|
|
167
|
+
triggerId: string;
|
|
168
|
+
listboxId: string;
|
|
169
|
+
ariaLabel?: string;
|
|
170
|
+
ariaLabelledBy?: string;
|
|
171
|
+
ariaDescribedBy?: string;
|
|
172
|
+
setOpen: (open: boolean) => void;
|
|
173
|
+
setValue: (value: OptionValue | undefined, textValue?: string) => void;
|
|
174
|
+
registerOption: (option: RegisteredOption) => void;
|
|
175
|
+
unregisterOption: (value: OptionValue) => void;
|
|
176
|
+
handleSelect: (index: number | null) => void;
|
|
177
|
+
refs: {
|
|
178
|
+
reference: React.RefObject<ReferenceType | null>;
|
|
179
|
+
floating: React.RefObject<HTMLElement | null>;
|
|
180
|
+
setReference: (node: ReferenceType | null) => void;
|
|
181
|
+
setFloating: (node: HTMLElement | null) => void;
|
|
182
|
+
};
|
|
183
|
+
listRef: MutableRefObject<(HTMLElement | null)[]>;
|
|
184
|
+
getReferenceProps: (userProps?: React.HTMLProps<Element>) => Record<string, unknown>;
|
|
185
|
+
getFloatingProps: (userProps?: React.HTMLProps<HTMLElement>) => Record<string, unknown>;
|
|
186
|
+
getItemProps: (userProps?: React.HTMLProps<HTMLElement>) => Record<string, unknown>;
|
|
187
|
+
};
|
|
188
|
+
/**
|
|
189
|
+
* State context: values that change during interaction.
|
|
190
|
+
* Subscribe only when you need reactive updates.
|
|
191
|
+
*/
|
|
192
|
+
export type SelectStateContextValue = {
|
|
193
|
+
open: boolean;
|
|
194
|
+
value: OptionValue | undefined;
|
|
195
|
+
getSelectedTextValue: () => string | undefined;
|
|
196
|
+
activeIndex: number | null;
|
|
197
|
+
highlightedValue: OptionValue | null;
|
|
198
|
+
orderedOptions: RegisteredOption[];
|
|
199
|
+
floatingStyles: CSSProperties;
|
|
200
|
+
floatingContext: FloatingContext;
|
|
201
|
+
};
|
|
202
|
+
/**
|
|
203
|
+
* Combined context value (for backwards compat and convenience hooks).
|
|
204
|
+
* @deprecated Prefer using useSelectActions + useSelectState separately.
|
|
205
|
+
*/
|
|
206
|
+
export type SelectContextValue = SelectActionsContextValue & SelectStateContextValue;
|
|
207
|
+
export type SelectContentContextValue = {
|
|
208
|
+
listRef: MutableRefObject<(HTMLElement | null)[]>;
|
|
209
|
+
activeIndex: number | null;
|
|
210
|
+
handleSelect: (index: number | null) => void;
|
|
211
|
+
orderedOptions: RegisteredOption[];
|
|
212
|
+
};
|
|
213
|
+
/**
|
|
214
|
+
* Convert side + align to Floating UI placement.
|
|
215
|
+
*/
|
|
216
|
+
export declare function toPlacement(side: Side, align: Align): Placement;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// -----------------------------------------------------------------------------
|
|
2
|
+
// Utility
|
|
3
|
+
// -----------------------------------------------------------------------------
|
|
4
|
+
/**
|
|
5
|
+
* Convert side + align to Floating UI placement.
|
|
6
|
+
*/
|
|
7
|
+
export function toPlacement(side, align) {
|
|
8
|
+
if (align === 'center')
|
|
9
|
+
return side;
|
|
10
|
+
return `${side}-${align}`;
|
|
11
|
+
}
|
|
@@ -6,7 +6,7 @@ const isBrowser = typeof document !== 'undefined';
|
|
|
6
6
|
// Sidebar (Root)
|
|
7
7
|
// =============================================================================
|
|
8
8
|
function SidebarRoot(props) {
|
|
9
|
-
const { position = 'left', drawer = false,
|
|
9
|
+
const { position = 'left', drawer = false, open = false, onClose, 'aria-label': ariaLabel, children, className, } = props;
|
|
10
10
|
const sidebarRef = useRef(null);
|
|
11
11
|
const restoreRef = useRef(null);
|
|
12
12
|
const rootClassName = ['tui-sidebar', className].filter(Boolean).join(' ');
|
|
@@ -16,7 +16,7 @@ function SidebarRoot(props) {
|
|
|
16
16
|
useEffect(() => {
|
|
17
17
|
if (!drawer || !isBrowser)
|
|
18
18
|
return;
|
|
19
|
-
if (
|
|
19
|
+
if (open) {
|
|
20
20
|
// Capture focus target when opening
|
|
21
21
|
restoreRef.current = document.activeElement;
|
|
22
22
|
}
|
|
@@ -28,41 +28,41 @@ function SidebarRoot(props) {
|
|
|
28
28
|
}
|
|
29
29
|
restoreRef.current = null;
|
|
30
30
|
}
|
|
31
|
-
}, [drawer,
|
|
31
|
+
}, [drawer, open]);
|
|
32
32
|
// ---------------------------------------------------------------------------
|
|
33
33
|
// Drawer mode: body scroll lock
|
|
34
34
|
// ---------------------------------------------------------------------------
|
|
35
35
|
useEffect(() => {
|
|
36
|
-
if (!drawer || !
|
|
36
|
+
if (!drawer || !open)
|
|
37
37
|
return;
|
|
38
38
|
document.body.classList.add('tui-sidebar-drawer-open');
|
|
39
39
|
return () => {
|
|
40
40
|
document.body.classList.remove('tui-sidebar-drawer-open');
|
|
41
41
|
};
|
|
42
|
-
}, [drawer,
|
|
42
|
+
}, [drawer, open]);
|
|
43
43
|
// ---------------------------------------------------------------------------
|
|
44
44
|
// Drawer mode: focus trap (handles Tab cycling and ESC to close)
|
|
45
45
|
// ---------------------------------------------------------------------------
|
|
46
46
|
useFocusTrap(sidebarRef, {
|
|
47
|
-
isActive: drawer &&
|
|
47
|
+
isActive: drawer && open,
|
|
48
48
|
onEscape: onClose,
|
|
49
49
|
});
|
|
50
50
|
// ---------------------------------------------------------------------------
|
|
51
51
|
// Drawer mode: initial focus
|
|
52
52
|
// ---------------------------------------------------------------------------
|
|
53
53
|
useEffect(() => {
|
|
54
|
-
if (!drawer || !
|
|
54
|
+
if (!drawer || !open)
|
|
55
55
|
return;
|
|
56
56
|
const sidebar = sidebarRef.current;
|
|
57
57
|
if (!sidebar)
|
|
58
58
|
return;
|
|
59
59
|
const target = getInitialFocus(sidebar);
|
|
60
60
|
target.focus({ preventScroll: true });
|
|
61
|
-
}, [drawer,
|
|
61
|
+
}, [drawer, open]);
|
|
62
62
|
// ---------------------------------------------------------------------------
|
|
63
63
|
// Sidebar content (shared between static and drawer modes)
|
|
64
64
|
// ---------------------------------------------------------------------------
|
|
65
|
-
const sidebarContent = (_jsx("aside", { ref: sidebarRef, className: rootClassName, "data-position": position, "aria-label": ariaLabel, "aria-modal": drawer &&
|
|
65
|
+
const sidebarContent = (_jsx("aside", { ref: sidebarRef, className: rootClassName, "data-position": position, "aria-label": ariaLabel, "aria-modal": drawer && open ? 'true' : undefined, tabIndex: drawer ? -1 : undefined, children: children }));
|
|
66
66
|
// ---------------------------------------------------------------------------
|
|
67
67
|
// Static mode: render directly
|
|
68
68
|
// ---------------------------------------------------------------------------
|
|
@@ -72,7 +72,7 @@ function SidebarRoot(props) {
|
|
|
72
72
|
// ---------------------------------------------------------------------------
|
|
73
73
|
// Drawer mode: render inline with fixed positioning (no portal needed)
|
|
74
74
|
// ---------------------------------------------------------------------------
|
|
75
|
-
if (!
|
|
75
|
+
if (!open) {
|
|
76
76
|
return null;
|
|
77
77
|
}
|
|
78
78
|
const drawerClassName = [
|
|
@@ -95,9 +95,9 @@ function SidebarHeader(props) {
|
|
|
95
95
|
// Sidebar.Nav
|
|
96
96
|
// =============================================================================
|
|
97
97
|
function SidebarNav(props) {
|
|
98
|
-
const {
|
|
98
|
+
const { 'aria-label': navAriaLabel, 'aria-labelledby': navAriaLabelledBy, children, className } = props;
|
|
99
99
|
const navClassName = ['tui-sidebar__nav', className].filter(Boolean).join(' ');
|
|
100
|
-
return (_jsx("nav", { className: navClassName, "aria-label":
|
|
100
|
+
return (_jsx("nav", { className: navClassName, "aria-label": navAriaLabel, "aria-labelledby": navAriaLabelledBy, children: children }));
|
|
101
101
|
}
|
|
102
102
|
SidebarHeader.displayName = 'Sidebar.Header';
|
|
103
103
|
SidebarNav.displayName = 'Sidebar.Nav';
|
|
@@ -14,7 +14,7 @@ export type SidebarProps = {
|
|
|
14
14
|
/**
|
|
15
15
|
* Drawer open state. Required when `drawer={true}`.
|
|
16
16
|
*/
|
|
17
|
-
|
|
17
|
+
open?: boolean;
|
|
18
18
|
/**
|
|
19
19
|
* Callback to close the drawer.
|
|
20
20
|
* Called when ESC is pressed or backdrop is clicked.
|
|
@@ -22,9 +22,9 @@ export type SidebarProps = {
|
|
|
22
22
|
onClose?: () => void;
|
|
23
23
|
/**
|
|
24
24
|
* Accessible label for the aside landmark.
|
|
25
|
-
* Typically omit this if using `
|
|
25
|
+
* Typically omit this if using `aria-label` on `Sidebar.Nav` instead.
|
|
26
26
|
*/
|
|
27
|
-
|
|
27
|
+
'aria-label'?: string;
|
|
28
28
|
/**
|
|
29
29
|
* Sidebar content (Header, Nav, etc.).
|
|
30
30
|
*/
|
|
@@ -49,11 +49,11 @@ export type SidebarNavProps = {
|
|
|
49
49
|
* Accessible label for the nav landmark.
|
|
50
50
|
* Use this for the primary label since nav is the actionable region.
|
|
51
51
|
*/
|
|
52
|
-
|
|
52
|
+
'aria-label'?: string;
|
|
53
53
|
/**
|
|
54
54
|
* ID of a visible heading that labels the navigation.
|
|
55
55
|
*/
|
|
56
|
-
|
|
56
|
+
'aria-labelledby'?: string;
|
|
57
57
|
/**
|
|
58
58
|
* Nav content (typically Accordion with StepList).
|
|
59
59
|
*/
|
|
@@ -60,5 +60,5 @@ export function StepIndicator(props) {
|
|
|
60
60
|
]
|
|
61
61
|
.filter(Boolean)
|
|
62
62
|
.join(' ');
|
|
63
|
-
return (_jsx("div", { className: rootClassName, children: _jsxs(Progress, { mode: "circle", variant: variant, size: size, value: displayValue, showLabels: showValue || hasIcon, labelPosition: "inside",
|
|
63
|
+
return (_jsx("div", { className: rootClassName, children: _jsxs(Progress, { mode: "circle", variant: variant, size: size, value: displayValue, showLabels: showValue || hasIcon, labelPosition: "inside", "aria-label": ariaLabel, children: [hasIcon && iconName && (_jsx(Icon, { name: iconName })), showValue && status === 'in-progress' && (_jsxs("span", { className: "tui-step-indicator__value", children: [Math.round(displayValue), "%"] }))] }) }));
|
|
64
64
|
}
|
|
@@ -7,11 +7,11 @@ import { StepIndicator } from '../StepIndicator/index.js';
|
|
|
7
7
|
// StepList (Root)
|
|
8
8
|
// =============================================================================
|
|
9
9
|
function StepListRoot(props) {
|
|
10
|
-
const { ariaLabel, ariaLabelledBy, ariaCurrent = 'step', current, onSelect, children, className, } = props;
|
|
10
|
+
const { 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, ariaCurrent = 'step', current, onSelect, children, className, } = props;
|
|
11
11
|
// Dev warning for missing nav label
|
|
12
12
|
if (import.meta.env.DEV && !ariaLabel && !ariaLabelledBy) {
|
|
13
13
|
console.warn('StepList: Navigation landmark requires a label. ' +
|
|
14
|
-
'Provide either `
|
|
14
|
+
'Provide either `aria-label` or `aria-labelledby` prop for screen reader users.');
|
|
15
15
|
}
|
|
16
16
|
const rootClassName = cx('tui-steplist', className);
|
|
17
17
|
// Memoize context value to prevent unnecessary re-renders of all items
|
|
@@ -5,14 +5,14 @@ export type { StepStatus };
|
|
|
5
5
|
export type StepListProps = {
|
|
6
6
|
/**
|
|
7
7
|
* Accessible label for the navigation landmark.
|
|
8
|
-
* Use either `
|
|
8
|
+
* Use either `aria-label` or `aria-labelledby`, not both.
|
|
9
9
|
*/
|
|
10
|
-
|
|
10
|
+
'aria-label'?: string;
|
|
11
11
|
/**
|
|
12
12
|
* ID of a visible heading that labels the navigation.
|
|
13
|
-
* Use either `
|
|
13
|
+
* Use either `aria-label` or `aria-labelledby`, not both.
|
|
14
14
|
*/
|
|
15
|
-
|
|
15
|
+
'aria-labelledby'?: string;
|
|
16
16
|
/**
|
|
17
17
|
* What `aria-current` value to use for the current item.
|
|
18
18
|
* - `'step'`: For step-by-step progressions (default)
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export declare const Switch: import("react").ForwardRefExoticComponent<Omit<import("react").ButtonHTMLAttributes<HTMLButtonElement>, "role" | "aria-checked" | "onChange"> & {
|
|
2
|
+
checked?: boolean;
|
|
3
|
+
defaultChecked?: boolean;
|
|
4
|
+
onCheckedChange?: (checked: boolean) => void;
|
|
5
|
+
size?: import("..").SizeStandard;
|
|
6
|
+
label?: import("react").ReactNode;
|
|
7
|
+
disabled?: boolean;
|
|
8
|
+
className?: string;
|
|
9
|
+
} & import("react").RefAttributes<HTMLButtonElement>>;
|
|
@@ -0,0 +1,91 @@
|
|
|
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
|
+
// Switch Component
|
|
9
|
+
// =============================================================================
|
|
10
|
+
//
|
|
11
|
+
// <button role="switch"> with animated thumb. Uses new tui-switch SCSS.
|
|
12
|
+
// Existing tui-toggle CSS untouched (stays for CSS-only usage).
|
|
13
|
+
//
|
|
14
|
+
// Bare (no label): returns <button> directly for Field.Control cloneElement.
|
|
15
|
+
// With label: wraps in <label>, native label click focuses the button.
|
|
16
|
+
//
|
|
17
|
+
// CSS token API:
|
|
18
|
+
// --tui-switch-accent Accent color for on state
|
|
19
|
+
// --tui-switch-track-off Track color when off
|
|
20
|
+
//
|
|
21
|
+
// =============================================================================
|
|
22
|
+
// Props that should route to the <button>, not the wrapper
|
|
23
|
+
const BUTTON_PROPS = new Set([
|
|
24
|
+
'id',
|
|
25
|
+
'aria-describedby',
|
|
26
|
+
'aria-invalid',
|
|
27
|
+
'aria-required',
|
|
28
|
+
'aria-label',
|
|
29
|
+
'aria-labelledby',
|
|
30
|
+
'form',
|
|
31
|
+
'tabIndex',
|
|
32
|
+
'onFocus',
|
|
33
|
+
'onBlur',
|
|
34
|
+
]);
|
|
35
|
+
export const Switch = forwardRef(function Switch({ checked: controlledChecked, defaultChecked = false, onCheckedChange, size = 'md', label, disabled, className, ...rest }, externalRef) {
|
|
36
|
+
const internalRef = useRef(null);
|
|
37
|
+
const sizeClass = size !== 'md' ? `is-size-${size}` : undefined;
|
|
38
|
+
const [checked, setChecked] = useControllableState({
|
|
39
|
+
value: controlledChecked,
|
|
40
|
+
defaultValue: defaultChecked,
|
|
41
|
+
onChange: onCheckedChange,
|
|
42
|
+
});
|
|
43
|
+
// Dev-only: warn if bare switch has no accessible name (fire once)
|
|
44
|
+
const hasWarnedRef = useRef(false);
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
if (hasWarnedRef.current)
|
|
47
|
+
return;
|
|
48
|
+
if (isDev() && !label) {
|
|
49
|
+
const hasName = 'aria-label' in rest || 'aria-labelledby' in rest;
|
|
50
|
+
if (!hasName) {
|
|
51
|
+
console.warn('Switch: Missing accessible name. Provide label prop, aria-label, or aria-labelledby.');
|
|
52
|
+
hasWarnedRef.current = true;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
56
|
+
}, []);
|
|
57
|
+
// Extract onClick from rest so prop spreading can't override internal handler
|
|
58
|
+
const { onClick: onClickProp, ...restWithoutClick } = rest;
|
|
59
|
+
const handleClick = (e) => {
|
|
60
|
+
setChecked((prev) => !prev);
|
|
61
|
+
onClickProp?.(e);
|
|
62
|
+
};
|
|
63
|
+
const isChecked = checked ?? false;
|
|
64
|
+
// Bare: no label — Field.Control can inject id/aria-* directly
|
|
65
|
+
if (!label) {
|
|
66
|
+
return (_jsx("button", { ref: composeRefs(internalRef, externalRef), type: "button", role: "switch", "aria-checked": isChecked, disabled: disabled, className: cx('tui-switch__track', sizeClass, isChecked && 'is-checked', className), onClick: handleClick, ...restWithoutClick, children: _jsx("span", { className: "tui-switch__thumb" }) }));
|
|
67
|
+
}
|
|
68
|
+
// Split rest props: some go on button, some on wrapper
|
|
69
|
+
const buttonProps = {};
|
|
70
|
+
const wrapperProps = {};
|
|
71
|
+
for (const [key, val] of Object.entries(restWithoutClick)) {
|
|
72
|
+
if (BUTTON_PROPS.has(key)) {
|
|
73
|
+
buttonProps[key] = val;
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
wrapperProps[key] = val;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
// With label: <label> wrapper provides click-anywhere-to-toggle natively.
|
|
80
|
+
// We intercept the label click to toggle state and prevent the browser's
|
|
81
|
+
// default label→button activation (which would double-toggle).
|
|
82
|
+
const handleLabelClick = (e) => {
|
|
83
|
+
// Prevent native label click from also activating the button
|
|
84
|
+
e.preventDefault();
|
|
85
|
+
if (disabled)
|
|
86
|
+
return;
|
|
87
|
+
internalRef.current?.focus();
|
|
88
|
+
setChecked((prev) => !prev);
|
|
89
|
+
};
|
|
90
|
+
return (_jsxs("label", { className: cx('tui-switch', sizeClass, disabled && 'is-disabled', className), onClick: handleLabelClick, ...wrapperProps, children: [_jsx("button", { ref: composeRefs(internalRef, externalRef), type: "button", role: "switch", "aria-checked": isChecked, disabled: disabled, className: cx('tui-switch__track', isChecked && 'is-checked'), ...buttonProps, children: _jsx("span", { className: "tui-switch__thumb" }) }), _jsx("span", { className: "tui-switch__label", children: label })] }));
|
|
91
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { Switch } from './Switch.js';
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ButtonHTMLAttributes, ReactNode } from 'react';
|
|
2
|
+
import type { SizeStandard } from '../../types/sizes';
|
|
3
|
+
export type SwitchProps = Omit<ButtonHTMLAttributes<HTMLButtonElement>, 'role' | 'onChange' | 'aria-checked'> & {
|
|
4
|
+
checked?: boolean;
|
|
5
|
+
defaultChecked?: boolean;
|
|
6
|
+
onCheckedChange?: (checked: boolean) => void;
|
|
7
|
+
size?: SizeStandard;
|
|
8
|
+
label?: ReactNode;
|
|
9
|
+
disabled?: boolean;
|
|
10
|
+
className?: string;
|
|
11
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export declare const TextInput: import("react").ForwardRefExoticComponent<Omit<import("react").InputHTMLAttributes<HTMLInputElement>, "size" | "type" | "prefix"> & {
|
|
2
|
+
type?: import("./types").TextInputType;
|
|
3
|
+
size?: import("..").SizeStandard;
|
|
4
|
+
prefix?: import("react").ReactNode;
|
|
5
|
+
suffix?: import("react").ReactNode;
|
|
6
|
+
className?: string;
|
|
7
|
+
inputClassName?: string;
|
|
8
|
+
} & import("react").RefAttributes<HTMLInputElement>>;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { forwardRef } from 'react';
|
|
3
|
+
import { cx } from '../../utils/cx.js';
|
|
4
|
+
// =============================================================================
|
|
5
|
+
// TextInput Component
|
|
6
|
+
// =============================================================================
|
|
7
|
+
//
|
|
8
|
+
// A controlled text input with optional prefix/suffix slots.
|
|
9
|
+
// Works with Field.Control for label, helper text, and error wiring.
|
|
10
|
+
//
|
|
11
|
+
// CSS token API (inherited from input styles):
|
|
12
|
+
// --tui-input-bg Background color
|
|
13
|
+
// --tui-input-fg Foreground/text color
|
|
14
|
+
// --tui-input-border Border color
|
|
15
|
+
// --tui-input-border-focus Focus state border color
|
|
16
|
+
// --tui-input-border-invalid Invalid state border color
|
|
17
|
+
// --tui-input-radius Border radius
|
|
18
|
+
//
|
|
19
|
+
// =============================================================================
|
|
20
|
+
export const TextInput = forwardRef(function TextInput({ type = 'text', size = 'md', prefix, suffix, className, inputClassName, disabled, ...rest }, ref) {
|
|
21
|
+
const sizeClass = size !== 'md' ? `is-size-${size}` : undefined;
|
|
22
|
+
// Always wrap in tui-input-group so className consistently targets the root.
|
|
23
|
+
// ARIA/form props from Field.Control reach the <input> via ...rest.
|
|
24
|
+
return (_jsxs("div", { className: cx('tui-input-group', sizeClass, disabled && 'is-disabled', className), children: [prefix && _jsx("span", { className: "tui-input-group__prefix", children: prefix }), _jsx("input", { ref: ref, type: type, disabled: disabled, className: cx('tui-input', inputClassName), ...rest }), suffix && _jsx("span", { className: "tui-input-group__suffix", children: suffix })] }));
|
|
25
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { TextInput } from './TextInput.js';
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { ReactNode, InputHTMLAttributes } from 'react';
|
|
2
|
+
import type { SizeStandard } from '../../types/sizes';
|
|
3
|
+
export type TextInputType = 'text' | 'email' | 'password' | 'url' | 'tel' | 'search' | 'number';
|
|
4
|
+
export type TextInputProps = Omit<InputHTMLAttributes<HTMLInputElement>, 'type' | 'prefix' | 'size'> & {
|
|
5
|
+
/**
|
|
6
|
+
* Input type.
|
|
7
|
+
* @default 'text'
|
|
8
|
+
*/
|
|
9
|
+
type?: TextInputType;
|
|
10
|
+
/**
|
|
11
|
+
* Control size.
|
|
12
|
+
* @default 'md'
|
|
13
|
+
*/
|
|
14
|
+
size?: SizeStandard;
|
|
15
|
+
/**
|
|
16
|
+
* Content to render before the input (icon, text).
|
|
17
|
+
*/
|
|
18
|
+
prefix?: ReactNode;
|
|
19
|
+
/**
|
|
20
|
+
* Content to render after the input (icon, button).
|
|
21
|
+
*/
|
|
22
|
+
suffix?: ReactNode;
|
|
23
|
+
/**
|
|
24
|
+
* Additional class name applied to the root wrapper element.
|
|
25
|
+
*/
|
|
26
|
+
className?: string;
|
|
27
|
+
/**
|
|
28
|
+
* Class name applied directly to the `<input>` element.
|
|
29
|
+
* Use for utilities like `tui-input-reset` that must target the input itself.
|
|
30
|
+
*/
|
|
31
|
+
inputClassName?: string;
|
|
32
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export declare const Textarea: import("react").ForwardRefExoticComponent<Omit<import("react").TextareaHTMLAttributes<HTMLTextAreaElement>, "size"> & {
|
|
2
|
+
size?: import("..").SizeStandard;
|
|
3
|
+
resize?: import("./types").TextareaResize;
|
|
4
|
+
autoResize?: boolean;
|
|
5
|
+
className?: string;
|
|
6
|
+
} & import("react").RefAttributes<HTMLTextAreaElement>>;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { forwardRef, useCallback, useEffect, useRef } from 'react';
|
|
3
|
+
import { cx } from '../../utils/cx.js';
|
|
4
|
+
import { composeRefs } from '../../utils/compose-refs.js';
|
|
5
|
+
// =============================================================================
|
|
6
|
+
// Textarea Component
|
|
7
|
+
// =============================================================================
|
|
8
|
+
//
|
|
9
|
+
// A controlled textarea with optional auto-resize.
|
|
10
|
+
// Works with Field.Control for label, helper text, and error wiring.
|
|
11
|
+
//
|
|
12
|
+
// CSS token API (inherited from input styles):
|
|
13
|
+
// --tui-input-bg Background color
|
|
14
|
+
// --tui-input-fg Foreground/text color
|
|
15
|
+
// --tui-input-border Border color
|
|
16
|
+
// --tui-input-border-focus Focus state border color
|
|
17
|
+
// --tui-input-border-invalid Invalid state border color
|
|
18
|
+
// --tui-input-radius Border radius
|
|
19
|
+
//
|
|
20
|
+
// =============================================================================
|
|
21
|
+
export const Textarea = forwardRef(function Textarea({ size = 'md', resize = 'vertical', autoResize = false, className, onInput, ...rest }, externalRef) {
|
|
22
|
+
const internalRef = useRef(null);
|
|
23
|
+
const sizeClass = size !== 'md' ? `is-size-${size}` : undefined;
|
|
24
|
+
// Auto-resize: adjust height to fit content
|
|
25
|
+
const adjustHeight = useCallback(() => {
|
|
26
|
+
const el = internalRef.current;
|
|
27
|
+
if (!el)
|
|
28
|
+
return;
|
|
29
|
+
// Reset to auto so scrollHeight reflects actual content height
|
|
30
|
+
el.style.height = 'auto';
|
|
31
|
+
el.style.height = `${el.scrollHeight}px`;
|
|
32
|
+
}, []);
|
|
33
|
+
// Size on mount and when value changes externally (controlled).
|
|
34
|
+
// Track rest.value so we only re-measure when content actually changes,
|
|
35
|
+
// not on every render.
|
|
36
|
+
const value = rest.value;
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
if (autoResize) {
|
|
39
|
+
adjustHeight();
|
|
40
|
+
}
|
|
41
|
+
}, [autoResize, adjustHeight, value]);
|
|
42
|
+
const handleInput = useCallback((e) => {
|
|
43
|
+
if (autoResize) {
|
|
44
|
+
adjustHeight();
|
|
45
|
+
}
|
|
46
|
+
onInput?.(e);
|
|
47
|
+
}, [autoResize, adjustHeight, onInput]);
|
|
48
|
+
return (_jsx("textarea", { ref: composeRefs(internalRef, externalRef), className: cx('tui-textarea', sizeClass, autoResize && 'is-autoresize', className), style: !autoResize ? { resize } : undefined, onInput: handleInput, ...rest }));
|
|
49
|
+
});
|