@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
|
@@ -25,45 +25,50 @@ function SidebarRoot(props) {
|
|
|
25
25
|
}, [open]);
|
|
26
26
|
const isClosing = drawer && !open && visible;
|
|
27
27
|
const shouldRender = drawer && (open || visible);
|
|
28
|
+
// Dismiss: set visible false and restore focus to the element that opened
|
|
29
|
+
// the drawer. Called from both the reduced-motion layout effect and the
|
|
30
|
+
// animation-end handler — these are the actual unmount points, so focus
|
|
31
|
+
// must be restored here (not in a useEffect, which may not fire before
|
|
32
|
+
// the synchronous re-render unmounts the component).
|
|
33
|
+
const dismiss = useCallback(() => {
|
|
34
|
+
setVisible(false);
|
|
35
|
+
const el = restoreRef.current;
|
|
36
|
+
if (el && typeof el.focus === 'function' && document.contains(el)) {
|
|
37
|
+
el.focus();
|
|
38
|
+
}
|
|
39
|
+
restoreRef.current = null;
|
|
40
|
+
}, []);
|
|
28
41
|
// When closing, check if animation will actually play (reduced motion).
|
|
29
|
-
// If not,
|
|
42
|
+
// If not, dismiss immediately to avoid getting stuck.
|
|
30
43
|
useLayoutEffect(() => {
|
|
31
44
|
if (!isClosing)
|
|
32
45
|
return;
|
|
33
46
|
const panel = panelRef.current;
|
|
34
47
|
if (!panel) {
|
|
35
|
-
|
|
48
|
+
dismiss();
|
|
36
49
|
return;
|
|
37
50
|
}
|
|
38
51
|
const style = getComputedStyle(panel);
|
|
39
52
|
if (!style.animationName || style.animationName === 'none') {
|
|
40
|
-
|
|
53
|
+
dismiss();
|
|
41
54
|
}
|
|
42
|
-
}, [isClosing]);
|
|
55
|
+
}, [isClosing, dismiss]);
|
|
43
56
|
const handleAnimationEnd = useCallback((e) => {
|
|
44
57
|
if (e.target === panelRef.current) {
|
|
45
|
-
|
|
58
|
+
dismiss();
|
|
46
59
|
}
|
|
47
|
-
}, []);
|
|
60
|
+
}, [dismiss]);
|
|
48
61
|
const rootClassName = ['tui-sidebar', className].filter(Boolean).join(' ');
|
|
49
62
|
// ---------------------------------------------------------------------------
|
|
50
63
|
// Drawer mode: capture trigger for focus restoration
|
|
51
64
|
// ---------------------------------------------------------------------------
|
|
52
|
-
|
|
53
|
-
|
|
65
|
+
// Capture the element that had focus when the drawer opens.
|
|
66
|
+
// Must be a layout effect so it runs BEFORE the initial focus layout effect
|
|
67
|
+
// moves focus into the drawer. Focus restoration is handled by `dismiss()`.
|
|
68
|
+
useLayoutEffect(() => {
|
|
69
|
+
if (!drawer || !isBrowser || !open)
|
|
54
70
|
return;
|
|
55
|
-
|
|
56
|
-
// Capture focus target when opening
|
|
57
|
-
restoreRef.current = document.activeElement;
|
|
58
|
-
}
|
|
59
|
-
else {
|
|
60
|
-
// Restore focus when closing (with DOM containment guard)
|
|
61
|
-
const el = restoreRef.current;
|
|
62
|
-
if (el && typeof el.focus === 'function' && document.contains(el)) {
|
|
63
|
-
el.focus();
|
|
64
|
-
}
|
|
65
|
-
restoreRef.current = null;
|
|
66
|
-
}
|
|
71
|
+
restoreRef.current = document.activeElement;
|
|
67
72
|
}, [drawer, open]);
|
|
68
73
|
// ---------------------------------------------------------------------------
|
|
69
74
|
// Drawer mode: body scroll lock
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import type { StepIndicatorProps } from './types';
|
|
2
|
-
export declare
|
|
2
|
+
export declare const StepIndicator: import("react").NamedExoticComponent<StepIndicatorProps>;
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { memo } from 'react';
|
|
3
|
+
import { defaultStepIndicatorLabels } from './types.js';
|
|
2
4
|
import { Progress } from '../Progress/index.js';
|
|
3
5
|
import { Icon } from '../Icon/index.js';
|
|
4
6
|
// =============================================================================
|
|
@@ -8,12 +10,6 @@ const statusIcons = {
|
|
|
8
10
|
complete: 'lms/checkmark',
|
|
9
11
|
locked: 'lms/lock',
|
|
10
12
|
};
|
|
11
|
-
const statusLabels = {
|
|
12
|
-
'not-started': 'Not started',
|
|
13
|
-
'in-progress': 'In progress',
|
|
14
|
-
complete: 'Complete',
|
|
15
|
-
locked: 'Locked',
|
|
16
|
-
};
|
|
17
13
|
// =============================================================================
|
|
18
14
|
// Status inference
|
|
19
15
|
// =============================================================================
|
|
@@ -27,8 +23,9 @@ function inferStatus(value) {
|
|
|
27
23
|
// =============================================================================
|
|
28
24
|
// Component
|
|
29
25
|
// =============================================================================
|
|
30
|
-
export function StepIndicator(props) {
|
|
31
|
-
const { value = 0, status: statusOverride, icon: customIcon, size = 'sm', showValue = false, label, className, } = props;
|
|
26
|
+
export const StepIndicator = memo(function StepIndicator(props) {
|
|
27
|
+
const { value = 0, status: statusOverride, icon: customIcon, size = 'sm', showValue = false, label, className, labels: labelsProp, } = props;
|
|
28
|
+
const labels = { ...defaultStepIndicatorLabels, ...labelsProp };
|
|
32
29
|
// Infer status from value, allow override
|
|
33
30
|
const status = statusOverride ?? inferStatus(value);
|
|
34
31
|
// When status is explicit, the visual should match the status, not the value
|
|
@@ -48,8 +45,15 @@ export function StepIndicator(props) {
|
|
|
48
45
|
const iconName = hasStatusIcon ? statusIcons[status] : customIcon;
|
|
49
46
|
// Use solid variant for complete state (filled circle)
|
|
50
47
|
const variant = status === 'complete' ? 'solid' : 'ring';
|
|
48
|
+
// Resolve status display name from labels
|
|
49
|
+
const statusLabelMap = {
|
|
50
|
+
'not-started': labels.notStarted,
|
|
51
|
+
'in-progress': labels.inProgress,
|
|
52
|
+
complete: labels.complete,
|
|
53
|
+
locked: labels.locked,
|
|
54
|
+
};
|
|
51
55
|
// Accessible label
|
|
52
|
-
const ariaLabel = label ??
|
|
56
|
+
const ariaLabel = label ?? labels.status(statusLabelMap[status]);
|
|
53
57
|
// Build class names
|
|
54
58
|
const rootClassName = [
|
|
55
59
|
'tui-step-indicator',
|
|
@@ -61,4 +65,4 @@ export function StepIndicator(props) {
|
|
|
61
65
|
.filter(Boolean)
|
|
62
66
|
.join(' ');
|
|
63
67
|
return (_jsx("div", { className: rootClassName, children: _jsxs(Progress, { mode: "circle", variant: variant, size: size, value: displayValue, showLabels: showValue || hasIcon, labelPosition: "inside", "aria-label": ariaLabel, children: [hasIcon && iconName && (_jsx(Icon, { name: iconName })), showValue && status === 'in-progress' && (_jsxs("span", { className: "tui-step-indicator__value", children: [Math.round(displayValue), "%"] }))] }) }));
|
|
64
|
-
}
|
|
68
|
+
});
|
|
@@ -9,6 +9,19 @@ export type { Size };
|
|
|
9
9
|
* - `locked`: Ring with lock icon (overrides value-based inference)
|
|
10
10
|
*/
|
|
11
11
|
export type StepStatus = 'not-started' | 'in-progress' | 'complete' | 'locked';
|
|
12
|
+
export type StepIndicatorLabels = {
|
|
13
|
+
/** Label for "Not started" status. */
|
|
14
|
+
notStarted?: string;
|
|
15
|
+
/** Label for "In progress" status. */
|
|
16
|
+
inProgress?: string;
|
|
17
|
+
/** Label for "Complete" status. */
|
|
18
|
+
complete?: string;
|
|
19
|
+
/** Label for "Locked" status. */
|
|
20
|
+
locked?: string;
|
|
21
|
+
/** Full aria-label template. Receives the resolved status label string. */
|
|
22
|
+
status?: (statusLabel: string) => string;
|
|
23
|
+
};
|
|
24
|
+
export declare const defaultStepIndicatorLabels: Required<StepIndicatorLabels>;
|
|
12
25
|
export type StepIndicatorProps = {
|
|
13
26
|
/**
|
|
14
27
|
* Progress value (0-100).
|
|
@@ -65,4 +78,9 @@ export type StepIndicatorProps = {
|
|
|
65
78
|
* Additional CSS class names.
|
|
66
79
|
*/
|
|
67
80
|
className?: string;
|
|
81
|
+
/**
|
|
82
|
+
* Override default English strings for i18n.
|
|
83
|
+
* Covers status display names and the aria-label template.
|
|
84
|
+
*/
|
|
85
|
+
labels?: StepIndicatorLabels;
|
|
68
86
|
};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
+
import type { BulkActionsBarLabels } from './types';
|
|
2
3
|
type BulkActionsBarProps = {
|
|
3
4
|
count: number;
|
|
4
5
|
onClear?: () => void;
|
|
@@ -7,6 +8,8 @@ type BulkActionsBarProps = {
|
|
|
7
8
|
className?: string;
|
|
8
9
|
/** ID of the table element this toolbar controls */
|
|
9
10
|
'aria-controls'?: string;
|
|
11
|
+
/** Override hardcoded English strings for i18n. */
|
|
12
|
+
labels?: BulkActionsBarLabels;
|
|
10
13
|
};
|
|
11
|
-
export declare function BulkActionsBar({ count, onClear, clearLabel, children, className, 'aria-controls': ariaControls, }: BulkActionsBarProps): import("react/jsx-runtime").JSX.Element | null;
|
|
14
|
+
export declare function BulkActionsBar({ count, onClear, clearLabel, children, className, 'aria-controls': ariaControls, labels: labelsProp, }: BulkActionsBarProps): import("react/jsx-runtime").JSX.Element | null;
|
|
12
15
|
export {};
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import React from 'react';
|
|
3
3
|
import { cx } from '../../utils/cx.js';
|
|
4
|
-
|
|
4
|
+
import { defaultBulkActionsBarLabels } from './types.js';
|
|
5
|
+
export function BulkActionsBar({ count, onClear, clearLabel = 'Clear', children, className, 'aria-controls': ariaControls, labels: labelsProp, }) {
|
|
6
|
+
const labels = { ...defaultBulkActionsBarLabels, ...labelsProp };
|
|
5
7
|
if (count === 0)
|
|
6
8
|
return null;
|
|
7
|
-
|
|
8
|
-
return (_jsxs("div", { role: "toolbar", "aria-label": label, "aria-controls": ariaControls, className: cx('tui-table__bulkActions', className), children: [_jsxs("span", { className: "tui-table__bulkActions__count", role: "status", children: [count, " selected"] }), _jsxs("div", { className: "tui-table__bulkActions__actions", children: [children, onClear && (_jsx("button", { type: "button", onClick: onClear, className: "tui-table__bulkActions__clear tui-button is-style-outline is-size-xs", children: clearLabel }))] })] }));
|
|
9
|
+
return (_jsxs("div", { role: "toolbar", "aria-label": labels.actions(count), "aria-controls": ariaControls, className: cx('tui-table__bulkActions', className), children: [_jsx("span", { className: "tui-table__bulkActions__count", role: "status", children: labels.selected(count) }), _jsxs("div", { className: "tui-table__bulkActions__actions", children: [children, onClear && (_jsx("button", { type: "button", onClick: onClear, className: "tui-table__bulkActions__clear tui-button is-style-outline is-size-xs", children: clearLabel }))] })] }));
|
|
9
10
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
2
|
import type { ColumnDef, SortingState, Table as TanTable, RowSelectionState, Updater } from '@tanstack/react-table';
|
|
3
|
+
import type { DataTableLabels } from './types';
|
|
3
4
|
type RowId = string | number;
|
|
4
5
|
type BulkSelectionRenderArgs<T> = {
|
|
5
6
|
selectedRows: T[];
|
|
@@ -17,6 +18,8 @@ export type DataTableProps<T> = {
|
|
|
17
18
|
loading?: boolean;
|
|
18
19
|
loadingMessage?: React.ReactNode;
|
|
19
20
|
emptyMessage?: React.ReactNode;
|
|
21
|
+
/** Override hardcoded English strings for i18n. Unset keys use English defaults. */
|
|
22
|
+
labels?: DataTableLabels;
|
|
20
23
|
renderPagination?: (table: TanTable<T>) => React.ReactNode;
|
|
21
24
|
paginationMode?: 'simple' | 'ends' | 'full' | 'smart';
|
|
22
25
|
paginationMaxNumbers?: number;
|
|
@@ -31,5 +34,5 @@ export type DataTableProps<T> = {
|
|
|
31
34
|
onSelectionChange?: (selectedRows: T[], selectedIds: (string | number)[]) => void;
|
|
32
35
|
renderBulkBar?: (args: BulkSelectionRenderArgs<T>) => React.ReactNode;
|
|
33
36
|
};
|
|
34
|
-
export declare function DataTable<T>({ data, columns, className, 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, defaultSorting, loading, loadingMessage, emptyMessage, renderPagination, paginationMode, paginationMaxNumbers, paginationNavStyle, pageSizeOptions, initialPageSize, enableRowSelection, getRowId, getRowLabelForSelection, onSelectionChange, renderBulkBar, rowSelection, onRowSelectionChange, }: DataTableProps<T>): import("react/jsx-runtime").JSX.Element;
|
|
37
|
+
export declare function DataTable<T>({ data, columns, className, 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, defaultSorting, loading, loadingMessage, emptyMessage, labels: labelsProp, renderPagination, paginationMode, paginationMaxNumbers, paginationNavStyle, pageSizeOptions, initialPageSize, enableRowSelection, getRowId, getRowLabelForSelection, onSelectionChange, renderBulkBar, rowSelection, onRowSelectionChange, }: DataTableProps<T>): import("react/jsx-runtime").JSX.Element;
|
|
35
38
|
export {};
|
|
@@ -4,9 +4,11 @@ import { cx } from '../../utils/cx.js';
|
|
|
4
4
|
import { useReactTable, getCoreRowModel, getSortedRowModel, getPaginationRowModel, flexRender } from '@tanstack/react-table';
|
|
5
5
|
import { BulkActionsBar } from './BulkActionsBar.js';
|
|
6
6
|
import { Pagination } from './Pagination.js';
|
|
7
|
-
|
|
7
|
+
import { defaultDataTableLabels } from './types.js';
|
|
8
|
+
export function DataTable({ data, columns, className, 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, defaultSorting = [], loading = false, loadingMessage = 'Loading...', emptyMessage = 'No results', labels: labelsProp, renderPagination, paginationMode = 'smart', paginationMaxNumbers = 7, paginationNavStyle = 'text', pageSizeOptions = [10, 20, 50], initialPageSize,
|
|
8
9
|
// selection
|
|
9
10
|
enableRowSelection = false, getRowId, getRowLabelForSelection, onSelectionChange, renderBulkBar, rowSelection, onRowSelectionChange, }) {
|
|
11
|
+
const labels = React.useMemo(() => ({ ...defaultDataTableLabels, ...labelsProp }), [labelsProp]);
|
|
10
12
|
const tableId = React.useId();
|
|
11
13
|
const [sorting, setSorting] = React.useState(defaultSorting);
|
|
12
14
|
const [uncontrolledRowSelection, setUncontrolledRowSelection] = React.useState({});
|
|
@@ -29,7 +31,7 @@ enableRowSelection = false, getRowId, getRowLabelForSelection, onSelectionChange
|
|
|
29
31
|
const isAllSelected = table.getIsAllPageRowsSelected();
|
|
30
32
|
const isSomeSelected = table.getIsSomePageRowsSelected();
|
|
31
33
|
const isIndeterminate = isSomeSelected && !isAllSelected;
|
|
32
|
-
return (_jsx("input", { type: "checkbox", "aria-label":
|
|
34
|
+
return (_jsx("input", { type: "checkbox", "aria-label": labels.selectAll, "aria-checked": isIndeterminate ? 'mixed' : isAllSelected, checked: isAllSelected, ref: (el) => {
|
|
33
35
|
if (el)
|
|
34
36
|
el.indeterminate = isIndeterminate;
|
|
35
37
|
}, onChange: table.getToggleAllPageRowsSelectedHandler() }));
|
|
@@ -37,7 +39,7 @@ enableRowSelection = false, getRowId, getRowLabelForSelection, onSelectionChange
|
|
|
37
39
|
cell: ({ row, table }) => {
|
|
38
40
|
const idx = row.index;
|
|
39
41
|
const original = row.original;
|
|
40
|
-
const labelText = getRowLabelForSelection?.(original, idx) ??
|
|
42
|
+
const labelText = getRowLabelForSelection?.(original, idx) ?? labels.selectRow(idx);
|
|
41
43
|
const handleChange = (e) => {
|
|
42
44
|
const willSelect = e.target.checked;
|
|
43
45
|
const isShift = e.nativeEvent.shiftKey;
|
|
@@ -75,7 +77,7 @@ enableRowSelection = false, getRowId, getRowLabelForSelection, onSelectionChange
|
|
|
75
77
|
},
|
|
76
78
|
enableSorting: false,
|
|
77
79
|
enableHiding: false,
|
|
78
|
-
}), [getRowLabelForSelection]);
|
|
80
|
+
}), [getRowLabelForSelection, labels]);
|
|
79
81
|
const effectiveColumns = React.useMemo(() => {
|
|
80
82
|
if (!enableRowSelection)
|
|
81
83
|
return columns;
|
|
@@ -144,17 +146,17 @@ enableRowSelection = false, getRowId, getRowLabelForSelection, onSelectionChange
|
|
|
144
146
|
return;
|
|
145
147
|
}
|
|
146
148
|
if (prevPageRef.current !== currentPageIndex) {
|
|
147
|
-
setAnnouncement(
|
|
149
|
+
setAnnouncement(labels.pageStatus(currentPageIndex + 1, pageCount));
|
|
148
150
|
}
|
|
149
151
|
else if (prevSortRef.current !== currentSorting && currentSorting.length > 0) {
|
|
150
152
|
const sort = currentSorting[0];
|
|
151
153
|
const col = columns.find(c => 'accessorKey' in c && c.accessorKey === sort.id);
|
|
152
154
|
const colName = col && typeof col.header === 'string' ? col.header : sort.id;
|
|
153
|
-
setAnnouncement(
|
|
155
|
+
setAnnouncement(labels.sorted(colName, sort.desc ? 'descending' : 'ascending'));
|
|
154
156
|
}
|
|
155
157
|
prevPageRef.current = currentPageIndex;
|
|
156
158
|
prevSortRef.current = currentSorting;
|
|
157
|
-
}, [currentPageIndex, currentSorting, pageCount, columns]);
|
|
159
|
+
}, [currentPageIndex, currentSorting, pageCount, columns, labels]);
|
|
158
160
|
return (_jsxs("div", { className: cx('tui-table-container', className), children: [_jsx("div", { role: "status", "aria-live": "polite", "aria-atomic": "true", className: "tui-visually-hidden", children: announcement }), showBulkBar && (renderBulkBar ? (renderBulkBar({
|
|
159
161
|
selectedRows,
|
|
160
162
|
selectedRowIds,
|
|
@@ -173,7 +175,7 @@ enableRowSelection = false, getRowId, getRowLabelForSelection, onSelectionChange
|
|
|
173
175
|
: sortDirection === 'desc'
|
|
174
176
|
? 'descending'
|
|
175
177
|
: 'none'
|
|
176
|
-
: undefined, children: canSort ? (_jsxs("button", { className: "tui-table__sortButton", onClick: header.column.getToggleSortingHandler(), "aria-label":
|
|
178
|
+
: undefined, children: canSort ? (_jsxs("button", { className: "tui-table__sortButton", onClick: header.column.getToggleSortingHandler(), "aria-label": labels.sortBy(headerLabel), children: [headerContent, _jsx("span", { className: "tui-table__sortButton__icon", "aria-hidden": "true", children: sortDirection === 'asc'
|
|
177
179
|
? '▲'
|
|
178
180
|
: sortDirection === 'desc'
|
|
179
181
|
? '▼'
|
|
@@ -1,2 +1,5 @@
|
|
|
1
1
|
export { DataTable } from './DataTable';
|
|
2
2
|
export type { DataTableProps } from './DataTable';
|
|
3
|
+
export { BulkActionsBar } from './BulkActionsBar';
|
|
4
|
+
export { defaultBulkActionsBarLabels, defaultDataTableLabels } from './types';
|
|
5
|
+
export type { BulkActionsBarLabels, DataTableLabels } from './types';
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export type BulkActionsBarLabels = {
|
|
2
|
+
/** Visible count text, e.g. `"3 selected"`. Default: `` `${count} selected` `` */
|
|
3
|
+
selected?: (count: number) => string;
|
|
4
|
+
/** Toolbar `aria-label`, e.g. `"Bulk actions for 3 selected rows"`. Default: `` `Bulk actions for ${count} selected row(s)` `` */
|
|
5
|
+
actions?: (count: number) => string;
|
|
6
|
+
};
|
|
7
|
+
export declare const defaultBulkActionsBarLabels: Required<BulkActionsBarLabels>;
|
|
8
|
+
export type DataTableLabels = {
|
|
9
|
+
/** Label for the "select all rows" checkbox. Default: `"Select all rows on this page"` */
|
|
10
|
+
selectAll?: string;
|
|
11
|
+
/** Label for individual row checkboxes. Default: `` `Select row ${index + 1}` `` */
|
|
12
|
+
selectRow?: (index: number) => string;
|
|
13
|
+
/** Live-region announcement when page changes. Default: `` `Page ${current} of ${total}` `` */
|
|
14
|
+
pageStatus?: (current: number, total: number) => string;
|
|
15
|
+
/** Label for column sort buttons. Default: `` `Sort by ${column}` `` */
|
|
16
|
+
sortBy?: (column: string) => string;
|
|
17
|
+
/** Live-region announcement after sorting. Default: `` `Sorted by ${column}, ${direction}` `` */
|
|
18
|
+
sorted?: (column: string, direction: 'ascending' | 'descending') => string;
|
|
19
|
+
};
|
|
20
|
+
export declare const defaultDataTableLabels: Required<DataTableLabels>;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export const defaultBulkActionsBarLabels = {
|
|
2
|
+
selected: (count) => `${count} selected`,
|
|
3
|
+
actions: (count) => `Bulk actions for ${count} selected row${count === 1 ? '' : 's'}`,
|
|
4
|
+
};
|
|
5
|
+
export const defaultDataTableLabels = {
|
|
6
|
+
selectAll: 'Select all rows on this page',
|
|
7
|
+
selectRow: (index) => `Select row ${index + 1}`,
|
|
8
|
+
pageStatus: (current, total) => `Page ${current} of ${total}`,
|
|
9
|
+
sortBy: (column) => `Sort by ${column}`,
|
|
10
|
+
sorted: (column, direction) => `Sorted by ${column}, ${direction}`,
|
|
11
|
+
};
|
package/components/Tabs/Tabs.js
CHANGED
|
@@ -291,6 +291,7 @@ function Tab({ value, icon, 'aria-label': ariaLabel, disabled = false, className
|
|
|
291
291
|
// =============================================================================
|
|
292
292
|
function TabPanel({ value, className, children }) {
|
|
293
293
|
const { activeValue, registerPanel, unregisterPanel, getTabId, getPanelId } = useTabsContext();
|
|
294
|
+
const panelRef = useRef(null);
|
|
294
295
|
const tabId = getTabId(value);
|
|
295
296
|
const panelId = getPanelId(value);
|
|
296
297
|
const isActive = activeValue === value;
|
|
@@ -299,11 +300,17 @@ function TabPanel({ value, className, children }) {
|
|
|
299
300
|
registerPanel(value);
|
|
300
301
|
return () => unregisterPanel(value);
|
|
301
302
|
}, [value, registerPanel, unregisterPanel]);
|
|
302
|
-
|
|
303
|
+
// Set inert via DOM property for React 18 compatibility.
|
|
304
|
+
// React 18 doesn't handle `inert` as a boolean prop — setting the DOM
|
|
305
|
+
// property directly works across React 18 and 19.
|
|
306
|
+
useEffect(() => {
|
|
307
|
+
if (panelRef.current) {
|
|
308
|
+
panelRef.current.inert = !isActive;
|
|
309
|
+
}
|
|
310
|
+
}, [isActive]);
|
|
311
|
+
return (_jsx("div", { ref: panelRef, role: "tabpanel", id: panelId, className: cx('tui-tabs__panel', className), "data-state": isActive ? 'active' : 'inactive', "aria-labelledby": tabId, "aria-hidden": isActive ? undefined : true,
|
|
303
312
|
// tabIndex={0} allows direct focus on panel for screen reader navigation
|
|
304
|
-
tabIndex: isActive ? 0 : undefined,
|
|
305
|
-
// inert prevents Tab navigation into hidden panel content
|
|
306
|
-
inert: !isActive || undefined, children: children }));
|
|
313
|
+
tabIndex: isActive ? 0 : undefined, children: children }));
|
|
307
314
|
}
|
|
308
315
|
// =============================================================================
|
|
309
316
|
// Compound Component Export
|
|
@@ -15,6 +15,7 @@ import { cx } from '../../utils/cx.js';
|
|
|
15
15
|
// --tui-input-border-focus Focus state border color
|
|
16
16
|
// --tui-input-border-invalid Invalid state border color
|
|
17
17
|
// --tui-input-radius Border radius
|
|
18
|
+
// --tui-input-fg-placeholder Placeholder text color
|
|
18
19
|
//
|
|
19
20
|
// =============================================================================
|
|
20
21
|
/** String or number content gets the `.is-text` visual treatment. */
|
|
@@ -23,7 +24,7 @@ export const TextInput = forwardRef(function TextInput({ type = 'text', size = '
|
|
|
23
24
|
const sizeClass = size !== 'md' ? `is-size-${size}` : undefined;
|
|
24
25
|
const handleGroupClick = useCallback((e) => {
|
|
25
26
|
const target = e.target;
|
|
26
|
-
if (target.closest('button, a, [role="button"]
|
|
27
|
+
if (target.closest('button, a, input, select, textarea, [tabindex]:not([tabindex="-1"]), [role="button"]'))
|
|
27
28
|
return;
|
|
28
29
|
e.currentTarget.querySelector('input')?.focus();
|
|
29
30
|
}, []);
|
|
@@ -14,10 +14,15 @@ export type TextInputProps = Omit<InputHTMLAttributes<HTMLInputElement>, 'type'
|
|
|
14
14
|
size?: SizeStandard;
|
|
15
15
|
/**
|
|
16
16
|
* Content to render before the input (icon, text).
|
|
17
|
+
* @remarks String addons (e.g. "$", "https://") are not part of the input's
|
|
18
|
+
* accessible name. Use `aria-label` on the input to provide full context
|
|
19
|
+
* (e.g. `aria-label="Amount in US dollars"` when prefix is "$").
|
|
17
20
|
*/
|
|
18
21
|
prefix?: ReactNode;
|
|
19
22
|
/**
|
|
20
23
|
* Content to render after the input (icon, button).
|
|
24
|
+
* @remarks String addons (e.g. "USD", ".com") are not part of the input's
|
|
25
|
+
* accessible name. Use `aria-label` on the input to provide full context.
|
|
21
26
|
*/
|
|
22
27
|
suffix?: ReactNode;
|
|
23
28
|
/**
|
|
@@ -26,7 +31,8 @@ export type TextInputProps = Omit<InputHTMLAttributes<HTMLInputElement>, 'type'
|
|
|
26
31
|
className?: string;
|
|
27
32
|
/**
|
|
28
33
|
* Class name applied directly to the `<input>` element.
|
|
29
|
-
* Use for utilities like `tui-input-reset`
|
|
34
|
+
* Use for utilities like `tui-input-reset` (removes browser-native search
|
|
35
|
+
* decoration, clear buttons, and spinners) that must target the input itself.
|
|
30
36
|
*/
|
|
31
37
|
inputClassName?: string;
|
|
32
38
|
};
|
|
@@ -16,9 +16,10 @@ import { composeRefs } from '../../utils/compose-refs.js';
|
|
|
16
16
|
// --tui-input-border-focus Focus state border color
|
|
17
17
|
// --tui-input-border-invalid Invalid state border color
|
|
18
18
|
// --tui-input-radius Border radius
|
|
19
|
+
// --tui-input-fg-placeholder Placeholder text color
|
|
19
20
|
//
|
|
20
21
|
// =============================================================================
|
|
21
|
-
export const Textarea = forwardRef(function Textarea({ size = 'md', resize = 'vertical', autoResize = false, className, onInput, ...rest }, externalRef) {
|
|
22
|
+
export const Textarea = forwardRef(function Textarea({ size = 'md', resize = 'vertical', autoResize = false, className, onInput, style, ...rest }, externalRef) {
|
|
22
23
|
const internalRef = useRef(null);
|
|
23
24
|
const sizeClass = size !== 'md' ? `is-size-${size}` : undefined;
|
|
24
25
|
// Auto-resize: adjust height to fit content
|
|
@@ -45,5 +46,5 @@ export const Textarea = forwardRef(function Textarea({ size = 'md', resize = 've
|
|
|
45
46
|
}
|
|
46
47
|
onInput?.(e);
|
|
47
48
|
}, [autoResize, adjustHeight, onInput]);
|
|
48
|
-
return (_jsx("textarea", { ref: composeRefs(internalRef, externalRef), className: cx('tui-textarea', sizeClass, autoResize && 'is-autoresize', className), style: !autoResize ? { resize } :
|
|
49
|
+
return (_jsx("textarea", { ref: composeRefs(internalRef, externalRef), className: cx('tui-textarea', sizeClass, autoResize && 'is-autoresize', className), style: { ...style, ...(!autoResize ? { resize } : {}) }, onInput: handleInput, ...rest }));
|
|
49
50
|
});
|
|
@@ -10,12 +10,17 @@ export type TextareaProps = Omit<TextareaHTMLAttributes<HTMLTextAreaElement>, 's
|
|
|
10
10
|
/**
|
|
11
11
|
* Resize behaviour.
|
|
12
12
|
* Ignored when `autoResize` is true (resize is disabled to avoid conflict).
|
|
13
|
+
* Takes precedence over `style.resize` when both are provided.
|
|
13
14
|
* @default 'vertical'
|
|
14
15
|
*/
|
|
15
16
|
resize?: TextareaResize;
|
|
16
17
|
/**
|
|
17
18
|
* Automatically grow height to fit content.
|
|
18
|
-
* Disables manual resize and
|
|
19
|
+
* Disables manual resize and sets `overflow: hidden`.
|
|
20
|
+
*
|
|
21
|
+
* **Warning:** If you apply an external `max-height` via CSS or `style`,
|
|
22
|
+
* content beyond that height will be clipped with no scrollbar.
|
|
23
|
+
* In that case, add `overflow-y: auto` to restore scrolling.
|
|
19
24
|
*/
|
|
20
25
|
autoResize?: boolean;
|
|
21
26
|
/**
|
|
@@ -2,7 +2,7 @@ import type { TooltipProviderProps, TooltipProps, TooltipTriggerProps, TooltipCo
|
|
|
2
2
|
declare function TooltipProviderComponent({ delayDuration, closeDelayDuration, children, }: TooltipProviderProps): import("react/jsx-runtime").JSX.Element;
|
|
3
3
|
declare function TooltipRoot({ open: controlledOpen, onOpenChange, defaultOpen, delayDuration: localDelay, children, }: TooltipProps): import("react/jsx-runtime").JSX.Element;
|
|
4
4
|
declare function TooltipTriggerComponent({ asChild, 'aria-label': ariaLabel, children, }: TooltipTriggerProps): import("react/jsx-runtime").JSX.Element;
|
|
5
|
-
declare function TooltipContentComponent(
|
|
5
|
+
declare function TooltipContentComponent(props: TooltipContentProps): import("react/jsx-runtime").JSX.Element | null;
|
|
6
6
|
type TooltipCompound = typeof TooltipRoot & {
|
|
7
7
|
Provider: typeof TooltipProviderComponent;
|
|
8
8
|
Trigger: typeof TooltipTriggerComponent;
|
|
@@ -120,12 +120,22 @@ function TooltipTriggerComponent({ asChild = false, 'aria-label': ariaLabel, chi
|
|
|
120
120
|
// =============================================================================
|
|
121
121
|
// TooltipContent
|
|
122
122
|
// =============================================================================
|
|
123
|
-
|
|
124
|
-
|
|
123
|
+
// Gate component: reads context to decide whether to mount the real content.
|
|
124
|
+
// This ensures useFloating and all other hooks in TooltipContentInner only
|
|
125
|
+
// run when the tooltip is actually open — not on every render cycle.
|
|
126
|
+
function TooltipContentComponent(props) {
|
|
127
|
+
const { open } = useTooltipContext();
|
|
128
|
+
if (!open)
|
|
129
|
+
return null;
|
|
130
|
+
return _jsx(TooltipContentInner, { ...props });
|
|
131
|
+
}
|
|
132
|
+
// Inner component: only mounted when open. All Floating UI hooks live here.
|
|
133
|
+
function TooltipContentInner({ side = 'top', align = 'center', sideOffset = 8, theme = 'dark', className, children, }) {
|
|
134
|
+
const { setOpen, triggerRef, contentId, cancelClose, handleClose } = useTooltipContext();
|
|
125
135
|
const arrowRef = useRef(null);
|
|
126
136
|
const { refs, floatingStyles, context } = useFloating({
|
|
127
137
|
placement: toPlacement(side, align),
|
|
128
|
-
open,
|
|
138
|
+
open: true, // Always true when mounted (gate handles the conditional)
|
|
129
139
|
middleware: [
|
|
130
140
|
offset(sideOffset),
|
|
131
141
|
flip(),
|
|
@@ -145,8 +155,6 @@ function TooltipContentComponent({ side = 'top', align = 'center', sideOffset =
|
|
|
145
155
|
// - Focus on trigger: close + stopPropagation (avoid closing parent modal)
|
|
146
156
|
// - Hover-only (focus elsewhere): close without stopPropagation
|
|
147
157
|
useEffect(() => {
|
|
148
|
-
if (!open)
|
|
149
|
-
return;
|
|
150
158
|
const handleKeyDown = (e) => {
|
|
151
159
|
if (e.key === 'Escape') {
|
|
152
160
|
const activeEl = document.activeElement;
|
|
@@ -160,22 +168,20 @@ function TooltipContentComponent({ side = 'top', align = 'center', sideOffset =
|
|
|
160
168
|
};
|
|
161
169
|
document.addEventListener('keydown', handleKeyDown);
|
|
162
170
|
return () => document.removeEventListener('keydown', handleKeyDown);
|
|
163
|
-
}, [
|
|
171
|
+
}, [setOpen, triggerRef]);
|
|
164
172
|
// Get portal root inside .tui-interface
|
|
165
173
|
const portalRoot = getPortalRootFor(triggerRef.current);
|
|
166
174
|
// Dev warning: tooltips should not contain interactive content (WCAG 1.4.13)
|
|
167
175
|
// Use Popover for interactive overlays instead
|
|
168
176
|
useEffect(() => {
|
|
169
|
-
if (isDev() &&
|
|
177
|
+
if (isDev() && refs.floating.current) {
|
|
170
178
|
const interactive = refs.floating.current.querySelectorAll('a, button, input, select, textarea, [tabindex]:not([tabindex="-1"])');
|
|
171
179
|
if (interactive.length > 0) {
|
|
172
180
|
console.warn('[Tooltip] Contains interactive elements which violates WCAG 1.4.13. ' +
|
|
173
181
|
'Tooltips should only contain plain text. Use Popover for interactive content.');
|
|
174
182
|
}
|
|
175
183
|
}
|
|
176
|
-
}, [
|
|
177
|
-
if (!open)
|
|
178
|
-
return null;
|
|
184
|
+
}, [refs.floating]);
|
|
179
185
|
return (_jsx(FloatingPortal, { root: portalRoot, children: _jsxs("div", { ref: refs.setFloating, id: contentId, role: "tooltip", className: cx('tui-tooltip', theme === 'light' && 'is-theme-light', className), style: floatingStyles, onMouseEnter: cancelClose, onMouseLeave: handleClose, children: [children, _jsx(FloatingArrow, { ref: arrowRef, context: context, className: "tui-tooltip__arrow" })] }) }));
|
|
180
186
|
}
|
|
181
187
|
export const Tooltip = TooltipRoot;
|