@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.
Files changed (135) hide show
  1. package/components/Card/Card.d.ts +1 -0
  2. package/components/Card/Card.js +17 -20
  3. package/components/Checkbox/Checkbox.d.ts +9 -0
  4. package/components/Checkbox/Checkbox.js +92 -0
  5. package/components/Checkbox/index.d.ts +2 -0
  6. package/components/Checkbox/index.js +1 -0
  7. package/components/Checkbox/types.d.ts +10 -0
  8. package/components/Checkbox/types.js +1 -0
  9. package/components/Chip/Chip.d.ts +4 -1
  10. package/components/Chip/Chip.js +32 -7
  11. package/components/ChipGroup/ChipGroup.d.ts +5 -0
  12. package/components/ChipGroup/ChipGroup.js +68 -0
  13. package/components/ChipGroup/ChipGroupContext.d.ts +3 -0
  14. package/components/ChipGroup/ChipGroupContext.js +5 -0
  15. package/components/ChipGroup/index.d.ts +3 -0
  16. package/components/ChipGroup/index.js +2 -0
  17. package/components/ChipGroup/types.d.ts +36 -0
  18. package/components/ChipGroup/types.js +1 -0
  19. package/components/Chips/Chips.d.ts +2 -0
  20. package/components/Chips/Chips.js +1 -1
  21. package/components/Combobox/Combobox.d.ts +33 -0
  22. package/components/Combobox/Combobox.js +466 -0
  23. package/components/Combobox/ComboboxContext.d.ts +8 -0
  24. package/components/Combobox/ComboboxContext.js +36 -0
  25. package/components/Combobox/index.d.ts +2 -0
  26. package/components/Combobox/index.js +1 -0
  27. package/components/Combobox/types.d.ts +204 -0
  28. package/components/Combobox/types.js +1 -0
  29. package/components/Dropdown/Dropdown.js +2 -1
  30. package/components/Field/Field.d.ts +39 -0
  31. package/components/Field/Field.js +92 -0
  32. package/components/Field/FieldContext.d.ts +16 -0
  33. package/components/Field/FieldContext.js +10 -0
  34. package/components/Field/index.d.ts +2 -0
  35. package/components/Field/index.js +1 -0
  36. package/components/Modal/Modal.d.ts +4 -4
  37. package/components/Modal/Modal.js +14 -12
  38. package/components/MoveHandle/MoveHandle.d.ts +2 -0
  39. package/components/MoveHandle/MoveHandle.js +84 -0
  40. package/components/MoveHandle/index.d.ts +2 -0
  41. package/components/MoveHandle/index.js +1 -0
  42. package/components/MoveHandle/types.d.ts +43 -0
  43. package/components/MoveHandle/types.js +1 -0
  44. package/components/MultiSelect/MultiSelect.d.ts +39 -0
  45. package/components/MultiSelect/MultiSelect.js +623 -0
  46. package/components/MultiSelect/MultiSelectContext.d.ts +20 -0
  47. package/components/MultiSelect/MultiSelectContext.js +56 -0
  48. package/components/MultiSelect/index.d.ts +2 -0
  49. package/components/MultiSelect/index.js +1 -0
  50. package/components/MultiSelect/types.d.ts +218 -0
  51. package/components/MultiSelect/types.js +3 -0
  52. package/components/Notice/Notice.d.ts +1 -1
  53. package/components/Notice/Notice.js +1 -1
  54. package/components/Progress/Progress.js +1 -1
  55. package/components/Progress/types.d.ts +7 -7
  56. package/components/Radio/Radio.d.ts +2 -0
  57. package/components/Radio/Radio.js +50 -0
  58. package/components/Radio/RadioGroup.d.ts +2 -0
  59. package/components/Radio/RadioGroup.js +54 -0
  60. package/components/Radio/RadioGroupContext.d.ts +3 -0
  61. package/components/Radio/RadioGroupContext.js +9 -0
  62. package/components/Radio/index.d.ts +8 -0
  63. package/components/Radio/index.js +6 -0
  64. package/components/Radio/types.d.ts +32 -0
  65. package/components/Radio/types.js +1 -0
  66. package/components/Rating/Rating.d.ts +5 -5
  67. package/components/Rating/Rating.js +2 -2
  68. package/components/SegmentedControl/SegmentedControl.js +20 -104
  69. package/components/SegmentedControl/types.d.ts +4 -8
  70. package/components/Select/Select.d.ts +39 -0
  71. package/components/Select/Select.js +497 -0
  72. package/components/Select/SelectContext.d.ts +20 -0
  73. package/components/Select/SelectContext.js +56 -0
  74. package/components/Select/index.d.ts +3 -0
  75. package/components/Select/index.js +1 -0
  76. package/components/Select/types.d.ts +216 -0
  77. package/components/Select/types.js +11 -0
  78. package/components/Sidebar/Sidebar.js +12 -12
  79. package/components/Sidebar/types.d.ts +5 -5
  80. package/components/StepIndicator/StepIndicator.js +1 -1
  81. package/components/StepList/StepList.js +2 -2
  82. package/components/StepList/types.d.ts +4 -4
  83. package/components/Switch/Switch.d.ts +9 -0
  84. package/components/Switch/Switch.js +91 -0
  85. package/components/Switch/index.d.ts +2 -0
  86. package/components/Switch/index.js +1 -0
  87. package/components/Switch/types.d.ts +11 -0
  88. package/components/Switch/types.js +1 -0
  89. package/components/TextInput/TextInput.d.ts +8 -0
  90. package/components/TextInput/TextInput.js +25 -0
  91. package/components/TextInput/index.d.ts +2 -0
  92. package/components/TextInput/index.js +1 -0
  93. package/components/TextInput/types.d.ts +32 -0
  94. package/components/TextInput/types.js +1 -0
  95. package/components/Textarea/Textarea.d.ts +6 -0
  96. package/components/Textarea/Textarea.js +49 -0
  97. package/components/Textarea/index.d.ts +2 -0
  98. package/components/Textarea/index.js +1 -0
  99. package/components/Textarea/types.d.ts +25 -0
  100. package/components/Textarea/types.js +1 -0
  101. package/components/index.d.ts +22 -0
  102. package/components/index.js +11 -0
  103. package/icons/icons.svg +2 -0
  104. package/icons/manifest.json +16 -0
  105. package/icons/registry.d.ts +4 -0
  106. package/icons/registry.js +2 -0
  107. package/icons/system/index.d.ts +4 -0
  108. package/icons/system/index.js +22 -0
  109. package/package.json +1 -1
  110. package/styles/all.css +1 -1
  111. package/styles/all.expanded.css +1838 -136
  112. package/styles/all.expanded.unlayered.css +1838 -136
  113. package/styles/all.unlayered.css +1 -1
  114. package/styles/components/_bundle.scss +22 -0
  115. package/styles/components/input/index.scss +5 -20
  116. package/styles/index.scss +21 -0
  117. package/styles/system/_control.scss +49 -0
  118. package/styles/system/_tokens.scss +124 -2
  119. package/styles/system/index.scss +2 -1
  120. package/styles/utilities/_index.scss +50 -0
  121. package/tui-manifest.json +907 -112
  122. package/utils/compose-events.d.ts +15 -0
  123. package/utils/compose-events.js +27 -0
  124. package/utils/hash.d.ts +15 -0
  125. package/utils/hash.js +32 -0
  126. package/utils/index.d.ts +3 -0
  127. package/utils/index.js +6 -0
  128. package/utils/is-dev.d.ts +5 -0
  129. package/utils/is-dev.js +7 -0
  130. package/utils/use-controllable-state.d.ts +19 -0
  131. package/utils/use-controllable-state.js +59 -0
  132. package/utils/use-roving-group.d.ts +33 -0
  133. package/utils/use-roving-group.js +123 -0
  134. package/utils/value-key.d.ts +16 -0
  135. 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), ...props, children: children }));
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,2 @@
1
+ export { Field } from './Field';
2
+ export type { FieldProps } from './Field';
@@ -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
- isOpen: boolean;
5
+ open: boolean;
6
6
  onClose: () => void;
