@tangible/ui 0.0.7 → 0.0.9
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/Accordion/Accordion.js +11 -3
- package/components/Avatar/Avatar.d.ts +1 -1
- package/components/Avatar/Avatar.js +5 -4
- package/components/Avatar/AvatarGroup.js +7 -5
- package/components/Avatar/index.d.ts +2 -2
- package/components/Avatar/index.js +1 -1
- package/components/Avatar/types.d.ts +27 -0
- package/components/Avatar/types.js +8 -0
- package/components/Button/Button.js +4 -2
- package/components/Button/index.d.ts +2 -1
- package/components/Button/index.js +1 -0
- package/components/Button/types.d.ts +10 -0
- package/components/Button/types.js +3 -1
- package/components/Checkbox/Checkbox.js +46 -11
- package/components/Checkbox/types.d.ts +9 -0
- package/components/Combobox/Combobox.d.ts +1 -1
- package/components/Combobox/Combobox.js +50 -7
- package/components/Combobox/index.d.ts +2 -1
- package/components/Combobox/index.js +1 -0
- package/components/Combobox/types.d.ts +9 -0
- package/components/Combobox/types.js +3 -1
- package/components/Dropdown/Dropdown.d.ts +1 -1
- package/components/Dropdown/Dropdown.js +32 -12
- package/components/Field/Field.d.ts +4 -1
- package/components/Field/Field.js +35 -14
- package/components/Field/FieldContext.d.ts +16 -0
- package/components/Field/FieldContext.js +3 -0
- package/components/Field/index.d.ts +2 -1
- package/components/Field/index.js +1 -0
- package/components/Icon/Icon.d.ts +1 -1
- package/components/Icon/Icon.js +2 -2
- package/components/Modal/Modal.d.ts +5 -1
- package/components/Modal/Modal.js +2 -2
- package/components/MoveHandle/MoveHandle.d.ts +1 -1
- package/components/MoveHandle/MoveHandle.js +4 -4
- package/components/MoveHandle/types.d.ts +1 -1
- package/components/MultiSelect/MultiSelect.d.ts +1 -1
- package/components/MultiSelect/MultiSelect.js +58 -19
- package/components/MultiSelect/index.d.ts +2 -1
- package/components/MultiSelect/index.js +1 -0
- package/components/MultiSelect/types.d.ts +34 -0
- package/components/MultiSelect/types.js +10 -0
- package/components/Pager/Pager.d.ts +7 -1
- package/components/Pager/Pager.js +7 -5
- package/components/Pager/index.d.ts +2 -0
- package/components/Pager/index.js +1 -0
- package/components/Pager/types.d.ts +37 -0
- package/components/Pager/types.js +12 -0
- package/components/Progress/Progress.d.ts +2 -1
- package/components/Progress/Progress.js +3 -3
- package/components/Rating/Rating.d.ts +2 -32
- package/components/Rating/Rating.js +5 -3
- package/components/Rating/index.d.ts +2 -1
- package/components/Rating/index.js +1 -0
- package/components/Rating/types.d.ts +41 -0
- package/components/Rating/types.js +4 -0
- package/components/SegmentedControl/SegmentedControl.js +6 -5
- package/components/SegmentedControl/types.d.ts +17 -5
- package/components/Select/Select.d.ts +1 -0
- package/components/Select/Select.js +131 -77
- package/components/Select/SelectContext.d.ts +4 -16
- package/components/Select/SelectContext.js +5 -35
- package/components/Select/types.d.ts +19 -19
- package/components/Sidebar/Sidebar.js +25 -20
- package/components/StepIndicator/StepIndicator.d.ts +1 -1
- package/components/StepIndicator/StepIndicator.js +14 -10
- package/components/StepIndicator/index.d.ts +2 -1
- package/components/StepIndicator/index.js +1 -0
- package/components/StepIndicator/types.d.ts +18 -0
- package/components/StepIndicator/types.js +7 -1
- package/components/Table/BulkActionsBar.d.ts +4 -1
- package/components/Table/BulkActionsBar.js +5 -4
- package/components/Table/DataTable.d.ts +4 -1
- package/components/Table/DataTable.js +10 -8
- package/components/Table/index.d.ts +3 -0
- package/components/Table/index.js +2 -0
- package/components/Table/types.d.ts +20 -0
- package/components/Table/types.js +11 -0
- package/components/Tabs/Tabs.js +11 -4
- package/components/TextInput/TextInput.js +2 -1
- package/components/TextInput/types.d.ts +7 -1
- package/components/Textarea/Textarea.js +3 -2
- package/components/Textarea/types.d.ts +6 -1
- package/components/Tooltip/Tooltip.d.ts +1 -1
- package/components/Tooltip/Tooltip.js +16 -10
- package/icons/icons.svg +29 -15
- package/icons/lms/index.d.ts +8 -0
- package/icons/lms/index.js +48 -4
- package/icons/manifest.json +112 -0
- package/icons/player/index.js +9 -9
- package/icons/registry.d.ts +28 -0
- package/icons/registry.js +14 -0
- package/icons/system/index.d.ts +20 -0
- package/icons/system/index.js +112 -2
- package/package.json +1 -1
- package/styles/all.css +1 -1
- package/styles/all.expanded.css +266 -59
- package/styles/all.expanded.unlayered.css +266 -59
- package/styles/all.unlayered.css +1 -1
- package/styles/components/input/index.scss +29 -7
- package/styles/system/_constants.scss +1 -1
- package/styles/system/_tokens.scss +1 -0
- package/tui-manifest.json +78 -52
|
@@ -246,10 +246,18 @@ function AccordionTrigger({ asChild = false, 'aria-label': ariaLabel, children,
|
|
|
246
246
|
// =============================================================================
|
|
247
247
|
function AccordionPanel({ landmark = false, children, className }) {
|
|
248
248
|
const { triggerId, panelId, isOpen } = useAccordionItemContext();
|
|
249
|
+
const panelRef = useRef(null);
|
|
249
250
|
const state = isOpen ? 'open' : 'closed';
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
251
|
+
// Set inert via DOM property for React 18 compatibility.
|
|
252
|
+
// React 18 doesn't handle `inert` as a boolean prop — it renders it as a
|
|
253
|
+
// string attribute and may not reliably remove it on re-renders.
|
|
254
|
+
// Setting the DOM property directly works across React 18 and 19.
|
|
255
|
+
React.useEffect(() => {
|
|
256
|
+
if (panelRef.current) {
|
|
257
|
+
panelRef.current.inert = !isOpen;
|
|
258
|
+
}
|
|
259
|
+
}, [isOpen]);
|
|
260
|
+
return (_jsx("div", { ref: panelRef, id: panelId, role: landmark ? 'region' : undefined, "aria-labelledby": landmark ? triggerId : undefined, className: cx('tui-accordion__panel', className), "data-state": state, "aria-hidden": !isOpen, children: _jsx("div", { className: "tui-accordion__panel-content", children: children }) }));
|
|
253
261
|
}
|
|
254
262
|
// =============================================================================
|
|
255
263
|
// Export Compound Component
|
|
@@ -8,4 +8,4 @@ import type { AvatarProps } from './types';
|
|
|
8
8
|
* - Shows placeholder icon if neither `src` nor `name` provided
|
|
9
9
|
* - Colors for initials are derived from the name hash for consistency
|
|
10
10
|
*/
|
|
11
|
-
export declare const Avatar: React.
|
|
11
|
+
export declare const Avatar: React.NamedExoticComponent<AvatarProps & React.RefAttributes<HTMLSpanElement>>;
|
|
@@ -3,7 +3,7 @@ import React, { useState, useMemo } from 'react';
|
|
|
3
3
|
import { cx } from '../../utils/cx.js';
|
|
4
4
|
import { Icon } from '../Icon/index.js';
|
|
5
5
|
import { Tooltip } from '../Tooltip/index.js';
|
|
6
|
-
import { AVATAR_COLORS } from './types.js';
|
|
6
|
+
import { AVATAR_COLORS, defaultAvatarLabels } from './types.js';
|
|
7
7
|
/**
|
|
8
8
|
* Generate initials from a name.
|
|
9
9
|
* "Mary Ghen" → "MG", "Bob" → "B", "Jean-Luc Picard" → "JP"
|
|
@@ -46,7 +46,7 @@ function getColorFromName(name, colors) {
|
|
|
46
46
|
* - Shows placeholder icon if neither `src` nor `name` provided
|
|
47
47
|
* - Colors for initials are derived from the name hash for consistency
|
|
48
48
|
*/
|
|
49
|
-
export const Avatar = React.forwardRef(({ src, name, size = 'md', shape = 'circle', color, indicator, indicatorLabel, indicatorPosition = 'bottom-right', tooltip, className, }, ref) => {
|
|
49
|
+
export const Avatar = React.memo(React.forwardRef(({ src, name, size = 'md', shape = 'circle', color, indicator, indicatorLabel, indicatorPosition = 'bottom-right', tooltip, labels: labelsProp, className, }, ref) => {
|
|
50
50
|
const [imgError, setImgError] = useState(false);
|
|
51
51
|
// Reset error state when src changes
|
|
52
52
|
React.useEffect(() => {
|
|
@@ -54,6 +54,7 @@ export const Avatar = React.forwardRef(({ src, name, size = 'md', shape = 'circl
|
|
|
54
54
|
}, [src]);
|
|
55
55
|
const initials = useMemo(() => (name ? getInitials(name) : ''), [name]);
|
|
56
56
|
const derivedColor = useMemo(() => color ?? (name ? getColorFromName(name, AVATAR_COLORS) : 'slate'), [color, name]);
|
|
57
|
+
const labels = useMemo(() => ({ ...defaultAvatarLabels, ...labelsProp }), [labelsProp]);
|
|
57
58
|
const showImage = src && !imgError;
|
|
58
59
|
const showInitials = !showImage && initials;
|
|
59
60
|
const showPlaceholder = !showImage && !initials;
|
|
@@ -65,12 +66,12 @@ export const Avatar = React.forwardRef(({ src, name, size = 'md', shape = 'circl
|
|
|
65
66
|
: {
|
|
66
67
|
role: 'img',
|
|
67
68
|
'aria-label': name && indicatorLabel
|
|
68
|
-
?
|
|
69
|
+
? labels.description(name, indicatorLabel)
|
|
69
70
|
: name || indicatorLabel,
|
|
70
71
|
}), children: [_jsxs("span", { className: "tui-avatar__content", children: [showImage && (_jsx("img", { src: src, alt: "", className: "tui-avatar__image", onError: () => setImgError(true) })), showInitials && (_jsx("span", { className: "tui-avatar__initials", "aria-hidden": "true", children: initials })), showPlaceholder && (_jsx(Icon, { name: "system/user-circle-outline", className: "tui-avatar__placeholder" }))] }), indicator && (_jsx("span", { className: cx('tui-avatar__indicator', `is-position-${indicatorPosition}`), "aria-hidden": "true", children: indicator }))] }));
|
|
71
72
|
if (showTooltip) {
|
|
72
73
|
return (_jsxs(Tooltip, { children: [_jsx(Tooltip.Trigger, { asChild: true, children: avatarElement }), _jsx(Tooltip.Content, { "aria-hidden": "true", children: name })] }));
|
|
73
74
|
}
|
|
74
75
|
return avatarElement;
|
|
75
|
-
});
|
|
76
|
+
}));
|
|
76
77
|
Avatar.displayName = 'Avatar';
|
|
@@ -2,6 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import React, { Children, isValidElement, cloneElement } from 'react';
|
|
3
3
|
import { cx } from '../../utils/cx.js';
|
|
4
4
|
import { OverlapStack } from '../OverlapStack/index.js';
|
|
5
|
+
import { defaultAvatarGroupLabels } from './types.js';
|
|
5
6
|
/**
|
|
6
7
|
* AvatarGroup displays multiple avatars with optional overlap.
|
|
7
8
|
*
|
|
@@ -10,7 +11,8 @@ import { OverlapStack } from '../OverlapStack/index.js';
|
|
|
10
11
|
* - Non-overlap mode uses flex with gap
|
|
11
12
|
* - Override overlap amount via `--tui-avatar-group-overlap` CSS property
|
|
12
13
|
*/
|
|
13
|
-
export const AvatarGroup = React.forwardRef(({ max, size, shape, overlap = true, groupLabel: groupLabelFn, children, className }, ref) => {
|
|
14
|
+
export const AvatarGroup = React.forwardRef(({ max, size, shape, overlap = true, groupLabel: groupLabelFn, labels: labelsProp, children, className }, ref) => {
|
|
15
|
+
const labels = { ...defaultAvatarGroupLabels, ...labelsProp };
|
|
14
16
|
const childArray = Children.toArray(children).filter(isValidElement);
|
|
15
17
|
const total = childArray.length;
|
|
16
18
|
// Clone children to inject size/shape props if provided at group level
|
|
@@ -23,14 +25,14 @@ export const AvatarGroup = React.forwardRef(({ max, size, shape, overlap = true,
|
|
|
23
25
|
}
|
|
24
26
|
return child;
|
|
25
27
|
});
|
|
26
|
-
// Descriptive label for the group
|
|
28
|
+
// Descriptive label for the group — groupLabel prop takes precedence over labels bag
|
|
27
29
|
const hasOverflow = max !== undefined && total > max;
|
|
28
30
|
const visibleCount = hasOverflow ? max : total;
|
|
29
31
|
const groupLabel = groupLabelFn
|
|
30
32
|
? groupLabelFn(total, visibleCount)
|
|
31
33
|
: hasOverflow
|
|
32
|
-
?
|
|
33
|
-
:
|
|
34
|
+
? labels.group(total, visibleCount)
|
|
35
|
+
: labels.groupAll(total);
|
|
34
36
|
// Non-overlap mode: simple flex layout
|
|
35
37
|
if (!overlap) {
|
|
36
38
|
return (_jsx("div", { ref: ref, className: cx('tui-avatar-group', className), role: "group", "aria-label": groupLabel, children: clonedChildren }));
|
|
@@ -39,7 +41,7 @@ export const AvatarGroup = React.forwardRef(({ max, size, shape, overlap = true,
|
|
|
39
41
|
// Note: We don't use OverlapStack's `frame` prop because its border-radius
|
|
40
42
|
// wouldn't match each avatar's shape. Instead, we apply ring styles via CSS
|
|
41
43
|
// directly to avatars inside .is-overlap groups.
|
|
42
|
-
return (_jsx(OverlapStack, { ref: ref, className: cx('tui-avatar-group', 'is-overlap', className), max: max, renderOverflow: (count) => (_jsx(AvatarOverflow, { count: count, size: size, shape: shape })), overflowLabel:
|
|
44
|
+
return (_jsx(OverlapStack, { ref: ref, className: cx('tui-avatar-group', 'is-overlap', className), max: max, renderOverflow: (count) => (_jsx(AvatarOverflow, { count: count, size: size, shape: shape })), overflowLabel: labels.overflow, "aria-label": groupLabel, children: clonedChildren }));
|
|
43
45
|
});
|
|
44
46
|
AvatarGroup.displayName = 'AvatarGroup';
|
|
45
47
|
function AvatarOverflow({ count, size, shape }) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Avatar as AvatarBase } from './Avatar';
|
|
2
2
|
import { AvatarGroup } from './AvatarGroup';
|
|
3
|
-
export type { AvatarProps, AvatarGroupProps, AvatarSize, AvatarShape, AvatarColor, IndicatorPosition, } from './types';
|
|
4
|
-
export { AVATAR_COLORS } from './types';
|
|
3
|
+
export type { AvatarProps, AvatarGroupProps, AvatarGroupLabels, AvatarLabels, AvatarSize, AvatarShape, AvatarColor, IndicatorPosition, } from './types';
|
|
4
|
+
export { AVATAR_COLORS, defaultAvatarGroupLabels, defaultAvatarLabels } from './types';
|
|
5
5
|
type AvatarCompound = typeof AvatarBase & {
|
|
6
6
|
Group: typeof AvatarGroup;
|
|
7
7
|
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Avatar as AvatarBase } from './Avatar.js';
|
|
2
2
|
import { AvatarGroup } from './AvatarGroup.js';
|
|
3
|
-
export { AVATAR_COLORS } from './types.js';
|
|
3
|
+
export { AVATAR_COLORS, defaultAvatarGroupLabels, defaultAvatarLabels } from './types.js';
|
|
4
4
|
export const Avatar = AvatarBase;
|
|
5
5
|
Avatar.Group = AvatarGroup;
|
|
6
6
|
// Named export for direct import
|
|
@@ -8,6 +8,14 @@ export type IndicatorPosition = 'top-left' | 'top-right' | 'bottom-left' | 'bott
|
|
|
8
8
|
*/
|
|
9
9
|
export type AvatarColor = 'coral' | 'amber' | 'lime' | 'teal' | 'cyan' | 'blue' | 'violet' | 'pink' | 'slate' | 'emerald';
|
|
10
10
|
export declare const AVATAR_COLORS: AvatarColor[];
|
|
11
|
+
/**
|
|
12
|
+
* Overridable strings for Avatar i18n.
|
|
13
|
+
*/
|
|
14
|
+
export type AvatarLabels = {
|
|
15
|
+
/** Accessible label when both name and indicator are present. @default `"${name}, ${indicatorLabel}"` */
|
|
16
|
+
description?: (name: string, indicatorLabel: string) => string;
|
|
17
|
+
};
|
|
18
|
+
export declare const defaultAvatarLabels: Required<AvatarLabels>;
|
|
11
19
|
export type AvatarProps = {
|
|
12
20
|
/** Image source URL */
|
|
13
21
|
src?: string;
|
|
@@ -31,9 +39,23 @@ export type AvatarProps = {
|
|
|
31
39
|
* Has no effect when `name` is not provided.
|
|
32
40
|
*/
|
|
33
41
|
tooltip?: boolean;
|
|
42
|
+
/** Overridable strings for i18n. */
|
|
43
|
+
labels?: AvatarLabels;
|
|
34
44
|
/** Additional CSS class */
|
|
35
45
|
className?: string;
|
|
36
46
|
};
|
|
47
|
+
/**
|
|
48
|
+
* Overridable strings for AvatarGroup i18n.
|
|
49
|
+
*/
|
|
50
|
+
export type AvatarGroupLabels = {
|
|
51
|
+
/** Label when some avatars are hidden. Default: `"${total} users, showing ${visible}"` */
|
|
52
|
+
group?: (total: number, visible: number) => string;
|
|
53
|
+
/** Label when all avatars are visible. Default: `"${total} users"` */
|
|
54
|
+
groupAll?: (total: number) => string;
|
|
55
|
+
/** Overflow badge label (AT-only). Default: `"${count} more users"` */
|
|
56
|
+
overflow?: (count: number) => string;
|
|
57
|
+
};
|
|
58
|
+
export declare const defaultAvatarGroupLabels: Required<AvatarGroupLabels>;
|
|
37
59
|
export type AvatarGroupProps = {
|
|
38
60
|
/** Maximum avatars to show before "+N" overflow */
|
|
39
61
|
max?: number;
|
|
@@ -48,6 +70,11 @@ export type AvatarGroupProps = {
|
|
|
48
70
|
* Default: "N users" or "N users, showing M".
|
|
49
71
|
*/
|
|
50
72
|
groupLabel?: (total: number, visible: number) => string;
|
|
73
|
+
/**
|
|
74
|
+
* Overridable strings for i18n. Coexists with `groupLabel` —
|
|
75
|
+
* `groupLabel` takes precedence for the group aria-label when provided.
|
|
76
|
+
*/
|
|
77
|
+
labels?: AvatarGroupLabels;
|
|
51
78
|
/** Children (Avatar components) */
|
|
52
79
|
children: React.ReactNode;
|
|
53
80
|
/** Additional CSS class */
|
|
@@ -10,3 +10,11 @@ export const AVATAR_COLORS = [
|
|
|
10
10
|
'slate',
|
|
11
11
|
'emerald',
|
|
12
12
|
];
|
|
13
|
+
export const defaultAvatarLabels = {
|
|
14
|
+
description: (name, indicatorLabel) => `${name}, ${indicatorLabel}`,
|
|
15
|
+
};
|
|
16
|
+
export const defaultAvatarGroupLabels = {
|
|
17
|
+
group: (total, visible) => `${total} users, showing ${visible}`,
|
|
18
|
+
groupAll: (total) => `${total} users`,
|
|
19
|
+
overflow: (count) => `${count} more users`,
|
|
20
|
+
};
|
|
@@ -3,9 +3,11 @@ import React, { forwardRef } from 'react';
|
|
|
3
3
|
import { cx } from '../../utils/cx.js';
|
|
4
4
|
import { Icon } from '../Icon/index.js';
|
|
5
5
|
import { getSafeRel } from '../../utils/polymorphic.js';
|
|
6
|
-
|
|
6
|
+
import { defaultButtonLabels } from './types.js';
|
|
7
|
+
export const Button = forwardRef(({ label, children, size = 'md', theme = 'primary', variant = 'solid', fullWidth, disabled = false, loading = false, loadingLabel: loadingLabelProp, labels: labelsProp, leftIconName, rightIconName, leftIcon, rightIcon, iconSize: iconSizeProp, className, target, rel, onClick, style, ...rest }, ref) => {
|
|
7
8
|
const isLink = typeof rest.href === 'string';
|
|
8
9
|
const isDisabled = disabled || loading;
|
|
10
|
+
const labels = { ...defaultButtonLabels, ...labelsProp };
|
|
9
11
|
// Auto-scale icon size with button size when not explicitly set
|
|
10
12
|
const iconSizeMap = { xs: 'xs', sm: 'xs', md: 'sm', lg: 'md' };
|
|
11
13
|
const iconSize = iconSizeProp ?? iconSizeMap[size];
|
|
@@ -32,7 +34,7 @@ export const Button = forwardRef(({ label, children, size = 'md', theme = 'prima
|
|
|
32
34
|
}
|
|
33
35
|
onClick?.(e);
|
|
34
36
|
};
|
|
35
|
-
return (_jsxs("a", { ref: ref, href: isDisabled ? undefined : href, className: classes, "aria-label": loadingLabel, "aria-disabled": isDisabled || undefined, "aria-busy": loading || undefined, tabIndex: isDisabled ? -1 : tabIndex, onClick: handleClick, "data-loading": loading || undefined, target: target, rel: safeRel, style: style, ...anchorRest, children: [content, target === '_blank' && (_jsx("span", { className: "tui-visually-hidden", children:
|
|
37
|
+
return (_jsxs("a", { ref: ref, href: isDisabled ? undefined : href, className: classes, "aria-label": loadingLabel, "aria-disabled": isDisabled || undefined, "aria-busy": loading || undefined, tabIndex: isDisabled ? -1 : tabIndex, onClick: handleClick, "data-loading": loading || undefined, target: target, rel: safeRel, style: style, ...anchorRest, children: [content, target === '_blank' && (_jsx("span", { className: "tui-visually-hidden", children: labels.newTab }))] }));
|
|
36
38
|
}
|
|
37
39
|
const buttonRest = rest;
|
|
38
40
|
return (_jsx("button", { ref: ref, type: buttonRest.type ?? 'button', className: classes, "aria-label": loadingLabel, disabled: isDisabled, "aria-busy": loading || undefined, onClick: onClick, "data-loading": loading || undefined, style: style, ...buttonRest, children: content }));
|
|
@@ -19,6 +19,11 @@ export type Theme = ThemeIntent | 'destructive';
|
|
|
19
19
|
* - `'link'`: Text-link styling, no background
|
|
20
20
|
*/
|
|
21
21
|
export type Variant = 'solid' | 'outline' | 'ghost' | 'link';
|
|
22
|
+
export type ButtonLabels = {
|
|
23
|
+
/** Label for the "(opens in new tab)" visually-hidden hint on external links. */
|
|
24
|
+
newTab?: string;
|
|
25
|
+
};
|
|
26
|
+
export declare const defaultButtonLabels: Required<ButtonLabels>;
|
|
22
27
|
type CommonProps = {
|
|
23
28
|
/**
|
|
24
29
|
* Button label text. If provided, renders as the button content.
|
|
@@ -68,6 +73,11 @@ type CommonProps = {
|
|
|
68
73
|
* @default `${label}, loading` (English)
|
|
69
74
|
*/
|
|
70
75
|
loadingLabel?: string;
|
|
76
|
+
/**
|
|
77
|
+
* Override default English strings for i18n.
|
|
78
|
+
* Covers strings not already handled by existing props (e.g. `loadingLabel`).
|
|
79
|
+
*/
|
|
80
|
+
labels?: ButtonLabels;
|
|
71
81
|
/**
|
|
72
82
|
* Link target (for anchor variant).
|
|
73
83
|
*/
|
|
@@ -8,13 +8,21 @@ import { isDev } from '../../utils/is-dev.js';
|
|
|
8
8
|
// Checkbox Component
|
|
9
9
|
// =============================================================================
|
|
10
10
|
//
|
|
11
|
-
//
|
|
11
|
+
// Custom <input type="checkbox"> with appearance: none, SVG checkmark/indeterminate
|
|
12
|
+
// icons via background-image, and token-driven colors.
|
|
12
13
|
//
|
|
13
14
|
// Bare (no label): returns <input> directly for Field.Control cloneElement.
|
|
14
15
|
// With label: wraps in <label class="tui-inline-choice">.
|
|
15
16
|
//
|
|
16
|
-
// CSS token API (
|
|
17
|
-
// --tui-
|
|
17
|
+
// CSS token API (component layer, read via fallback):
|
|
18
|
+
// --tui-checkbox-accent Accent color → --tui-input-accent → --tui-theme-primary-base
|
|
19
|
+
// --tui-checkbox-border Border color → --tui-color-border
|
|
20
|
+
// --tui-checkbox-border-invalid Invalid border → --tui-theme-danger-base
|
|
21
|
+
// --tui-checkbox-radius Border radius → --tui-radius-sm
|
|
22
|
+
// --tui-checkbox-bg Background → --tui-color-bg
|
|
23
|
+
// --tui-checkbox-size Font-size (controls box size via em units)
|
|
24
|
+
// --tui-checkbox-gap Gap between checkbox and label (labeled mode)
|
|
25
|
+
// --tui-checkbox-label-color Label text colour (labeled mode)
|
|
18
26
|
//
|
|
19
27
|
// =============================================================================
|
|
20
28
|
// Props that should route to the <input>, not the wrapper label
|
|
@@ -23,6 +31,7 @@ const INPUT_PROPS = new Set([
|
|
|
23
31
|
'name',
|
|
24
32
|
'value',
|
|
25
33
|
'aria-describedby',
|
|
34
|
+
'aria-errormessage',
|
|
26
35
|
'aria-invalid',
|
|
27
36
|
'aria-required',
|
|
28
37
|
'aria-label',
|
|
@@ -31,6 +40,9 @@ const INPUT_PROPS = new Set([
|
|
|
31
40
|
'required',
|
|
32
41
|
'tabIndex',
|
|
33
42
|
'autoFocus',
|
|
43
|
+
'onClick',
|
|
44
|
+
'onKeyDown',
|
|
45
|
+
'onKeyUp',
|
|
34
46
|
'onFocus',
|
|
35
47
|
'onBlur',
|
|
36
48
|
]);
|
|
@@ -67,16 +79,21 @@ export const Checkbox = forwardRef(function Checkbox({ checked: controlledChecke
|
|
|
67
79
|
}
|
|
68
80
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
69
81
|
}, []);
|
|
70
|
-
const handleChange = () => {
|
|
71
|
-
//
|
|
72
|
-
|
|
82
|
+
const handleChange = (e) => {
|
|
83
|
+
// Use native checked value — clicking an indeterminate checkbox produces
|
|
84
|
+
// checked=true, which !prev would incorrectly invert to false.
|
|
85
|
+
setChecked(e.target.checked);
|
|
73
86
|
};
|
|
74
87
|
const isChecked = checked ?? false;
|
|
75
|
-
//
|
|
88
|
+
// Shared input element — single source of truth for both render paths.
|
|
89
|
+
// In labeled mode, extraProps contains only INPUT_PROPS; in bare mode,
|
|
90
|
+
// all rest props go directly on the input.
|
|
91
|
+
const renderInput = (extraProps, extraClass) => (_jsx("input", { ref: composeRefs(internalRef, externalRef), type: "checkbox", checked: isChecked, disabled: disabled, className: extraClass, "aria-checked": indeterminate && !isChecked ? 'mixed' : undefined, onChange: handleChange, ...extraProps }));
|
|
92
|
+
// Bare: no label — Field.Control can inject id/aria-* via cloneElement
|
|
76
93
|
if (!label) {
|
|
77
|
-
return (
|
|
94
|
+
return renderInput(rest, className);
|
|
78
95
|
}
|
|
79
|
-
//
|
|
96
|
+
// Labeled: split rest props between input and wrapper
|
|
80
97
|
const inputProps = {};
|
|
81
98
|
const wrapperProps = {};
|
|
82
99
|
for (const [key, val] of Object.entries(rest)) {
|
|
@@ -87,6 +104,24 @@ export const Checkbox = forwardRef(function Checkbox({ checked: controlledChecke
|
|
|
87
104
|
wrapperProps[key] = val;
|
|
88
105
|
}
|
|
89
106
|
}
|
|
90
|
-
//
|
|
91
|
-
|
|
107
|
+
// DEV: warn if input-like props leaked to the wrapper
|
|
108
|
+
if (isDev()) {
|
|
109
|
+
const suspect = Object.keys(wrapperProps).filter((k) => k.startsWith('aria-') || k.startsWith('on') || k === 'tabIndex');
|
|
110
|
+
if (suspect.length > 0) {
|
|
111
|
+
console.warn(`Checkbox: Props [${suspect.join(', ')}] ended up on the <label> wrapper. ` +
|
|
112
|
+
'Add them to INPUT_PROPS if they belong on the <input>.');
|
|
113
|
+
}
|
|
114
|
+
// Warn about dual-labelling conflicts
|
|
115
|
+
if (inputProps['aria-labelledby']) {
|
|
116
|
+
console.warn('Checkbox: Both `label` prop and `aria-labelledby` are present. ' +
|
|
117
|
+
'`aria-labelledby` takes precedence — the `label` prop text will not be the accessible name. ' +
|
|
118
|
+
'Prefer one labelling mechanism.');
|
|
119
|
+
}
|
|
120
|
+
if (inputProps['aria-label']) {
|
|
121
|
+
console.warn('Checkbox: Both `label` prop and `aria-label` are present. ' +
|
|
122
|
+
'`aria-label` takes precedence — the visible label text will not be the accessible name.');
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return (_jsxs("label", { className: cx('tui-inline-choice', disabled && 'is-disabled', className), ...wrapperProps, children: [renderInput(inputProps), _jsx("span", { children: label })] }));
|
|
92
126
|
});
|
|
127
|
+
Checkbox.displayName = 'Checkbox';
|
|
@@ -4,6 +4,15 @@ export type CheckboxProps = Omit<InputHTMLAttributes<HTMLInputElement>, 'type' |
|
|
|
4
4
|
defaultChecked?: boolean;
|
|
5
5
|
onCheckedChange?: (checked: boolean) => void;
|
|
6
6
|
indeterminate?: boolean;
|
|
7
|
+
/**
|
|
8
|
+
* When provided, renders the checkbox inside a `<label>` with this content.
|
|
9
|
+
* When omitted, renders a bare `<input>` — you must provide an accessible name
|
|
10
|
+
* via `aria-label`, `aria-labelledby`, or a wrapping `Field.Control` + `Field.Label`.
|
|
11
|
+
*
|
|
12
|
+
* @remarks When used inside `Field.Control`, `Field.Label` injects `aria-labelledby`
|
|
13
|
+
* onto the input. If both `label` and `Field.Label` are present, AT may concatenate
|
|
14
|
+
* both names. Prefer one labelling mechanism — either `label` prop or `Field.Label`.
|
|
15
|
+
*/
|
|
7
16
|
label?: ReactNode;
|
|
8
17
|
disabled?: boolean;
|
|
9
18
|
className?: string;
|
|
@@ -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, clearable, '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, labels: labelsProp, children, }: ComboboxProps): import("react/jsx-runtime").JSX.Element;
|
|
3
3
|
declare namespace ComboboxRoot {
|
|
4
4
|
var displayName: string;
|
|
5
5
|
}
|
|
@@ -9,13 +9,15 @@ import { hashForId } from '../../utils/hash.js';
|
|
|
9
9
|
import { composeEventHandlers } from '../../utils/compose-events.js';
|
|
10
10
|
import { Icon } from '../Icon/index.js';
|
|
11
11
|
import { ComboboxActionsContext, ComboboxStateContext, ComboboxContentContext, useComboboxContext, useComboboxContentContext, } from './ComboboxContext.js';
|
|
12
|
+
import { defaultComboboxLabels } from './types.js';
|
|
12
13
|
// =============================================================================
|
|
13
14
|
// Combobox Root
|
|
14
15
|
// =============================================================================
|
|
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
|
+
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, labels: labelsProp, children, }) {
|
|
17
|
+
const labels = { ...defaultComboboxLabels, ...labelsProp };
|
|
16
18
|
// Controlled/uncontrolled value (initialize from defaultValue)
|
|
17
19
|
const [uncontrolledValue, setUncontrolledValue] = useState(defaultValue);
|
|
18
|
-
const isValueControlled = controlledValue !== undefined;
|
|
20
|
+
const isValueControlled = useRef(controlledValue !== undefined).current;
|
|
19
21
|
const value = isValueControlled ? controlledValue : uncontrolledValue;
|
|
20
22
|
// Controlled/uncontrolled inputValue
|
|
21
23
|
const [uncontrolledInputValue, setUncontrolledInputValue] = useState('');
|
|
@@ -41,6 +43,9 @@ function ComboboxRoot({ value: controlledValue, defaultValue, onValueChange, inp
|
|
|
41
43
|
// Option registration
|
|
42
44
|
const optionsRef = useRef(new Map());
|
|
43
45
|
const [registryVersion, setRegistryVersion] = useState(0);
|
|
46
|
+
// Track open state via ref so unregisterOption can check synchronously
|
|
47
|
+
const openRef = useRef(false);
|
|
48
|
+
openRef.current = open;
|
|
44
49
|
// IDs
|
|
45
50
|
const baseId = useId();
|
|
46
51
|
const inputId = `${baseId}-input`;
|
|
@@ -153,9 +158,22 @@ function ComboboxRoot({ value: controlledValue, defaultValue, onValueChange, inp
|
|
|
153
158
|
setRegistryVersion((v) => v + 1);
|
|
154
159
|
}, []);
|
|
155
160
|
const unregisterOption = useCallback((optionValue) => {
|
|
161
|
+
// When the dropdown closes, options unmount and call unregister in cleanup.
|
|
162
|
+
// Skip the delete to preserve the registry — filtering and input display
|
|
163
|
+
// depend on it while closed. Options re-register on next open (same keys).
|
|
164
|
+
if (!openRef.current)
|
|
165
|
+
return;
|
|
156
166
|
optionsRef.current.delete(toKey(optionValue));
|
|
157
167
|
setRegistryVersion((v) => v + 1);
|
|
158
168
|
}, []);
|
|
169
|
+
// Flush stale registry on open. Options that were registered before close
|
|
170
|
+
// may no longer exist (parent changed children while closed). Clearing
|
|
171
|
+
// before the new options mount ensures no orphaned entries accumulate.
|
|
172
|
+
useLayoutEffect(() => {
|
|
173
|
+
if (open) {
|
|
174
|
+
optionsRef.current.clear();
|
|
175
|
+
}
|
|
176
|
+
}, [open]);
|
|
159
177
|
// Active option ID for aria-activedescendant (hash-based for stability during filtering)
|
|
160
178
|
const activeOptionId = activeIndex >= 0 && orderedOptions[activeIndex]
|
|
161
179
|
? `${listboxId}-opt-${hashForId(toKey(orderedOptions[activeIndex].value))}`
|
|
@@ -291,6 +309,7 @@ function ComboboxRoot({ value: controlledValue, defaultValue, onValueChange, inp
|
|
|
291
309
|
// Handle clear button - clears both input text and selected value
|
|
292
310
|
const handleClear = useCallback((e) => {
|
|
293
311
|
e.preventDefault(); // Keep focus in input
|
|
312
|
+
e.stopPropagation(); // Don't open dropdown
|
|
294
313
|
setInputValue('');
|
|
295
314
|
clearValue();
|
|
296
315
|
inputRef.current?.focus();
|
|
@@ -365,7 +384,7 @@ function ComboboxRoot({ value: controlledValue, defaultValue, onValueChange, inp
|
|
|
365
384
|
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
385
|
inputRef.current = node;
|
|
367
386
|
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 }), clearable && inputValue && !disabled && (_jsx("
|
|
387
|
+
}, 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("button", { type: "button", className: "tui-combobox__clear", onClick: handleClear, onMouseDown: (e) => e.preventDefault(), "aria-label": labels.clear, tabIndex: -1, 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
388
|
}
|
|
370
389
|
ComboboxRoot.displayName = 'Combobox';
|
|
371
390
|
// =============================================================================
|
|
@@ -373,6 +392,12 @@ ComboboxRoot.displayName = 'Combobox';
|
|
|
373
392
|
// =============================================================================
|
|
374
393
|
function ComboboxContentComponent({ className, children }) {
|
|
375
394
|
const { open, listboxId, inputId, refs, floatingStyles, getFloatingProps, listRef, activeIndex, orderedOptions, } = useComboboxContext();
|
|
395
|
+
// Track whether dropdown has ever been opened. Before first open, mount
|
|
396
|
+
// children in a hidden div for option registration (defaultValue resolution).
|
|
397
|
+
// After first open, only mount children when open (in portal).
|
|
398
|
+
const hasEverOpened = useRef(false);
|
|
399
|
+
if (open)
|
|
400
|
+
hasEverOpened.current = true;
|
|
376
401
|
const portalRoot = getPortalRootFor(refs.reference.current);
|
|
377
402
|
const contentContext = useMemo(() => ({
|
|
378
403
|
listRef,
|
|
@@ -380,7 +405,7 @@ function ComboboxContentComponent({ className, children }) {
|
|
|
380
405
|
orderedOptions,
|
|
381
406
|
}), [listRef, activeIndex, orderedOptions]);
|
|
382
407
|
// Always render for option registration
|
|
383
|
-
return (_jsxs(_Fragment, { children: [!open && (_jsx("div", { style: { display: 'none' }, "aria-hidden": "true", children: _jsx(ComboboxContentContext.Provider, { value: contentContext, children: children }) })), open && (_jsx(FloatingPortal, { root: portalRoot, children: _jsx("div", { ref: refs.setFloating, id: listboxId, role: "listbox", "aria-labelledby": inputId, className: cx('tui-combobox__content', className), style: {
|
|
408
|
+
return (_jsxs(_Fragment, { children: [!open && !hasEverOpened.current && (_jsx("div", { id: listboxId, role: "listbox", style: { display: 'none' }, "aria-hidden": "true", children: _jsx(ComboboxContentContext.Provider, { value: contentContext, children: children }) })), open && (_jsx(FloatingPortal, { root: portalRoot, children: _jsx("div", { ref: refs.setFloating, id: listboxId, role: "listbox", "aria-labelledby": inputId, className: cx('tui-combobox__content', className), style: {
|
|
384
409
|
...floatingStyles,
|
|
385
410
|
minWidth: refs.reference.current?.offsetWidth,
|
|
386
411
|
pointerEvents: 'auto',
|
|
@@ -395,9 +420,11 @@ function ComboboxOptionComponent({ value: optionValue, disabled = false, textVal
|
|
|
395
420
|
const { listRef, activeIndex, orderedOptions } = useComboboxContentContext();
|
|
396
421
|
const ref = useRef(null);
|
|
397
422
|
const textValue = explicitTextValue ?? (typeof children === 'string' ? children : '');
|
|
398
|
-
// Warn in dev if textValue couldn't be derived
|
|
423
|
+
// Warn in dev if textValue couldn't be derived (fire once per mount)
|
|
424
|
+
const warnedTextValueRef = useRef(false);
|
|
399
425
|
useEffect(() => {
|
|
400
|
-
if (isDev() && !textValue) {
|
|
426
|
+
if (isDev() && !textValue && !warnedTextValueRef.current) {
|
|
427
|
+
warnedTextValueRef.current = true;
|
|
401
428
|
console.warn(`Combobox.Option with value="${optionValue}" has no textValue. Provide textValue prop when children is not a string.`);
|
|
402
429
|
}
|
|
403
430
|
}, [textValue, optionValue]);
|
|
@@ -440,7 +467,16 @@ ComboboxOptionComponent.displayName = 'Combobox.Option';
|
|
|
440
467
|
// =============================================================================
|
|
441
468
|
function ComboboxGroupComponent({ className, children }) {
|
|
442
469
|
const groupId = useId();
|
|
443
|
-
|
|
470
|
+
const groupRef = useRef(null);
|
|
471
|
+
const [hasLabel, setHasLabel] = useState(false);
|
|
472
|
+
// Check if a Label child rendered — guard aria-labelledby to prevent dangling reference
|
|
473
|
+
useLayoutEffect(() => {
|
|
474
|
+
if (groupRef.current) {
|
|
475
|
+
const labelEl = groupRef.current.querySelector(`#${CSS.escape(`${groupId}-label`)}`);
|
|
476
|
+
setHasLabel(!!labelEl);
|
|
477
|
+
}
|
|
478
|
+
}, [groupId, children]);
|
|
479
|
+
return (_jsx("div", { ref: groupRef, role: "group", "aria-labelledby": hasLabel ? `${groupId}-label` : undefined, className: cx('tui-combobox__group', className), children: _jsx(ComboboxGroupContext.Provider, { value: { groupId }, children: children }) }));
|
|
444
480
|
}
|
|
445
481
|
ComboboxGroupComponent.displayName = 'Combobox.Group';
|
|
446
482
|
const ComboboxGroupContext = React.createContext(null);
|
|
@@ -449,6 +485,13 @@ const ComboboxGroupContext = React.createContext(null);
|
|
|
449
485
|
// =============================================================================
|
|
450
486
|
function ComboboxLabelComponent({ className, children }) {
|
|
451
487
|
const groupContext = React.useContext(ComboboxGroupContext);
|
|
488
|
+
const warnedRef = useRef(false);
|
|
489
|
+
useEffect(() => {
|
|
490
|
+
if (isDev() && !groupContext && !warnedRef.current) {
|
|
491
|
+
warnedRef.current = true;
|
|
492
|
+
console.warn('Combobox.Label should be used inside Combobox.Group for accessibility.');
|
|
493
|
+
}
|
|
494
|
+
}, [groupContext]);
|
|
452
495
|
return (_jsx("div", { id: groupContext ? `${groupContext.groupId}-label` : undefined, className: cx('tui-combobox__label', className), children: children }));
|
|
453
496
|
}
|
|
454
497
|
ComboboxLabelComponent.displayName = 'Combobox.Label';
|
|
@@ -1,2 +1,3 @@
|
|
|
1
1
|
export { Combobox, ComboboxContent, ComboboxOption, ComboboxGroup, ComboboxLabel, useCombobox, } from './Combobox';
|
|
2
|
-
export type { ComboboxProps, ComboboxContentProps, ComboboxOptionProps, ComboboxGroupProps, ComboboxLabelProps, FilterMode as ComboboxFilterMode, RegisteredOption as ComboboxRegisteredOption, } from './types';
|
|
2
|
+
export type { ComboboxProps, ComboboxContentProps, ComboboxOptionProps, ComboboxGroupProps, ComboboxLabelProps, ComboboxLabels, FilterMode as ComboboxFilterMode, RegisteredOption as ComboboxRegisteredOption, } from './types';
|
|
3
|
+
export { defaultComboboxLabels } from './types';
|
|
@@ -2,6 +2,11 @@ import type { FloatingContext, ReferenceType } from '@floating-ui/react';
|
|
|
2
2
|
import type { RefObject, CSSProperties, MutableRefObject } from 'react';
|
|
3
3
|
import type { SizeStandard } from '../../types/sizes';
|
|
4
4
|
import type { OptionValue } from '../../utils/value-key';
|
|
5
|
+
export type ComboboxLabels = {
|
|
6
|
+
/** Label for the clear button. */
|
|
7
|
+
clear?: string;
|
|
8
|
+
};
|
|
9
|
+
export declare const defaultComboboxLabels: Required<ComboboxLabels>;
|
|
5
10
|
/**
|
|
6
11
|
* Controls when filtering should be active.
|
|
7
12
|
* - 'always': Consumer filters whenever inputValue changes (default)
|
|
@@ -96,6 +101,10 @@ export type ComboboxProps = {
|
|
|
96
101
|
* Use for utilities like `tui-input-reset` that must target the input itself.
|
|
97
102
|
*/
|
|
98
103
|
inputClassName?: string;
|
|
104
|
+
/**
|
|
105
|
+
* Override default English strings for i18n.
|
|
106
|
+
*/
|
|
107
|
+
labels?: ComboboxLabels;
|
|
99
108
|
children: React.ReactNode;
|
|
100
109
|
};
|
|
101
110
|
export type ComboboxContentProps = {
|
|
@@ -7,7 +7,7 @@ declare function DropdownTriggerComponent({ asChild, children }: DropdownTrigger
|
|
|
7
7
|
declare namespace DropdownTriggerComponent {
|
|
8
8
|
var displayName: string;
|
|
9
9
|
}
|
|
10
|
-
declare function DropdownContentComponent(
|
|
10
|
+
declare function DropdownContentComponent(props: DropdownContentProps): import("react/jsx-runtime").JSX.Element | null;
|
|
11
11
|
declare namespace DropdownContentComponent {
|
|
12
12
|
var displayName: string;
|
|
13
13
|
}
|