@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,204 @@
|
|
|
1
|
+
import type { 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
|
+
* Controls when filtering should be active.
|
|
7
|
+
* - 'always': Consumer filters whenever inputValue changes (default)
|
|
8
|
+
* - 'when-searching': Query becomes empty when not actively searching,
|
|
9
|
+
* allowing consumers to show all options when browsing a selection
|
|
10
|
+
*/
|
|
11
|
+
export type FilterMode = 'always' | 'when-searching';
|
|
12
|
+
export type ComboboxProps = {
|
|
13
|
+
/**
|
|
14
|
+
* Control size.
|
|
15
|
+
* @default 'md'
|
|
16
|
+
*/
|
|
17
|
+
size?: SizeStandard;
|
|
18
|
+
/**
|
|
19
|
+
* Controlled selected value.
|
|
20
|
+
*/
|
|
21
|
+
value?: OptionValue;
|
|
22
|
+
/**
|
|
23
|
+
* Default value for uncontrolled usage.
|
|
24
|
+
*/
|
|
25
|
+
defaultValue?: OptionValue;
|
|
26
|
+
/**
|
|
27
|
+
* Callback when selection changes.
|
|
28
|
+
* Called with the option's value when user selects, or undefined when cleared.
|
|
29
|
+
* Does NOT mutate inputValue — consumer should update inputValue in handler if desired.
|
|
30
|
+
*/
|
|
31
|
+
onValueChange?: (value: OptionValue | undefined) => void;
|
|
32
|
+
/**
|
|
33
|
+
* Controlled input value for search/filter.
|
|
34
|
+
*/
|
|
35
|
+
inputValue?: string;
|
|
36
|
+
/**
|
|
37
|
+
* Callback when input value changes (typing, clear).
|
|
38
|
+
*/
|
|
39
|
+
onInputChange?: (value: string) => void;
|
|
40
|
+
/**
|
|
41
|
+
* Controls filtering behavior.
|
|
42
|
+
* - 'always': Filter on inputValue at all times (default)
|
|
43
|
+
* - 'when-searching': Query is empty when not actively typing,
|
|
44
|
+
* so consumers can show all options when user is browsing a selection
|
|
45
|
+
* @default 'always'
|
|
46
|
+
*/
|
|
47
|
+
filterMode?: FilterMode;
|
|
48
|
+
/**
|
|
49
|
+
* Callback when the effective query changes.
|
|
50
|
+
* Query differs from inputValue when filterMode='when-searching':
|
|
51
|
+
* it becomes empty after selection to show all options.
|
|
52
|
+
*/
|
|
53
|
+
onQueryChange?: (query: string) => void;
|
|
54
|
+
/**
|
|
55
|
+
* Controlled open state.
|
|
56
|
+
*/
|
|
57
|
+
open?: boolean;
|
|
58
|
+
/**
|
|
59
|
+
* Default open state for uncontrolled usage.
|
|
60
|
+
* @default false
|
|
61
|
+
*/
|
|
62
|
+
defaultOpen?: boolean;
|
|
63
|
+
/**
|
|
64
|
+
* Callback when open state changes.
|
|
65
|
+
*/
|
|
66
|
+
onOpenChange?: (open: boolean) => void;
|
|
67
|
+
/**
|
|
68
|
+
* Whether the combobox is disabled.
|
|
69
|
+
* @default false
|
|
70
|
+
*/
|
|
71
|
+
disabled?: boolean;
|
|
72
|
+
/**
|
|
73
|
+
* Placeholder text for the input.
|
|
74
|
+
*/
|
|
75
|
+
placeholder?: string;
|
|
76
|
+
/**
|
|
77
|
+
* Whether to open the listbox when the input receives focus.
|
|
78
|
+
* @default true
|
|
79
|
+
*/
|
|
80
|
+
openOnFocus?: boolean;
|
|
81
|
+
/**
|
|
82
|
+
* Accessible name for the combobox.
|
|
83
|
+
*/
|
|
84
|
+
'aria-label'?: string;
|
|
85
|
+
/**
|
|
86
|
+
* ID of element that labels this combobox.
|
|
87
|
+
*/
|
|
88
|
+
'aria-labelledby'?: string;
|
|
89
|
+
/**
|
|
90
|
+
* Class name applied directly to the `<input>` element.
|
|
91
|
+
* Use for utilities like `tui-input-reset` that must target the input itself.
|
|
92
|
+
*/
|
|
93
|
+
inputClassName?: string;
|
|
94
|
+
children: React.ReactNode;
|
|
95
|
+
};
|
|
96
|
+
export type ComboboxContentProps = {
|
|
97
|
+
/**
|
|
98
|
+
* Additional CSS class names.
|
|
99
|
+
*/
|
|
100
|
+
className?: string;
|
|
101
|
+
children: React.ReactNode;
|
|
102
|
+
};
|
|
103
|
+
export type ComboboxOptionProps = {
|
|
104
|
+
/**
|
|
105
|
+
* The value for this option. Required and must be unique.
|
|
106
|
+
* Can be string or number.
|
|
107
|
+
*/
|
|
108
|
+
value: OptionValue;
|
|
109
|
+
/**
|
|
110
|
+
* Whether this option is disabled.
|
|
111
|
+
* @default false
|
|
112
|
+
*/
|
|
113
|
+
disabled?: boolean;
|
|
114
|
+
/**
|
|
115
|
+
* Text value used for display when selected.
|
|
116
|
+
* Required when children is not a string.
|
|
117
|
+
* If children is a string, defaults to that.
|
|
118
|
+
*/
|
|
119
|
+
textValue?: string;
|
|
120
|
+
/**
|
|
121
|
+
* Additional CSS class names.
|
|
122
|
+
*/
|
|
123
|
+
className?: string;
|
|
124
|
+
children: React.ReactNode;
|
|
125
|
+
};
|
|
126
|
+
export type ComboboxGroupProps = {
|
|
127
|
+
/**
|
|
128
|
+
* Additional CSS class names.
|
|
129
|
+
*/
|
|
130
|
+
className?: string;
|
|
131
|
+
children: React.ReactNode;
|
|
132
|
+
};
|
|
133
|
+
export type ComboboxLabelProps = {
|
|
134
|
+
/**
|
|
135
|
+
* Additional CSS class names.
|
|
136
|
+
*/
|
|
137
|
+
className?: string;
|
|
138
|
+
children: React.ReactNode;
|
|
139
|
+
};
|
|
140
|
+
/**
|
|
141
|
+
* Registration record for an option.
|
|
142
|
+
*/
|
|
143
|
+
export type RegisteredOption = {
|
|
144
|
+
value: OptionValue;
|
|
145
|
+
ref: RefObject<HTMLElement | null>;
|
|
146
|
+
disabled: boolean;
|
|
147
|
+
textValue: string;
|
|
148
|
+
};
|
|
149
|
+
/**
|
|
150
|
+
* Stable context: config, IDs, refs, and stable callbacks.
|
|
151
|
+
*/
|
|
152
|
+
export type ComboboxActionsContextValue = {
|
|
153
|
+
disabled: boolean;
|
|
154
|
+
placeholder: string;
|
|
155
|
+
openOnFocus: boolean;
|
|
156
|
+
inputId: string;
|
|
157
|
+
listboxId: string;
|
|
158
|
+
ariaLabel?: string;
|
|
159
|
+
ariaLabelledBy?: string;
|
|
160
|
+
setOpen: (open: boolean) => void;
|
|
161
|
+
setInputValue: (value: string) => void;
|
|
162
|
+
selectOption: (value: OptionValue) => void;
|
|
163
|
+
registerOption: (option: RegisteredOption) => void;
|
|
164
|
+
unregisterOption: (value: OptionValue) => void;
|
|
165
|
+
refs: {
|
|
166
|
+
reference: React.RefObject<ReferenceType | null>;
|
|
167
|
+
floating: React.RefObject<HTMLElement | null>;
|
|
168
|
+
setReference: (node: ReferenceType | null) => void;
|
|
169
|
+
setFloating: (node: HTMLElement | null) => void;
|
|
170
|
+
};
|
|
171
|
+
inputRef: RefObject<HTMLInputElement | null>;
|
|
172
|
+
listRef: MutableRefObject<(HTMLElement | null)[]>;
|
|
173
|
+
getReferenceProps: (userProps?: React.HTMLProps<Element>) => Record<string, unknown>;
|
|
174
|
+
getFloatingProps: (userProps?: React.HTMLProps<HTMLElement>) => Record<string, unknown>;
|
|
175
|
+
getItemProps: (userProps?: React.HTMLProps<HTMLElement>) => Record<string, unknown>;
|
|
176
|
+
};
|
|
177
|
+
/**
|
|
178
|
+
* State context: values that change during interaction.
|
|
179
|
+
*/
|
|
180
|
+
export type ComboboxStateContextValue = {
|
|
181
|
+
open: boolean;
|
|
182
|
+
value: OptionValue | undefined;
|
|
183
|
+
inputValue: string;
|
|
184
|
+
/**
|
|
185
|
+
* The effective query to filter on.
|
|
186
|
+
* When filterMode='when-searching', this is empty after selection
|
|
187
|
+
* (allowing all options to show). Otherwise equals inputValue.
|
|
188
|
+
*/
|
|
189
|
+
query: string;
|
|
190
|
+
activeIndex: number;
|
|
191
|
+
activeOptionId: string | undefined;
|
|
192
|
+
orderedOptions: RegisteredOption[];
|
|
193
|
+
floatingStyles: CSSProperties;
|
|
194
|
+
floatingContext: FloatingContext;
|
|
195
|
+
};
|
|
196
|
+
/**
|
|
197
|
+
* Combined context value.
|
|
198
|
+
*/
|
|
199
|
+
export type ComboboxContextValue = ComboboxActionsContextValue & ComboboxStateContextValue;
|
|
200
|
+
export type ComboboxContentContextValue = {
|
|
201
|
+
listRef: MutableRefObject<(HTMLElement | null)[]>;
|
|
202
|
+
activeIndex: number;
|
|
203
|
+
orderedOptions: RegisteredOption[];
|
|
204
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -204,7 +204,7 @@ function DropdownItemComponent({ onSelect, href, target = '_self', disabled = fa
|
|
|
204
204
|
});
|
|
205
205
|
}
|
|
206
206
|
}, [disabled, onSelect, keepOpen, setOpen, triggerRef]);
|
|
207
|
-
return (_jsx(Button, { variant: "ghost", href: disabled ? undefined : href, target: href ? target : undefined, disabled: disabled, onClick: handleClick, className: cx('tui-dropdown__item', className),
|
|
207
|
+
return (_jsx(Button, { ...props, variant: "ghost", href: disabled ? undefined : href, target: href ? target : undefined, disabled: disabled, onClick: handleClick, className: cx('tui-dropdown__item', className), children: children }));
|
|
208
208
|
}
|
|
209
209
|
DropdownItemComponent.displayName = 'Dropdown.Item';
|
|
210
210
|
export const Dropdown = DropdownRoot;
|
|
@@ -216,4 +216,5 @@ export const DropdownTrigger = DropdownTriggerComponent;
|
|
|
216
216
|
export const DropdownContent = DropdownContentComponent;
|
|
217
217
|
export const DropdownItem = DropdownItemComponent;
|
|
218
218
|
// Hook for advanced use cases (custom items that need to close the dropdown)
|
|
219
|
+
// eslint-disable-next-line react-refresh/only-export-components
|
|
219
220
|
export { useDropdownContext as useDropdown } from './DropdownContext.js';
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
export type FieldProps = {
|
|
3
|
+
/** Whether the field has an error state */
|
|
4
|
+
error?: boolean;
|
|
5
|
+
/** Whether the field is required */
|
|
6
|
+
required?: boolean;
|
|
7
|
+
/** Whether the field is disabled */
|
|
8
|
+
disabled?: boolean;
|
|
9
|
+
/** Inline layout: label and control on same row */
|
|
10
|
+
inline?: boolean;
|
|
11
|
+
/** Additional class name for the field wrapper */
|
|
12
|
+
className?: string;
|
|
13
|
+
children?: React.ReactNode;
|
|
14
|
+
};
|
|
15
|
+
type LabelProps = React.LabelHTMLAttributes<HTMLLabelElement> & {
|
|
16
|
+
/** Visually hide the label while keeping it accessible */
|
|
17
|
+
hidden?: boolean;
|
|
18
|
+
className?: string;
|
|
19
|
+
children?: React.ReactNode;
|
|
20
|
+
};
|
|
21
|
+
type ControlProps = {
|
|
22
|
+
children: React.ReactElement;
|
|
23
|
+
};
|
|
24
|
+
type HelperTextProps = React.HTMLAttributes<HTMLDivElement> & {
|
|
25
|
+
className?: string;
|
|
26
|
+
children?: React.ReactNode;
|
|
27
|
+
};
|
|
28
|
+
type ErrorProps = React.HTMLAttributes<HTMLDivElement> & {
|
|
29
|
+
className?: string;
|
|
30
|
+
children?: React.ReactNode;
|
|
31
|
+
};
|
|
32
|
+
type FieldCompound = React.ForwardRefExoticComponent<FieldProps & React.RefAttributes<HTMLDivElement>> & {
|
|
33
|
+
Label: React.FC<LabelProps>;
|
|
34
|
+
Control: React.FC<ControlProps>;
|
|
35
|
+
HelperText: React.FC<HelperTextProps>;
|
|
36
|
+
Error: React.FC<ErrorProps>;
|
|
37
|
+
};
|
|
38
|
+
export declare const Field: FieldCompound;
|
|
39
|
+
export {};
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import React, { forwardRef, useId, useMemo, cloneElement, isValidElement, Children, } from 'react';
|
|
3
|
+
import { cx } from '../../utils/cx.js';
|
|
4
|
+
import { FieldContext, useFieldContext } from './FieldContext.js';
|
|
5
|
+
export const Field = forwardRef(function Field({ error = false, required = false, disabled = false, inline = false, className, children, }, ref) {
|
|
6
|
+
const baseId = useId();
|
|
7
|
+
const controlId = `${baseId}-control`;
|
|
8
|
+
const helperTextId = `${baseId}-helper`;
|
|
9
|
+
const errorId = `${baseId}-error`;
|
|
10
|
+
const contextValue = useMemo(() => ({
|
|
11
|
+
controlId,
|
|
12
|
+
helperTextId,
|
|
13
|
+
errorId,
|
|
14
|
+
hasError: error,
|
|
15
|
+
required,
|
|
16
|
+
disabled,
|
|
17
|
+
}), [controlId, helperTextId, errorId, error, required, disabled]);
|
|
18
|
+
const classes = cx('tui-field', error && 'is-error', disabled && 'is-disabled', inline && 'is-layout-inline', className);
|
|
19
|
+
return (_jsx(FieldContext.Provider, { value: contextValue, children: _jsx("div", { ref: ref, className: classes, children: children }) }));
|
|
20
|
+
});
|
|
21
|
+
// =============================================================================
|
|
22
|
+
// Field.Label
|
|
23
|
+
// =============================================================================
|
|
24
|
+
function FieldLabel({ hidden = false, className, children, ...rest }) {
|
|
25
|
+
const { controlId, required } = useFieldContext();
|
|
26
|
+
const classes = cx('tui-field__label', hidden && 'tui-visually-hidden', className);
|
|
27
|
+
return (_jsxs("label", { htmlFor: controlId, className: classes, ...rest, children: [children, required && (_jsxs(_Fragment, { children: [_jsx("span", { className: "tui-field__required", "aria-hidden": "true", children: "*" }), _jsx("span", { className: "tui-visually-hidden", children: "required" })] }))] }));
|
|
28
|
+
}
|
|
29
|
+
FieldLabel.displayName = 'Field.Label';
|
|
30
|
+
// =============================================================================
|
|
31
|
+
// Field.Control
|
|
32
|
+
// =============================================================================
|
|
33
|
+
function FieldControl({ children }) {
|
|
34
|
+
const { controlId, helperTextId, errorId, hasError, required, disabled, } = useFieldContext();
|
|
35
|
+
const child = Children.only(children);
|
|
36
|
+
if (!isValidElement(child)) {
|
|
37
|
+
throw new Error('Field.Control expects a single React element as its child');
|
|
38
|
+
}
|
|
39
|
+
const childProps = child.props;
|
|
40
|
+
// Build aria-describedby, merging with any existing value from child
|
|
41
|
+
// Per ARIA spec, referencing non-existent IDs is valid (AT ignores them)
|
|
42
|
+
const describedByParts = [];
|
|
43
|
+
if (typeof childProps['aria-describedby'] === 'string') {
|
|
44
|
+
describedByParts.push(childProps['aria-describedby']);
|
|
45
|
+
}
|
|
46
|
+
describedByParts.push(helperTextId);
|
|
47
|
+
if (hasError) {
|
|
48
|
+
describedByParts.push(errorId);
|
|
49
|
+
}
|
|
50
|
+
const describedBy = describedByParts.join(' ');
|
|
51
|
+
// Clone child with a11y props
|
|
52
|
+
// Note: aria-invalid and aria-required must be string "true", not boolean
|
|
53
|
+
return cloneElement(child, {
|
|
54
|
+
id: controlId,
|
|
55
|
+
'aria-describedby': describedBy,
|
|
56
|
+
'aria-invalid': hasError ? 'true' : undefined,
|
|
57
|
+
'aria-required': required ? 'true' : undefined,
|
|
58
|
+
disabled: disabled || childProps.disabled,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
FieldControl.displayName = 'Field.Control';
|
|
62
|
+
// =============================================================================
|
|
63
|
+
// Field.HelperText
|
|
64
|
+
// =============================================================================
|
|
65
|
+
function FieldHelperText({ className, children, ...rest }) {
|
|
66
|
+
const { helperTextId } = useFieldContext();
|
|
67
|
+
if (!children)
|
|
68
|
+
return null;
|
|
69
|
+
const classes = cx('tui-field__helper', className);
|
|
70
|
+
return (_jsx("div", { id: helperTextId, className: classes, ...rest, children: children }));
|
|
71
|
+
}
|
|
72
|
+
FieldHelperText.displayName = 'Field.HelperText';
|
|
73
|
+
// =============================================================================
|
|
74
|
+
// Field.Error
|
|
75
|
+
// =============================================================================
|
|
76
|
+
function FieldError({ className, children, ...rest }) {
|
|
77
|
+
const { errorId, hasError } = useFieldContext();
|
|
78
|
+
// Only render if there's an error and content
|
|
79
|
+
if (!hasError || !children)
|
|
80
|
+
return null;
|
|
81
|
+
const classes = cx('tui-field__error', className);
|
|
82
|
+
// role="alert" implies aria-live="assertive" - no need to add aria-live
|
|
83
|
+
return (_jsx("div", { id: errorId, className: classes, role: "alert", ...rest, children: children }));
|
|
84
|
+
}
|
|
85
|
+
FieldError.displayName = 'Field.Error';
|
|
86
|
+
// =============================================================================
|
|
87
|
+
// Compound Export
|
|
88
|
+
// =============================================================================
|
|
89
|
+
Field.Label = FieldLabel;
|
|
90
|
+
Field.Control = FieldControl;
|
|
91
|
+
Field.HelperText = FieldHelperText;
|
|
92
|
+
Field.Error = FieldError;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export type FieldContextValue = {
|
|
2
|
+
/** ID for the form control element */
|
|
3
|
+
controlId: string;
|
|
4
|
+
/** ID for helper text (for aria-describedby) */
|
|
5
|
+
helperTextId: string;
|
|
6
|
+
/** ID for error message (for aria-describedby) */
|
|
7
|
+
errorId: string;
|
|
8
|
+
/** Whether the field has an error */
|
|
9
|
+
hasError: boolean;
|
|
10
|
+
/** Whether the field is required */
|
|
11
|
+
required: boolean;
|
|
12
|
+
/** Whether the field is disabled */
|
|
13
|
+
disabled: boolean;
|
|
14
|
+
};
|
|
15
|
+
export declare const FieldContext: import("react").Context<FieldContextValue | null>;
|
|
16
|
+
export declare function useFieldContext(): FieldContextValue;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { createContext, useContext } from 'react';
|
|
2
|
+
export const FieldContext = createContext(null);
|
|
3
|
+
export function useFieldContext() {
|
|
4
|
+
const context = useContext(FieldContext);
|
|
5
|
+
if (!context) {
|
|
6
|
+
throw new Error('Field compound components must be used within a Field. ' +
|
|
7
|
+
'Wrap your Field.Label, Field.Control, Field.HelperText, and Field.Error in a <Field> component.');
|
|
8
|
+
}
|
|
9
|
+
return context;
|
|
10
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { Field } from './Field.js';
|
|
@@ -2,13 +2,13 @@ import React from 'react';
|
|
|
2
2
|
import type { SizeStandard } from '../../types';
|
|
3
3
|
type Size = SizeStandard;
|
|
4
4
|
export type ModalProps = {
|
|
5
|
-
|
|
5
|
+
open: boolean;
|
|
6
6
|
onClose: () => void;
|
|
7
7
|
size?: Size;
|
|
8
8
|
stickyHead?: boolean;
|
|
9
9
|
stickyFoot?: boolean;
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
'aria-labelledby'?: string;
|
|
11
|
+
'aria-describedby'?: string;
|
|
12
12
|
initialFocusSelector?: string;
|
|
13
13
|
container?: Element | null;
|
|
14
14
|
showCloseButton?: boolean;
|
|
@@ -17,7 +17,7 @@ export type ModalProps = {
|
|
|
17
17
|
closeOnEscape?: boolean;
|
|
18
18
|
children?: React.ReactNode;
|
|
19
19
|
};
|
|
20
|
-
declare function ModalRoot({
|
|
20
|
+
declare function ModalRoot({ open, onClose, size, stickyHead, stickyFoot, 'aria-labelledby': labelledBy, 'aria-describedby': describedBy, initialFocusSelector, container, showCloseButton, closeLabel, closeOnBackdropClick, closeOnEscape, children, }: ModalProps): React.ReactPortal | null;
|
|
21
21
|
type ModalCloseProps = {
|
|
22
22
|
label?: string;
|
|
23
23
|
className?: string;
|
|
@@ -8,7 +8,7 @@ import { useFocusTrap, getInitialFocus } from '../../utils/focus-trap.js';
|
|
|
8
8
|
import { ModalContext, useModalContext } from './context.js';
|
|
9
9
|
import { IconButton } from '../IconButton/index.js';
|
|
10
10
|
const isBrowser = typeof document !== 'undefined';
|
|
11
|
-
function ModalRoot({
|
|
11
|
+
function ModalRoot({ open, onClose, size = 'md', stickyHead, stickyFoot, 'aria-labelledby': labelledBy, 'aria-describedby': describedBy, initialFocusSelector, container, showCloseButton, closeLabel = 'Close', closeOnBackdropClick = true, closeOnEscape = true, children, }) {
|
|
12
12
|
const dialogRef = useRef(null);
|
|
13
13
|
const restoreRef = useRef(null);
|
|
14
14
|
const [mount, setMount] = useState(null);
|
|
@@ -21,7 +21,7 @@ function ModalRoot({ isOpen, onClose, size = 'md', stickyHead, stickyFoot, label
|
|
|
21
21
|
// the captured element may not be meaningful — focus restore still works,
|
|
22
22
|
// portal scoping may fall back to global interface.
|
|
23
23
|
useLayoutEffect(() => {
|
|
24
|
-
if (!
|
|
24
|
+
if (!open || !isBrowser) {
|
|
25
25
|
return;
|
|
26
26
|
}
|
|
27
27
|
if (mount)
|
|
@@ -29,10 +29,10 @@ function ModalRoot({ isOpen, onClose, size = 'md', stickyHead, stickyFoot, label
|
|
|
29
29
|
const trigger = document.activeElement;
|
|
30
30
|
restoreRef.current = trigger;
|
|
31
31
|
setMount(container ?? getPortalRootFor(trigger));
|
|
32
|
-
}, [
|
|
32
|
+
}, [open, container, mount]);
|
|
33
33
|
// Reset state when modal closes, restore focus to trigger.
|
|
34
34
|
useEffect(() => {
|
|
35
|
-
if (
|
|
35
|
+
if (open)
|
|
36
36
|
return;
|
|
37
37
|
const el = restoreRef.current;
|
|
38
38
|
if (el && typeof el.focus === 'function') {
|
|
@@ -40,25 +40,27 @@ function ModalRoot({ isOpen, onClose, size = 'md', stickyHead, stickyFoot, label
|
|
|
40
40
|
}
|
|
41
41
|
restoreRef.current = null;
|
|
42
42
|
setMount(null);
|
|
43
|
-
}, [
|
|
43
|
+
}, [open]);
|
|
44
44
|
// Body scroll lock.
|
|
45
45
|
useEffect(() => {
|
|
46
|
-
if (!
|
|
46
|
+
if (!open)
|
|
47
47
|
return;
|
|
48
48
|
document.body.classList.add('tui-modal-open');
|
|
49
49
|
return () => {
|
|
50
50
|
document.body.classList.remove('tui-modal-open');
|
|
51
51
|
};
|
|
52
|
-
}, [
|
|
52
|
+
}, [open]);
|
|
53
53
|
// Focus trap (handles Tab cycling and ESC to close).
|
|
54
54
|
useFocusTrap(dialogRef, {
|
|
55
|
-
|
|
55
|
+
// Modal mount is two-phase (capture portal root, then render portal).
|
|
56
|
+
// Activate trap only once dialog is actually mounted.
|
|
57
|
+
isActive: open && !!mount,
|
|
56
58
|
onEscape: onClose,
|
|
57
59
|
escapeDeactivates: closeOnEscape,
|
|
58
60
|
});
|
|
59
61
|
// Make scrollable region focusable; set initial focus.
|
|
60
62
|
useEffect(() => {
|
|
61
|
-
if (!
|
|
63
|
+
if (!open)
|
|
62
64
|
return;
|
|
63
65
|
const dialog = dialogRef.current;
|
|
64
66
|
if (!dialog)
|
|
@@ -96,12 +98,12 @@ function ModalRoot({ isOpen, onClose, size = 'md', stickyHead, stickyFoot, label
|
|
|
96
98
|
`Ensure an element with id="${labelledBy}" exists inside the modal.`);
|
|
97
99
|
}
|
|
98
100
|
}
|
|
99
|
-
}, [
|
|
101
|
+
}, [open, mount, initialFocusSelector, labelledBy]);
|
|
100
102
|
// Memoize context value to prevent unnecessary re-renders
|
|
101
103
|
const contextValue = useMemo(() => ({ onClose }), [onClose]);
|
|
102
|
-
if (!
|
|
104
|
+
if (!open || !mount)
|
|
103
105
|
return null;
|
|
104
|
-
return createPortal(_jsx(ModalContext.Provider, { value: contextValue, children: _jsxs("div", { className: "tui-modal", "data-state": "open", children: [_jsx("div", { className: "tui-modal__backdrop", onClick: closeOnBackdropClick ? onClose : undefined }), _jsxs("div", { ref: dialogRef, role: "dialog", "aria-modal": "true", "aria-labelledby": labelledBy, "aria-describedby": describedBy, className: cx('tui-modal__dialog', `is-size-${size}`, stickyHead && 'is-sticky-head', stickyFoot && 'is-sticky-foot'), tabIndex: -1, children: [showCloseButton && (_jsx(IconButton, { icon: "system/close", label: closeLabel, variant: "ghost", size: "sm", onClick: onClose, className: "tui-modal__close", showTooltip: true })), children] })] }) }), mount);
|
|
106
|
+
return createPortal(_jsx(ModalContext.Provider, { value: contextValue, children: _jsxs("div", { className: "tui-modal", "data-state": "open", style: { pointerEvents: 'auto' }, children: [_jsx("div", { className: "tui-modal__backdrop", onClick: closeOnBackdropClick ? onClose : undefined }), _jsxs("div", { ref: dialogRef, role: "dialog", "aria-modal": "true", "aria-labelledby": labelledBy, "aria-describedby": describedBy, className: cx('tui-modal__dialog', `is-size-${size}`, stickyHead && 'is-sticky-head', stickyFoot && 'is-sticky-foot'), tabIndex: -1, children: [showCloseButton && (_jsx(IconButton, { icon: "system/close", label: closeLabel, variant: "ghost", size: "sm", onClick: onClose, className: "tui-modal__close", showTooltip: true })), children] })] }) }), mount);
|
|
105
107
|
}
|
|
106
108
|
function ModalClose({ label = 'Close', className }) {
|
|
107
109
|
const { onClose } = useModalContext();
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { type MultiSelectProps, type MultiSelectTriggerProps, type MultiSelectContentProps, type MultiSelectOptionProps, type MultiSelectGroupProps, type MultiSelectLabelProps } from './types';
|
|
2
|
+
declare function MultiSelectRoot({ id: triggerIdProp, value: controlledValue, defaultValue, onValueChange, open: controlledOpen, defaultOpen, onOpenChange, disabled, placeholder, size, display, maxChips, max, onMaxReached, 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, 'aria-describedby': ariaDescribedBy, children, }: MultiSelectProps): import("react/jsx-runtime").JSX.Element;
|
|
3
|
+
declare namespace MultiSelectRoot {
|
|
4
|
+
var displayName: string;
|
|
5
|
+
}
|
|
6
|
+
declare function MultiSelectTriggerComponent({ asChild, className, children, }: MultiSelectTriggerProps): import("react/jsx-runtime").JSX.Element;
|
|
7
|
+
declare namespace MultiSelectTriggerComponent {
|
|
8
|
+
var displayName: string;
|
|
9
|
+
}
|
|
10
|
+
declare function MultiSelectContentComponent({ className, children }: MultiSelectContentProps): import("react/jsx-runtime").JSX.Element;
|
|
11
|
+
declare namespace MultiSelectContentComponent {
|
|
12
|
+
var displayName: string;
|
|
13
|
+
}
|
|
14
|
+
declare function MultiSelectOptionComponent({ value: optionValue, disabled, textValue: explicitTextValue, className, children, }: MultiSelectOptionProps): import("react/jsx-runtime").JSX.Element;
|
|
15
|
+
declare namespace MultiSelectOptionComponent {
|
|
16
|
+
var displayName: string;
|
|
17
|
+
}
|
|
18
|
+
declare function MultiSelectGroupComponent({ className, children }: MultiSelectGroupProps): import("react/jsx-runtime").JSX.Element;
|
|
19
|
+
declare namespace MultiSelectGroupComponent {
|
|
20
|
+
var displayName: string;
|
|
21
|
+
}
|
|
22
|
+
declare function MultiSelectLabelComponent({ className, children }: MultiSelectLabelProps): import("react/jsx-runtime").JSX.Element;
|
|
23
|
+
declare namespace MultiSelectLabelComponent {
|
|
24
|
+
var displayName: string;
|
|
25
|
+
}
|
|
26
|
+
type MultiSelectCompound = typeof MultiSelectRoot & {
|
|
27
|
+
Trigger: typeof MultiSelectTriggerComponent;
|
|
28
|
+
Content: typeof MultiSelectContentComponent;
|
|
29
|
+
Option: typeof MultiSelectOptionComponent;
|
|
30
|
+
Group: typeof MultiSelectGroupComponent;
|
|
31
|
+
Label: typeof MultiSelectLabelComponent;
|
|
32
|
+
};
|
|
33
|
+
export declare const MultiSelect: MultiSelectCompound;
|
|
34
|
+
export declare const MultiSelectTrigger: typeof MultiSelectTriggerComponent;
|
|
35
|
+
export declare const MultiSelectContent: typeof MultiSelectContentComponent;
|
|
36
|
+
export declare const MultiSelectOption: typeof MultiSelectOptionComponent;
|
|
37
|
+
export declare const MultiSelectGroup: typeof MultiSelectGroupComponent;
|
|
38
|
+
export declare const MultiSelectLabel: typeof MultiSelectLabelComponent;
|
|
39
|
+
export { useMultiSelectContext as useMultiSelect } from './MultiSelectContext';
|