7
7
  size?: Size;
8
8
  stickyHead?: boolean;
9
9
  stickyFoot?: boolean;
10
- labelledBy?: string;
11
- describedBy?: string;
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({ isOpen, onClose, size, stickyHead, stickyFoot, labelledBy, describedBy, initialFocusSelector, container, showCloseButton, closeLabel, closeOnBackdropClick, closeOnEscape, children, }: ModalProps): React.ReactPortal | null;
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({ isOpen, onClose, size = 'md', stickyHead, stickyFoot, labelledBy, describedBy, initialFocusSelector, container, showCloseButton, closeLabel = 'Close', closeOnBackdropClick = true, closeOnEscape = true, children, }) {
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 (!isOpen || !isBrowser) {
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
- }, [isOpen, container, mount]);
32
+ }, [open, container, mount]);
33
33
  // Reset state when modal closes, restore focus to trigger.
34
34
  useEffect(() => {
35
- if (isOpen)
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
- }, [isOpen]);
43
+ }, [open]);
44
44
  // Body scroll lock.
45
45
  useEffect(() => {
46
- if (!isOpen)
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
- }, [isOpen]);
52
+ }, [open]);
53
53
  // Focus trap (handles Tab cycling and ESC to close).
54
54
  useFocusTrap(dialogRef, {
55
- isActive: isOpen,
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 (!isOpen)
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
- }, [isOpen, initialFocusSelector, labelledBy]);
101
+ }, [open, mount, initialFocusSelector, labelledBy]);
100
102
  // Memoize context value to prevent unnecessary re-renders
