@scalewing/react 1.42.0 → 1.45.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 +33 -0
- package/README.md +65 -1
- package/dist/components/ActionMenu.d.ts +4 -2
- package/dist/components/ActionMenu.js +4 -8
- package/dist/components/Article.d.ts +19 -2
- package/dist/components/Article.js +9 -5
- package/dist/components/DescriptionList.d.ts +12 -2
- package/dist/components/DescriptionList.js +7 -3
- package/dist/components/FileField.d.ts +15 -4
- package/dist/components/FileField.js +13 -4
- package/dist/components/FilterChips.d.ts +1 -1
- package/dist/components/FilterChips.js +1 -1
- package/dist/components/FilterChipsRow.d.ts +24 -0
- package/dist/components/FilterChipsRow.js +14 -0
- package/dist/components/ListRow.d.ts +18 -2
- package/dist/components/ListRow.js +7 -4
- package/dist/components/action-menu/assert-action-menu.d.ts +13 -0
- package/dist/components/action-menu/assert-action-menu.js +21 -0
- package/dist/components/description-list-layout.d.ts +10 -0
- package/dist/components/description-list-layout.js +13 -0
- package/dist/components/file-field/file-field-checks.d.ts +2 -2
- package/dist/components/file-field/file-field-checks.js +4 -2
- package/dist/components/filter-chips/FilterChip.js +1 -1
- package/dist/components/filter-chips/options.d.ts +10 -0
- package/dist/components/filter-chips/options.js +8 -0
- package/dist/components/filter-chips/validate.d.ts +3 -3
- package/dist/components/filter-chips/validate.js +8 -4
- package/dist/components/list/ListRowContent.d.ts +9 -2
- package/dist/components/list/ListRowContent.js +7 -4
- package/dist/components/list/ListRowItem.js +1 -1
- package/dist/components/list/row-naming.js +1 -1
- package/dist/components/slot.d.ts +9 -0
- package/dist/components/slot.js +13 -0
- package/dist/components/steps/Step.d.ts +10 -1
- package/dist/components/steps/Step.js +15 -19
- package/dist/components/steps/StepTitleLine.d.ts +19 -0
- package/dist/components/steps/StepTitleLine.js +13 -0
- package/dist/components/steps/StepTitleWords.d.ts +27 -0
- package/dist/components/steps/StepTitleWords.js +21 -0
- package/dist/components/steps/assert-step.d.ts +7 -0
- package/dist/components/steps/assert-step.js +19 -0
- package/dist/css/css-action-menu-trigger.d.ts +6 -2
- package/dist/css/css-action-menu-trigger.js +50 -2
- package/dist/css/css-action-menu.js +4 -2
- package/dist/css/css-components.js +3 -0
- package/dist/css/css-description-list.d.ts +1 -0
- package/dist/css/css-description-list.js +24 -0
- package/dist/css/css-file-field.js +15 -1
- package/dist/css/css-filter-chip-tones.d.ts +3 -0
- package/dist/css/css-filter-chip-tones.js +28 -0
- package/dist/css/css-filter-chips-layout.d.ts +1 -0
- package/dist/css/css-filter-chips-layout.js +1 -1
- package/dist/css/css-filter-chips-row.d.ts +2 -0
- package/dist/css/css-filter-chips-row.js +79 -0
- package/dist/css/css-filter-chips.js +8 -0
- package/dist/css/css-font-mono.d.ts +3 -0
- package/dist/css/css-font-mono.js +23 -0
- package/dist/css/css-list-row-truncate.d.ts +2 -0
- package/dist/css/css-list-row-truncate.js +29 -0
- package/dist/css/css-list.js +5 -1
- package/dist/css/css-steps-title-detail.d.ts +2 -0
- package/dist/css/css-steps-title-detail.js +36 -0
- package/dist/css/css-steps.d.ts +2 -2
- package/dist/css/css-steps.js +6 -2
- package/dist/css/css-utilities.js +3 -0
- package/dist/css/spacing-classes.d.ts +1 -1
- package/dist/css/spacing-classes.js +1 -0
- package/dist/css/stylesheet.js +2 -0
- package/dist/index.d.ts +6 -4
- package/dist/index.js +3 -2
- package/dist/list-options.d.ts +7 -6
- package/dist/list-options.js +8 -6
- package/dist/styles.css +164 -0
- package/package.json +1 -1
- package/dist/components/list/row-slot.d.ts +0 -8
- package/dist/components/list/row-slot.js +0 -12
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
import { type ActionMenuTriggerAppearance } from '../../css/css-action-menu-trigger.js';
|
|
3
|
+
import { type ActionMenuItem } from '../ActionMenu.js';
|
|
4
|
+
/**
|
|
5
|
+
* Fails closed on a menu it cannot draw: an item's blank detail or
|
|
6
|
+
* detailLang (or a detailLang with no detail), an unknown trigger
|
|
7
|
+
* appearance, or an icon trigger with no glyph.
|
|
8
|
+
*/
|
|
9
|
+
export declare function assertActionMenu({ items, trigger, triggerAppearance, }: {
|
|
10
|
+
items: readonly ActionMenuItem[];
|
|
11
|
+
trigger: ReactNode;
|
|
12
|
+
triggerAppearance: ActionMenuTriggerAppearance;
|
|
13
|
+
}): void;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { actionMenuTriggerAppearances, } from '../../css/css-action-menu-trigger.js';
|
|
2
|
+
import { hasSlot } from '../slot.js';
|
|
3
|
+
/** Whether an icon trigger has a glyph to draw (blank words are none). */
|
|
4
|
+
function hasGlyph(trigger) {
|
|
5
|
+
return (hasSlot(trigger) && !(typeof trigger === 'string' && trigger.trim() === ''));
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Fails closed on a menu it cannot draw: an item's blank detail or
|
|
9
|
+
* detailLang (or a detailLang with no detail), an unknown trigger
|
|
10
|
+
* appearance, or an icon trigger with no glyph.
|
|
11
|
+
*/
|
|
12
|
+
export function assertActionMenu({ items, trigger, triggerAppearance, }) {
|
|
13
|
+
if (items.some((item) => (item.detail !== undefined && !item.detail.trim()) ||
|
|
14
|
+
(item.detailLang !== undefined &&
|
|
15
|
+
(!item.detailLang.trim() || item.detail === undefined))))
|
|
16
|
+
throw new RangeError('ActionMenu item detail and detailLang must not be blank; detailLang needs a detail');
|
|
17
|
+
if (!actionMenuTriggerAppearances.includes(triggerAppearance))
|
|
18
|
+
throw new RangeError(`Unknown ActionMenu triggerAppearance: ${String(triggerAppearance)}`);
|
|
19
|
+
if (triggerAppearance === 'icon' && !hasGlyph(trigger))
|
|
20
|
+
throw new RangeError('An icon ActionMenu trigger needs its glyph');
|
|
21
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How a DescriptionList lays out its rows: `columns` (the default), the
|
|
3
|
+
* terms in a start column and the details beside them, each term over its
|
|
4
|
+
* detail below `md`; `spread`, each row's term at its start and its detail
|
|
5
|
+
* at its end, side by side at every width.
|
|
6
|
+
*/
|
|
7
|
+
export declare const descriptionListLayouts: readonly ["columns", "spread"];
|
|
8
|
+
export type DescriptionListLayout = (typeof descriptionListLayouts)[number];
|
|
9
|
+
/** Fails closed on a layout the list cannot draw. */
|
|
10
|
+
export declare function assertDescriptionListLayout(layout: unknown): void;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { isOneOf } from '../is-one-of.js';
|
|
2
|
+
/**
|
|
3
|
+
* How a DescriptionList lays out its rows: `columns` (the default), the
|
|
4
|
+
* terms in a start column and the details beside them, each term over its
|
|
5
|
+
* detail below `md`; `spread`, each row's term at its start and its detail
|
|
6
|
+
* at its end, side by side at every width.
|
|
7
|
+
*/
|
|
8
|
+
export const descriptionListLayouts = ['columns', 'spread'];
|
|
9
|
+
/** Fails closed on a layout the list cannot draw. */
|
|
10
|
+
export function assertDescriptionListLayout(layout) {
|
|
11
|
+
if (!isOneOf(descriptionListLayouts, layout))
|
|
12
|
+
throw new RangeError(`Unknown DescriptionList layout: ${String(layout)}`);
|
|
13
|
+
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type { FileFieldValue } from '../FileField.js';
|
|
2
2
|
/**
|
|
3
3
|
* Fails closed on a FileField it cannot name or show: an empty label,
|
|
4
|
-
* choose or change words, a blank drop hint, or a chosen file with no name
|
|
5
|
-
* or a blank
|
|
4
|
+
* choose or change words, a blank drop hint, or a chosen file with no name,
|
|
5
|
+
* a blank detail or a blank preview address.
|
|
6
6
|
*/
|
|
7
7
|
export declare function assertFileField(props: {
|
|
8
8
|
label: string;
|
|
@@ -3,8 +3,8 @@ function isBlank(text) {
|
|
|
3
3
|
}
|
|
4
4
|
/**
|
|
5
5
|
* Fails closed on a FileField it cannot name or show: an empty label,
|
|
6
|
-
* choose or change words, a blank drop hint, or a chosen file with no name
|
|
7
|
-
* or a blank
|
|
6
|
+
* choose or change words, a blank drop hint, or a chosen file with no name,
|
|
7
|
+
* a blank detail or a blank preview address.
|
|
8
8
|
*/
|
|
9
9
|
export function assertFileField(props) {
|
|
10
10
|
for (const key of ['label', 'chooseLabel', 'changeLabel'])
|
|
@@ -18,4 +18,6 @@ export function assertFileField(props) {
|
|
|
18
18
|
throw new RangeError('FileField value name must be non-empty text');
|
|
19
19
|
if (props.value.detail !== undefined && isBlank(props.value.detail))
|
|
20
20
|
throw new RangeError('FileField value detail must be non-empty text');
|
|
21
|
+
if (props.value.preview !== undefined && isBlank(props.value.preview))
|
|
22
|
+
throw new RangeError('FileField value preview must be an image address');
|
|
21
23
|
}
|
|
@@ -13,7 +13,7 @@ function ChipCount({ count, appearance, }) {
|
|
|
13
13
|
}
|
|
14
14
|
/** One chip: a native radio or checkbox under its face. */
|
|
15
15
|
export function FilterChip({ option, name, type, checked, required, tabStopHere, countAppearance, onCheckedChange, }) {
|
|
16
|
-
return (_jsxs("label", { className: cx('sw-filter-chip', option.count === 0 && 'sw-filter-chip-quiet'), children: [_jsx("input", { checked: checked, className: "sw-filter-chip-input", disabled: option.disabled, name: name, onChange: (event) => {
|
|
16
|
+
return (_jsxs("label", { className: cx('sw-filter-chip', option.count === 0 && 'sw-filter-chip-quiet', option.tone === 'warning' && 'sw-filter-chip-warning'), children: [_jsx("input", { checked: checked, className: "sw-filter-chip-input", disabled: option.disabled, name: name, onChange: (event) => {
|
|
17
17
|
if (!option.disabled)
|
|
18
18
|
onCheckedChange(event.currentTarget.checked);
|
|
19
19
|
}, required: required, tabIndex: tabStopHere ? tabStop : undefined, type: type, value: option.value }), _jsxs("span", { className: "sw-filter-chip-face", children: [option.label, option.count === undefined ? null : (_jsx(ChipCount, { appearance: countAppearance, count: option.count }))] })] }));
|
|
@@ -1,9 +1,19 @@
|
|
|
1
1
|
import { type RequiredMark } from '../FieldLabelText.js';
|
|
2
|
+
/**
|
|
3
|
+
* A chip's colour while it is not chosen: `neutral` (the default), the
|
|
4
|
+
* glass chip; `warning`, the warning tint with its words in the warning
|
|
5
|
+
* ink, for a choice that shows what needs a look ("Missing a
|
|
6
|
+
* translation"). Its words must say so; the tint only draws the eye. A
|
|
7
|
+
* chosen chip is the accent, whatever its tone.
|
|
8
|
+
*/
|
|
9
|
+
export declare const filterChipTones: readonly ["neutral", "warning"];
|
|
10
|
+
export type FilterChipTone = (typeof filterChipTones)[number];
|
|
2
11
|
export type FilterChipOption = {
|
|
3
12
|
value: string;
|
|
4
13
|
label: string;
|
|
5
14
|
count?: number;
|
|
6
15
|
disabled?: boolean;
|
|
16
|
+
tone?: FilterChipTone;
|
|
7
17
|
};
|
|
8
18
|
/**
|
|
9
19
|
* How a chip's count is drawn: `figure` (the default), a muted tabular
|
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A chip's colour while it is not chosen: `neutral` (the default), the
|
|
3
|
+
* glass chip; `warning`, the warning tint with its words in the warning
|
|
4
|
+
* ink, for a choice that shows what needs a look ("Missing a
|
|
5
|
+
* translation"). Its words must say so; the tint only draws the eye. A
|
|
6
|
+
* chosen chip is the accent, whatever its tone.
|
|
7
|
+
*/
|
|
8
|
+
export const filterChipTones = ['neutral', 'warning'];
|
|
1
9
|
/**
|
|
2
10
|
* How a chip's count is drawn: `figure` (the default), a muted tabular
|
|
3
11
|
* figure after a space; `inline`, the label's own words and type, after a
|
|
@@ -2,8 +2,8 @@ import { type FilterChipsProps } from './options.js';
|
|
|
2
2
|
/**
|
|
3
3
|
* Fails closed on chips it cannot draw: an empty label, no options, blank
|
|
4
4
|
* or repeated values, a chosen value that is no option, a count that is
|
|
5
|
-
* not a whole number from 0, an unknown count appearance or
|
|
6
|
-
* a multiple choice asked to be required or given a value
|
|
7
|
-
* list.
|
|
5
|
+
* not a whole number from 0, an unknown chip tone, count appearance or
|
|
6
|
+
* overflow, and a multiple choice asked to be required or given a value
|
|
7
|
+
* that is not a list.
|
|
8
8
|
*/
|
|
9
9
|
export declare function assertFilterChips(props: FilterChipsProps): void;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { assertRequiredMark } from '../FieldLabelText.js';
|
|
2
|
-
import { filterChipCountAppearances, filterChipsOverflows, } from './options.js';
|
|
2
|
+
import { filterChipCountAppearances, filterChipsOverflows, filterChipTones, } from './options.js';
|
|
3
3
|
function isCount(count) {
|
|
4
4
|
return count === undefined || (Number.isInteger(count) && count >= 0);
|
|
5
5
|
}
|
|
@@ -11,9 +11,9 @@ function chosenValues(props) {
|
|
|
11
11
|
/**
|
|
12
12
|
* Fails closed on chips it cannot draw: an empty label, no options, blank
|
|
13
13
|
* or repeated values, a chosen value that is no option, a count that is
|
|
14
|
-
* not a whole number from 0, an unknown count appearance or
|
|
15
|
-
* a multiple choice asked to be required or given a value
|
|
16
|
-
* list.
|
|
14
|
+
* not a whole number from 0, an unknown chip tone, count appearance or
|
|
15
|
+
* overflow, and a multiple choice asked to be required or given a value
|
|
16
|
+
* that is not a list.
|
|
17
17
|
*/
|
|
18
18
|
export function assertFilterChips(props) {
|
|
19
19
|
const { label, options } = props;
|
|
@@ -39,6 +39,10 @@ export function assertFilterChips(props) {
|
|
|
39
39
|
: 'value must match an option or be empty');
|
|
40
40
|
if (options.some((option) => !isCount(option.count)))
|
|
41
41
|
throw new RangeError('count must be a non-negative integer');
|
|
42
|
+
const unknownTone = options.find((option) => option.tone !== undefined &&
|
|
43
|
+
!filterChipTones.includes(option.tone));
|
|
44
|
+
if (unknownTone)
|
|
45
|
+
throw new RangeError(`Unknown FilterChips option tone: ${String(unknownTone.tone)}`);
|
|
42
46
|
const countAppearance = props.countAppearance ?? 'figure';
|
|
43
47
|
if (!filterChipCountAppearances.includes(countAppearance))
|
|
44
48
|
throw new RangeError(`Unknown FilterChips countAppearance: ${String(countAppearance)}`);
|
|
@@ -5,6 +5,8 @@ export type ListRowContentProps = {
|
|
|
5
5
|
/** Prefixes the ids that name and describe a pressable row. */
|
|
6
6
|
id: string;
|
|
7
7
|
title: string;
|
|
8
|
+
/** The title's language when it is not the page's; on its words only. */
|
|
9
|
+
titleLang?: string;
|
|
8
10
|
detail?: string;
|
|
9
11
|
value?: string;
|
|
10
12
|
valueDetail?: string;
|
|
@@ -17,10 +19,15 @@ export type ListRowContentProps = {
|
|
|
17
19
|
/** The unread words; the dot shows while they are given. */
|
|
18
20
|
unreadLabel?: string;
|
|
19
21
|
tag?: ReactNode;
|
|
22
|
+
/** One line each for the title and detail, cut with "…", the words in a tooltip. */
|
|
23
|
+
truncate: boolean;
|
|
20
24
|
};
|
|
21
25
|
/**
|
|
22
26
|
* A list row's parts in reading order: the hidden leading mark, the title
|
|
23
27
|
* (with the unread dot and its hidden words) over its detail, the value
|
|
24
|
-
* over its detail, the tag, then the chevron or check.
|
|
28
|
+
* over its detail, the tag, then the chevron or check. A truncated row
|
|
29
|
+
* sets its title's words in their own box, so the "…" never hides the dot.
|
|
30
|
+
* `titleLang` goes on the title's box, which holds its words and the
|
|
31
|
+
* wordless dot only; the unread words after it stay in the page's language.
|
|
25
32
|
*/
|
|
26
|
-
export declare function ListRowContent({ id, title, detail, value, valueDetail, valueTone, valueBadge, leading, tone, titleAppearance, accessory, unreadLabel, tag, }: ListRowContentProps): import("react").JSX.Element;
|
|
33
|
+
export declare function ListRowContent({ id, title, titleLang, detail, value, valueDetail, valueTone, valueBadge, leading, tone, titleAppearance, accessory, unreadLabel, tag, truncate, }: ListRowContentProps): import("react").JSX.Element;
|
|
@@ -2,12 +2,15 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
2
2
|
import { cx } from '../../class-names.js';
|
|
3
3
|
import { Badge } from '../Badge.js';
|
|
4
4
|
import { ListRowAccessoryMark } from './ListRowAccessoryMark.js';
|
|
5
|
-
import { hasSlot } from '
|
|
5
|
+
import { hasSlot } from '../slot.js';
|
|
6
6
|
/**
|
|
7
7
|
* A list row's parts in reading order: the hidden leading mark, the title
|
|
8
8
|
* (with the unread dot and its hidden words) over its detail, the value
|
|
9
|
-
* over its detail, the tag, then the chevron or check.
|
|
9
|
+
* over its detail, the tag, then the chevron or check. A truncated row
|
|
10
|
+
* sets its title's words in their own box, so the "…" never hides the dot.
|
|
11
|
+
* `titleLang` goes on the title's box, which holds its words and the
|
|
12
|
+
* wordless dot only; the unread words after it stay in the page's language.
|
|
10
13
|
*/
|
|
11
|
-
export function ListRowContent({ id, title, detail, value, valueDetail, valueTone, valueBadge, leading, tone, titleAppearance, accessory, unreadLabel, tag, }) {
|
|
12
|
-
return (_jsxs(_Fragment, { children: [!hasSlot(leading) ? null : (_jsx("span", { "aria-hidden": "true", className: "sw-list-row-leading", children: leading })), _jsxs("span", { className: "sw-list-row-label", children: [_jsxs("span", { className: cx('sw-list-row-title', titleAppearance === 'plain' && 'sw-list-row-title-plain', tone === 'danger' && 'sw-list-row-title-danger'), id: `${id}-title`, children: [title, unreadLabel === undefined ? null : (_jsx("span", { "aria-hidden": "true", className: "sw-list-row-unread" }))] }), unreadLabel === undefined ? null : (_jsx("span", { className: "sw-sr-only", id: `${id}-unread`, children: unreadLabel })), detail ? (_jsx("span", { className: "sw-list-row-detail", id: `${id}-detail`, children: detail })) : null] }), value === undefined ? null : (_jsxs("span", { className: "sw-list-row-end", children: [_jsx("span", { className: cx('sw-list-row-value', valueTone && `sw-list-row-value-${valueTone}`), id: `${id}-value`, children: valueBadge ? _jsx(Badge, { tone: valueBadge, children: value }) : value }), valueDetail ? (_jsx("span", { className: "sw-list-row-value-detail", id: `${id}-value-detail`, children: valueDetail })) : null] })), !hasSlot(tag) ? null : (_jsx("span", { className: "sw-list-row-tag", id: `${id}-tag`, children: tag })), _jsx(ListRowAccessoryMark, { kind: accessory })] }));
|
|
14
|
+
export function ListRowContent({ id, title, titleLang, detail, value, valueDetail, valueTone, valueBadge, leading, tone, titleAppearance, accessory, unreadLabel, tag, truncate, }) {
|
|
15
|
+
return (_jsxs(_Fragment, { children: [!hasSlot(leading) ? null : (_jsx("span", { "aria-hidden": "true", className: "sw-list-row-leading", children: leading })), _jsxs("span", { className: "sw-list-row-label", children: [_jsxs("span", { className: cx('sw-list-row-title', titleAppearance === 'plain' && 'sw-list-row-title-plain', tone === 'danger' && 'sw-list-row-title-danger'), id: `${id}-title`, lang: titleLang, children: [truncate ? (_jsx("span", { className: "sw-list-row-title-text", title: title, children: title })) : (title), unreadLabel === undefined ? null : (_jsx("span", { "aria-hidden": "true", className: "sw-list-row-unread" }))] }), unreadLabel === undefined ? null : (_jsx("span", { className: "sw-sr-only", id: `${id}-unread`, children: unreadLabel })), detail ? (_jsx("span", { className: "sw-list-row-detail", id: `${id}-detail`, title: truncate ? detail : undefined, children: detail })) : null] }), value === undefined ? null : (_jsxs("span", { className: "sw-list-row-end", children: [_jsx("span", { className: cx('sw-list-row-value', valueTone && `sw-list-row-value-${valueTone}`), id: `${id}-value`, children: valueBadge ? _jsx(Badge, { tone: valueBadge, children: value }) : value }), valueDetail ? (_jsx("span", { className: "sw-list-row-value-detail", id: `${id}-value-detail`, children: valueDetail })) : null] })), !hasSlot(tag) ? null : (_jsx("span", { className: "sw-list-row-tag", id: `${id}-tag`, children: tag })), _jsx(ListRowAccessoryMark, { kind: accessory })] }));
|
|
13
16
|
}
|
|
@@ -2,7 +2,7 @@ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-run
|
|
|
2
2
|
import { useContext } from 'react';
|
|
3
3
|
import { cx } from '../../class-names.js';
|
|
4
4
|
import { ListRowContainer } from './list-row-container.js';
|
|
5
|
-
import { hasSlot } from '
|
|
5
|
+
import { hasSlot } from '../slot.js';
|
|
6
6
|
/**
|
|
7
7
|
* A row's list item: an `li` in a ListGroup's list, a `div` elsewhere. A
|
|
8
8
|
* row with an `action` holds that control after the row, a sibling of the
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* Whether a component's slot (a row's `leading`, `tag` or `action`, a
|
|
4
|
+
* chips row's `end`) holds something to draw. React renders nothing for
|
|
5
|
+
* `undefined`, `null`, a boolean (the `false` of
|
|
6
|
+
* `canArchive && <ActionMenu …/>`) or an empty string, so those are an
|
|
7
|
+
* empty slot and add no box and no gap.
|
|
8
|
+
*/
|
|
9
|
+
export declare function hasSlot(node: ReactNode): boolean;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether a component's slot (a row's `leading`, `tag` or `action`, a
|
|
3
|
+
* chips row's `end`) holds something to draw. React renders nothing for
|
|
4
|
+
* `undefined`, `null`, a boolean (the `false` of
|
|
5
|
+
* `canArchive && <ActionMenu …/>`) or an empty string, so those are an
|
|
6
|
+
* empty slot and add no box and no gap.
|
|
7
|
+
*/
|
|
8
|
+
export function hasSlot(node) {
|
|
9
|
+
return (node !== undefined &&
|
|
10
|
+
node !== null &&
|
|
11
|
+
node !== '' &&
|
|
12
|
+
typeof node !== 'boolean');
|
|
13
|
+
}
|
|
@@ -11,6 +11,14 @@ export type StepProps = {
|
|
|
11
11
|
editLabel?: string;
|
|
12
12
|
/** Muted words after a skipped step's title, such as "not needed". */
|
|
13
13
|
skippedReason?: string;
|
|
14
|
+
/**
|
|
15
|
+
* Muted words after an open step's title, on its line, such as
|
|
16
|
+
* "Optional" or "Wetlands done · Woodlands empty". Drawn while the step
|
|
17
|
+
* is open (current or error, or any step of an open list) and outside
|
|
18
|
+
* the heading, so the heading and the region keep the title's name; an
|
|
19
|
+
* open list's region is described by them.
|
|
20
|
+
*/
|
|
21
|
+
titleDetail?: string;
|
|
14
22
|
/** Overrides the list's heading level for this step's title. */
|
|
15
23
|
headingLevel?: StepHeadingLevel;
|
|
16
24
|
/** The step's fields, shown while it is current or in error, or always in an open list. */
|
|
@@ -21,6 +29,7 @@ export type StepProps = {
|
|
|
21
29
|
* and, by status, its fields (current, error) or one folded line (a done
|
|
22
30
|
* step's summary and Edit; an upcoming or skipped step's title alone). In
|
|
23
31
|
* an open list every step shows its fields, inside a region named by its
|
|
24
|
-
* title.
|
|
32
|
+
* title. An open step's `titleDetail` is muted words beside its title,
|
|
33
|
+
* outside the heading.
|
|
25
34
|
*/
|
|
26
35
|
export declare function Step(props: StepProps): import("react").JSX.Element;
|
|
@@ -1,22 +1,13 @@
|
|
|
1
1
|
'use client';
|
|
2
|
-
import { jsx as _jsx,
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
3
|
import { Children, useContext, useEffect, useId, useRef, } from 'react';
|
|
4
4
|
import { cx } from '../../class-names.js';
|
|
5
5
|
import { Button } from '../Button.js';
|
|
6
|
-
import { isOpenStep,
|
|
6
|
+
import { isOpenStep, stepStatusWord, stepTakesFocus, } from './step-status.js';
|
|
7
|
+
import { assertStep } from './assert-step.js';
|
|
7
8
|
import { StepPlaceContext } from './steps-context.js';
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
throw new RangeError('Step title must not be empty');
|
|
11
|
-
if (!stepStatuses.includes(props.status))
|
|
12
|
-
throw new RangeError(`Unknown Step status: ${String(props.status)}`);
|
|
13
|
-
if (!stepHeadingLevels.includes(level))
|
|
14
|
-
throw new RangeError('Step headingLevel must be 2, 3 or 4');
|
|
15
|
-
if (props.onEdit && !props.editLabel?.trim())
|
|
16
|
-
throw new RangeError('Step onEdit needs an editLabel');
|
|
17
|
-
if (props.skippedReason !== undefined && !props.skippedReason.trim())
|
|
18
|
-
throw new RangeError('Step skippedReason must not be empty');
|
|
19
|
-
}
|
|
9
|
+
import { StepTitleLine } from './StepTitleLine.js';
|
|
10
|
+
import { StepTitleWords } from './StepTitleWords.js';
|
|
20
11
|
/**
|
|
21
12
|
* Moves focus to the heading when the step becomes current from a folded
|
|
22
13
|
* state, unless an earlier step of the list already took it in this commit
|
|
@@ -46,7 +37,8 @@ function useFocusWhenOpened(status, heading, focusClaimRef, open) {
|
|
|
46
37
|
* and, by status, its fields (current, error) or one folded line (a done
|
|
47
38
|
* step's summary and Edit; an upcoming or skipped step's title alone). In
|
|
48
39
|
* an open list every step shows its fields, inside a region named by its
|
|
49
|
-
* title.
|
|
40
|
+
* title. An open step's `titleDetail` is muted words beside its title,
|
|
41
|
+
* outside the heading.
|
|
50
42
|
*/
|
|
51
43
|
export function Step(props) {
|
|
52
44
|
const place = useContext(StepPlaceContext);
|
|
@@ -54,17 +46,21 @@ export function Step(props) {
|
|
|
54
46
|
throw new RangeError('Step must be a child of Steps');
|
|
55
47
|
const level = props.headingLevel ?? place.headingLevel;
|
|
56
48
|
assertStep(props, level);
|
|
57
|
-
const { status, title, summary, onEdit, editLabel, skippedReason, children } = props;
|
|
49
|
+
const { status, title, summary, onEdit, editLabel, skippedReason, titleDetail, children, } = props;
|
|
58
50
|
const heading = useRef(null);
|
|
59
51
|
const titleId = useId();
|
|
60
52
|
const editId = useId();
|
|
53
|
+
const detailId = useId();
|
|
61
54
|
useFocusWhenOpened(status, heading, place.focusClaimRef, place.open);
|
|
62
55
|
const Heading = `h${level}`;
|
|
63
56
|
const reason = status === 'skipped' ? skippedReason : undefined;
|
|
64
57
|
const word = stepStatusWord(status, place.labels, reason !== undefined);
|
|
65
58
|
const done = status === 'done';
|
|
66
|
-
const
|
|
67
|
-
const
|
|
59
|
+
const shownOpen = place.open || isOpenStep(status);
|
|
60
|
+
const detail = shownOpen ? titleDetail : undefined;
|
|
61
|
+
const headingElement = (_jsx(Heading, { ref: heading, className: "sw-steps-title", tabIndex: -1, children: word === null ? (_jsx(StepTitleWords, { reason: reason, title: title, titleId: titleId, word: null })) : (_jsx(StepTitleWords, { title: title, titleId: titleId, word: word })) }));
|
|
62
|
+
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 }), _jsx(StepTitleLine, { detail: detail, detailId: detailId, hasDetail: titleDetail !== undefined, children: headingElement }), 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] }));
|
|
63
|
+
const shown = shownOpen && Children.toArray(children).length > 0;
|
|
68
64
|
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) }));
|
|
65
|
+
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-describedby": detail ? detailId : undefined, "aria-labelledby": titleId, className: "sw-steps-region", children: content })) : (content) }));
|
|
70
66
|
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
export type StepTitleLineProps = {
|
|
3
|
+
/** The step's heading. */
|
|
4
|
+
children: ReactNode;
|
|
5
|
+
/** Whether the step has `titleDetail` at all, open or folded. */
|
|
6
|
+
hasDetail: boolean;
|
|
7
|
+
/** The muted words to draw now (an open step's), or none. */
|
|
8
|
+
detail?: string;
|
|
9
|
+
/** Their id, which an open list's region is described by. */
|
|
10
|
+
detailId: string;
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* A step's heading and, after it on a line that wraps, its muted
|
|
14
|
+
* `titleDetail` words, outside the heading. The line is there whenever the
|
|
15
|
+
* step has words, drawn or not, so the heading keeps its element (and its
|
|
16
|
+
* focus) as the step opens and folds; a step without words has the bare
|
|
17
|
+
* heading, as before.
|
|
18
|
+
*/
|
|
19
|
+
export declare function StepTitleLine({ children, hasDetail, detail, detailId, }: StepTitleLineProps): string | number | bigint | boolean | Iterable<ReactNode> | Promise<string | number | bigint | boolean | import("react").ReactPortal | import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>> | Iterable<ReactNode> | null | undefined> | import("react").JSX.Element | null | undefined;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* A step's heading and, after it on a line that wraps, its muted
|
|
4
|
+
* `titleDetail` words, outside the heading. The line is there whenever the
|
|
5
|
+
* step has words, drawn or not, so the heading keeps its element (and its
|
|
6
|
+
* focus) as the step opens and folds; a step without words has the bare
|
|
7
|
+
* heading, as before.
|
|
8
|
+
*/
|
|
9
|
+
export function StepTitleLine({ children, hasDetail, detail, detailId, }) {
|
|
10
|
+
if (!hasDetail)
|
|
11
|
+
return children;
|
|
12
|
+
return (_jsxs("div", { className: "sw-steps-title-line", children: [children, detail ? (_jsx("span", { className: "sw-steps-title-detail", id: detailId, children: detail })) : null] }));
|
|
13
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export type StepTitleWordsProps = {
|
|
2
|
+
/** The id the step's region and its Edit button are named by. */
|
|
3
|
+
titleId: string;
|
|
4
|
+
title: string;
|
|
5
|
+
} & ({
|
|
6
|
+
/** The status word read after the title. */
|
|
7
|
+
word: string;
|
|
8
|
+
reason?: never;
|
|
9
|
+
} | {
|
|
10
|
+
word: null;
|
|
11
|
+
/** A skipped step's muted reason after its title. */
|
|
12
|
+
reason?: string;
|
|
13
|
+
});
|
|
14
|
+
/**
|
|
15
|
+
* The words inside a step's heading. Without a status word they are the
|
|
16
|
+
* title (and a skipped step's reason, which never comes with a word) as
|
|
17
|
+
* drawn. With one, the drawn title is hidden from assistive technology and
|
|
18
|
+
* one visually hidden line holds the whole spoken name ("The site, Done"):
|
|
19
|
+
* a hidden box is laid out as a block, so a browser computing the name
|
|
20
|
+
* puts a space before it ("The site , Done") when the title and the word
|
|
21
|
+
* sit in separate boxes.
|
|
22
|
+
*
|
|
23
|
+
* `aria-hidden` goes on the title's own box, never a wrapper: the region
|
|
24
|
+
* and the Edit button name themselves by its id, and Chrome reads a
|
|
25
|
+
* referenced box inside a hidden wrapper into the heading's name too.
|
|
26
|
+
*/
|
|
27
|
+
export declare function StepTitleWords(props: StepTitleWordsProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* The words inside a step's heading. Without a status word they are the
|
|
4
|
+
* title (and a skipped step's reason, which never comes with a word) as
|
|
5
|
+
* drawn. With one, the drawn title is hidden from assistive technology and
|
|
6
|
+
* one visually hidden line holds the whole spoken name ("The site, Done"):
|
|
7
|
+
* a hidden box is laid out as a block, so a browser computing the name
|
|
8
|
+
* puts a space before it ("The site , Done") when the title and the word
|
|
9
|
+
* sit in separate boxes.
|
|
10
|
+
*
|
|
11
|
+
* `aria-hidden` goes on the title's own box, never a wrapper: the region
|
|
12
|
+
* and the Edit button name themselves by its id, and Chrome reads a
|
|
13
|
+
* referenced box inside a hidden wrapper into the heading's name too.
|
|
14
|
+
*/
|
|
15
|
+
export function StepTitleWords(props) {
|
|
16
|
+
const { titleId, title } = props;
|
|
17
|
+
if (props.word !== null)
|
|
18
|
+
return (_jsxs(_Fragment, { children: [_jsx("span", { "aria-hidden": "true", id: titleId, children: title }), _jsx("span", { className: "sw-sr-only", children: `${title}, ${props.word}` })] }));
|
|
19
|
+
const { reason } = props;
|
|
20
|
+
return (_jsxs(_Fragment, { children: [_jsx("span", { id: titleId, children: title }), reason ? (_jsxs(_Fragment, { children: [' ', _jsx("span", { className: "sw-steps-reason", children: reason })] })) : null] }));
|
|
21
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { StepProps } from './Step.js';
|
|
2
|
+
import { type StepHeadingLevel } from './step-status.js';
|
|
3
|
+
/**
|
|
4
|
+
* Fails closed on a step it cannot draw: a blank title, reason or title
|
|
5
|
+
* detail, an unknown status or heading level, or an Edit with no words.
|
|
6
|
+
*/
|
|
7
|
+
export declare function assertStep(props: StepProps, level: StepHeadingLevel): void;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { stepHeadingLevels, stepStatuses, } from './step-status.js';
|
|
2
|
+
/**
|
|
3
|
+
* Fails closed on a step it cannot draw: a blank title, reason or title
|
|
4
|
+
* detail, an unknown status or heading level, or an Edit with no words.
|
|
5
|
+
*/
|
|
6
|
+
export function assertStep(props, level) {
|
|
7
|
+
if (!props.title.trim())
|
|
8
|
+
throw new RangeError('Step title must not be empty');
|
|
9
|
+
if (!stepStatuses.includes(props.status))
|
|
10
|
+
throw new RangeError(`Unknown Step status: ${String(props.status)}`);
|
|
11
|
+
if (!stepHeadingLevels.includes(level))
|
|
12
|
+
throw new RangeError('Step headingLevel must be 2, 3 or 4');
|
|
13
|
+
if (props.onEdit && !props.editLabel?.trim())
|
|
14
|
+
throw new RangeError('Step onEdit needs an editLabel');
|
|
15
|
+
if (props.skippedReason !== undefined && !props.skippedReason.trim())
|
|
16
|
+
throw new RangeError('Step skippedReason must not be empty');
|
|
17
|
+
if (props.titleDetail !== undefined && !props.titleDetail.trim())
|
|
18
|
+
throw new RangeError('Step titleDetail must not be empty');
|
|
19
|
+
}
|
|
@@ -1,11 +1,15 @@
|
|
|
1
|
-
export declare const actionMenuTriggerAppearances: readonly ["framed", "text"];
|
|
1
|
+
export declare const actionMenuTriggerAppearances: readonly ["framed", "text", "icon"];
|
|
2
2
|
/**
|
|
3
3
|
* How an ActionMenu's trigger looks: `framed` (the default) is a small
|
|
4
4
|
* outlined control; `text` is its words in the text color with a chevron
|
|
5
5
|
* after them and no fill or outline until hovered or open, such as a
|
|
6
|
-
* person's name that opens their account menu in a header
|
|
6
|
+
* person's name that opens their account menu in a header; `icon` is the
|
|
7
|
+
* consumer's glyph alone in a round quiet button, no chevron and no
|
|
8
|
+
* outline until hovered or open, such as a row's ••• menu.
|
|
7
9
|
*/
|
|
8
10
|
export type ActionMenuTriggerAppearance = (typeof actionMenuTriggerAppearances)[number];
|
|
9
11
|
export declare const actionMenuTriggerTextClass = "sw-action-menu-trigger-text";
|
|
12
|
+
export declare const actionMenuTriggerIconClass = "sw-action-menu-trigger-icon";
|
|
10
13
|
export declare function cssActionMenuTriggerClasses(): string;
|
|
14
|
+
export declare function cssActionMenuTriggerIconClasses(): string;
|
|
11
15
|
export declare function actionMenuTriggerClassCatalog(): string[];
|
|
@@ -3,9 +3,11 @@ import { chevronStroke } from './chevron.js';
|
|
|
3
3
|
import { quietFill } from './quiet-fill.js';
|
|
4
4
|
import { coarsePointerQuery, touchTarget } from './touch-target.js';
|
|
5
5
|
const label = typographyVariants.label;
|
|
6
|
-
export const actionMenuTriggerAppearances = ['framed', 'text'];
|
|
6
|
+
export const actionMenuTriggerAppearances = ['framed', 'text', 'icon'];
|
|
7
7
|
export const actionMenuTriggerTextClass = 'sw-action-menu-trigger-text';
|
|
8
|
+
export const actionMenuTriggerIconClass = 'sw-action-menu-trigger-icon';
|
|
8
9
|
const text = `.sw-action-menu-trigger.${actionMenuTriggerTextClass}`;
|
|
10
|
+
const icon = `.sw-action-menu-trigger.${actionMenuTriggerIconClass}`;
|
|
9
11
|
/*
|
|
10
12
|
* A text trigger: no fill and a transparent border (kept, so forced colors
|
|
11
13
|
* can draw one), its words in label type at medium weight in the text
|
|
@@ -61,6 +63,52 @@ ${text}[aria-expanded='true'] {
|
|
|
61
63
|
${text}[aria-expanded='true'] { border-color: Highlight; }
|
|
62
64
|
}`;
|
|
63
65
|
}
|
|
66
|
+
/*
|
|
67
|
+
* An icon trigger: the consumer's glyph alone, centred in a round button
|
|
68
|
+
* as wide as it is tall (the sm control height, the 44 px target on a
|
|
69
|
+
* coarse pointer), in the muted colour, with no fill, no chevron and a
|
|
70
|
+
* transparent border (kept for forced colors). Like the text trigger it
|
|
71
|
+
* takes the quiet fill while open and under a hovering pointer. Its name
|
|
72
|
+
* is the menu's label, so the glyph says nothing on its own.
|
|
73
|
+
*/
|
|
74
|
+
export function cssActionMenuTriggerIconClasses() {
|
|
75
|
+
return `${icon} {
|
|
76
|
+
align-items: center;
|
|
77
|
+
background: transparent;
|
|
78
|
+
border-color: transparent;
|
|
79
|
+
border-radius: var(--sw-radius-pill);
|
|
80
|
+
color: var(--sw-color-muted);
|
|
81
|
+
display: inline-flex;
|
|
82
|
+
justify-content: center;
|
|
83
|
+
min-height: var(--sw-control-sm-min-height);
|
|
84
|
+
min-width: var(--sw-control-sm-min-height);
|
|
85
|
+
padding: 0;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
${icon}[aria-expanded='true'] {
|
|
89
|
+
background: ${quietFill};
|
|
90
|
+
color: var(--sw-color-text);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
@media (hover: hover) {
|
|
94
|
+
${icon}:hover:not(:disabled) {
|
|
95
|
+
background: ${quietFill};
|
|
96
|
+
color: var(--sw-color-text);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
@media ${coarsePointerQuery} {
|
|
101
|
+
${icon} {
|
|
102
|
+
min-height: ${touchTarget};
|
|
103
|
+
min-width: ${touchTarget};
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
@media (forced-colors: active) {
|
|
108
|
+
${icon} { color: ButtonText; }
|
|
109
|
+
${icon}[aria-expanded='true'] { border-color: Highlight; }
|
|
110
|
+
}`;
|
|
111
|
+
}
|
|
64
112
|
export function actionMenuTriggerClassCatalog() {
|
|
65
|
-
return [actionMenuTriggerTextClass];
|
|
113
|
+
return [actionMenuTriggerTextClass, actionMenuTriggerIconClass];
|
|
66
114
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { typographyVariants } from '@scalewing/tokens';
|
|
2
2
|
import { chevronStroke } from './chevron.js';
|
|
3
|
-
import { actionMenuTriggerClassCatalog, cssActionMenuTriggerClasses, } from './css-action-menu-trigger.js';
|
|
3
|
+
import { actionMenuTriggerClassCatalog, cssActionMenuTriggerClasses, cssActionMenuTriggerIconClasses, } from './css-action-menu-trigger.js';
|
|
4
4
|
import { zIndex } from './stacking.js';
|
|
5
5
|
import { coarsePointerQuery, touchTarget } from './touch-target.js';
|
|
6
6
|
import { quietFill } from './quiet-fill.js';
|
|
@@ -166,7 +166,9 @@ export function cssActionMenuClasses() {
|
|
|
166
166
|
}
|
|
167
167
|
}
|
|
168
168
|
|
|
169
|
-
${cssActionMenuTriggerClasses()}
|
|
169
|
+
${cssActionMenuTriggerClasses()}
|
|
170
|
+
|
|
171
|
+
${cssActionMenuTriggerIconClasses()}`;
|
|
170
172
|
}
|
|
171
173
|
export function actionMenuClassCatalog() {
|
|
172
174
|
return [
|
|
@@ -36,6 +36,7 @@ import { cssMultiSelectClasses } from './css-multi-select.js';
|
|
|
36
36
|
import { cssSplitClasses } from './css-split.js';
|
|
37
37
|
import { cssSwitchClasses } from './css-switch.js';
|
|
38
38
|
import { cssTextClasses } from './css-text.js';
|
|
39
|
+
import { cssFontMonoClasses } from './css-font-mono.js';
|
|
39
40
|
import { cssSpinnerClasses } from './css-spinner.js';
|
|
40
41
|
import { cssProgressClasses } from './css-progress.js';
|
|
41
42
|
import { cssNoticeClasses } from './css-notice.js';
|
|
@@ -134,6 +135,8 @@ ${cssPopupHostRules()}
|
|
|
134
135
|
|
|
135
136
|
${cssTextClasses()}
|
|
136
137
|
|
|
138
|
+
${cssFontMonoClasses()}
|
|
139
|
+
|
|
137
140
|
${cssButtonClasses()}
|
|
138
141
|
|
|
139
142
|
${cssCheckboxClasses()}
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* a term sits over its detail. A term and a detail are columns of lines:
|
|
8
8
|
* each child is its own line with its own line height, so a caption term
|
|
9
9
|
* starts level with a caption detail instead of on the list's taller line.
|
|
10
|
+
* `spread` puts each detail at its row's end, beside its term at every width.
|
|
10
11
|
*/
|
|
11
12
|
export declare function cssDescriptionListClasses(): string;
|
|
12
13
|
export declare function descriptionListClassCatalog(): string[];
|