@scalewing/react 1.31.0 → 1.33.0
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/CHANGELOG.md +44 -0
- package/README.md +20 -6
- package/dist/components/ActionMenu.d.ts +10 -1
- package/dist/components/ActionMenu.js +7 -2
- package/dist/components/AppHeader.d.ts +9 -1
- package/dist/components/AppHeader.js +6 -2
- package/dist/components/Box.d.ts +28 -2
- package/dist/components/Box.js +4 -2
- package/dist/components/Button.d.ts +27 -2
- package/dist/components/Button.js +5 -3
- package/dist/components/ButtonGroup.d.ts +9 -1
- package/dist/components/ButtonGroup.js +8 -2
- package/dist/components/Combobox.js +2 -2
- package/dist/components/DateField.d.ts +9 -1
- package/dist/components/DateField.js +4 -3
- package/dist/components/DenominationGrid.d.ts +5 -3
- package/dist/components/DenominationGrid.js +21 -11
- package/dist/components/Dialog.d.ts +9 -1
- package/dist/components/Dialog.js +8 -4
- package/dist/components/Field.d.ts +8 -0
- package/dist/components/Field.js +4 -3
- package/dist/components/FieldLabelText.d.ts +21 -3
- package/dist/components/FieldLabelText.js +16 -4
- package/dist/components/FilterChips.d.ts +9 -1
- package/dist/components/FilterChips.js +4 -2
- package/dist/components/Nav.d.ts +14 -2
- package/dist/components/Nav.js +6 -2
- package/dist/components/SegmentedControl.d.ts +5 -2
- package/dist/components/SegmentedControl.js +2 -1
- package/dist/components/Select.d.ts +9 -1
- package/dist/components/Select.js +4 -3
- package/dist/components/Stepper.d.ts +13 -0
- package/dist/components/Stepper.js +8 -2
- package/dist/components/combobox/types.d.ts +8 -0
- package/dist/components/denomination-grid/DenominationStrip.d.ts +4 -2
- package/dist/components/denomination-grid/DenominationStrip.js +3 -2
- package/dist/components/denomination-grid/DenominationTiles.d.ts +1 -1
- package/dist/components/denomination-grid/EntryCell.js +1 -1
- package/dist/components/denomination-grid/StripRow.js +5 -2
- package/dist/components/denomination-grid/types.d.ts +35 -1
- package/dist/components/dialog/DialogTitle.d.ts +5 -3
- package/dist/components/dialog/DialogTitle.js +4 -4
- package/dist/css/css-action-menu-trigger.d.ts +11 -0
- package/dist/css/css-action-menu-trigger.js +66 -0
- package/dist/css/css-action-menu.js +7 -2
- package/dist/css/css-app-header-bar.d.ts +10 -0
- package/dist/css/css-app-header-bar.js +26 -0
- package/dist/css/css-box-border.d.ts +6 -1
- package/dist/css/css-box-border.js +8 -4
- package/dist/css/css-box-link.d.ts +13 -0
- package/dist/css/css-box-link.js +50 -0
- package/dist/css/css-button-group-attached.js +5 -2
- package/dist/css/css-button-group-track.d.ts +11 -0
- package/dist/css/css-button-group-track.js +48 -0
- package/dist/css/css-button.d.ts +19 -2
- package/dist/css/css-button.js +31 -1
- package/dist/css/css-chrome.js +14 -1
- package/dist/css/css-combobox.js +2 -1
- package/dist/css/css-components.js +3 -0
- package/dist/css/css-data.js +3 -2
- package/dist/css/css-date-field.js +2 -1
- package/dist/css/css-denomination-appearance.d.ts +21 -0
- package/dist/css/css-denomination-appearance.js +114 -0
- package/dist/css/css-denomination-cell-tones.d.ts +8 -0
- package/dist/css/css-denomination-cell-tones.js +20 -1
- package/dist/css/css-denomination-grid.js +4 -0
- package/dist/css/css-dialog-banded.d.ts +11 -0
- package/dist/css/css-dialog-banded.js +84 -0
- package/dist/css/css-document.js +3 -0
- package/dist/css/css-multi-select.js +2 -1
- package/dist/css/css-nav-words.d.ts +13 -0
- package/dist/css/css-nav-words.js +51 -0
- package/dist/css/css-notice.d.ts +4 -11
- package/dist/css/css-notice.js +6 -22
- package/dist/css/css-scroll-region.d.ts +7 -3
- package/dist/css/css-scroll-region.js +8 -3
- package/dist/css/css-segmented-raised.d.ts +3 -0
- package/dist/css/css-segmented-raised.js +72 -0
- package/dist/css/css-segmented.js +9 -3
- package/dist/css/css-select.js +2 -1
- package/dist/css/css-split.js +2 -1
- package/dist/css/css-stepper.js +7 -0
- package/dist/css/quiet-fill.d.ts +7 -0
- package/dist/css/quiet-fill.js +7 -0
- package/dist/css/stacking.js +2 -1
- package/dist/css/stylesheet.js +4 -0
- package/dist/css/tone-tint.d.ts +21 -0
- package/dist/css/tone-tint.js +23 -0
- package/dist/index.d.ts +13 -10
- package/dist/index.js +10 -7
- package/dist/styles.css +313 -10
- package/package.json +1 -1
|
@@ -1,27 +1,37 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { cx } from '../class-names.js';
|
|
2
3
|
import { denominationLabelWidths } from '../css/css-denomination-aligned.js';
|
|
4
|
+
import { denominationGridFrames, denominationTotalEmphases, denominationTotalsStrongClass, } from '../css/css-denomination-appearance.js';
|
|
5
|
+
import { denominationCellToneAppearances, denominationCellTonesPillClass, } from '../css/css-denomination-cell-tones.js';
|
|
3
6
|
import { denominationLabelAppearances, denominationLabelsPlainClass, } from '../css/css-denomination-label.js';
|
|
4
7
|
import { DenominationStrip } from './denomination-grid/DenominationStrip.js';
|
|
5
8
|
import { DenominationTiles } from './denomination-grid/DenominationTiles.js';
|
|
6
9
|
import { assertDenominationGrid } from './denomination-grid/validate.js';
|
|
7
10
|
import { assertDenominationEntries, assertDenominationExtras, } from './denomination-grid/validate-entry.js';
|
|
8
|
-
export { denominationLabelAppearances };
|
|
9
|
-
|
|
11
|
+
export { denominationCellToneAppearances, denominationGridFrames, denominationLabelAppearances, denominationTotalEmphases, };
|
|
12
|
+
/** Fails closed on a value outside a closed set, naming the prop. */
|
|
13
|
+
function assertOneOf(values, value, prop) {
|
|
14
|
+
if (!values.includes(value))
|
|
15
|
+
throw new RangeError(`Unknown ${prop}: ${String(value)}`);
|
|
16
|
+
}
|
|
17
|
+
export function DenominationGrid({ label, columns, rows, layout = 'strip', subtotal, zeroLabel = '—', totalLabel, rowRole = 'region', labelWidth, labelAppearance = 'tone', frame = 'none', totalEmphasis = 'default', cellToneAppearance = 'text', }) {
|
|
10
18
|
assertDenominationGrid(label, columns, rows);
|
|
11
19
|
assertDenominationExtras(layout, columns, rows);
|
|
12
20
|
assertDenominationEntries(layout, columns, rows);
|
|
13
|
-
if (
|
|
14
|
-
throw new RangeError('zeroLabel must
|
|
21
|
+
if (typeof zeroLabel !== 'string')
|
|
22
|
+
throw new RangeError('zeroLabel must be a string');
|
|
15
23
|
if (totalLabel !== undefined && !totalLabel.trim())
|
|
16
24
|
throw new RangeError('totalLabel must not be empty');
|
|
17
25
|
if (rowRole !== 'region' && rowRole !== 'group')
|
|
18
26
|
throw new RangeError("rowRole must be 'region' or 'group'");
|
|
19
|
-
if (labelWidth !== undefined
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
27
|
+
if (labelWidth !== undefined)
|
|
28
|
+
assertOneOf(denominationLabelWidths, labelWidth, 'labelWidth');
|
|
29
|
+
assertOneOf(denominationLabelAppearances, labelAppearance, 'labelAppearance');
|
|
30
|
+
assertOneOf(denominationGridFrames, frame, 'frame');
|
|
31
|
+
assertOneOf(denominationTotalEmphases, totalEmphasis, 'totalEmphasis');
|
|
32
|
+
assertOneOf(denominationCellToneAppearances, cellToneAppearance, 'cellToneAppearance');
|
|
33
|
+
const appearanceClass = cx(labelAppearance === 'plain' && denominationLabelsPlainClass, totalEmphasis === 'strong' && denominationTotalsStrongClass, cellToneAppearance === 'pill' && denominationCellTonesPillClass);
|
|
24
34
|
if (layout === 'tiles')
|
|
25
|
-
return (_jsx(DenominationTiles, { className:
|
|
26
|
-
return (_jsx(DenominationStrip, { className:
|
|
35
|
+
return (_jsx(DenominationTiles, { className: appearanceClass || undefined, columns: columns, label: label, rowRole: rowRole, rows: rows, subtotal: subtotal, zeroLabel: zeroLabel }));
|
|
36
|
+
return (_jsx(DenominationStrip, { className: appearanceClass || undefined, columns: columns, framed: frame === 'table', label: label, labelWidth: labelWidth, rows: rows, totalLabel: totalLabel, zeroLabel: zeroLabel }));
|
|
27
37
|
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { type DialogHTMLAttributes, type ReactNode } from 'react';
|
|
2
2
|
import { type Breakpoint } from '../css/breakpoints.js';
|
|
3
|
+
import { type DialogAppearance, dialogAppearances } from '../css/css-dialog-banded.js';
|
|
3
4
|
import { type ActionBarSummary } from './action-bar/ActionBarSummary.js';
|
|
5
|
+
export { dialogAppearances, type DialogAppearance };
|
|
4
6
|
export declare const dialogSizes: readonly ["md", "lg", "xl"];
|
|
5
7
|
/**
|
|
6
8
|
* `md` is the reading width (32rem) for a message or a short form; `lg`
|
|
@@ -74,6 +76,12 @@ export type DialogProps = Omit<DialogHTMLAttributes<HTMLDialogElement>, 'onClose
|
|
|
74
76
|
summary?: ActionBarSummary;
|
|
75
77
|
/** `alertdialog` for a warning that interrupts. Default `dialog`. */
|
|
76
78
|
role?: DialogRole;
|
|
79
|
+
/**
|
|
80
|
+
* `banded` draws the dialog on the solid surface with its title row and
|
|
81
|
+
* footer as full-width bands over hairlines, and the close button on a
|
|
82
|
+
* quiet round fill; `glass` (the default) is one frosted panel.
|
|
83
|
+
*/
|
|
84
|
+
appearance?: DialogAppearance;
|
|
77
85
|
children: ReactNode;
|
|
78
86
|
};
|
|
79
|
-
export declare function Dialog({ 'aria-describedby': describedBy, children, className, closeLabel, description, footer, icon, role, summary, onCancel, onClose, onKeyDown, onPointerDown, onSubmit, open, sheetBelow, size, title, titleLevel, ...rest }: DialogProps): import("react").JSX.Element;
|
|
87
|
+
export declare function Dialog({ 'aria-describedby': describedBy, appearance, children, className, closeLabel, description, footer, icon, role, summary, onCancel, onClose, onKeyDown, onPointerDown, onSubmit, open, sheetBelow, size, title, titleLevel, ...rest }: DialogProps): import("react").JSX.Element;
|
|
@@ -3,17 +3,21 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
import { useId } from 'react';
|
|
4
4
|
import { cx } from '../class-names.js';
|
|
5
5
|
import { dialogSheetClass } from '../css/css-dialog.js';
|
|
6
|
+
import { dialogAppearances, dialogBandedClass, } from '../css/css-dialog-banded.js';
|
|
6
7
|
import { spacingClassNames } from '../spacing-classes.js';
|
|
7
8
|
import { useDialogCloseRequests } from './dialog-close-requests.js';
|
|
8
9
|
import { assertActionBarSummary, } from './action-bar/ActionBarSummary.js';
|
|
9
10
|
import { DialogFooter } from './dialog/DialogFooter.js';
|
|
10
11
|
import { DialogTitle } from './dialog/DialogTitle.js';
|
|
11
12
|
import { Stack } from './Stack.js';
|
|
13
|
+
export { dialogAppearances };
|
|
12
14
|
export const dialogSizes = ['md', 'lg', 'xl'];
|
|
13
15
|
const dialogRoles = ['dialog', 'alertdialog'];
|
|
14
|
-
function assertDialogProps(size, role, description, summary) {
|
|
16
|
+
function assertDialogProps(size, role, description, summary, appearance) {
|
|
15
17
|
if (!dialogSizes.includes(size))
|
|
16
18
|
throw new RangeError(`Unknown Dialog size: ${String(size)}`);
|
|
19
|
+
if (!dialogAppearances.includes(appearance))
|
|
20
|
+
throw new RangeError(`Unknown Dialog appearance: ${String(appearance)}`);
|
|
17
21
|
if (role !== undefined && !dialogRoles.includes(role))
|
|
18
22
|
throw new RangeError(`Unknown Dialog role: ${String(role)}`);
|
|
19
23
|
if (description !== undefined &&
|
|
@@ -22,8 +26,8 @@ function assertDialogProps(size, role, description, summary) {
|
|
|
22
26
|
if (summary)
|
|
23
27
|
assertActionBarSummary(summary);
|
|
24
28
|
}
|
|
25
|
-
export function Dialog({ 'aria-describedby': describedBy, children, className, closeLabel, description, footer, icon, role, summary, onCancel, onClose, onKeyDown, onPointerDown, onSubmit, open, sheetBelow, size = 'md', title, titleLevel = 3, ...rest }) {
|
|
26
|
-
assertDialogProps(size, role, description, summary);
|
|
29
|
+
export function Dialog({ 'aria-describedby': describedBy, appearance = 'glass', children, className, closeLabel, description, footer, icon, role, summary, onCancel, onClose, onKeyDown, onPointerDown, onSubmit, open, sheetBelow, size = 'md', title, titleLevel = 3, ...rest }) {
|
|
30
|
+
assertDialogProps(size, role, description, summary, appearance);
|
|
27
31
|
const titleId = useId();
|
|
28
32
|
const descriptionId = useId();
|
|
29
33
|
const hasFooter = Boolean(footer) || Boolean(summary);
|
|
@@ -35,5 +39,5 @@ export function Dialog({ 'aria-describedby': describedBy, children, className, c
|
|
|
35
39
|
onPointerDown,
|
|
36
40
|
onSubmit,
|
|
37
41
|
});
|
|
38
|
-
return (_jsx("dialog", { ...rest, ref: nodeRef, "aria-describedby": describedBy ?? (description ? descriptionId : undefined), "aria-labelledby": titleId, className: cx('sw-dialog', size !== 'md' && `sw-dialog-${size}`, sheetBelow && dialogSheetClass(sheetBelow), ...spacingClassNames({ padding: 5 }), className), role: role === 'alertdialog' ? 'alertdialog' : undefined, ...handlers, children: _jsxs(Stack, { gap: 4, children: [_jsx(DialogTitle, { closeLabel: closeLabel, description: description, descriptionId: descriptionId, icon: icon, id: titleId, level: titleLevel === 2 ? 2 : 3, onClose: onClose, title: title }), children, hasFooter ? (_jsx(DialogFooter, { summary: summary, children: footer })) : null] }) }));
|
|
42
|
+
return (_jsx("dialog", { ...rest, ref: nodeRef, "aria-describedby": describedBy ?? (description ? descriptionId : undefined), "aria-labelledby": titleId, className: cx('sw-dialog', size !== 'md' && `sw-dialog-${size}`, sheetBelow && dialogSheetClass(sheetBelow), appearance === 'banded' && dialogBandedClass, ...spacingClassNames({ padding: 5 }), className), role: role === 'alertdialog' ? 'alertdialog' : undefined, ...handlers, children: _jsxs(Stack, { gap: 4, children: [_jsx(DialogTitle, { banded: appearance === 'banded', closeLabel: closeLabel, description: description, descriptionId: descriptionId, icon: icon, id: titleId, level: titleLevel === 2 ? 2 : 3, onClose: onClose, title: title }), children, hasFooter ? (_jsx(DialogFooter, { summary: summary, children: footer })) : null] }) }));
|
|
39
43
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type SpacingStep } from '@scalewing/tokens';
|
|
2
2
|
import { type ReactNode } from 'react';
|
|
3
|
+
import { type RequiredMark } from './FieldLabelText.js';
|
|
3
4
|
export type FieldSize = 'xs' | 'md';
|
|
4
5
|
export declare const fieldVariants: readonly ["outlined", "search"];
|
|
5
6
|
export type FieldVariant = (typeof fieldVariants)[number];
|
|
@@ -33,6 +34,13 @@ export type FieldProps = {
|
|
|
33
34
|
*/
|
|
34
35
|
changed?: boolean;
|
|
35
36
|
required?: boolean;
|
|
37
|
+
/**
|
|
38
|
+
* `hidden` leaves out the label's " *" while the control stays required
|
|
39
|
+
* (and announced so); `visible` (default) draws it.
|
|
40
|
+
* When hidden, say in visible text which fields must be filled; a
|
|
41
|
+
* placeholder alone is not enough.
|
|
42
|
+
*/
|
|
43
|
+
requiredMark?: RequiredMark;
|
|
36
44
|
/** Short text inside the control frame before the value, such as a currency sign. Not part of the value. */
|
|
37
45
|
prefix?: string;
|
|
38
46
|
/** Short text inside the control frame after the value, such as a unit. Not part of the value. */
|
package/dist/components/Field.js
CHANGED
|
@@ -3,7 +3,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
import { cloneElement, isValidElement, useId, } from 'react';
|
|
4
4
|
import { adornedControlAria, FieldAdornment, } from './FieldAdornment.js';
|
|
5
5
|
import { FieldErrorRegion } from './FieldErrorRegion.js';
|
|
6
|
-
import { FieldLabelText } from './FieldLabelText.js';
|
|
6
|
+
import { assertRequiredMark, FieldLabelText, } from './FieldLabelText.js';
|
|
7
7
|
import { SearchFrame } from './field/SearchFrame.js';
|
|
8
8
|
import { Stack } from './Stack.js';
|
|
9
9
|
export const fieldVariants = ['outlined', 'search'];
|
|
@@ -41,7 +41,7 @@ function passesInvalid(props) {
|
|
|
41
41
|
return props.invalid !== undefined || props.changed !== undefined;
|
|
42
42
|
}
|
|
43
43
|
export function Field(props) {
|
|
44
|
-
const { children, gap = 1, label, labelVisuallyHidden = false, size = 'md', description, error, invalid = false, changed = false, required = false, prefix, suffix, variant = 'outlined', leading, clearLabel = '', onClear, } = props;
|
|
44
|
+
const { children, gap = 1, label, labelVisuallyHidden = false, size = 'md', description, error, invalid = false, changed = false, required = false, requiredMark = 'visible', prefix, suffix, variant = 'outlined', leading, clearLabel = '', onClear, } = props;
|
|
45
45
|
assertSearch(props, children);
|
|
46
46
|
const search = variant === 'search';
|
|
47
47
|
const controlId = useId();
|
|
@@ -50,6 +50,7 @@ export function Field(props) {
|
|
|
50
50
|
const labelId = useId();
|
|
51
51
|
const prefixId = useId();
|
|
52
52
|
const suffixId = useId();
|
|
53
|
+
assertRequiredMark(requiredMark);
|
|
53
54
|
const message = error || description;
|
|
54
55
|
const marksInvalid = Boolean(error) || invalid;
|
|
55
56
|
const adorned = Boolean(prefix || suffix);
|
|
@@ -65,7 +66,7 @@ export function Field(props) {
|
|
|
65
66
|
.join(' ');
|
|
66
67
|
// A search field hides its whole label element instead (see below).
|
|
67
68
|
const labelHidden = labelVisuallyHidden && !search;
|
|
68
|
-
const labelText = (_jsx(FieldLabelText, { label: label, required: required, size: size, visuallyHidden: labelHidden }));
|
|
69
|
+
const labelText = (_jsx(FieldLabelText, { label: label, required: required, requiredMark: requiredMark, size: size, visuallyHidden: labelHidden }));
|
|
69
70
|
if (!isValidElement(children) ||
|
|
70
71
|
typeof children.type !== 'string' ||
|
|
71
72
|
!['input', 'select', 'textarea'].includes(children.type)) {
|
|
@@ -1,13 +1,31 @@
|
|
|
1
|
+
export declare const requiredMarks: readonly ["visible", "hidden"];
|
|
2
|
+
/**
|
|
3
|
+
* Whether a required control's label shows the required mark: `visible`
|
|
4
|
+
* (the default) draws " *" after the label; `hidden` leaves it out, for a
|
|
5
|
+
* form that marks nothing as required (or marks what is optional instead).
|
|
6
|
+
* The control stays required either way, so assistive technology still
|
|
7
|
+
* hears it. When hidden, say in visible text which fields must be filled
|
|
8
|
+
* (a form-level note, or "Required" in the description); a placeholder
|
|
9
|
+
* alone is not enough (WCAG 3.3.2).
|
|
10
|
+
*/
|
|
11
|
+
export type RequiredMark = (typeof requiredMarks)[number];
|
|
12
|
+
/** Fails closed on a value that is not a known required mark. */
|
|
13
|
+
export declare function assertRequiredMark(value: unknown): void;
|
|
14
|
+
/** Whether the mark is drawn: a required control whose mark is visible. */
|
|
15
|
+
export declare function showsRequiredMark(required: boolean, requiredMark: RequiredMark): boolean;
|
|
1
16
|
/**
|
|
2
17
|
* A form control's label words and its required mark, as `Field` draws
|
|
3
18
|
* them. The `<label>` around it inherits the canvas's body type, so its
|
|
4
19
|
* line box, and the gap to the control below, is the same for every field
|
|
5
|
-
* that uses it (`Field`, `DateField` and `Select
|
|
6
|
-
*
|
|
20
|
+
* that uses it (`Field`, `DateField` and `Select`; `Combobox` through
|
|
21
|
+
* `Field`, and `FilterChips` shares `showsRequiredMark` for its legend). The mark is `aria-hidden`: the
|
|
22
|
+
* control's own required state is what assistive technology reads, and
|
|
23
|
+
* `requiredMark="hidden"` leaves it out.
|
|
7
24
|
*/
|
|
8
|
-
export declare function FieldLabelText({ label, required, size, visuallyHidden, }: {
|
|
25
|
+
export declare function FieldLabelText({ label, required, requiredMark, size, visuallyHidden, }: {
|
|
9
26
|
label: string;
|
|
10
27
|
required?: boolean;
|
|
28
|
+
requiredMark?: RequiredMark;
|
|
11
29
|
size?: 'xs' | 'md';
|
|
12
30
|
visuallyHidden?: boolean;
|
|
13
31
|
}): import("react").JSX.Element;
|
|
@@ -1,12 +1,24 @@
|
|
|
1
1
|
import { jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Text } from './Text.js';
|
|
3
|
+
export const requiredMarks = ['visible', 'hidden'];
|
|
4
|
+
/** Fails closed on a value that is not a known required mark. */
|
|
5
|
+
export function assertRequiredMark(value) {
|
|
6
|
+
if (!requiredMarks.includes(value))
|
|
7
|
+
throw new RangeError(`Unknown requiredMark: ${String(value)}`);
|
|
8
|
+
}
|
|
9
|
+
/** Whether the mark is drawn: a required control whose mark is visible. */
|
|
10
|
+
export function showsRequiredMark(required, requiredMark) {
|
|
11
|
+
return required && requiredMark === 'visible';
|
|
12
|
+
}
|
|
3
13
|
/**
|
|
4
14
|
* A form control's label words and its required mark, as `Field` draws
|
|
5
15
|
* them. The `<label>` around it inherits the canvas's body type, so its
|
|
6
16
|
* line box, and the gap to the control below, is the same for every field
|
|
7
|
-
* that uses it (`Field`, `DateField` and `Select
|
|
8
|
-
*
|
|
17
|
+
* that uses it (`Field`, `DateField` and `Select`; `Combobox` through
|
|
18
|
+
* `Field`, and `FilterChips` shares `showsRequiredMark` for its legend). The mark is `aria-hidden`: the
|
|
19
|
+
* control's own required state is what assistive technology reads, and
|
|
20
|
+
* `requiredMark="hidden"` leaves it out.
|
|
9
21
|
*/
|
|
10
|
-
export function FieldLabelText({ label, required = false, size = 'md', visuallyHidden = false, }) {
|
|
11
|
-
return (_jsxs(Text, { as: "span", className: visuallyHidden ? 'sw-sr-only' : undefined, variant: size === 'xs' ? 'caption' : 'label', children: [label, required ? (_jsxs("span", { "aria-hidden": "true", className: "sw-field-required", children: [' ', "*"] })) : null] }));
|
|
22
|
+
export function FieldLabelText({ label, required = false, requiredMark = 'visible', size = 'md', visuallyHidden = false, }) {
|
|
23
|
+
return (_jsxs(Text, { as: "span", className: visuallyHidden ? 'sw-sr-only' : undefined, variant: size === 'xs' ? 'caption' : 'label', children: [label, showsRequiredMark(required, requiredMark) ? (_jsxs("span", { "aria-hidden": "true", className: "sw-field-required", children: [' ', "*"] })) : null] }));
|
|
12
24
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type RequiredMark } from './FieldLabelText.js';
|
|
1
2
|
export type FilterChipOption = {
|
|
2
3
|
value: string;
|
|
3
4
|
label: string;
|
|
@@ -16,6 +17,13 @@ export type FilterChipsProps = {
|
|
|
16
17
|
* which reads the radios' own state).
|
|
17
18
|
*/
|
|
18
19
|
required?: boolean;
|
|
20
|
+
/**
|
|
21
|
+
* `hidden` leaves out the label's " *" while the control stays required
|
|
22
|
+
* (and announced so); `visible` (default) draws it.
|
|
23
|
+
* When hidden, say in visible text which fields must be filled; a
|
|
24
|
+
* placeholder alone is not enough.
|
|
25
|
+
*/
|
|
26
|
+
requiredMark?: RequiredMark;
|
|
19
27
|
/**
|
|
20
28
|
* A message under the chips, such as "Pick a reason": a polite live
|
|
21
29
|
* region linked to the group by `aria-describedby`; the group is marked
|
|
@@ -26,4 +34,4 @@ export type FilterChipsProps = {
|
|
|
26
34
|
/** More ids that describe the group, such as a hint beside it. */
|
|
27
35
|
'aria-describedby'?: string;
|
|
28
36
|
};
|
|
29
|
-
export declare function FilterChips({ label, value, onChange, options, required, error, 'aria-describedby': describedByProp, }: FilterChipsProps): import("react").JSX.Element;
|
|
37
|
+
export declare function FilterChips({ label, value, onChange, options, required, requiredMark, error, 'aria-describedby': describedByProp, }: FilterChipsProps): import("react").JSX.Element;
|
|
@@ -3,10 +3,11 @@ import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-run
|
|
|
3
3
|
import { useId } from 'react';
|
|
4
4
|
import { cx } from '../class-names.js';
|
|
5
5
|
import { FieldErrorRegion } from './FieldErrorRegion.js';
|
|
6
|
+
import { assertRequiredMark, showsRequiredMark, } from './FieldLabelText.js';
|
|
6
7
|
function isCount(count) {
|
|
7
8
|
return count === undefined || (Number.isInteger(count) && count >= 0);
|
|
8
9
|
}
|
|
9
|
-
export function FilterChips({ label, value, onChange, options, required = false, error, 'aria-describedby': describedByProp, }) {
|
|
10
|
+
export function FilterChips({ label, value, onChange, options, required = false, requiredMark = 'visible', error, 'aria-describedby': describedByProp, }) {
|
|
10
11
|
const name = useId();
|
|
11
12
|
const errorId = useId();
|
|
12
13
|
const describedBy = [describedByProp?.trim() || null, error ? errorId : null]
|
|
@@ -15,6 +16,7 @@ export function FilterChips({ label, value, onChange, options, required = false,
|
|
|
15
16
|
const values = options.map((option) => option.value);
|
|
16
17
|
if (!label.trim())
|
|
17
18
|
throw new RangeError('label must not be empty');
|
|
19
|
+
assertRequiredMark(requiredMark);
|
|
18
20
|
if (options.length === 0 ||
|
|
19
21
|
options.some((option) => !option.value.trim() || !option.label.trim()))
|
|
20
22
|
throw new RangeError('options must have nonempty values and labels');
|
|
@@ -24,7 +26,7 @@ export function FilterChips({ label, value, onChange, options, required = false,
|
|
|
24
26
|
throw new RangeError('value must match an option or be empty');
|
|
25
27
|
if (options.some((option) => !isCount(option.count)))
|
|
26
28
|
throw new RangeError('count must be a non-negative integer');
|
|
27
|
-
return (_jsxs("fieldset", { "aria-describedby": describedBy || undefined, "aria-invalid": error ? true : undefined, className: "sw-filter-chips", children: [_jsxs("legend", { className: "sw-filter-chips-legend", children: [label, required ? (_jsxs("span", { "aria-hidden": "true", className: "sw-field-required", children: [' ', "*"] })) : null] }), _jsx("span", { className: "sw-filter-chips-options", children: options.map((option) => (_jsxs("label", { className: cx('sw-filter-chip', option.count === 0 && 'sw-filter-chip-quiet'), children: [_jsx("input", { checked: option.value === value, className: "sw-filter-chip-input", disabled: option.disabled, name: name, onChange: (event) => {
|
|
29
|
+
return (_jsxs("fieldset", { "aria-describedby": describedBy || undefined, "aria-invalid": error ? true : undefined, className: "sw-filter-chips", children: [_jsxs("legend", { className: "sw-filter-chips-legend", children: [label, showsRequiredMark(required, requiredMark) ? (_jsxs("span", { "aria-hidden": "true", className: "sw-field-required", children: [' ', "*"] })) : null] }), _jsx("span", { className: "sw-filter-chips-options", children: options.map((option) => (_jsxs("label", { className: cx('sw-filter-chip', option.count === 0 && 'sw-filter-chip-quiet'), children: [_jsx("input", { checked: option.value === value, className: "sw-filter-chip-input", disabled: option.disabled, name: name, onChange: (event) => {
|
|
28
30
|
if (event.currentTarget.checked && !option.disabled)
|
|
29
31
|
onChange(option.value);
|
|
30
32
|
}, required: required, type: "radio", value: option.value }), _jsxs("span", { className: "sw-filter-chip-face", children: [option.label, option.count === undefined ? null : (_jsxs(_Fragment, { children: [' ', _jsx("span", { className: "sw-filter-chip-count", children: option.count })] }))] })] }, option.value))) }), _jsx(FieldErrorRegion, { className: "sw-filter-chips-error", id: errorId, message: error })] }));
|
package/dist/components/Nav.d.ts
CHANGED
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
import { type NavAppearance, navAppearances } from '../css/css-nav-words.js';
|
|
1
2
|
import { type BoxProps } from './Box.js';
|
|
2
|
-
export
|
|
3
|
-
export
|
|
3
|
+
export { navAppearances, type NavAppearance };
|
|
4
|
+
export type NavProps = Omit<BoxProps, 'as'> & {
|
|
5
|
+
/**
|
|
6
|
+
* `words` draws every destination (a link or a Button) as plain words:
|
|
7
|
+
* the current one (`aria-current`) in the text color at label weight,
|
|
8
|
+
* the others muted, with no fill or underline. The words must be visible:
|
|
9
|
+
* a glyph-only destination has no weight to change, so its current mark
|
|
10
|
+
* would be color alone (keep `default` there). `default` keeps each
|
|
11
|
+
* destination's own look.
|
|
12
|
+
*/
|
|
13
|
+
appearance?: NavAppearance;
|
|
14
|
+
};
|
|
15
|
+
export declare function Nav({ appearance, className, ...rest }: NavProps): import("react").JSX.Element;
|
package/dist/components/Nav.js
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
3
|
import { cx } from '../class-names.js';
|
|
4
|
+
import { navAppearances, navWordsClass, } from '../css/css-nav-words.js';
|
|
4
5
|
import { Box } from './Box.js';
|
|
5
|
-
export
|
|
6
|
-
|
|
6
|
+
export { navAppearances };
|
|
7
|
+
export function Nav({ appearance = 'default', className, ...rest }) {
|
|
8
|
+
if (!navAppearances.includes(appearance))
|
|
9
|
+
throw new RangeError(`Unknown Nav appearance: ${String(appearance)}`);
|
|
10
|
+
return (_jsx(Box, { as: "nav", className: cx('sw-nav', appearance === 'words' && navWordsClass, className), ...rest }));
|
|
7
11
|
}
|
|
@@ -22,9 +22,12 @@ type SegmentedLabel = {
|
|
|
22
22
|
* `compact` is the quiet chip track for a section switch; `filled` gives every
|
|
23
23
|
* segment the same width and paints the selected one in the accent, for a
|
|
24
24
|
* choice that decides what a form does. It sizes to its container: the full
|
|
25
|
-
* width in a Stack, the width of its widest label in an Inline.
|
|
25
|
+
* width in a Stack, the width of its widest label in an Inline. `raised` is
|
|
26
|
+
* a grey track whose selected segment is the surface raised by a small
|
|
27
|
+
* shadow, every segment at the md control height with its words in the
|
|
28
|
+
* text color, sized to its segments even in a Stack (unlike `compact`).
|
|
26
29
|
*/
|
|
27
|
-
export type SegmentedControlVariant = 'compact' | 'filled';
|
|
30
|
+
export type SegmentedControlVariant = 'compact' | 'filled' | 'raised';
|
|
28
31
|
/**
|
|
29
32
|
* A disabled control can never report a change, so it needs no `onChange`;
|
|
30
33
|
* a live one must have it.
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { forwardRef, useId } from 'react';
|
|
4
4
|
import { cx } from '../class-names.js';
|
|
5
|
+
import { segmentedRaisedClass } from '../css/css-segmented-raised.js';
|
|
5
6
|
import { FieldErrorRegion } from './FieldErrorRegion.js';
|
|
6
7
|
export const SegmentedControl = forwardRef(function SegmentedControl({ disabled = false, error, items, onChange, required = false, value, variant = 'compact', ...labelProps }, ref) {
|
|
7
8
|
const errorId = useId();
|
|
@@ -23,7 +24,7 @@ export const SegmentedControl = forwardRef(function SegmentedControl({ disabled
|
|
|
23
24
|
}
|
|
24
25
|
// The wrapper is always rendered, so an error that appears later swaps
|
|
25
26
|
// text into a live region that already exists and the group keeps focus.
|
|
26
|
-
return (_jsxs("div", { className: cx('sw-segmented-field', variant === 'filled' && 'sw-segmented-field-filled'), children: [_jsx("div", { ref: ref, "aria-describedby": error ? errorId : undefined, "aria-disabled": disabled || undefined, "aria-invalid": error ? true : undefined, "aria-required": required || undefined, className: cx('sw-segmented', variant === 'filled' && 'sw-segmented-filled', withIcons && 'sw-segmented-with-icons', disabled && 'sw-segmented-disabled'), onKeyDown: onKeyDown, role: "radiogroup", ...labelProps, children: items.map((item) => {
|
|
27
|
+
return (_jsxs("div", { className: cx('sw-segmented-field', variant === 'filled' && 'sw-segmented-field-filled'), children: [_jsx("div", { ref: ref, "aria-describedby": error ? errorId : undefined, "aria-disabled": disabled || undefined, "aria-invalid": error ? true : undefined, "aria-required": required || undefined, className: cx('sw-segmented', variant === 'filled' && 'sw-segmented-filled', variant === 'raised' && segmentedRaisedClass, withIcons && 'sw-segmented-with-icons', disabled && 'sw-segmented-disabled'), onKeyDown: onKeyDown, role: "radiogroup", ...labelProps, children: items.map((item) => {
|
|
27
28
|
const selected = item.id === value;
|
|
28
29
|
return (_jsxs("button", { "aria-checked": selected, className: cx('sw-segmented-item', selected && 'sw-segmented-item-selected'), disabled: disabled, onClick: () => {
|
|
29
30
|
if (!selected) {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type SelectOption } from '../select-list.js';
|
|
2
2
|
import { type FieldSize } from './Field.js';
|
|
3
|
+
import { type RequiredMark } from './FieldLabelText.js';
|
|
3
4
|
export type { SelectOption };
|
|
4
5
|
/**
|
|
5
6
|
* `content` (the default) sizes the field to its longest option, as a native
|
|
@@ -26,6 +27,13 @@ export type SelectProps = {
|
|
|
26
27
|
placeholder?: string;
|
|
27
28
|
/** Marks the label as Field does and sets `aria-required`. */
|
|
28
29
|
required?: boolean;
|
|
30
|
+
/**
|
|
31
|
+
* `hidden` leaves out the label's " *" while the control stays required
|
|
32
|
+
* (and announced so); `visible` (default) draws it.
|
|
33
|
+
* When hidden, say in visible text which fields must be filled; a
|
|
34
|
+
* placeholder alone is not enough.
|
|
35
|
+
*/
|
|
36
|
+
requiredMark?: RequiredMark;
|
|
29
37
|
/** A validation message under the control, wired as Field's error is. */
|
|
30
38
|
error?: string;
|
|
31
39
|
/**
|
|
@@ -35,4 +43,4 @@ export type SelectProps = {
|
|
|
35
43
|
*/
|
|
36
44
|
width?: SelectWidth;
|
|
37
45
|
};
|
|
38
|
-
export declare function Select({ action, label, labelVisuallyHidden, onChange, options, size, value, placeholder, required, error, width, }: SelectProps): import("react").JSX.Element;
|
|
46
|
+
export declare function Select({ action, label, labelVisuallyHidden, onChange, options, size, value, placeholder, required, requiredMark, error, width, }: SelectProps): import("react").JSX.Element;
|
|
@@ -4,12 +4,13 @@ import { useId, useRef } from 'react';
|
|
|
4
4
|
import { cx } from '../class-names.js';
|
|
5
5
|
import { selectedSelectIndex } from '../select-list.js';
|
|
6
6
|
import { FieldErrorRegion } from './FieldErrorRegion.js';
|
|
7
|
-
import { FieldLabelText } from './FieldLabelText.js';
|
|
7
|
+
import { assertRequiredMark, FieldLabelText, } from './FieldLabelText.js';
|
|
8
8
|
import { SelectListbox } from './select/SelectListbox.js';
|
|
9
9
|
import { SelectValue } from './select/SelectValue.js';
|
|
10
10
|
import { useSelectList } from './select/use-select-list.js';
|
|
11
11
|
import { Stack } from './Stack.js';
|
|
12
|
-
export function Select({ action, label, labelVisuallyHidden = false, onChange, options, size = 'md', value, placeholder, required = false, error, width = 'content', }) {
|
|
12
|
+
export function Select({ action, label, labelVisuallyHidden = false, onChange, options, size = 'md', value, placeholder, required = false, requiredMark = 'visible', error, width = 'content', }) {
|
|
13
|
+
assertRequiredMark(requiredMark);
|
|
13
14
|
if (placeholder !== undefined && !placeholder.trim())
|
|
14
15
|
throw new RangeError('placeholder must not be empty');
|
|
15
16
|
const labelId = useId();
|
|
@@ -22,7 +23,7 @@ export function Select({ action, label, labelVisuallyHidden = false, onChange, o
|
|
|
22
23
|
const showsPlaceholder = placeholder !== undefined &&
|
|
23
24
|
!options.some((option) => option.value === value);
|
|
24
25
|
const { commit, highlight, onTriggerKeyDown, open, openList, setHighlight, setOpen, } = useSelectList({ action, onChange, options, rootRef, value });
|
|
25
|
-
return (_jsx("div", { ref: rootRef, className: cx('sw-select', size === 'xs' && 'sw-select-xs', width === 'full' && 'sw-select-full', error && 'sw-select-invalid'), children: _jsxs(Stack, { gap: labelVisuallyHidden ? 0 : 1, children: [_jsx("label", { htmlFor: triggerId, id: labelId, children: _jsx(FieldLabelText, { label: label, required: required, size: size, visuallyHidden: labelVisuallyHidden }) }), _jsxs("div", { className: "sw-select-control", children: [_jsx("button", { "aria-activedescendant": open ? `${optionIdPrefix}-${highlight}` : undefined, "aria-controls": open ? listId : undefined, "aria-describedby": error ? errorId : undefined, "aria-expanded": open, "aria-haspopup": "listbox", "aria-invalid": error ? true : undefined, "aria-labelledby": labelId, "aria-required": required || undefined, className: "sw-select-trigger", id: triggerId, onBlur: (event) => {
|
|
26
|
+
return (_jsx("div", { ref: rootRef, className: cx('sw-select', size === 'xs' && 'sw-select-xs', width === 'full' && 'sw-select-full', error && 'sw-select-invalid'), children: _jsxs(Stack, { gap: labelVisuallyHidden ? 0 : 1, children: [_jsx("label", { htmlFor: triggerId, id: labelId, children: _jsx(FieldLabelText, { label: label, required: required, requiredMark: requiredMark, size: size, visuallyHidden: labelVisuallyHidden }) }), _jsxs("div", { className: "sw-select-control", children: [_jsx("button", { "aria-activedescendant": open ? `${optionIdPrefix}-${highlight}` : undefined, "aria-controls": open ? listId : undefined, "aria-describedby": error ? errorId : undefined, "aria-expanded": open, "aria-haspopup": "listbox", "aria-invalid": error ? true : undefined, "aria-labelledby": labelId, "aria-required": required || undefined, className: "sw-select-trigger", id: triggerId, onBlur: (event) => {
|
|
26
27
|
const next = event.relatedTarget;
|
|
27
28
|
if (next instanceof Node && rootRef.current?.contains(next)) {
|
|
28
29
|
return;
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
/** `fixed` sizes the count for a few digits; `full` fills its container, such as a table cell. */
|
|
2
2
|
export type StepperWidth = 'fixed' | 'full';
|
|
3
|
+
export declare const stepperLimitAppearances: readonly ["faded", "plain"];
|
|
4
|
+
/**
|
|
5
|
+
* How − looks at `min` and + at `max`, where a press cannot move the count:
|
|
6
|
+
* `faded` (the default) fades the button as any disabled control; `plain`
|
|
7
|
+
* keeps its usual look. Either way the button is disabled.
|
|
8
|
+
*/
|
|
9
|
+
export type StepperLimitAppearance = (typeof stepperLimitAppearances)[number];
|
|
3
10
|
export type StepperProps = {
|
|
4
11
|
/** Names the count. Keep it visible unless headers around it say it (`labelVisuallyHidden`). */
|
|
5
12
|
label: string;
|
|
@@ -35,6 +42,12 @@ export type StepperProps = {
|
|
|
35
42
|
placeholder?: string;
|
|
36
43
|
name?: string;
|
|
37
44
|
width?: StepperWidth;
|
|
45
|
+
/**
|
|
46
|
+
* `plain` keeps − at `min` and + at `max` in their usual look instead of
|
|
47
|
+
* fading them (`faded`, the default). The button is still disabled, and a
|
|
48
|
+
* disabled or read-only Stepper is unchanged.
|
|
49
|
+
*/
|
|
50
|
+
limitAppearance?: StepperLimitAppearance;
|
|
38
51
|
};
|
|
39
52
|
/**
|
|
40
53
|
* A whole-number count between − and + buttons that can also be typed: one
|
|
@@ -5,6 +5,7 @@ import { cx } from '../class-names.js';
|
|
|
5
5
|
import { assertStepperBounds, readStepperEntry, stepperMoves, stepperText, } from '../stepper-count.js';
|
|
6
6
|
import { FieldErrorRegion } from './FieldErrorRegion.js';
|
|
7
7
|
import { FieldLabelText } from './FieldLabelText.js';
|
|
8
|
+
export const stepperLimitAppearances = ['faded', 'plain'];
|
|
8
9
|
/** Keeps focus where it was (the count, or nowhere) when a button is pressed. */
|
|
9
10
|
function keepFocus(event) {
|
|
10
11
|
event.preventDefault();
|
|
@@ -14,9 +15,11 @@ function keepFocus(event) {
|
|
|
14
15
|
* tab stop (the count, a spin button that ArrowUp and ArrowDown step), with
|
|
15
16
|
* the buttons as pointer targets beside it. − stops at `min`, + at `max`.
|
|
16
17
|
*/
|
|
17
|
-
export const Stepper = forwardRef(function Stepper({ label, labelVisuallyHidden = false, value, onChange, min = 0, max = Number.POSITIVE_INFINITY, step = 1, decrementLabel, incrementLabel, error, changed = false, disabled = false, readOnly = false, valueText, placeholder, name, width = 'fixed', }, ref) {
|
|
18
|
+
export const Stepper = forwardRef(function Stepper({ label, labelVisuallyHidden = false, value, onChange, min = 0, max = Number.POSITIVE_INFINITY, step = 1, decrementLabel, incrementLabel, error, changed = false, disabled = false, readOnly = false, valueText, placeholder, name, width = 'fixed', limitAppearance = 'faded', }, ref) {
|
|
18
19
|
const bounds = { min, max, step };
|
|
19
20
|
assertStepperBounds(bounds);
|
|
21
|
+
if (!stepperLimitAppearances.includes(limitAppearance))
|
|
22
|
+
throw new RangeError(`Unknown Stepper limitAppearance: ${String(limitAppearance)}`);
|
|
20
23
|
if (value !== null && !Number.isSafeInteger(value))
|
|
21
24
|
throw new RangeError('Stepper value must be a whole number or null');
|
|
22
25
|
const inputId = useId();
|
|
@@ -70,7 +73,10 @@ export const Stepper = forwardRef(function Stepper({ label, labelVisuallyHidden
|
|
|
70
73
|
move(target);
|
|
71
74
|
}
|
|
72
75
|
const invalid = Boolean(error);
|
|
73
|
-
return (_jsxs("div", { className: cx('sw-stepper', width === 'full' && 'sw-stepper-full', invalid && 'sw-stepper-invalid', changed && !invalid && 'sw-stepper-changed', readOnly && 'sw-stepper-readonly'
|
|
76
|
+
return (_jsxs("div", { className: cx('sw-stepper', width === 'full' && 'sw-stepper-full', invalid && 'sw-stepper-invalid', changed && !invalid && 'sw-stepper-changed', readOnly && 'sw-stepper-readonly', limitAppearance === 'plain' &&
|
|
77
|
+
!disabled &&
|
|
78
|
+
!readOnly &&
|
|
79
|
+
'sw-stepper-limit-plain'), children: [_jsx("label", { className: labelVisuallyHidden ? 'sw-sr-only' : undefined, htmlFor: inputId, id: labelId, children: _jsx(FieldLabelText, { label: label }) }), _jsxs("div", { "aria-labelledby": labelId, className: "sw-stepper-track", role: "group", children: [_jsx("button", { "aria-controls": inputId, "aria-label": decrementLabel, className: "sw-stepper-button", disabled: moves.down === null, onClick: () => move(moves.down), onMouseDown: keepFocus, tabIndex: -1, type: "button", children: _jsx("span", { "aria-hidden": "true", children: '−' }) }), _jsx("input", { ref: ref, "aria-describedby": invalid ? errorId : undefined, "aria-invalid": invalid || undefined, "aria-valuemax": Number.isFinite(max) ? max : undefined, "aria-valuemin": min, "aria-valuenow": value ?? undefined, "aria-valuetext": valueText, autoComplete: "off", className: "sw-stepper-input", disabled: disabled, id: inputId,
|
|
74
80
|
// iOS's numeric pad has no minus key, so a range below zero takes
|
|
75
81
|
// the text keyboard.
|
|
76
82
|
inputMode: allowsNegative ? 'text' : 'numeric', name: name, onBlur: () => setDraft(null), onChange: onType, onKeyDown: onKeyDown, placeholder: placeholder, readOnly: readOnly, role: "spinbutton", type: "text", value: text }), _jsx("button", { "aria-controls": inputId, "aria-label": incrementLabel, className: "sw-stepper-button", disabled: moves.up === null, onClick: () => move(moves.up), onMouseDown: keepFocus, tabIndex: -1, type: "button", children: _jsx("span", { "aria-hidden": "true", children: "+" }) })] }), _jsx(FieldErrorRegion, { className: "sw-field-error", id: errorId, message: error })] }));
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type BadgeTone } from '../../css/css-data.js';
|
|
2
|
+
import { type RequiredMark } from '../FieldLabelText.js';
|
|
2
3
|
/** A short tag after an option's label, such as "Warning" or "Blocked". */
|
|
3
4
|
export type ComboboxBadge = {
|
|
4
5
|
label: string;
|
|
@@ -58,6 +59,13 @@ export type ComboboxProps = {
|
|
|
58
59
|
description?: string;
|
|
59
60
|
error?: string;
|
|
60
61
|
required?: boolean;
|
|
62
|
+
/**
|
|
63
|
+
* `hidden` leaves out the label's " *" while the input stays required
|
|
64
|
+
* (and announced so); `visible` (default) draws it.
|
|
65
|
+
* When hidden, say in visible text which fields must be filled; a
|
|
66
|
+
* placeholder alone is not enough.
|
|
67
|
+
*/
|
|
68
|
+
requiredMark?: RequiredMark;
|
|
61
69
|
disabled?: boolean;
|
|
62
70
|
size?: ComboboxSize;
|
|
63
71
|
};
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { type DenominationGridProps } from './types.js';
|
|
2
2
|
type StripProps = Required<Pick<DenominationGridProps, 'label' | 'columns' | 'rows' | 'zeroLabel'>> & Pick<DenominationGridProps, 'totalLabel' | 'labelWidth'> & {
|
|
3
|
-
/** The
|
|
3
|
+
/** The grid's appearance classes (labels, totals, cell tones), if any. */
|
|
4
4
|
className?: string;
|
|
5
|
+
/** The `table` frame: a hairline around the region and a heading-row fill. */
|
|
6
|
+
framed: boolean;
|
|
5
7
|
};
|
|
6
|
-
export declare function DenominationStrip({ className, label, columns, rows, zeroLabel, totalLabel, labelWidth, }: StripProps): import("react").JSX.Element;
|
|
8
|
+
export declare function DenominationStrip({ className, framed, label, columns, rows, zeroLabel, totalLabel, labelWidth, }: StripProps): import("react").JSX.Element;
|
|
7
9
|
export {};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { cx } from '../../class-names.js';
|
|
3
3
|
import { denominationLabelWidthClass } from '../../css/css-denomination-aligned.js';
|
|
4
|
+
import { denominationFramedClass, denominationScrollFramedClass, } from '../../css/css-denomination-appearance.js';
|
|
4
5
|
import { ScrollRegion } from '../ScrollRegion.js';
|
|
5
6
|
import { ColumnHeadText } from './ColumnHeadText.js';
|
|
6
7
|
import { StripRow } from './StripRow.js';
|
|
@@ -16,7 +17,7 @@ function TotalHead({ totalLabel }) {
|
|
|
16
17
|
return _jsx("td", { className: "sw-denomination-corner sw-denomination-total-head" });
|
|
17
18
|
return (_jsx("th", { className: "sw-denomination-head sw-denomination-total-head", scope: "col", children: _jsx("span", { className: "sw-denomination-total-label", children: totalLabel }) }));
|
|
18
19
|
}
|
|
19
|
-
export function DenominationStrip({ className, label, columns, rows, zeroLabel, totalLabel, labelWidth, }) {
|
|
20
|
+
export function DenominationStrip({ className, framed, label, columns, rows, zeroLabel, totalLabel, labelWidth, }) {
|
|
20
21
|
const hasTotals = rows.some((row) => row.total !== undefined);
|
|
21
22
|
// An aligned strip sizes its columns from the region's width; it needs
|
|
22
23
|
// the column count and whether a total column takes a share.
|
|
@@ -26,5 +27,5 @@ export function DenominationStrip({ className, label, columns, rows, zeroLabel,
|
|
|
26
27
|
'--sw-denomination-columns': columns.length,
|
|
27
28
|
'--sw-denomination-total': hasTotals ? 1 : 0,
|
|
28
29
|
};
|
|
29
|
-
return (_jsx(ScrollRegion, { "aria-label": label, className: cx('sw-denomination-scroll', aligned && 'sw-denomination-scroll-aligned'), children: _jsxs("table", { className: cx('sw-denomination-grid sw-denomination-strip', labelWidth && 'sw-denomination-strip-aligned', rows.some((row) => row.entry) && 'sw-denomination-strip-entry', labelWidth && denominationLabelWidthClass(labelWidth), className), style: aligned, children: [_jsx("caption", { className: "sw-sr-only", children: label }), _jsx("thead", { children: _jsxs("tr", { children: [_jsx("td", { className: "sw-denomination-corner" }), columns.map((column) => (_jsx("th", { className: "sw-denomination-head", scope: "col", children: _jsx(ColumnHeadText, { column: column }) }, column.key))), hasTotals ? _jsx(TotalHead, { totalLabel: totalLabel }) : null] }) }), _jsx("tbody", { children: rows.map((row) => (_jsx(StripRow, { columns: columns, hasTotals: hasTotals, row: row, zeroLabel: zeroLabel }, row.id))) })] }) }));
|
|
30
|
+
return (_jsx(ScrollRegion, { "aria-label": label, className: cx('sw-denomination-scroll', aligned && 'sw-denomination-scroll-aligned', framed && denominationScrollFramedClass), children: _jsxs("table", { className: cx('sw-denomination-grid sw-denomination-strip', labelWidth && 'sw-denomination-strip-aligned', rows.some((row) => row.entry) && 'sw-denomination-strip-entry', labelWidth && denominationLabelWidthClass(labelWidth), framed && denominationFramedClass, className), style: aligned, children: [_jsx("caption", { className: "sw-sr-only", children: label }), _jsx("thead", { children: _jsxs("tr", { children: [_jsx("td", { className: "sw-denomination-corner" }), columns.map((column) => (_jsx("th", { className: "sw-denomination-head", scope: "col", children: _jsx(ColumnHeadText, { column: column }) }, column.key))), hasTotals ? _jsx(TotalHead, { totalLabel: totalLabel }) : null] }) }), _jsx("tbody", { children: rows.map((row) => (_jsx(StripRow, { columns: columns, hasTotals: hasTotals, row: row, zeroLabel: zeroLabel }, row.id))) })] }) }));
|
|
30
31
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type DenominationGridProps } from './types.js';
|
|
2
2
|
type TilesProps = Required<Pick<DenominationGridProps, 'label' | 'columns' | 'rows' | 'zeroLabel' | 'rowRole'>> & Pick<DenominationGridProps, 'subtotal'> & {
|
|
3
|
-
/** The
|
|
3
|
+
/** The grid's appearance classes (labels, totals), if any. */
|
|
4
4
|
className?: string;
|
|
5
5
|
};
|
|
6
6
|
export declare function DenominationTiles({ className, label, columns, rows, rowRole, subtotal, zeroLabel, }: TilesProps): import("react").JSX.Element;
|
|
@@ -12,5 +12,5 @@ import { columnName } from './ColumnHeadText.js';
|
|
|
12
12
|
export function EntryCell({ row, entry, column, index }) {
|
|
13
13
|
const max = entry.max?.[index] ?? undefined;
|
|
14
14
|
const tone = row.cellTones?.[index];
|
|
15
|
-
return (_jsx("td", { className: cx('sw-denomination-entry', ...denominationCellToneClassNames(tone)), children: _jsx(Stepper, { changed: tone === 'accent', decrementLabel: entry.decrementLabel, disabled: row.disabled, error: entry.errors?.[index] ?? undefined, incrementLabel: entry.incrementLabel, label: `${row.label} ${columnName(column)}`, labelVisuallyHidden: true, max: max, onChange: (value) => entry.onChange(column.key, value), placeholder: entry.placeholder, value: row.cells[index] ?? null, width: "full" }) }));
|
|
15
|
+
return (_jsx("td", { className: cx('sw-denomination-entry', ...denominationCellToneClassNames(tone)), children: _jsx(Stepper, { changed: tone === 'accent', decrementLabel: entry.decrementLabel, disabled: row.disabled, error: entry.errors?.[index] ?? undefined, incrementLabel: entry.incrementLabel, label: `${row.label} ${columnName(column)}`, labelVisuallyHidden: true, limitAppearance: entry.limitAppearance, max: max, onChange: (value) => entry.onChange(column.key, value), placeholder: entry.placeholder, value: row.cells[index] ?? null, width: "full" }) }));
|
|
16
16
|
}
|
|
@@ -4,10 +4,13 @@ import { denominationCellToneClassNames } from '../../css/css-denomination-cell-
|
|
|
4
4
|
import { denominationCellView } from '../../denomination-cells.js';
|
|
5
5
|
import { EntryCell } from './EntryCell.js';
|
|
6
6
|
import { RowLabelLine } from './RowLabelLine.js';
|
|
7
|
-
/**
|
|
7
|
+
/**
|
|
8
|
+
* One count, read-only: the zero label, a signed delta, and the cell's tone.
|
|
9
|
+
* The value has its own box, which the `pill` tone appearance fills.
|
|
10
|
+
*/
|
|
8
11
|
function CountCell({ row, index, zeroLabel, }) {
|
|
9
12
|
const view = denominationCellView(row.cells[index] ?? null, row.signed ?? false, zeroLabel);
|
|
10
|
-
return (_jsx("td", { className: cx('sw-denomination-cell', `sw-denomination-cell-${view.state}`, ...denominationCellToneClassNames(row.cellTones?.[index])), children: view.text }));
|
|
13
|
+
return (_jsx("td", { className: cx('sw-denomination-cell', `sw-denomination-cell-${view.state}`, ...denominationCellToneClassNames(row.cellTones?.[index])), children: _jsx("span", { className: "sw-denomination-cell-value", children: view.text }) }));
|
|
11
14
|
}
|
|
12
15
|
/**
|
|
13
16
|
* One strip row: its pinned label (with the phone total under it), a count
|