@scalewing/react 1.35.1 → 1.37.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 +17 -0
- package/README.md +7 -3
- package/dist/components/ActionMenu.js +2 -1
- package/dist/components/Box.js +5 -2
- package/dist/components/Button.js +3 -2
- package/dist/components/DateField.js +2 -1
- package/dist/components/DenominationGrid.d.ts +1 -1
- package/dist/components/DenominationGrid.js +2 -0
- package/dist/components/FilterChips.js +3 -1
- package/dist/components/MultiSelect.js +2 -1
- package/dist/components/RadioGroup.js +3 -1
- package/dist/components/SectionNav.js +2 -1
- package/dist/components/SegmentedControl.js +5 -1
- package/dist/components/Select.js +2 -1
- package/dist/components/Stepper.d.ts +5 -0
- package/dist/components/Stepper.js +4 -2
- package/dist/components/Steps.d.ts +11 -2
- package/dist/components/Steps.js +5 -3
- package/dist/components/Switch.js +2 -1
- package/dist/components/checkbox/CheckboxControl.d.ts +1 -1
- package/dist/components/checkbox/CheckboxControl.js +3 -2
- package/dist/components/date-field/CalendarHeader.js +2 -1
- package/dist/components/denomination-grid/DenominationStrip.js +3 -1
- package/dist/components/denomination-grid/EntryCell.d.ts +3 -1
- package/dist/components/denomination-grid/EntryCell.js +2 -2
- package/dist/components/denomination-grid/StripRow.d.ts +8 -2
- package/dist/components/denomination-grid/StripRow.js +14 -5
- package/dist/components/denomination-grid/StripTotal.d.ts +11 -0
- package/dist/components/denomination-grid/StripTotal.js +12 -0
- package/dist/components/denomination-grid/StripValueRow.d.ts +25 -0
- package/dist/components/denomination-grid/StripValueRow.js +27 -0
- package/dist/components/denomination-grid/types.d.ts +19 -0
- package/dist/components/denomination-grid/validate-value-row.d.ts +6 -0
- package/dist/components/denomination-grid/validate-value-row.js +30 -0
- package/dist/components/field/SearchFrame.js +2 -1
- package/dist/components/radio-group/RadioGroupOptionLabel.d.ts +3 -1
- package/dist/components/radio-group/RadioGroupOptionLabel.js +3 -2
- package/dist/components/steps/Step.d.ts +4 -2
- package/dist/components/steps/Step.js +15 -7
- package/dist/components/steps/steps-context.d.ts +5 -3
- package/dist/components/table/TableCell.js +2 -1
- package/dist/css/css-button.js +17 -5
- package/dist/css/css-denomination-grid.js +4 -0
- package/dist/css/css-denomination-value-row.d.ts +3 -0
- package/dist/css/css-denomination-value-row.js +54 -0
- package/dist/css/css-steps.d.ts +3 -1
- package/dist/css/css-steps.js +18 -3
- package/dist/index.d.ts +1 -1
- package/dist/styles.css +58 -5
- package/dist/tab-stop.d.ts +22 -0
- package/dist/tab-stop.js +23 -0
- package/package.json +1 -1
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { cx } from '../../class-names.js';
|
|
3
3
|
import { denominationCellToneClassNames } from '../../css/css-denomination-cell-tones.js';
|
|
4
4
|
import { denominationCellView } from '../../denomination-cells.js';
|
|
5
5
|
import { EntryCell } from './EntryCell.js';
|
|
6
6
|
import { RowLabelLine } from './RowLabelLine.js';
|
|
7
|
+
import { StripTotalCell, StripTotalInline } from './StripTotal.js';
|
|
8
|
+
import { hasValue, StripValueRow, valueCellId } from './StripValueRow.js';
|
|
7
9
|
/**
|
|
8
10
|
* One count, read-only: the zero label, a signed delta, and the cell's tone.
|
|
9
11
|
* The value has its own box, which the `pill` tone appearance fills.
|
|
@@ -14,9 +16,16 @@ function CountCell({ row, index, zeroLabel, }) {
|
|
|
14
16
|
}
|
|
15
17
|
/**
|
|
16
18
|
* One strip row: its pinned label (with the phone total under it), a count
|
|
17
|
-
* per column, read-only or entered, and its total.
|
|
19
|
+
* per column, read-only or entered, and its total. With a `valueRow`, the
|
|
20
|
+
* value line follows it and takes the total; each entry count is described
|
|
21
|
+
* by the value under it.
|
|
18
22
|
*/
|
|
19
|
-
export function StripRow({ row, columns, zeroLabel, hasTotals, }) {
|
|
20
|
-
const { entry } = row;
|
|
21
|
-
|
|
23
|
+
export function StripRow({ row, columns, zeroLabel, hasTotals, idPrefix, rowIndex, }) {
|
|
24
|
+
const { entry, valueRow } = row;
|
|
25
|
+
// With a value line, the total is on it, not on the counts.
|
|
26
|
+
const total = valueRow ? undefined : row.total;
|
|
27
|
+
const describedBy = (index) => hasValue(valueRow?.cells[index])
|
|
28
|
+
? valueCellId(idPrefix, rowIndex, index)
|
|
29
|
+
: undefined;
|
|
30
|
+
return (_jsxs(_Fragment, { children: [_jsxs("tr", { "aria-disabled": row.disabled || undefined, className: cx('sw-denomination-row', `sw-denomination-row-${row.tone ?? 'neutral'}`, row.signed && 'sw-denomination-row-signed', row.emphasis === 'quiet' && 'sw-denomination-row-quiet', row.disabled && 'sw-denomination-row-disabled'), children: [_jsx("th", { className: "sw-denomination-label", scope: "row", children: _jsxs("span", { className: "sw-denomination-label-body", children: [_jsx(RowLabelLine, { row: row }), _jsx(StripTotalInline, { total: total })] }) }), columns.map((column, index) => entry ? (_jsx(EntryCell, { column: column, describedBy: describedBy(index), entry: entry, index: index, row: row }, column.key)) : (_jsx(CountCell, { index: index, row: row, zeroLabel: zeroLabel }, column.key))), hasTotals ? _jsx(StripTotalCell, { total: total }) : null] }), valueRow ? (_jsx(StripValueRow, { columns: columns, hasTotals: hasTotals, idPrefix: idPrefix, row: row, rowIndex: rowIndex, valueRow: valueRow })) : null] }));
|
|
22
31
|
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A strip row's total cell: the consumer's total, or empty. Below md it is
|
|
3
|
+
* visually hidden, and `StripTotalInline` shows it under the row's name.
|
|
4
|
+
*/
|
|
5
|
+
export declare function StripTotalCell({ total }: {
|
|
6
|
+
total: string | undefined;
|
|
7
|
+
}): import("react").JSX.Element;
|
|
8
|
+
/** The total under a row's name on a phone; the cell reads it to everyone. */
|
|
9
|
+
export declare function StripTotalInline({ total }: {
|
|
10
|
+
total: string | undefined;
|
|
11
|
+
}): import("react").JSX.Element | null;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* A strip row's total cell: the consumer's total, or empty. Below md it is
|
|
4
|
+
* visually hidden, and `StripTotalInline` shows it under the row's name.
|
|
5
|
+
*/
|
|
6
|
+
export function StripTotalCell({ total }) {
|
|
7
|
+
return (_jsx("td", { className: "sw-denomination-total", children: _jsx("span", { className: "sw-denomination-total-value", children: total ?? '' }) }));
|
|
8
|
+
}
|
|
9
|
+
/** The total under a row's name on a phone; the cell reads it to everyone. */
|
|
10
|
+
export function StripTotalInline({ total }) {
|
|
11
|
+
return total !== undefined ? (_jsx("span", { "aria-hidden": "true", className: "sw-denomination-total-inline", children: total })) : null;
|
|
12
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { type DenominationGridColumn, type DenominationGridRow, type DenominationGridValueRow } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* The id of a value cell, which the count above it is described by: the
|
|
4
|
+
* grid's prefix and the cell's place, never consumer text, so it is always
|
|
5
|
+
* one valid token of an id list.
|
|
6
|
+
*/
|
|
7
|
+
export declare function valueCellId(idPrefix: string, rowIndex: number, columnIndex: number): string;
|
|
8
|
+
/** Whether a value cell shows something: not `null` and not blank. */
|
|
9
|
+
export declare function hasValue(cell: string | null | undefined): cell is string;
|
|
10
|
+
type StripValueRowProps = {
|
|
11
|
+
row: DenominationGridRow;
|
|
12
|
+
valueRow: DenominationGridValueRow;
|
|
13
|
+
columns: readonly DenominationGridColumn[];
|
|
14
|
+
hasTotals: boolean;
|
|
15
|
+
/** A per-grid prefix that keeps the value cells' ids unique on the page. */
|
|
16
|
+
idPrefix: string;
|
|
17
|
+
rowIndex: number;
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* The value line under a strip row: its name in the label column (with the
|
|
21
|
+
* row's total under it on a phone), a centred value per column, and the
|
|
22
|
+
* row's total. It keeps the row's tone, so a toned total stays toned.
|
|
23
|
+
*/
|
|
24
|
+
export declare function StripValueRow({ row, valueRow, columns, hasTotals, idPrefix, rowIndex, }: StripValueRowProps): import("react").JSX.Element;
|
|
25
|
+
export {};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { cx } from '../../class-names.js';
|
|
3
|
+
import { StripTotalCell, StripTotalInline } from './StripTotal.js';
|
|
4
|
+
import { denominationValueRowClass } from '../../css/css-denomination-value-row.js';
|
|
5
|
+
/**
|
|
6
|
+
* The id of a value cell, which the count above it is described by: the
|
|
7
|
+
* grid's prefix and the cell's place, never consumer text, so it is always
|
|
8
|
+
* one valid token of an id list.
|
|
9
|
+
*/
|
|
10
|
+
export function valueCellId(idPrefix, rowIndex, columnIndex) {
|
|
11
|
+
return `${idPrefix}value-${rowIndex}-${columnIndex}`;
|
|
12
|
+
}
|
|
13
|
+
/** Whether a value cell shows something: not `null` and not blank. */
|
|
14
|
+
export function hasValue(cell) {
|
|
15
|
+
return typeof cell === 'string' && cell.trim() !== '';
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* The value line under a strip row: its name in the label column (with the
|
|
19
|
+
* row's total under it on a phone), a centred value per column, and the
|
|
20
|
+
* row's total. It keeps the row's tone, so a toned total stays toned.
|
|
21
|
+
*/
|
|
22
|
+
export function StripValueRow({ row, valueRow, columns, hasTotals, idPrefix, rowIndex, }) {
|
|
23
|
+
return (_jsxs("tr", { "aria-disabled": row.disabled || undefined, className: cx('sw-denomination-row', denominationValueRowClass, `sw-denomination-row-${row.tone ?? 'neutral'}`, row.disabled && 'sw-denomination-row-disabled'), children: [_jsx("th", { "aria-label": `${row.label} ${valueRow.label}`, className: "sw-denomination-label", scope: "row", children: _jsxs("span", { className: "sw-denomination-label-body", children: [_jsx("span", { className: "sw-denomination-label-line", children: _jsx("span", { className: "sw-denomination-label-text", children: valueRow.label }) }), _jsx(StripTotalInline, { total: row.total })] }) }), columns.map((column, index) => {
|
|
24
|
+
const cell = valueRow.cells[index];
|
|
25
|
+
return (_jsx("td", { className: "sw-denomination-value", id: valueCellId(idPrefix, rowIndex, index), children: hasValue(cell) ? cell : '' }, column.key));
|
|
26
|
+
}), hasTotals ? _jsx(StripTotalCell, { total: row.total }) : null] }));
|
|
27
|
+
}
|
|
@@ -45,6 +45,20 @@ export type DenominationGridEntry = {
|
|
|
45
45
|
*/
|
|
46
46
|
limitAppearance?: StepperLimitAppearance;
|
|
47
47
|
};
|
|
48
|
+
/**
|
|
49
|
+
* A line under a strip row that gives each column's value, such as the
|
|
50
|
+
* money its count adds up to: its own table row on the subtle fill, named
|
|
51
|
+
* in the label column, one consumer-formatted value per column centred
|
|
52
|
+
* under its count (`null` or `''` leaves a cell blank, such as for a zero
|
|
53
|
+
* count), and the row's `total` in the total column. The grid does no
|
|
54
|
+
* arithmetic and no currency formatting.
|
|
55
|
+
*/
|
|
56
|
+
export type DenominationGridValueRow = {
|
|
57
|
+
/** The line's name in the label column, such as "Value". */
|
|
58
|
+
label: string;
|
|
59
|
+
/** One formatted value, or `null` for none, per column. */
|
|
60
|
+
cells: readonly (string | null)[];
|
|
61
|
+
};
|
|
48
62
|
export type DenominationGridRow = {
|
|
49
63
|
id: string;
|
|
50
64
|
label: string;
|
|
@@ -80,6 +94,11 @@ export type DenominationGridRow = {
|
|
|
80
94
|
* `entry.errors` wins.
|
|
81
95
|
*/
|
|
82
96
|
entry?: DenominationGridEntry;
|
|
97
|
+
/**
|
|
98
|
+
* A value line under this row (the `strip` layout only); the row's
|
|
99
|
+
* `total` moves to it, and each entry count is described by its value.
|
|
100
|
+
*/
|
|
101
|
+
valueRow?: DenominationGridValueRow;
|
|
83
102
|
};
|
|
84
103
|
export type DenominationGridProps = {
|
|
85
104
|
label: string;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { type DenominationGridColumn, type DenominationGridLayout, type DenominationGridRow } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Value rows: the strip layout only, a name that is not blank, and one
|
|
4
|
+
* formatted value or null per column.
|
|
5
|
+
*/
|
|
6
|
+
export declare function assertDenominationValueRows(layout: DenominationGridLayout, columns: readonly DenominationGridColumn[], rows: readonly DenominationGridRow[]): void;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Value rows: the strip layout only, a name that is not blank, and one
|
|
3
|
+
* formatted value or null per column.
|
|
4
|
+
*/
|
|
5
|
+
export function assertDenominationValueRows(layout, columns, rows) {
|
|
6
|
+
const valueRows = rows.flatMap((row) => row.valueRow == null ? [] : [row.valueRow]);
|
|
7
|
+
if (valueRows.length === 0)
|
|
8
|
+
return;
|
|
9
|
+
if (layout !== 'strip')
|
|
10
|
+
throw new RangeError('value rows need the strip layout');
|
|
11
|
+
for (const valueRow of valueRows) {
|
|
12
|
+
if (typeof valueRow !== 'object')
|
|
13
|
+
throw new RangeError('a value row is an object of label and cells');
|
|
14
|
+
if (typeof valueRow.label !== 'string' || !valueRow.label.trim())
|
|
15
|
+
throw new RangeError('a value row needs a label');
|
|
16
|
+
if (!fitsColumns(valueRow.cells, columns.length))
|
|
17
|
+
throw new RangeError('a value row needs one string or null per column');
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
function fitsColumns(cells, columnCount) {
|
|
21
|
+
if (!Array.isArray(cells) || cells.length !== columnCount)
|
|
22
|
+
return false;
|
|
23
|
+
// An index loop, not every(): every() skips the holes of a sparse array.
|
|
24
|
+
for (let index = 0; index < columnCount; index += 1) {
|
|
25
|
+
const cell = cells[index];
|
|
26
|
+
if (cell !== null && typeof cell !== 'string')
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { tabStop } from '../../tab-stop.js';
|
|
2
3
|
/*
|
|
3
4
|
* A press on the frame's chrome (the glyph or the padding) focuses the
|
|
4
5
|
* input, as on an adorned field; a press on the input keeps the browser's
|
|
@@ -28,5 +29,5 @@ export function SearchFrame({ children, clearLabel, leading, onClear, showClear,
|
|
|
28
29
|
?.querySelector('input')
|
|
29
30
|
?.focus();
|
|
30
31
|
onClear();
|
|
31
|
-
}, type: "button", children: _jsx("span", { "aria-hidden": "true", className: "sw-field-search-cross" }) })) : null] }));
|
|
32
|
+
}, tabIndex: tabStop, type: "button", children: _jsx("span", { "aria-hidden": "true", className: "sw-field-search-cross" }) })) : null] }));
|
|
32
33
|
}
|
|
@@ -27,6 +27,8 @@ type RadioGroupOptionLabelProps = {
|
|
|
27
27
|
name: string;
|
|
28
28
|
checked: boolean;
|
|
29
29
|
required: boolean;
|
|
30
|
+
/** Whether this radio is its group's explicit Tab stop (`radioTabStopValue`). */
|
|
31
|
+
isTabStop: boolean;
|
|
30
32
|
onSelect: () => void;
|
|
31
33
|
};
|
|
32
34
|
/**
|
|
@@ -35,5 +37,5 @@ type RadioGroupOptionLabelProps = {
|
|
|
35
37
|
* share a wrapping body, and the radio is named by the label text alone and
|
|
36
38
|
* described by the description.
|
|
37
39
|
*/
|
|
38
|
-
export declare function RadioGroupOptionLabel({ option, name, checked, required, onSelect, }: RadioGroupOptionLabelProps): import("react").JSX.Element;
|
|
40
|
+
export declare function RadioGroupOptionLabel({ option, name, checked, required, isTabStop, onSelect, }: RadioGroupOptionLabelProps): import("react").JSX.Element;
|
|
39
41
|
export {};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useId } from 'react';
|
|
3
|
+
import { tabStop } from '../../tab-stop.js';
|
|
3
4
|
/** Whether React renders something for a description: not nothing, a boolean or ''. */
|
|
4
5
|
function hasDescription(description) {
|
|
5
6
|
return (description !== undefined &&
|
|
@@ -13,7 +14,7 @@ function hasDescription(description) {
|
|
|
13
14
|
* share a wrapping body, and the radio is named by the label text alone and
|
|
14
15
|
* described by the description.
|
|
15
16
|
*/
|
|
16
|
-
export function RadioGroupOptionLabel({ option, name, checked, required, onSelect, }) {
|
|
17
|
+
export function RadioGroupOptionLabel({ option, name, checked, required, isTabStop, onSelect, }) {
|
|
17
18
|
const id = useId();
|
|
18
19
|
const textId = `${id}-text`;
|
|
19
20
|
const descriptionId = `${id}-description`;
|
|
@@ -22,5 +23,5 @@ export function RadioGroupOptionLabel({ option, name, checked, required, onSelec
|
|
|
22
23
|
return (_jsxs("label", { className: "sw-radio-group-option", children: [_jsxs("span", { className: "sw-radio-group-control", children: [_jsx("input", { "aria-describedby": described ? descriptionId : undefined, "aria-labelledby": described ? textId : undefined, checked: checked, className: "sw-radio-group-input", disabled: option.disabled, name: name, onChange: (event) => {
|
|
23
24
|
if (event.currentTarget.checked)
|
|
24
25
|
onSelect();
|
|
25
|
-
}, required: required, type: "radio", value: option.value }), _jsx("span", { "aria-hidden": "true", className: "sw-radio-group-mark" })] }), option.icon ? (_jsx("span", { "aria-hidden": "true", className: "sw-radio-group-icon", children: option.icon })) : null, described ? (_jsxs("span", { className: "sw-radio-group-body", children: [text, _jsx("span", { className: "sw-radio-group-option-description", id: descriptionId, children: option.description })] })) : (text)] }));
|
|
26
|
+
}, required: required, tabIndex: isTabStop ? tabStop : undefined, type: "radio", value: option.value }), _jsx("span", { "aria-hidden": "true", className: "sw-radio-group-mark" })] }), option.icon ? (_jsx("span", { "aria-hidden": "true", className: "sw-radio-group-icon", children: option.icon })) : null, described ? (_jsxs("span", { className: "sw-radio-group-body", children: [text, _jsx("span", { className: "sw-radio-group-option-description", id: descriptionId, children: option.description })] })) : (text)] }));
|
|
26
27
|
}
|
|
@@ -13,12 +13,14 @@ export type StepProps = {
|
|
|
13
13
|
skippedReason?: string;
|
|
14
14
|
/** Overrides the list's heading level for this step's title. */
|
|
15
15
|
headingLevel?: StepHeadingLevel;
|
|
16
|
-
/** The step's fields, shown while it is current or in error. */
|
|
16
|
+
/** The step's fields, shown while it is current or in error, or always in an open list. */
|
|
17
17
|
children?: ReactNode;
|
|
18
18
|
};
|
|
19
19
|
/**
|
|
20
20
|
* One step of a `Steps` list: a numbered marker, the title as a heading,
|
|
21
21
|
* and, by status, its fields (current, error) or one folded line (a done
|
|
22
|
-
* step's summary and Edit; an upcoming or skipped step's title alone).
|
|
22
|
+
* step's summary and Edit; an upcoming or skipped step's title alone). In
|
|
23
|
+
* an open list every step shows its fields, inside a region named by its
|
|
24
|
+
* title.
|
|
23
25
|
*/
|
|
24
26
|
export declare function Step(props: StepProps): import("react").JSX.Element;
|
|
@@ -21,12 +21,15 @@ function assertStep(props, level) {
|
|
|
21
21
|
* Moves focus to the heading when the step becomes current from a folded
|
|
22
22
|
* state, unless an earlier step of the list already took it in this commit
|
|
23
23
|
* (effects run in list order, so the first one wins; the claim is released
|
|
24
|
-
* after the flush).
|
|
24
|
+
* after the flush). In an open list nothing folds or opens, so focus stays
|
|
25
|
+
* where the person put it.
|
|
25
26
|
*/
|
|
26
|
-
function useFocusWhenOpened(status, heading, focusClaimRef) {
|
|
27
|
+
function useFocusWhenOpened(status, heading, focusClaimRef, open) {
|
|
27
28
|
const previous = useRef(undefined);
|
|
28
29
|
useEffect(() => {
|
|
29
|
-
if (
|
|
30
|
+
if (!open &&
|
|
31
|
+
stepTakesFocus(previous.current, status) &&
|
|
32
|
+
!focusClaimRef.current) {
|
|
30
33
|
focusClaimRef.current = true;
|
|
31
34
|
heading.current?.focus();
|
|
32
35
|
// One commit's effects run in one flush: release the claim after it,
|
|
@@ -36,12 +39,14 @@ function useFocusWhenOpened(status, heading, focusClaimRef) {
|
|
|
36
39
|
});
|
|
37
40
|
}
|
|
38
41
|
previous.current = status;
|
|
39
|
-
}, [status, heading, focusClaimRef]);
|
|
42
|
+
}, [status, heading, focusClaimRef, open]);
|
|
40
43
|
}
|
|
41
44
|
/**
|
|
42
45
|
* One step of a `Steps` list: a numbered marker, the title as a heading,
|
|
43
46
|
* and, by status, its fields (current, error) or one folded line (a done
|
|
44
|
-
* step's summary and Edit; an upcoming or skipped step's title alone).
|
|
47
|
+
* step's summary and Edit; an upcoming or skipped step's title alone). In
|
|
48
|
+
* an open list every step shows its fields, inside a region named by its
|
|
49
|
+
* title.
|
|
45
50
|
*/
|
|
46
51
|
export function Step(props) {
|
|
47
52
|
const place = useContext(StepPlaceContext);
|
|
@@ -53,10 +58,13 @@ export function Step(props) {
|
|
|
53
58
|
const heading = useRef(null);
|
|
54
59
|
const titleId = useId();
|
|
55
60
|
const editId = useId();
|
|
56
|
-
useFocusWhenOpened(status, heading, place.focusClaimRef);
|
|
61
|
+
useFocusWhenOpened(status, heading, place.focusClaimRef, place.open);
|
|
57
62
|
const Heading = `h${level}`;
|
|
58
63
|
const reason = status === 'skipped' ? skippedReason : undefined;
|
|
59
64
|
const word = stepStatusWord(status, place.labels, reason !== undefined);
|
|
60
65
|
const done = status === 'done';
|
|
61
|
-
|
|
66
|
+
const header = (_jsxs("div", { className: "sw-steps-header", children: [_jsx("span", { "aria-hidden": "true", className: "sw-steps-marker", children: status === 'error' ? '!' : done ? null : place.number }), _jsxs(Heading, { ref: heading, className: "sw-steps-title", tabIndex: -1, children: [_jsx("span", { id: titleId, children: title }), reason ? (_jsxs(_Fragment, { children: [' ', _jsx("span", { className: "sw-steps-reason", children: reason })] })) : null, word ? _jsxs("span", { className: "sw-sr-only", children: [", ", word] }) : null] }), done && summary ? (_jsx("span", { className: "sw-steps-summary", children: summary })) : null, done && onEdit ? (_jsx(Button, { "aria-labelledby": `${editId} ${titleId}`, className: "sw-steps-edit", id: editId, onPress: onEdit, size: "sm", variant: "ghost", children: editLabel })) : null] }));
|
|
67
|
+
const shown = (place.open || isOpenStep(status)) && Children.toArray(children).length > 0;
|
|
68
|
+
const content = (_jsxs(_Fragment, { children: [header, shown ? _jsx("div", { className: "sw-steps-body", children: children }) : null] }));
|
|
69
|
+
return (_jsx("li", { "aria-current": !place.open && status === 'current' ? 'step' : undefined, className: cx('sw-steps-item', `sw-steps-item-${status}`), children: place.open ? (_jsx("section", { "aria-labelledby": titleId, className: "sw-steps-region", children: content })) : (content) }));
|
|
62
70
|
}
|
|
@@ -2,15 +2,17 @@ import { type RefObject } from 'react';
|
|
|
2
2
|
import { type StepHeadingLevel, type StepsLabels } from './step-status.js';
|
|
3
3
|
/**
|
|
4
4
|
* What a `Step` learns from its `Steps`: its number, its heading level, the
|
|
5
|
-
* status words,
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* status words, the list's focus claim for this commit (the first step to
|
|
6
|
+
* open in a commit takes focus and releases the claim after that commit's
|
|
7
|
+
* effects), and whether every step is open.
|
|
8
8
|
*/
|
|
9
9
|
export type StepPlace = {
|
|
10
10
|
number: number;
|
|
11
11
|
headingLevel: StepHeadingLevel;
|
|
12
12
|
labels: StepsLabels;
|
|
13
13
|
focusClaimRef: RefObject<boolean>;
|
|
14
|
+
/** Every step of the list is open (`Steps open`). */
|
|
15
|
+
open: boolean;
|
|
14
16
|
};
|
|
15
17
|
/** Null outside `Steps`: a `Step` must be one of a list's steps. */
|
|
16
18
|
export declare const StepPlaceContext: import("react").Context<StepPlace | null>;
|
|
@@ -3,6 +3,7 @@ import { forwardRef, } from 'react';
|
|
|
3
3
|
import { cx } from '../../class-names.js';
|
|
4
4
|
import { assertTableCellTone, } from '../../css/css-table-ledger.js';
|
|
5
5
|
import { assertTableColumnWidth, tableColumnWidthClass, } from './table-layout.js';
|
|
6
|
+
import { tabStop } from '../../tab-stop.js';
|
|
6
7
|
const tableSorts = ['ascending', 'descending', 'none'];
|
|
7
8
|
/** A cell's press is a function, and never on a sortable header. */
|
|
8
9
|
function assertPress(onPress, sort) {
|
|
@@ -35,5 +36,5 @@ export const TableCell = forwardRef(function TableCell({ align = 'start', as = '
|
|
|
35
36
|
: align === 'end'
|
|
36
37
|
? 'sw-table-end'
|
|
37
38
|
: undefined;
|
|
38
|
-
return (_jsx(Component, { ref: ref, "aria-sort": sort === undefined || sort === 'none' ? undefined : sort, className: cx(alignmentClass, truncate && 'sw-table-clip', tone && `sw-table-tone-${tone}`, width && tableColumnWidthClass(width), className), ...rest, children: sort !== undefined && onSort ? (_jsxs("button", { className: "sw-table-sort", onClick: onSort, type: "button", children: [children, _jsx("span", { "aria-hidden": "true", className: cx('sw-table-sort-glyph', `sw-table-sort-${sort}`) })] })) : onPress ? (_jsx("button", { className: "sw-table-action", onClick: onPress, type: "button", children: children })) : (children) }));
|
|
39
|
+
return (_jsx(Component, { ref: ref, "aria-sort": sort === undefined || sort === 'none' ? undefined : sort, className: cx(alignmentClass, truncate && 'sw-table-clip', tone && `sw-table-tone-${tone}`, width && tableColumnWidthClass(width), className), ...rest, children: sort !== undefined && onSort ? (_jsxs("button", { className: "sw-table-sort", onClick: onSort, tabIndex: tabStop, type: "button", children: [children, _jsx("span", { "aria-hidden": "true", className: cx('sw-table-sort-glyph', `sw-table-sort-${sort}`) })] })) : onPress ? (_jsx("button", { className: "sw-table-action", onClick: onPress, tabIndex: tabStop, type: "button", children: children })) : (children) }));
|
|
39
40
|
});
|
package/dist/css/css-button.js
CHANGED
|
@@ -35,11 +35,11 @@ function variantRules() {
|
|
|
35
35
|
* A neutral disabled button: the quiet fill with its words in the muted
|
|
36
36
|
* color at 48 %, about #AEAEAE on the grey (a disabled control needs no
|
|
37
37
|
* contrast, and the faded words say it cannot be pressed), at full
|
|
38
|
-
* opacity so no accent shows through.
|
|
39
|
-
* Forced colors draw it as any disabled button.
|
|
38
|
+
* opacity so no accent shows through. The same for one that is only
|
|
39
|
+
* \`aria-disabled\`. Forced colors draw it as any disabled button.
|
|
40
40
|
*/
|
|
41
41
|
function disabledNeutralRules() {
|
|
42
|
-
return `.sw-button-disabled-neutral:disabled:not(.sw-button-ghost) {
|
|
42
|
+
return `.sw-button-disabled-neutral:is(:disabled, [aria-disabled='true']):not(.sw-button-ghost) {
|
|
43
43
|
background: ${quietFill};
|
|
44
44
|
border-color: transparent;
|
|
45
45
|
color: color-mix(in srgb, var(--sw-color-muted) 48%, transparent);
|
|
@@ -73,6 +73,14 @@ function pressedRules() {
|
|
|
73
73
|
.sw-button[aria-pressed='true']::after { border: ${pressedRingWidth - pressedRingGap}px solid Highlight; border-radius: inherit; content: ''; inset: calc(-1px - ${pressedRingWidth}px); pointer-events: none; position: absolute; }
|
|
74
74
|
}`;
|
|
75
75
|
}
|
|
76
|
+
/* The system greys a disabled button in forced colors, not one that is
|
|
77
|
+
only aria-disabled: grey it the same, over the neutral look's words. */
|
|
78
|
+
function ariaDisabledForcedRules() {
|
|
79
|
+
return `@media (forced-colors: active) {
|
|
80
|
+
.sw-button[aria-disabled='true'],
|
|
81
|
+
.sw-button-disabled-neutral[aria-disabled='true']:not(.sw-button-ghost) { border-color: GrayText; color: GrayText; }
|
|
82
|
+
}`;
|
|
83
|
+
}
|
|
76
84
|
function sizeRules() {
|
|
77
85
|
return buttonSizes
|
|
78
86
|
.map((size) => {
|
|
@@ -114,14 +122,18 @@ export function cssButtonClasses() {
|
|
|
114
122
|
outline-offset: var(--sw-focus-ring-offset);
|
|
115
123
|
}
|
|
116
124
|
|
|
117
|
-
|
|
125
|
+
/* A button that is off but still pressed and focused (\`aria-disabled\`,
|
|
126
|
+
for a submit that, pressed while off, shows what to fix) looks as a
|
|
127
|
+
disabled one; forced colors grey it too, after the pressed rules. */
|
|
128
|
+
.sw-button:disabled,
|
|
129
|
+
.sw-button[aria-disabled='true'] {
|
|
118
130
|
cursor: not-allowed;
|
|
119
131
|
opacity: var(--sw-disabled-opacity);
|
|
120
132
|
}
|
|
121
|
-
|
|
122
133
|
${variantRules()}
|
|
123
134
|
${disabledNeutralRules()}
|
|
124
135
|
${pressedRules()}
|
|
136
|
+
${ariaDisabledForcedRules()}
|
|
125
137
|
${sizeRules()}
|
|
126
138
|
${groupRules()}
|
|
127
139
|
|
|
@@ -5,6 +5,7 @@ import { cssDenominationAppearanceClasses, denominationAppearanceClassCatalog, }
|
|
|
5
5
|
import { cssDenominationLabelClasses, denominationLabelClassCatalog, } from './css-denomination-label.js';
|
|
6
6
|
import { cssDenominationAlignedClasses, denominationAlignedClassCatalog, } from './css-denomination-aligned.js';
|
|
7
7
|
import { cssDenominationEntryClasses, denominationEntryClassCatalog, } from './css-denomination-entry.js';
|
|
8
|
+
import { cssDenominationValueRowClasses, denominationValueRowClassCatalog, } from './css-denomination-value-row.js';
|
|
8
9
|
import { cssDenominationCellToneClasses, denominationCellToneClassCatalog, } from './css-denomination-cell-tones.js';
|
|
9
10
|
import { pinnedStartShadeRules, scrollRegionRules, } from './css-scroll-region.js';
|
|
10
11
|
import { visuallyHiddenDeclarations } from './css-utilities.js';
|
|
@@ -260,6 +261,8 @@ ${cssDenominationLabelClasses()}
|
|
|
260
261
|
|
|
261
262
|
${cssDenominationAppearanceClasses()}
|
|
262
263
|
|
|
264
|
+
${cssDenominationValueRowClasses()}
|
|
265
|
+
|
|
263
266
|
@media (forced-colors: active) {
|
|
264
267
|
.sw-denomination-label { border-inline-start-color: CanvasText; color: CanvasText; }
|
|
265
268
|
.sw-denomination-strip .sw-denomination-label::before { background: CanvasText; forced-color-adjust: none; }
|
|
@@ -303,5 +306,6 @@ export function denominationGridClassCatalog() {
|
|
|
303
306
|
...denominationAlignedClassCatalog(),
|
|
304
307
|
...denominationLabelClassCatalog(),
|
|
305
308
|
...denominationAppearanceClassCatalog(),
|
|
309
|
+
...denominationValueRowClassCatalog(),
|
|
306
310
|
];
|
|
307
311
|
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { typographyVariants } from '@scalewing/tokens';
|
|
2
|
+
import { breakpointQuery } from './breakpoints.js';
|
|
3
|
+
const data = typographyVariants.data;
|
|
4
|
+
const label = typographyVariants.label;
|
|
5
|
+
export const denominationValueRowClass = 'sw-denomination-value-row';
|
|
6
|
+
const valueRow = `.sw-denomination-strip .${denominationValueRowClass}`;
|
|
7
|
+
/*
|
|
8
|
+
* A value line under a strip row: the subtle fill across the row (its
|
|
9
|
+
* pinned label too), each value at label size in data weight and tabular
|
|
10
|
+
* figures (the framed counts' size), centred
|
|
11
|
+
* under its count in the text colour, never wrapped, padded a space-2 step
|
|
12
|
+
* down (space-1 below md): shorter than framed counts above it. A disabled
|
|
13
|
+
* row's line fades its values with the row. Emitted after
|
|
14
|
+
* the frame's rules and one class more specific, so it wins at every width.
|
|
15
|
+
*/
|
|
16
|
+
export function cssDenominationValueRowClasses() {
|
|
17
|
+
return `${valueRow},
|
|
18
|
+
${valueRow} .sw-denomination-label {
|
|
19
|
+
background: var(--sw-color-subtle);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
${valueRow} :is(th, td) {
|
|
23
|
+
padding-block: var(--sw-space-2);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
${valueRow} .sw-denomination-value {
|
|
27
|
+
color: var(--sw-color-text);
|
|
28
|
+
font-size: ${label.fontSize}px;
|
|
29
|
+
font-variant-numeric: tabular-nums;
|
|
30
|
+
font-weight: ${data.fontWeight};
|
|
31
|
+
letter-spacing: ${label.letterSpacing}px;
|
|
32
|
+
line-height: ${label.lineHeight}px;
|
|
33
|
+
text-align: center;
|
|
34
|
+
white-space: nowrap;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
${valueRow}.sw-denomination-row-disabled .sw-denomination-value {
|
|
38
|
+
opacity: var(--sw-disabled-opacity);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
@media ${breakpointQuery('below', 'md')} {
|
|
42
|
+
${valueRow} :is(th, td) { padding-block: var(--sw-space-1); }
|
|
43
|
+
${valueRow} .sw-denomination-total { padding: 0; }
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
@media (forced-colors: active) {
|
|
47
|
+
${valueRow},
|
|
48
|
+
${valueRow} .sw-denomination-label { background: Canvas; }
|
|
49
|
+
${valueRow} .sw-denomination-value { color: CanvasText; }
|
|
50
|
+
}`;
|
|
51
|
+
}
|
|
52
|
+
export function denominationValueRowClassCatalog() {
|
|
53
|
+
return [denominationValueRowClass, 'sw-denomination-value'];
|
|
54
|
+
}
|
package/dist/css/css-steps.d.ts
CHANGED
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
* gap apart. Each has a header line (marker, title, and on a done step its
|
|
4
4
|
* summary and Edit) and, while open (current or error), a body indented to
|
|
5
5
|
* the title. An open step's title is the title variant; a folded one's is
|
|
6
|
-
* body size at label weight, muted ahead of the form or skipped.
|
|
6
|
+
* body size at label weight, muted ahead of the form or skipped. In an
|
|
7
|
+
* open list (`sw-steps-open`) every step is open: each panel takes the open
|
|
8
|
+
* padding and title, its header and body in one region.
|
|
7
9
|
*/
|
|
8
10
|
export declare function cssStepsClasses(): string;
|
|
9
11
|
export declare function stepsClassCatalog(): string[];
|
package/dist/css/css-steps.js
CHANGED
|
@@ -72,7 +72,9 @@ function forcedColorRules() {
|
|
|
72
72
|
* gap apart. Each has a header line (marker, title, and on a done step its
|
|
73
73
|
* summary and Edit) and, while open (current or error), a body indented to
|
|
74
74
|
* the title. An open step's title is the title variant; a folded one's is
|
|
75
|
-
* body size at label weight, muted ahead of the form or skipped.
|
|
75
|
+
* body size at label weight, muted ahead of the form or skipped. In an
|
|
76
|
+
* open list (`sw-steps-open`) every step is open: each panel takes the open
|
|
77
|
+
* padding and title, its header and body in one region.
|
|
76
78
|
*/
|
|
77
79
|
export function cssStepsClasses() {
|
|
78
80
|
return `.sw-steps {
|
|
@@ -99,10 +101,20 @@ export function cssStepsClasses() {
|
|
|
99
101
|
}
|
|
100
102
|
|
|
101
103
|
.sw-steps-item-current,
|
|
102
|
-
.sw-steps-item-error
|
|
104
|
+
.sw-steps-item-error,
|
|
105
|
+
.sw-steps-open > .sw-steps-item {
|
|
103
106
|
--sw-steps-padding: var(--sw-space-4);
|
|
104
107
|
}
|
|
105
108
|
|
|
109
|
+
/* An open list's step is a region holding its header and body, spaced as
|
|
110
|
+
the item spaces them in a stepped list. */
|
|
111
|
+
.sw-steps-region {
|
|
112
|
+
display: flex;
|
|
113
|
+
flex-direction: column;
|
|
114
|
+
gap: var(--sw-space-4);
|
|
115
|
+
min-width: 0;
|
|
116
|
+
}
|
|
117
|
+
|
|
106
118
|
/* The marker sits at the top of the header, so the connector always starts
|
|
107
119
|
under it; each line of the header is centred on the marker's height. */
|
|
108
120
|
.sw-steps-header {
|
|
@@ -141,7 +153,8 @@ export function cssStepsClasses() {
|
|
|
141
153
|
}
|
|
142
154
|
|
|
143
155
|
.sw-steps-item-current .sw-steps-title,
|
|
144
|
-
.sw-steps-item-error .sw-steps-title
|
|
156
|
+
.sw-steps-item-error .sw-steps-title,
|
|
157
|
+
.sw-steps-open > .sw-steps-item .sw-steps-title {
|
|
145
158
|
font-size: ${title.fontSize}px;
|
|
146
159
|
letter-spacing: ${title.letterSpacing}px;
|
|
147
160
|
line-height: ${title.lineHeight}px;
|
|
@@ -186,6 +199,7 @@ ${forcedColorRules()}`;
|
|
|
186
199
|
export function stepsClassCatalog() {
|
|
187
200
|
return [
|
|
188
201
|
'sw-steps',
|
|
202
|
+
'sw-steps-open',
|
|
189
203
|
'sw-steps-item',
|
|
190
204
|
'sw-steps-item-upcoming',
|
|
191
205
|
'sw-steps-item-current',
|
|
@@ -199,5 +213,6 @@ export function stepsClassCatalog() {
|
|
|
199
213
|
'sw-steps-summary',
|
|
200
214
|
'sw-steps-edit',
|
|
201
215
|
'sw-steps-body',
|
|
216
|
+
'sw-steps-region',
|
|
202
217
|
];
|
|
203
218
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -58,6 +58,6 @@ export type { GridColumns, GridProps } from './components/Grid.js';
|
|
|
58
58
|
export type { GridColumnSpan } from './css/css-grid-span.js';
|
|
59
59
|
export type { ColumnSpanProps } from './column-span-classes.js';
|
|
60
60
|
export { DenominationGrid, denominationCellToneAppearances, denominationGridFrames, denominationLabelAppearances, denominationTotalEmphases, } from './components/DenominationGrid.js';
|
|
61
|
-
export type { DenominationCellToneAppearance, DenominationGridColumn, DenominationGridEntry, DenominationGridFrame, DenominationGridLayout, DenominationGridProps, DenominationGridRow, DenominationGridRowEmphasis, DenominationGridRowRole, DenominationGridTone, DenominationLabelAppearance, DenominationLabelWidth, DenominationTotalEmphasis, } from './components/DenominationGrid.js';
|
|
61
|
+
export type { DenominationCellToneAppearance, DenominationGridColumn, DenominationGridEntry, DenominationGridFrame, DenominationGridLayout, DenominationGridProps, DenominationGridRow, DenominationGridRowEmphasis, DenominationGridRowRole, DenominationGridTone, DenominationGridValueRow, DenominationLabelAppearance, DenominationLabelWidth, DenominationTotalEmphasis, } from './components/DenominationGrid.js';
|
|
62
62
|
export { FilterChips } from './components/FilterChips.js';
|
|
63
63
|
export type { FilterChipOption, FilterChipsProps, } from './components/FilterChips.js';
|