@tangible/ui 0.0.6 → 0.0.7
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/Combobox/Combobox.d.ts +1 -1
- package/components/Combobox/Combobox.js +2 -2
- package/components/Combobox/types.d.ts +5 -0
- package/components/Field/Field.js +14 -4
- package/components/Field/FieldContext.d.ts +2 -0
- package/components/Radio/Radio.d.ts +4 -0
- package/components/Radio/Radio.js +15 -5
- package/components/Radio/RadioGroup.d.ts +1 -1
- package/components/Radio/RadioGroup.js +2 -2
- package/components/Radio/types.d.ts +10 -0
- package/components/Switch/Switch.js +28 -14
- package/package.json +1 -1
- package/styles/all.css +1 -1
- package/styles/all.expanded.css +160 -77
- package/styles/all.expanded.unlayered.css +160 -77
- package/styles/all.unlayered.css +1 -1
- package/styles/utilities/_index.scss +14 -4
- package/tui-manifest.json +31 -4
- package/utils/use-roving-group.js +9 -6
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ComboboxProps, ComboboxContentProps, ComboboxOptionProps, ComboboxGroupProps, ComboboxLabelProps } from './types';
|
|
2
|
-
declare function ComboboxRoot({ value: controlledValue, defaultValue, onValueChange, inputValue: controlledInputValue, onInputChange, open: controlledOpen, defaultOpen, onOpenChange, disabled, placeholder, size, openOnFocus, filterMode, onQueryChange, 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, inputClassName, children, }: ComboboxProps): import("react/jsx-runtime").JSX.Element;
|
|
2
|
+
declare function ComboboxRoot({ value: controlledValue, defaultValue, onValueChange, inputValue: controlledInputValue, onInputChange, open: controlledOpen, defaultOpen, onOpenChange, disabled, placeholder, size, openOnFocus, filterMode, onQueryChange, clearable, 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, inputClassName, children, }: ComboboxProps): import("react/jsx-runtime").JSX.Element;
|
|
3
3
|
declare namespace ComboboxRoot {
|
|
4
4
|
var displayName: string;
|
|
5
5
|
}
|
|
@@ -12,7 +12,7 @@ import { ComboboxActionsContext, ComboboxStateContext, ComboboxContentContext, u
|
|
|
12
12
|
// =============================================================================
|
|
13
13
|
// Combobox Root
|
|
14
14
|
// =============================================================================
|
|
15
|
-
function ComboboxRoot({ value: controlledValue, defaultValue, onValueChange, inputValue: controlledInputValue, onInputChange, open: controlledOpen, defaultOpen, onOpenChange, disabled = false, placeholder = '', size = 'md', openOnFocus = true, filterMode = 'always', onQueryChange, 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, inputClassName, children, }) {
|
|
15
|
+
function ComboboxRoot({ value: controlledValue, defaultValue, onValueChange, inputValue: controlledInputValue, onInputChange, open: controlledOpen, defaultOpen, onOpenChange, disabled = false, placeholder = '', size = 'md', openOnFocus = true, filterMode = 'always', onQueryChange, clearable = true, 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, inputClassName, children, }) {
|
|
16
16
|
// Controlled/uncontrolled value (initialize from defaultValue)
|
|
17
17
|
const [uncontrolledValue, setUncontrolledValue] = useState(defaultValue);
|
|
18
18
|
const isValueControlled = controlledValue !== undefined;
|
|
@@ -365,7 +365,7 @@ function ComboboxRoot({ value: controlledValue, defaultValue, onValueChange, inp
|
|
|
365
365
|
return (_jsx(ComboboxActionsContext.Provider, { value: actionsValue, children: _jsx(ComboboxStateContext.Provider, { value: stateValue, children: _jsxs("div", { className: "tui-combobox", children: [_jsxs("div", { className: "tui-combobox__input-wrapper", children: [_jsx("input", { ref: (node) => {
|
|
366
366
|
inputRef.current = node;
|
|
367
367
|
refs.setReference(node);
|
|
368
|
-
}, type: "text", id: inputId, className: cx('tui-combobox__input', size !== 'md' && `is-size-${size}`, inputClassName), role: "combobox", "aria-expanded": open, "aria-controls": listboxId, "aria-autocomplete": "list", "aria-activedescendant": activeOptionId, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, disabled: disabled, placeholder: placeholder, value: inputValue, autoComplete: "off", ...referenceProps, onChange: handleInputChange }), inputValue && !disabled && (_jsx("span", { className: "tui-combobox__clear", onPointerDown: handleClear, "aria-hidden": "true", children: _jsx(Icon, { name: "system/close", size: "sm" }) })), _jsx("span", { className: "tui-combobox__icon", "aria-hidden": "true", children: _jsx(Icon, { name: "system/chevron-down", size: "sm" }) })] }), children] }) }) }));
|
|
368
|
+
}, type: "text", id: inputId, className: cx('tui-combobox__input', size !== 'md' && `is-size-${size}`, inputClassName), role: "combobox", "aria-expanded": open, "aria-controls": listboxId, "aria-autocomplete": "list", "aria-activedescendant": activeOptionId, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, disabled: disabled, placeholder: placeholder, value: inputValue, autoComplete: "off", ...referenceProps, onChange: handleInputChange }), clearable && inputValue && !disabled && (_jsx("span", { className: "tui-combobox__clear", onPointerDown: handleClear, "aria-hidden": "true", children: _jsx(Icon, { name: "system/close", size: "sm" }) })), _jsx("span", { className: "tui-combobox__icon", "aria-hidden": "true", children: _jsx(Icon, { name: "system/chevron-down", size: "sm" }) })] }), children] }) }) }));
|
|
369
369
|
}
|
|
370
370
|
ComboboxRoot.displayName = 'Combobox';
|
|
371
371
|
// =============================================================================
|
|
@@ -86,6 +86,11 @@ export type ComboboxProps = {
|
|
|
86
86
|
* ID of element that labels this combobox.
|
|
87
87
|
*/
|
|
88
88
|
'aria-labelledby'?: string;
|
|
89
|
+
/**
|
|
90
|
+
* Whether to show the clear button when a value is present.
|
|
91
|
+
* @default true
|
|
92
|
+
*/
|
|
93
|
+
clearable?: boolean;
|
|
89
94
|
/**
|
|
90
95
|
* Class name applied directly to the `<input>` element.
|
|
91
96
|
* Use for utilities like `tui-input-reset` that must target the input itself.
|
|
@@ -5,16 +5,18 @@ import { FieldContext, useFieldContext } from './FieldContext.js';
|
|
|
5
5
|
export const Field = forwardRef(function Field({ error = false, required = false, disabled = false, inline = false, className, children, }, ref) {
|
|
6
6
|
const baseId = useId();
|
|
7
7
|
const controlId = `${baseId}-control`;
|
|
8
|
+
const labelId = `${baseId}-label`;
|
|
8
9
|
const helperTextId = `${baseId}-helper`;
|
|
9
10
|
const errorId = `${baseId}-error`;
|
|
10
11
|
const contextValue = useMemo(() => ({
|
|
11
12
|
controlId,
|
|
13
|
+
labelId,
|
|
12
14
|
helperTextId,
|
|
13
15
|
errorId,
|
|
14
16
|
hasError: error,
|
|
15
17
|
required,
|
|
16
18
|
disabled,
|
|
17
|
-
}), [controlId, helperTextId, errorId, error, required, disabled]);
|
|
19
|
+
}), [controlId, labelId, helperTextId, errorId, error, required, disabled]);
|
|
18
20
|
const classes = cx('tui-field', error && 'is-error', disabled && 'is-disabled', inline && 'is-layout-inline', className);
|
|
19
21
|
return (_jsx(FieldContext.Provider, { value: contextValue, children: _jsx("div", { ref: ref, className: classes, children: children }) }));
|
|
20
22
|
});
|
|
@@ -22,16 +24,16 @@ export const Field = forwardRef(function Field({ error = false, required = false
|
|
|
22
24
|
// Field.Label
|
|
23
25
|
// =============================================================================
|
|
24
26
|
function FieldLabel({ hidden = false, className, children, ...rest }) {
|
|
25
|
-
const { controlId, required } = useFieldContext();
|
|
27
|
+
const { controlId, labelId, required } = useFieldContext();
|
|
26
28
|
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" })] }))] }));
|
|
29
|
+
return (_jsxs("label", { id: labelId, 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
30
|
}
|
|
29
31
|
FieldLabel.displayName = 'Field.Label';
|
|
30
32
|
// =============================================================================
|
|
31
33
|
// Field.Control
|
|
32
34
|
// =============================================================================
|
|
33
35
|
function FieldControl({ children }) {
|
|
34
|
-
const { controlId, helperTextId, errorId, hasError, required, disabled, } = useFieldContext();
|
|
36
|
+
const { controlId, labelId, helperTextId, errorId, hasError, required, disabled, } = useFieldContext();
|
|
35
37
|
const child = Children.only(children);
|
|
36
38
|
if (!isValidElement(child)) {
|
|
37
39
|
throw new Error('Field.Control expects a single React element as its child');
|
|
@@ -48,10 +50,18 @@ function FieldControl({ children }) {
|
|
|
48
50
|
describedByParts.push(errorId);
|
|
49
51
|
}
|
|
50
52
|
const describedBy = describedByParts.join(' ');
|
|
53
|
+
// Build aria-labelledby for non-labelable elements (<button>, <div>, etc.)
|
|
54
|
+
// <label htmlFor> only works with labelable elements (input, textarea, select,
|
|
55
|
+
// meter, output, progress). For everything else (Switch, future Slider, etc.)
|
|
56
|
+
// aria-labelledby provides the accessible name. For native inputs this is
|
|
57
|
+
// redundant with htmlFor but harmless — aria-labelledby takes priority in the
|
|
58
|
+
// accessible name algorithm and points at the same label text.
|
|
59
|
+
const labelledBy = childProps['aria-labelledby'] ?? labelId;
|
|
51
60
|
// Clone child with a11y props
|
|
52
61
|
// Note: aria-invalid and aria-required must be string "true", not boolean
|
|
53
62
|
return cloneElement(child, {
|
|
54
63
|
id: controlId,
|
|
64
|
+
'aria-labelledby': labelledBy,
|
|
55
65
|
'aria-describedby': describedBy,
|
|
56
66
|
'aria-invalid': hasError ? 'true' : undefined,
|
|
57
67
|
'aria-required': required ? 'true' : undefined,
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
export type FieldContextValue = {
|
|
2
2
|
/** ID for the form control element */
|
|
3
3
|
controlId: string;
|
|
4
|
+
/** ID for the label element (for aria-labelledby on non-labelable controls) */
|
|
5
|
+
labelId: string;
|
|
4
6
|
/** ID for helper text (for aria-describedby) */
|
|
5
7
|
helperTextId: string;
|
|
6
8
|
/** ID for error message (for aria-describedby) */
|
|
@@ -1,2 +1,6 @@
|
|
|
1
1
|
import type { RadioProps } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* Ref targets the inner `<button role="radio">`, not the outer wrapper `<div>`.
|
|
4
|
+
* This is intentional — roving tabindex and focus management operate on the button.
|
|
5
|
+
*/
|
|
2
6
|
export declare const Radio: import("react").ForwardRefExoticComponent<RadioProps & import("react").RefAttributes<HTMLButtonElement>>;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { forwardRef, useCallback, useEffect, useRef } from 'react';
|
|
2
|
+
import { forwardRef, useCallback, useEffect, useId, useRef } from 'react';
|
|
3
3
|
import { cx } from '../../utils/cx.js';
|
|
4
4
|
import { composeRefs } from '../../utils/compose-refs.js';
|
|
5
5
|
import { toKey } from '../../utils/value-key.js';
|
|
@@ -14,8 +14,14 @@ import { useRadioGroupContext } from './RadioGroupContext.js';
|
|
|
14
14
|
// Arrow keys in the group move focus AND select.
|
|
15
15
|
//
|
|
16
16
|
// =============================================================================
|
|
17
|
-
|
|
17
|
+
/**
|
|
18
|
+
* Ref targets the inner `<button role="radio">`, not the outer wrapper `<div>`.
|
|
19
|
+
* This is intentional — roving tabindex and focus management operate on the button.
|
|
20
|
+
*/
|
|
21
|
+
export const Radio = forwardRef(function Radio({ value, label, description, disabled = false, 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, className, children, }, externalRef) {
|
|
18
22
|
const { selectedValue, focusableValue, rootDisabled, registerItem, unregisterItem, onSelect, } = useRadioGroupContext();
|
|
23
|
+
const id = useId();
|
|
24
|
+
const descriptionId = `${id}-desc`;
|
|
19
25
|
const isSelected = selectedValue !== undefined && toKey(selectedValue) === toKey(value);
|
|
20
26
|
const isDisabled = rootDisabled || disabled;
|
|
21
27
|
const isFocusable = focusableValue !== undefined && toKey(focusableValue) === toKey(value);
|
|
@@ -24,8 +30,8 @@ export const Radio = forwardRef(function Radio({ value, label, disabled = false,
|
|
|
24
30
|
useEffect(() => {
|
|
25
31
|
if (hasWarnedRef.current)
|
|
26
32
|
return;
|
|
27
|
-
if (isDev() && !label) {
|
|
28
|
-
console.warn('Radio: Missing accessible name. Provide a label prop.');
|
|
33
|
+
if (isDev() && !label && !ariaLabel && !ariaLabelledBy) {
|
|
34
|
+
console.warn('Radio: Missing accessible name. Provide a label, aria-label, or aria-labelledby prop.');
|
|
29
35
|
hasWarnedRef.current = true;
|
|
30
36
|
}
|
|
31
37
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
@@ -45,6 +51,10 @@ export const Radio = forwardRef(function Radio({ value, label, disabled = false,
|
|
|
45
51
|
return;
|
|
46
52
|
onSelect(value);
|
|
47
53
|
};
|
|
48
|
-
|
|
54
|
+
const hasExpandedContent = !!(description || children);
|
|
55
|
+
return (_jsxs("div", { className: cx('tui-radio', hasExpandedContent && 'has-content', className), children: [_jsxs("button", { ref: composeRefs(callbackRef, externalRef), type: "button", role: "radio", className: "tui-radio__control", "aria-checked": isSelected, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, "aria-describedby": description ? descriptionId : undefined,
|
|
56
|
+
// Item-level disabled: native disabled (removes from focus cycle).
|
|
57
|
+
// Group-level disabled: aria-disabled (preserves AT group context).
|
|
58
|
+
disabled: disabled || undefined, "aria-disabled": rootDisabled || undefined, tabIndex: isFocusable ? 0 : -1, onClick: handleClick, children: [_jsx("span", { className: "tui-radio__indicator", "aria-hidden": "true" }), label && _jsx("span", { className: "tui-radio__label", children: label })] }), hasExpandedContent && (_jsxs("div", { className: "tui-radio__body", children: [description && (_jsx("p", { id: descriptionId, className: "tui-radio__description", children: description })), children] }))] }));
|
|
49
59
|
});
|
|
50
60
|
Radio.displayName = 'Radio';
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import type { RadioGroupProps } from './types';
|
|
2
|
-
export declare function RadioGroup({ value: controlledValue, defaultValue, onValueChange, disabled, orientation, loop, 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, className, children, }: RadioGroupProps): import("react/jsx-runtime").JSX.Element;
|
|
2
|
+
export declare function RadioGroup({ value: controlledValue, defaultValue, onValueChange, disabled, orientation, loop, 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, 'aria-describedby': ariaDescribedBy, 'aria-invalid': ariaInvalid, 'aria-required': ariaRequired, className, children, }: RadioGroupProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -17,7 +17,7 @@ import { isDev } from '../../utils/is-dev.js';
|
|
|
17
17
|
// --tui-radio-accent Accent color for selected state
|
|
18
18
|
//
|
|
19
19
|
// =============================================================================
|
|
20
|
-
export function RadioGroup({ value: controlledValue, defaultValue, onValueChange, disabled = false, orientation = 'vertical', loop = true, 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, className, children, }) {
|
|
20
|
+
export function RadioGroup({ value: controlledValue, defaultValue, onValueChange, disabled = false, orientation = 'vertical', loop = true, 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, 'aria-describedby': ariaDescribedBy, 'aria-invalid': ariaInvalid, 'aria-required': ariaRequired, className, children, }) {
|
|
21
21
|
const [selectedValue, setSelectedValue] = useControllableState({
|
|
22
22
|
value: controlledValue,
|
|
23
23
|
defaultValue,
|
|
@@ -50,5 +50,5 @@ export function RadioGroup({ value: controlledValue, defaultValue, onValueChange
|
|
|
50
50
|
unregisterItem,
|
|
51
51
|
onSelect,
|
|
52
52
|
}), [selectedValue, focusableValue, disabled, orientation, registerItem, unregisterItem, onSelect]);
|
|
53
|
-
return (_jsx(RadioGroupContext.Provider, { value: contextValue, children: _jsx("div", { role: "radiogroup", className: cx('tui-radio-group', orientation === 'horizontal' && 'is-horizontal', className), "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, "aria-
|
|
53
|
+
return (_jsx(RadioGroupContext.Provider, { value: contextValue, children: _jsx("div", { role: "radiogroup", className: cx('tui-radio-group', orientation === 'horizontal' && 'is-horizontal', className), "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, "aria-describedby": ariaDescribedBy, "aria-invalid": ariaInvalid, "aria-required": ariaRequired, "aria-disabled": disabled || undefined, "aria-orientation": orientation, onKeyDown: handleKeyDown, children: children }) }));
|
|
54
54
|
}
|
|
@@ -10,14 +10,24 @@ export type RadioGroupProps = {
|
|
|
10
10
|
loop?: boolean;
|
|
11
11
|
'aria-label'?: string;
|
|
12
12
|
'aria-labelledby'?: string;
|
|
13
|
+
'aria-describedby'?: string;
|
|
14
|
+
'aria-invalid'?: boolean | 'true' | 'false';
|
|
15
|
+
'aria-required'?: boolean | 'true' | 'false';
|
|
13
16
|
className?: string;
|
|
14
17
|
children: ReactNode;
|
|
15
18
|
};
|
|
16
19
|
export type RadioProps = {
|
|
17
20
|
value: OptionValue;
|
|
18
21
|
label?: ReactNode;
|
|
22
|
+
/**
|
|
23
|
+
* Description text displayed below the label.
|
|
24
|
+
*/
|
|
25
|
+
description?: string;
|
|
19
26
|
disabled?: boolean;
|
|
27
|
+
'aria-label'?: string;
|
|
28
|
+
'aria-labelledby'?: string;
|
|
20
29
|
className?: string;
|
|
30
|
+
children?: ReactNode;
|
|
21
31
|
};
|
|
22
32
|
export type RadioItemRecord = RovingItemRecord;
|
|
23
33
|
export type RadioGroupContextValue = {
|
|
@@ -8,18 +8,13 @@ import { isDev } from '../../utils/is-dev.js';
|
|
|
8
8
|
// Switch Component
|
|
9
9
|
// =============================================================================
|
|
10
10
|
//
|
|
11
|
-
// <button role="switch"> with animated thumb. Uses
|
|
12
|
-
// Existing tui-toggle CSS untouched (stays for CSS-only usage).
|
|
11
|
+
// <button role="switch"> with animated thumb. Uses tui-switch SCSS.
|
|
13
12
|
//
|
|
14
13
|
// Bare (no label): returns <button> directly for Field.Control cloneElement.
|
|
15
|
-
// With label: wraps in <label>,
|
|
16
|
-
//
|
|
17
|
-
// CSS token API:
|
|
18
|
-
// --tui-switch-accent Accent color for on state
|
|
19
|
-
// --tui-switch-track-off Track color when off
|
|
14
|
+
// With label: wraps in <label>, label-text clicks forward to button.
|
|
20
15
|
//
|
|
21
16
|
// =============================================================================
|
|
22
|
-
// Props that should route to the <button>, not the wrapper
|
|
17
|
+
// Props that should route to the <button>, not the wrapper.
|
|
23
18
|
const BUTTON_PROPS = new Set([
|
|
24
19
|
'id',
|
|
25
20
|
'aria-describedby',
|
|
@@ -29,6 +24,7 @@ const BUTTON_PROPS = new Set([
|
|
|
29
24
|
'aria-labelledby',
|
|
30
25
|
'form',
|
|
31
26
|
'tabIndex',
|
|
27
|
+
'onClick',
|
|
32
28
|
'onFocus',
|
|
33
29
|
'onBlur',
|
|
34
30
|
]);
|
|
@@ -57,13 +53,18 @@ export const Switch = forwardRef(function Switch({ checked: controlledChecked, d
|
|
|
57
53
|
// Extract onClick from rest so prop spreading can't override internal handler
|
|
58
54
|
const { onClick: onClickProp, ...restWithoutClick } = rest;
|
|
59
55
|
const handleClick = (e) => {
|
|
56
|
+
if (disabled)
|
|
57
|
+
return;
|
|
60
58
|
setChecked((prev) => !prev);
|
|
61
59
|
onClickProp?.(e);
|
|
62
60
|
};
|
|
63
61
|
const isChecked = checked ?? false;
|
|
62
|
+
// Shared button element — single source of truth for both render paths.
|
|
63
|
+
// In labeled mode, buttonProps/sizeClass are set differently (see below).
|
|
64
|
+
const renderButton = (extraProps, extraClass) => (_jsx("button", { ref: composeRefs(internalRef, externalRef), type: "button", role: "switch", "aria-checked": isChecked, disabled: disabled, className: cx('tui-switch__track', extraClass), onClick: handleClick, ...extraProps, children: _jsx("span", { className: "tui-switch__thumb" }) }));
|
|
64
65
|
// Bare: no label — Field.Control can inject id/aria-* directly
|
|
65
66
|
if (!label) {
|
|
66
|
-
return (
|
|
67
|
+
return renderButton(restWithoutClick, cx(sizeClass, className));
|
|
67
68
|
}
|
|
68
69
|
// Split rest props: some go on button, some on wrapper
|
|
69
70
|
const buttonProps = {};
|
|
@@ -76,16 +77,29 @@ export const Switch = forwardRef(function Switch({ checked: controlledChecked, d
|
|
|
76
77
|
wrapperProps[key] = val;
|
|
77
78
|
}
|
|
78
79
|
}
|
|
79
|
-
//
|
|
80
|
-
|
|
81
|
-
|
|
80
|
+
// DEV: warn if button-like props leaked to the wrapper
|
|
81
|
+
if (isDev()) {
|
|
82
|
+
const suspect = Object.keys(wrapperProps).filter((k) => k.startsWith('aria-') || k.startsWith('on') || k === 'tabIndex');
|
|
83
|
+
if (suspect.length > 0) {
|
|
84
|
+
console.warn(`Switch: Props [${suspect.join(', ')}] ended up on the <label> wrapper. ` +
|
|
85
|
+
'Add them to BUTTON_PROPS if they belong on the <button>.');
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
// <label> wrapping a <button> causes native click-forwarding (button IS
|
|
89
|
+
// a labelable element). Without preventDefault, clicking the button fires
|
|
90
|
+
// handleClick AND the forwarded click — double-toggle.
|
|
91
|
+
// We suppress native forwarding. Clicks on the button are handled by its
|
|
92
|
+
// own onClick (handleClick). Clicks on the label text toggle state here.
|
|
82
93
|
const handleLabelClick = (e) => {
|
|
83
|
-
// Prevent native label click from also activating the button
|
|
84
94
|
e.preventDefault();
|
|
95
|
+
// Clicks on/inside the button are handled by handleClick (via bubbling)
|
|
96
|
+
if (internalRef.current?.contains(e.target))
|
|
97
|
+
return;
|
|
85
98
|
if (disabled)
|
|
86
99
|
return;
|
|
87
100
|
internalRef.current?.focus();
|
|
88
101
|
setChecked((prev) => !prev);
|
|
89
102
|
};
|
|
90
|
-
return (_jsxs("label", { className: cx('tui-switch', sizeClass, disabled && 'is-disabled', className), onClick: handleLabelClick, ...wrapperProps, children: [
|
|
103
|
+
return (_jsxs("label", { className: cx('tui-switch', sizeClass, disabled && 'is-disabled', className), onClick: handleLabelClick, ...wrapperProps, children: [renderButton(buttonProps), _jsx("span", { className: "tui-switch__label", children: label })] }));
|
|
91
104
|
});
|
|
105
|
+
Switch.displayName = 'Switch';
|