@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
|
@@ -11,6 +11,34 @@ export type MultiSelectValue = Array<string | number>;
|
|
|
11
11
|
* Display mode for the trigger.
|
|
12
12
|
*/
|
|
13
13
|
export type DisplayMode = 'count' | 'chips';
|
|
14
|
+
/**
|
|
15
|
+
* Overridable label strings for i18n.
|
|
16
|
+
*
|
|
17
|
+
* Static strings use plain `string`, dynamic strings use function signatures.
|
|
18
|
+
* All keys are optional — defaults are English.
|
|
19
|
+
*/
|
|
20
|
+
export type MultiSelectLabels = {
|
|
21
|
+
/**
|
|
22
|
+
* Trigger text in count display mode.
|
|
23
|
+
* @default (count) => `${count} selected`
|
|
24
|
+
*/
|
|
25
|
+
selected?: (count: number) => string;
|
|
26
|
+
/**
|
|
27
|
+
* Overflow badge text in chips display mode.
|
|
28
|
+
* @default (count) => `+${count} more`
|
|
29
|
+
*/
|
|
30
|
+
more?: (count: number) => string;
|
|
31
|
+
/**
|
|
32
|
+
* Screen reader status announcement. Called on every selection change.
|
|
33
|
+
* When `max` is defined and `count >= max`, include a "maximum reached" note.
|
|
34
|
+
* @default (count, max) => count === 0 ? '0 items selected' : `${count} item${count === 1 ? '' : 's'} selected${max !== undefined && count >= max ? `. Maximum of ${max} reached` : ''}`
|
|
35
|
+
*/
|
|
36
|
+
status?: (count: number, max?: number) => string;
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* Default English labels. Exported for reference or spread-override patterns.
|
|
40
|
+
*/
|
|
41
|
+
export declare const defaultMultiSelectLabels: Required<MultiSelectLabels>;
|
|
14
42
|
export type MultiSelectProps = {
|
|
15
43
|
/**
|
|
16
44
|
* Control size.
|
|
@@ -73,6 +101,11 @@ export type MultiSelectProps = {
|
|
|
73
101
|
* Callback when max selections is reached.
|
|
74
102
|
*/
|
|
75
103
|
onMaxReached?: () => void;
|
|
104
|
+
/**
|
|
105
|
+
* Override internal display and screen reader strings for i18n.
|
|
106
|
+
* All keys are optional — omitted keys use English defaults.
|
|
107
|
+
*/
|
|
108
|
+
labels?: MultiSelectLabels;
|
|
76
109
|
/**
|
|
77
110
|
* Accessible name for the select.
|
|
78
111
|
*/
|
|
@@ -169,6 +202,7 @@ export type MultiSelectActionsContextValue = {
|
|
|
169
202
|
maxChips: number;
|
|
170
203
|
max: number | undefined;
|
|
171
204
|
size: SizeStandard;
|
|
205
|
+
labels: Required<MultiSelectLabels>;
|
|
172
206
|
triggerId: string;
|
|
173
207
|
listboxId: string;
|
|
174
208
|
ariaLabel?: string;
|
|
@@ -1,3 +1,13 @@
|
|
|
1
1
|
import { toKey } from '../../utils/value-key.js';
|
|
2
2
|
// Re-export shared value-key types so existing consumers don't break
|
|
3
3
|
export { toKey };
|
|
4
|
+
/**
|
|
5
|
+
* Default English labels. Exported for reference or spread-override patterns.
|
|
6
|
+
*/
|
|
7
|
+
export const defaultMultiSelectLabels = {
|
|
8
|
+
selected: (count) => `${count} selected`,
|
|
9
|
+
more: (count) => `+${count} more`,
|
|
10
|
+
status: (count, max) => count === 0
|
|
11
|
+
? '0 items selected'
|
|
12
|
+
: `${count} item${count === 1 ? '' : 's'} selected${max !== undefined && count >= max ? `. Maximum of ${max} reached` : ''}`,
|
|
13
|
+
};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type PagerLabels } from './types';
|
|
1
2
|
export type PagerMode = 'simple' | 'ends' | 'full' | 'smart';
|
|
2
3
|
export type PagerProps = {
|
|
3
4
|
/** Current page (1-indexed) */
|
|
@@ -22,5 +23,10 @@ export type PagerProps = {
|
|
|
22
23
|
hidden?: boolean;
|
|
23
24
|
/** Additional class name */
|
|
24
25
|
className?: string;
|
|
26
|
+
/**
|
|
27
|
+
* Overridable label strings for i18n.
|
|
28
|
+
* All keys are optional — defaults are English.
|
|
29
|
+
*/
|
|
30
|
+
labels?: PagerLabels;
|
|
25
31
|
};
|
|
26
|
-
export declare function Pager({ currentPage, totalPages, onPageChange, pageSize, pageSizeOptions, onPageSizeChange, mode, maxNumbers, navStyle, hidden, className, }: PagerProps): import("react/jsx-runtime").JSX.Element | null;
|
|
32
|
+
export declare function Pager({ currentPage, totalPages, onPageChange, pageSize, pageSizeOptions, onPageSizeChange, mode, maxNumbers, navStyle, hidden, className, labels: labelsProp, }: PagerProps): import("react/jsx-runtime").JSX.Element | null;
|
|
@@ -3,6 +3,7 @@ import * as React from 'react';
|
|
|
3
3
|
import { cx } from '../../utils/cx.js';
|
|
4
4
|
import { Button } from '../Button/index.js';
|
|
5
5
|
import { IconButton } from '../IconButton/index.js';
|
|
6
|
+
import { defaultPagerLabels } from './types.js';
|
|
6
7
|
// =============================================================================
|
|
7
8
|
// Pager Component
|
|
8
9
|
// =============================================================================
|
|
@@ -121,7 +122,8 @@ function buildItems(total, current, mode, maxNumbers) {
|
|
|
121
122
|
// -----------------------------------------------------------------------------
|
|
122
123
|
// Component
|
|
123
124
|
// -----------------------------------------------------------------------------
|
|
124
|
-
export function Pager({ currentPage, totalPages, onPageChange, pageSize, pageSizeOptions, onPageSizeChange, mode = 'smart', maxNumbers = DEFAULT_MAX_SLOTS, navStyle = 'text', hidden = false, className, }) {
|
|
125
|
+
export function Pager({ currentPage, totalPages, onPageChange, pageSize, pageSizeOptions, onPageSizeChange, mode = 'smart', maxNumbers = DEFAULT_MAX_SLOTS, navStyle = 'text', hidden = false, className, labels: labelsProp, }) {
|
|
126
|
+
const labels = React.useMemo(() => ({ ...defaultPagerLabels, ...labelsProp }), [labelsProp]);
|
|
125
127
|
// Normalise inputs
|
|
126
128
|
const total = Math.max(1, totalPages);
|
|
127
129
|
const current = Math.min(total, Math.max(1, currentPage));
|
|
@@ -130,13 +132,13 @@ export function Pager({ currentPage, totalPages, onPageChange, pageSize, pageSiz
|
|
|
130
132
|
if (hidden)
|
|
131
133
|
return null;
|
|
132
134
|
const showPageSizeSelector = pageSizeOptions && pageSizeOptions.length > 0 && onPageSizeChange;
|
|
133
|
-
return (_jsxs("div", { className: cx('tui-pager', className), children: [_jsx("nav", { className: "tui-pager__nav", "aria-label":
|
|
135
|
+
return (_jsxs("div", { className: cx('tui-pager', className), children: [_jsx("nav", { className: "tui-pager__nav", "aria-label": labels.navigation, children: items.map((it, i) => {
|
|
134
136
|
if (it.kind === 'ellipsis') {
|
|
135
137
|
return (_jsx("span", { className: "tui-pager__ellipsis", "aria-hidden": true, children: "..." }, `e${i}`));
|
|
136
138
|
}
|
|
137
139
|
if (it.kind === 'prev' || it.kind === 'next') {
|
|
138
140
|
const isPrev = it.kind === 'prev';
|
|
139
|
-
const label = isPrev ?
|
|
141
|
+
const label = isPrev ? labels.previous : labels.next;
|
|
140
142
|
if (navStyle === 'icon') {
|
|
141
143
|
return (_jsx(IconButton, { icon: isPrev ? 'system/chevron-left' : 'system/chevron-right', label: label, showTooltip: true, size: "sm", theme: "secondary", variant: "outline", disabled: it.disabled, onClick: () => go(it.page), className: "tui-pager__item" }, it.kind));
|
|
142
144
|
}
|
|
@@ -144,8 +146,8 @@ export function Pager({ currentPage, totalPages, onPageChange, pageSize, pageSiz
|
|
|
144
146
|
}
|
|
145
147
|
// Page number
|
|
146
148
|
if (it.kind === 'page') {
|
|
147
|
-
return (_jsx(Button, { size: "sm", theme: it.current ? 'primary' : 'secondary', variant: it.current ? 'solid' : 'outline', "aria-current": it.current ? 'page' : undefined, "aria-label":
|
|
149
|
+
return (_jsx(Button, { size: "sm", theme: it.current ? 'primary' : 'secondary', variant: it.current ? 'solid' : 'outline', "aria-current": it.current ? 'page' : undefined, "aria-label": labels.page(it.page), onClick: () => go(it.page), className: "tui-pager__item", children: it.page }, it.page));
|
|
148
150
|
}
|
|
149
151
|
return null;
|
|
150
|
-
}) }), _jsxs("div", { className: "tui-pager__info", children: [
|
|
152
|
+
}) }), _jsxs("div", { className: "tui-pager__info", children: [_jsx("span", { children: labels.pageStatus(current, total) }), showPageSizeSelector && (_jsxs("label", { className: "tui-pager__page-size-label", children: [_jsx("span", { className: "tui-visually-hidden", children: labels.itemsPerPage }), _jsx("select", { className: "tui-input", value: pageSize, onChange: (e) => onPageSizeChange(Number(e.target.value)), children: pageSizeOptions.map((s) => (_jsx("option", { value: s, children: labels.perPage(s) }, s))) })] }))] })] }));
|
|
151
153
|
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Overridable label strings for i18n.
|
|
3
|
+
*
|
|
4
|
+
* Static strings use plain `string`, dynamic strings use function signatures.
|
|
5
|
+
* All keys are optional — defaults are English.
|
|
6
|
+
*/
|
|
7
|
+
export type PagerLabels = {
|
|
8
|
+
/** Label for previous page button (aria-label on icon style, aria-label on text style).
|
|
9
|
+
* @default "Previous page"
|
|
10
|
+
*/
|
|
11
|
+
previous?: string;
|
|
12
|
+
/** Label for next page button.
|
|
13
|
+
* @default "Next page"
|
|
14
|
+
*/
|
|
15
|
+
next?: string;
|
|
16
|
+
/** Label for individual page buttons.
|
|
17
|
+
* @default (n) => `Page ${n}`
|
|
18
|
+
*/
|
|
19
|
+
page?: (n: number) => string;
|
|
20
|
+
/** aria-label for the nav landmark.
|
|
21
|
+
* @default "Pagination"
|
|
22
|
+
*/
|
|
23
|
+
navigation?: string;
|
|
24
|
+
/** "Page X of Y" status text.
|
|
25
|
+
* @default (current, total) => `Page ${current} of ${total}`
|
|
26
|
+
*/
|
|
27
|
+
pageStatus?: (current: number, total: number) => string;
|
|
28
|
+
/** Visually-hidden label for the page size selector.
|
|
29
|
+
* @default "Items per page"
|
|
30
|
+
*/
|
|
31
|
+
itemsPerPage?: string;
|
|
32
|
+
/** Option text in the page size selector.
|
|
33
|
+
* @default (n) => `${n} / page`
|
|
34
|
+
*/
|
|
35
|
+
perPage?: (n: number) => string;
|
|
36
|
+
};
|
|
37
|
+
export declare const defaultPagerLabels: Required<PagerLabels>;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// =============================================================================
|
|
2
|
+
// Pager Types
|
|
3
|
+
// =============================================================================
|
|
4
|
+
export const defaultPagerLabels = {
|
|
5
|
+
previous: 'Previous page',
|
|
6
|
+
next: 'Next page',
|
|
7
|
+
page: (n) => `Page ${n}`,
|
|
8
|
+
navigation: 'Pagination',
|
|
9
|
+
pageStatus: (current, total) => `Page ${current} of ${total}`,
|
|
10
|
+
itemsPerPage: 'Items per page',
|
|
11
|
+
perPage: (n) => `${n} / page`,
|
|
12
|
+
};
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
-
import React from 'react';
|
|
2
|
+
import React, { memo } from 'react';
|
|
3
3
|
import { useProgressSegments } from './useProgressSegments.js';
|
|
4
4
|
import { cx } from '../../utils/cx.js';
|
|
5
5
|
import { isDev } from '../../utils/is-dev.js';
|
|
6
6
|
// =============================================================================
|
|
7
7
|
// COMPONENT
|
|
8
8
|
// =============================================================================
|
|
9
|
-
export function Progress(props) {
|
|
9
|
+
export const Progress = memo(function Progress(props) {
|
|
10
10
|
const { children, mode = 'line', size = 'md', max = 100, showLabels = true, 'aria-labelledby': labelledBy, 'aria-label': ariaLabel, defaultLabel, className, } = props;
|
|
11
11
|
// Determine mode
|
|
12
12
|
const isSegmented = 'segments' in props && Array.isArray(props.segments);
|
|
@@ -98,4 +98,4 @@ export function Progress(props) {
|
|
|
98
98
|
// Above/below/inline positions: render labels outside the track
|
|
99
99
|
const labelRow = (_jsxs("div", { className: "tui-progress__labels", children: [labelStart && _jsx("span", { className: "tui-progress__label is-start", children: labelStart }), labelEnd && _jsx("span", { className: "tui-progress__label is-end", children: labelEnd })] }));
|
|
100
100
|
return (_jsxs("div", { ...rootProps, children: [labelPosition === 'above' && labelRow, labelPosition === 'inline' ? (_jsxs("div", { className: "tui-progress__inline", children: [labelStart && _jsx("span", { className: "tui-progress__label is-start", children: labelStart }), trackContent, labelEnd && _jsx("span", { className: "tui-progress__label is-end", children: labelEnd })] })) : (trackContent), labelPosition === 'below' && labelRow, !labelledBy && !ariaLabel && defaultLabel && (_jsx("span", { className: "visually-hidden", children: defaultLabel }))] }));
|
|
101
|
-
}
|
|
101
|
+
});
|
|
@@ -1,32 +1,2 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
|
|
3
|
-
type Theme = ThemeFull;
|
|
4
|
-
export type RatingProps = {
|
|
5
|
-
/** Controlled value (1..max). Use with onValueChange */
|
|
6
|
-
value?: number;
|
|
7
|
-
/** Uncontrolled initial value */
|
|
8
|
-
defaultValue?: number;
|
|
9
|
-
/** Maximum icons shown */
|
|
10
|
-
max?: number;
|
|
11
|
-
/** Disable interaction (keeps semantics) */
|
|
12
|
-
disabled?: boolean;
|
|
13
|
-
/** Presentational readOnly (no form semantics) */
|
|
14
|
-
readOnly?: boolean;
|
|
15
|
-
/** Name for the radio group (if you care about form posts) */
|
|
16
|
-
name?: string;
|
|
17
|
-
/** Size maps to icon + spacing */
|
|
18
|
-
size?: Size;
|
|
19
|
-
/** Theme feeds foreground color tokens */
|
|
20
|
-
theme?: Theme;
|
|
21
|
-
/** Called when the value changes */
|
|
22
|
-
onValueChange?: (value: number) => void;
|
|
23
|
-
/** Allow clicking the current selection to clear back to 0 */
|
|
24
|
-
allowClear?: boolean;
|
|
25
|
-
className?: string;
|
|
26
|
-
/** Gap override (e.g. '0.25rem') – otherwise uses density utilities */
|
|
27
|
-
gap?: string;
|
|
28
|
-
/** Accessible label for the rating group. Defaults to "Rating: X of Y" */
|
|
29
|
-
'aria-label'?: string;
|
|
30
|
-
};
|
|
31
|
-
export declare function Rating({ value, defaultValue, max, disabled, readOnly, name, size, theme, onValueChange, allowClear, className, gap, 'aria-label': ariaLabel, }: RatingProps): import("react/jsx-runtime").JSX.Element;
|
|
32
|
-
export {};
|
|
1
|
+
import type { RatingProps } from './types';
|
|
2
|
+
export declare function Rating({ value, defaultValue, max, disabled, readOnly, name, size, theme, onValueChange, allowClear, className, gap, 'aria-label': ariaLabel, labels: labelsProp, }: RatingProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -2,10 +2,12 @@ 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 { Icon } from '../Icon/index.js';
|
|
5
|
-
|
|
5
|
+
import { defaultRatingLabels } from './types.js';
|
|
6
|
+
export function Rating({ value, defaultValue = 0, max = 5, disabled, readOnly, name, size = 'lg', theme = 'secondary', onValueChange, allowClear, className, gap, 'aria-label': ariaLabel, labels: labelsProp, }) {
|
|
6
7
|
const isControlled = value != null;
|
|
7
8
|
const [internal, setInternal] = React.useState(defaultValue);
|
|
8
9
|
const current = isControlled ? value : internal;
|
|
10
|
+
const labels = { ...defaultRatingLabels, ...labelsProp };
|
|
9
11
|
const generatedId = React.useId();
|
|
10
12
|
const groupName = name ?? generatedId;
|
|
11
13
|
const setValue = (v) => {
|
|
@@ -62,13 +64,13 @@ export function Rating({ value, defaultValue = 0, max = 5, disabled, readOnly, n
|
|
|
62
64
|
return;
|
|
63
65
|
}
|
|
64
66
|
};
|
|
65
|
-
const defaultAriaLabel =
|
|
67
|
+
const defaultAriaLabel = labels.rating(current, max);
|
|
66
68
|
// Roving tabindex: only the selected radio (or first if none) is in tab order
|
|
67
69
|
const focusableIndex = current > 0 ? current : 1;
|
|
68
70
|
return (_jsx("div", { className: cx('tui-rating', `is-size-${size}`, `is-theme-${theme}`, disabled && 'is-disabled', className), style: { gap }, role: readOnly ? 'img' : 'radiogroup', "aria-label": ariaLabel ?? defaultAriaLabel, onKeyDown: readOnly ? undefined : onKeyDown, children: Array.from({ length: max }).map((_, i) => {
|
|
69
71
|
const n = i + 1;
|
|
70
72
|
const checked = current >= n;
|
|
71
73
|
const id = `${groupName}__star-${n}`;
|
|
72
|
-
return (_jsxs("div", { className: "tui-rating__item", children: [!readOnly && (_jsx("input", { className: "tui-visually-hidden", type: "radio", id: id, name: groupName, value: n, checked: current === n, "aria-checked": current === n, tabIndex: n === focusableIndex ? 0 : -1, onChange: () => handleSelect(n), disabled: disabled })), readOnly ? (_jsx("span", { className: cx('tui-rating__star', checked && 'is-active'), children: _jsx(Icon, { name: checked ? 'system/star-fill' : 'system/star-outline' }) })) : (_jsxs("label", { className: cx('tui-rating__star', checked && 'is-active'), htmlFor: id, tabIndex: -1, children: [_jsx(Icon, { name: checked ? 'system/star-fill' : 'system/star-outline' }), _jsx("span", { className: "tui-visually-hidden", children:
|
|
74
|
+
return (_jsxs("div", { className: "tui-rating__item", children: [!readOnly && (_jsx("input", { className: "tui-visually-hidden", type: "radio", id: id, name: groupName, value: n, checked: current === n, "aria-checked": current === n, tabIndex: n === focusableIndex ? 0 : -1, onChange: () => handleSelect(n), disabled: disabled })), readOnly ? (_jsx("span", { className: cx('tui-rating__star', checked && 'is-active'), children: _jsx(Icon, { name: checked ? 'system/star-fill' : 'system/star-outline' }) })) : (_jsxs("label", { className: cx('tui-rating__star', checked && 'is-active'), htmlFor: id, tabIndex: -1, children: [_jsx(Icon, { name: checked ? 'system/star-fill' : 'system/star-outline' }), _jsx("span", { className: "tui-visually-hidden", children: labels.value(n, max) })] }))] }, n));
|
|
73
75
|
}) }));
|
|
74
76
|
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { SizeStandard, Theme as ThemeFull } from '../../types';
|
|
2
|
+
export type Size = SizeStandard;
|
|
3
|
+
export type Theme = ThemeFull;
|
|
4
|
+
export type RatingLabels = {
|
|
5
|
+
/** Group label for the radiogroup/img. Receives current value and max. */
|
|
6
|
+
rating?: (value: number, max: number) => string;
|
|
7
|
+
/** Visually-hidden label for each star. Receives star number and max. */
|
|
8
|
+
value?: (n: number, max: number) => string;
|
|
9
|
+
};
|
|
10
|
+
export declare const defaultRatingLabels: Required<RatingLabels>;
|
|
11
|
+
export type RatingProps = {
|
|
12
|
+
/** Controlled value (1..max). Use with onValueChange */
|
|
13
|
+
value?: number;
|
|
14
|
+
/** Uncontrolled initial value */
|
|
15
|
+
defaultValue?: number;
|
|
16
|
+
/** Maximum icons shown */
|
|
17
|
+
max?: number;
|
|
18
|
+
/** Disable interaction (keeps semantics) */
|
|
19
|
+
disabled?: boolean;
|
|
20
|
+
/** Presentational readOnly (no form semantics) */
|
|
21
|
+
readOnly?: boolean;
|
|
22
|
+
/** Name for the radio group (if you care about form posts) */
|
|
23
|
+
name?: string;
|
|
24
|
+
/** Size maps to icon + spacing */
|
|
25
|
+
size?: Size;
|
|
26
|
+
/** Theme feeds foreground color tokens */
|
|
27
|
+
theme?: Theme;
|
|
28
|
+
/** Called when the value changes */
|
|
29
|
+
onValueChange?: (value: number) => void;
|
|
30
|
+
/** Allow clicking the current selection to clear back to 0 */
|
|
31
|
+
allowClear?: boolean;
|
|
32
|
+
className?: string;
|
|
33
|
+
/** Gap override (e.g. '0.25rem') – otherwise uses density utilities */
|
|
34
|
+
gap?: string;
|
|
35
|
+
/** Accessible label for the rating group. Defaults to "Rating: X of Y" */
|
|
36
|
+
'aria-label'?: string;
|
|
37
|
+
/**
|
|
38
|
+
* Override default English strings for i18n.
|
|
39
|
+
*/
|
|
40
|
+
labels?: RatingLabels;
|
|
41
|
+
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
|
2
|
+
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
3
3
|
import { cx } from '../../utils/cx.js';
|
|
4
4
|
import { isDev } from '../../utils/is-dev.js';
|
|
5
5
|
import { toKey } from '../../utils/value-key.js';
|
|
@@ -8,9 +8,10 @@ import { SegmentedControlContext, useSegmentedControlContext } from './Segmented
|
|
|
8
8
|
// =============================================================================
|
|
9
9
|
// SegmentedControl Root
|
|
10
10
|
// =============================================================================
|
|
11
|
-
function SegmentedControlRoot({ value: controlledValue, defaultValue, onValueChange, variant = 'pill', size = 'md', orientation = 'horizontal', loop = true, disabled = false, 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, className, children, }) {
|
|
11
|
+
function SegmentedControlRoot({ value: controlledValue, defaultValue, onValueChange, variant = 'pill', size = 'md', orientation = 'horizontal', loop = true, wrap = false, disabled = false, 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, className, children, }) {
|
|
12
12
|
const [internalValue, setInternalValue] = useState(defaultValue);
|
|
13
|
-
|
|
13
|
+
// Lock controlled/uncontrolled decision at mount to prevent mode switching
|
|
14
|
+
const isControlled = useRef(controlledValue !== undefined).current;
|
|
14
15
|
const selectedValue = isControlled ? controlledValue : internalValue;
|
|
15
16
|
// Selection handler
|
|
16
17
|
const onSelect = useCallback((newValue) => {
|
|
@@ -28,7 +29,7 @@ function SegmentedControlRoot({ value: controlledValue, defaultValue, onValueCha
|
|
|
28
29
|
disabled,
|
|
29
30
|
loop,
|
|
30
31
|
orientation,
|
|
31
|
-
orientationKeyboard:
|
|
32
|
+
orientationKeyboard: false,
|
|
32
33
|
});
|
|
33
34
|
// Dev-only: Warn if missing accessible name
|
|
34
35
|
useEffect(() => {
|
|
@@ -60,7 +61,7 @@ function SegmentedControlRoot({ value: controlledValue, defaultValue, onValueCha
|
|
|
60
61
|
unregisterItem,
|
|
61
62
|
onSelect,
|
|
62
63
|
]);
|
|
63
|
-
return (_jsx(SegmentedControlContext.Provider, { value: contextValue, children: _jsx("div", { role: "radiogroup", className: cx('tui-segmented', `is-variant-${variant}`, `is-size-${size}`, orientation === 'vertical' && 'is-vertical', className), "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, "aria-disabled": disabled || undefined, "aria-orientation": orientation, onKeyDown: handleKeyDown, children: children }) }));
|
|
64
|
+
return (_jsx(SegmentedControlContext.Provider, { value: contextValue, children: _jsx("div", { role: "radiogroup", className: cx('tui-segmented', `is-variant-${variant}`, `is-size-${size}`, orientation === 'vertical' && 'is-vertical', wrap && 'is-wrap', className), "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, "aria-disabled": disabled || undefined, "aria-orientation": orientation, onKeyDown: handleKeyDown, children: children }) }));
|
|
64
65
|
}
|
|
65
66
|
// =============================================================================
|
|
66
67
|
// SegmentedControl.Item
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import type { ReactNode } from 'react';
|
|
2
2
|
import type { RovingItemRecord } from '../../utils/use-roving-group';
|
|
3
|
+
import type { SizeStandard } from '../../types/sizes';
|
|
3
4
|
export type SegmentedControlValue = string | number;
|
|
4
5
|
export type SegmentedControlVariant = 'pill' | 'outline' | 'underline';
|
|
5
|
-
export type SegmentedControlSize =
|
|
6
|
+
export type SegmentedControlSize = SizeStandard;
|
|
6
7
|
export type SegmentedControlOrientation = 'horizontal' | 'vertical';
|
|
7
8
|
export type SegmentedControlProps = {
|
|
8
9
|
/** Controlled selected value */
|
|
@@ -19,11 +20,19 @@ export type SegmentedControlProps = {
|
|
|
19
20
|
orientation?: SegmentedControlOrientation;
|
|
20
21
|
/** Whether arrow keys wrap around */
|
|
21
22
|
loop?: boolean;
|
|
23
|
+
/** Allow items to wrap to multiple lines (default: false) */
|
|
24
|
+
wrap?: boolean;
|
|
22
25
|
/** Disable all items */
|
|
23
26
|
disabled?: boolean;
|
|
24
|
-
/**
|
|
27
|
+
/**
|
|
28
|
+
* Accessible label for the radiogroup.
|
|
29
|
+
* At least one of `aria-label` or `aria-labelledby` is required.
|
|
30
|
+
*/
|
|
25
31
|
'aria-label'?: string;
|
|
26
|
-
/**
|
|
32
|
+
/**
|
|
33
|
+
* ID of element that labels this control.
|
|
34
|
+
* At least one of `aria-label` or `aria-labelledby` is required.
|
|
35
|
+
*/
|
|
27
36
|
'aria-labelledby'?: string;
|
|
28
37
|
/** Additional classes */
|
|
29
38
|
className?: string;
|
|
@@ -34,9 +43,12 @@ export type SegmentedControlItemProps = {
|
|
|
34
43
|
value: SegmentedControlValue;
|
|
35
44
|
/** Disable this item */
|
|
36
45
|
disabled?: boolean;
|
|
37
|
-
/**
|
|
46
|
+
/**
|
|
47
|
+
* Icon element rendered before the label.
|
|
48
|
+
* @remarks Icon-only items (no `children`) must provide `aria-label`.
|
|
49
|
+
*/
|
|
38
50
|
icon?: ReactNode;
|
|
39
|
-
/** Accessible label for icon-only items */
|
|
51
|
+
/** Accessible label — required for icon-only items */
|
|
40
52
|
'aria-label'?: string;
|
|
41
53
|
/** Additional classes */
|
|
42
54
|
className?: string;
|
|
@@ -37,3 +37,4 @@ export declare const SelectOption: typeof SelectOptionComponent;
|
|
|
37
37
|
export declare const SelectGroup: typeof SelectGroupComponent;
|
|
38
38
|
export declare const SelectLabel: typeof SelectLabelComponent;
|
|
39
39
|
export { useSelectContext as useSelect } from './SelectContext';
|
|
40
|
+
export type { SelectContextValue } from './types';
|