101
103
  const contextValue = useMemo(() => ({ onClose }), [onClose]);
102
- if (!isOpen || !mount)
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,2 @@
1
+ import type { MoveHandleProps } from './types';
2
+ export declare const MoveHandle: import("react").ForwardRefExoticComponent<MoveHandleProps & import("react").RefAttributes<HTMLElement>>;
@@ -0,0 +1,84 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { forwardRef, useCallback, useEffect, useId, useRef } from 'react';
3
+ import { cx } from '../../utils/cx.js';
4
+ import { isDev } from '../../utils/is-dev.js';
5
+ import { Icon } from '../Icon/index.js';
6
+ // =============================================================================
7
+ // MoveHandle Component
8
+ // =============================================================================
9
+ //
10
+ // Compact reorder control for sortable lists. Shows a drag handle with optional
11
+ // up/down chevron buttons and a position index badge.
12
+ //
13
+ // When `index` is provided, shows the number at rest and swaps to the drag
14
+ // handle icon on hover/focus-within (CSS-driven, no JS state).
15
+ //
16
+ // Forwards ref to the root element (div for full, button for handle).
17
+ //
18
+ // Modes:
19
+ // full — Background panel with arrows, index, lock (default)
20
+ // handle — Bare drag icon button, no chrome
21
+ //
22
+ // CSS token API (never defined, read via fallback):
23
+ // --tui-move-handle-size Override container size
24
+ // --tui-move-handle-icon-size Override icon size
25
+ //
26
+ // =============================================================================
27
+ export const MoveHandle = forwardRef(function MoveHandle({ mode = 'full', size = 'md', index, locked = false, onMoveUp, onMoveDown, canMoveUp = true, canMoveDown = true, labels, dragHandleProps, 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, className, }, ref) {
28
+ // All hooks must be called unconditionally (rules of hooks)
29
+ const innerRef = useRef(null);
30
+ const mergedRef = useCallback((node) => {
31
+ innerRef.current = node;
32
+ if (typeof ref === 'function')
33
+ ref(node);
34
+ else if (ref)
35
+ ref.current = node;
36
+ }, [ref]);
37
+ const hasWarnedRef = useRef(false);
38
+ useEffect(() => {
39
+ if (mode === 'handle')
40
+ return;
41
+ if (hasWarnedRef.current)
42
+ return;
43
+ if (isDev() && !ariaLabel && !ariaLabelledBy) {
44
+ console.warn('MoveHandle: Missing accessible name. Provide aria-label or aria-labelledby.');
45
+ hasWarnedRef.current = true;
46
+ }
47
+ // eslint-disable-next-line react-hooks/exhaustive-deps
48
+ }, []);
49
+ const lockedDescId = useId();
50
+ // Focus recovery: when a move button becomes disabled after a reorder,
51
+ // redirect focus to the opposite button or drag handle
52
+ useEffect(() => {
53
+ if (mode === 'handle')
54
+ return;
55
+ const group = innerRef.current;
56
+ if (!group)
57
+ return;
58
+ const active = document.activeElement;
59
+ if (active instanceof HTMLButtonElement &&
60
+ active.disabled &&
61
+ group.contains(active)) {
62
+ const fallback = group.querySelector('.tui-move-handle__up:not(:disabled), .tui-move-handle__down:not(:disabled)') ??
63
+ group.querySelector('.tui-move-handle__handle');
64
+ fallback?.focus();
65
+ }
66
+ }, [mode, canMoveUp, canMoveDown]);
67
+ // Drag handle label precedence: dragHandleProps > labels.drag > default
68
+ const resolvedDragLabel = dragHandleProps?.['aria-label'] ?? labels?.drag ?? 'Drag to reorder';
69
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
70
+ const { 'aria-label': _dhLabel, ...restDragProps } = dragHandleProps ?? {};
71
+ // ----- Handle mode: just the drag icon button -----
72
+ // Uses ref directly (not mergedRef) — innerRef is unused for this path.
73
+ // Focus recovery and dev warning effects early-return for handle mode.
74
+ if (mode === 'handle') {
75
+ return (_jsx("button", { ref: ref, type: "button", className: cx('tui-move-handle', 'is-handle', className), "aria-label": resolvedDragLabel, ...restDragProps, children: _jsx(Icon, { name: "system/drag" }) }));
76
+ }
77
+ // ----- Full mode -----
78
+ const hasIndex = index != null;
79
+ const hasArrows = !!(onMoveUp || onMoveDown);
80
+ const resolvedLockedDesc = locked
81
+ ? (labels?.locked ?? 'This item is locked and cannot be reordered')
82
+ : undefined;
83
+ return (_jsxs("div", { ref: mergedRef, role: "group", "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, "aria-describedby": locked ? lockedDescId : undefined, "aria-disabled": locked || undefined, className: cx('tui-move-handle', `is-size-${size}`, locked && 'is-locked', hasIndex && 'has-index', className), children: [locked && (_jsx("span", { id: lockedDescId, className: "tui-visually-hidden", children: resolvedLockedDesc })), onMoveUp && (_jsx("button", { type: "button", className: "tui-move-handle__up", "aria-label": labels?.moveUp ?? 'Move up', disabled: locked || !canMoveUp, onClick: onMoveUp, children: _jsx(Icon, { name: "system/chevron-up" }) })), _jsx("div", { className: "tui-move-handle__center", children: locked ? (_jsx("span", { className: "tui-move-handle__lock", "aria-hidden": "true", children: _jsx(Icon, { name: "system/lock" }) })) : (_jsxs(_Fragment, { children: [hasIndex && (_jsx("span", { className: "tui-move-handle__index", "aria-hidden": "true", children: index })), _jsx("button", { type: "button", className: "tui-move-handle__handle", "aria-label": resolvedDragLabel, tabIndex: hasArrows ? -1 : 0, ...restDragProps, children: _jsx(Icon, { name: "system/handle-alt" }) })] })) }), onMoveDown && (_jsx("button", { type: "button", className: "tui-move-handle__down", "aria-label": labels?.moveDown ?? 'Move down', disabled: locked || !canMoveDown, onClick: onMoveDown, children: _jsx(Icon, { name: "system/chevron-down" }) }))] }));
84
+ });
@@ -0,0 +1,2 @@
1
+ export { MoveHandle } from './MoveHandle';
2
+ export type { MoveHandleLabels, MoveHandleProps, MoveHandleSize, MoveHandleMode } from './types';
@@ -0,0 +1 @@
1
+ export { MoveHandle } from './MoveHandle.js';
@@ -0,0 +1,43 @@
1
+ import type { HTMLAttributes } from 'react';
2
+ export type MoveHandleSize = 'sm' | 'md';
3
+ export type MoveHandleMode = 'full' | 'handle';
4
+ export interface MoveHandleLabels {
5
+ /** Label for the move-up button. Include item name for clear AT context, e.g. "Move Introduction up". Default: "Move up" */
6
+ moveUp?: string;
7
+ /** Label for the move-down button. Include item name for clear AT context, e.g. "Move Introduction down". Default: "Move down" */
8
+ moveDown?: string;
9
+ /**
10
+ * Label for the drag handle button. Default: "Drag to reorder".
11
+ * Consumers add position info if needed. dragHandleProps['aria-label'] takes precedence.
12
+ */
13
+ drag?: string;
14
+ /** Descriptive text announced when locked (via aria-describedby). Default: "This item is locked and cannot be reordered". */
15
+ locked?: string;
16
+ }
17
+ export interface MoveHandleProps {
18
+ /** Structural mode. 'full' (default) shows background panel with arrows/index. 'handle' shows only the bare drag icon button. */
19
+ mode?: MoveHandleMode;
20
+ /** Component scale. sm = 32px, md = 40px. Ignored when mode is 'handle'. */
21
+ size?: MoveHandleSize;
22
+ /** Position index. When provided, shows number at rest, drag handle on hover. */
23
+ index?: number;
24
+ /** When true, shows lock icon and disables all interaction. */
25
+ locked?: boolean;
26
+ /** Called when the "move up" button is clicked. Button not rendered when omitted. */
27
+ onMoveUp?: () => void;
28
+ /** Called when the "move down" button is clicked. Button not rendered when omitted. */
29
+ onMoveDown?: () => void;
30
+ /** When false, disables the move-up button without hiding it. Default: true. */
31
+ canMoveUp?: boolean;
32
+ /** When false, disables the move-down button without hiding it. Default: true. */
33
+ canMoveDown?: boolean;
34
+ /** Override internal button labels for i18n. */
35
+ labels?: MoveHandleLabels;
36
+ /** Props to spread on the drag handle button (e.g., from dnd-kit useSortable). */
37
+ dragHandleProps?: HTMLAttributes<HTMLButtonElement>;
38
+ /** Accessible label for the control group. Required if aria-labelledby is not set. */
39
+ 'aria-label'?: string;
40
+ /** ID of element labelling this group. Required if aria-label is not set. */
41
+ 'aria-labelledby'?: string;
42
+ className?: string;
43
+ }
@@ -0,0 +1 @@
1
+ export {};