@scalewing/react 1.30.0 → 1.32.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 +43 -0
- package/README.md +23 -5
- 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 +10 -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.d.ts +14 -0
- package/dist/components/Combobox.js +125 -0
- 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 +25 -0
- package/dist/components/Field.js +56 -6
- 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/ComboboxPopup.d.ts +25 -0
- package/dist/components/combobox/ComboboxPopup.js +68 -0
- package/dist/components/combobox/entries.d.ts +18 -0
- package/dist/components/combobox/entries.js +34 -0
- package/dist/components/combobox/types.d.ts +71 -0
- package/dist/components/combobox/types.js +1 -0
- package/dist/components/combobox/validate.d.ts +3 -0
- package/dist/components/combobox/validate.js +44 -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/components/field/SearchFrame.d.ts +16 -0
- package/dist/components/field/SearchFrame.js +32 -0
- 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-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.d.ts +2 -0
- package/dist/css/css-combobox.js +136 -0
- package/dist/css/css-components.js +6 -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-field-search.d.ts +2 -0
- package/dist/css/css-field-search.js +118 -0
- package/dist/css/css-field.js +4 -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-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 +6 -2
- 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 +11 -8
- package/dist/styles.css +505 -12
- package/package.json +1 -1
|
@@ -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 })] }));
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { type RefObject } from 'react';
|
|
2
|
+
import { type ComboboxEntry } from './entries.js';
|
|
3
|
+
import { type ComboboxGroup } from './types.js';
|
|
4
|
+
type ComboboxPopupProps = {
|
|
5
|
+
listId: string;
|
|
6
|
+
listLabel: string;
|
|
7
|
+
groups: readonly ComboboxGroup[];
|
|
8
|
+
entries: readonly ComboboxEntry[];
|
|
9
|
+
activeId: string | undefined;
|
|
10
|
+
emptyText: string;
|
|
11
|
+
note?: string;
|
|
12
|
+
noteId: string;
|
|
13
|
+
inputRef: RefObject<HTMLInputElement | null>;
|
|
14
|
+
onActivate: (entry: ComboboxEntry) => void;
|
|
15
|
+
onChoose: (entry: ComboboxEntry) => void;
|
|
16
|
+
onDismiss: () => void;
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* The open list beside the input, on the popover layer and at least as
|
|
20
|
+
* wide as the input: a heading per group, each option's label, badge and
|
|
21
|
+
* description, then the action; the empty text when nothing matches, and
|
|
22
|
+
* the note under it all.
|
|
23
|
+
*/
|
|
24
|
+
export declare function ComboboxPopup({ activeId, emptyText, entries, groups, inputRef, listId, listLabel, note, noteId, onActivate, onChoose, onDismiss, }: ComboboxPopupProps): import("react").JSX.Element;
|
|
25
|
+
export {};
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { Fragment, useLayoutEffect, useRef, } from 'react';
|
|
4
|
+
import { cx } from '../../class-names.js';
|
|
5
|
+
import { Badge } from '../Badge.js';
|
|
6
|
+
import { useAnchoredPopover } from '../use-anchored-popover.js';
|
|
7
|
+
function keepInputFocus(event) {
|
|
8
|
+
// A press in the list never takes focus from the input.
|
|
9
|
+
event.preventDefault();
|
|
10
|
+
}
|
|
11
|
+
function EntryBadge({ badge }) {
|
|
12
|
+
if (!badge)
|
|
13
|
+
return null;
|
|
14
|
+
return (_jsx(Badge, { size: "sm", tone: badge.tone ?? 'neutral', children: badge.label }));
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* The open list beside the input, on the popover layer and at least as
|
|
18
|
+
* wide as the input: a heading per group, each option's label, badge and
|
|
19
|
+
* description, then the action; the empty text when nothing matches, and
|
|
20
|
+
* the note under it all.
|
|
21
|
+
*/
|
|
22
|
+
export function ComboboxPopup({ activeId, emptyText, entries, groups, inputRef, listId, listLabel, note, noteId, onActivate, onChoose, onDismiss, }) {
|
|
23
|
+
const popupRef = useRef(null);
|
|
24
|
+
useAnchoredPopover(popupRef, inputRef, inputRef, onDismiss, {
|
|
25
|
+
align: 'start',
|
|
26
|
+
flipInline: true,
|
|
27
|
+
});
|
|
28
|
+
// At least as wide as the input, and kept so when the input resizes.
|
|
29
|
+
useLayoutEffect(() => {
|
|
30
|
+
const popup = popupRef.current;
|
|
31
|
+
const input = inputRef.current;
|
|
32
|
+
if (!popup || !input)
|
|
33
|
+
return;
|
|
34
|
+
const fit = () => {
|
|
35
|
+
popup.style.minWidth = `${input.offsetWidth}px`;
|
|
36
|
+
};
|
|
37
|
+
fit();
|
|
38
|
+
if (typeof ResizeObserver === 'undefined')
|
|
39
|
+
return;
|
|
40
|
+
const observer = new ResizeObserver(fit);
|
|
41
|
+
observer.observe(input);
|
|
42
|
+
return () => observer.disconnect();
|
|
43
|
+
}, [inputRef]);
|
|
44
|
+
useLayoutEffect(() => {
|
|
45
|
+
if (activeId)
|
|
46
|
+
document.getElementById(activeId)?.scrollIntoView?.({ block: 'nearest' });
|
|
47
|
+
}, [activeId]);
|
|
48
|
+
const options = entries.filter((entry) => entry.kind === 'option');
|
|
49
|
+
const action = entries.find((entry) => entry.kind === 'action');
|
|
50
|
+
function renderEntry(entry) {
|
|
51
|
+
const disabled = entry.kind === 'option' && entry.option.disabled;
|
|
52
|
+
return (_jsx("div", { "aria-disabled": disabled || undefined, "aria-selected": entry.id === activeId, className: cx('sw-combobox-option', entry.kind === 'action' && 'sw-combobox-action'), id: entry.id, onClick: () => onChoose(entry), onMouseDown: keepInputFocus,
|
|
53
|
+
// The pointer moves the active option, as the arrow keys do.
|
|
54
|
+
onMouseMove: () => {
|
|
55
|
+
if (entry.id !== activeId)
|
|
56
|
+
onActivate(entry);
|
|
57
|
+
}, role: "option", children: entry.kind === 'option' ? (_jsxs(_Fragment, { children: [_jsx("span", { className: "sw-combobox-label", children: entry.option.label }), _jsx(EntryBadge, { badge: entry.option.badge }), entry.option.description ? (_jsx("span", { className: "sw-combobox-description", children: entry.option.description })) : null] })) : (_jsxs(_Fragment, { children: [_jsx("span", { className: "sw-combobox-label", children: entry.action.label }), _jsx(EntryBadge, { badge: entry.action.badge })] })) }, entry.id));
|
|
58
|
+
}
|
|
59
|
+
return (_jsxs("div", { className: "sw-combobox-popup", onMouseDown: keepInputFocus, ref: popupRef, children: [entries.length > 0 ? (_jsxs("div", { "aria-label": listLabel, className: "sw-combobox-list", id: listId, role: "listbox", children: [groups.map((group, groupIndex) => {
|
|
60
|
+
const groupEntries = options.filter((entry) => entry.kind === 'option' && entry.groupIndex === groupIndex);
|
|
61
|
+
if (groupEntries.length === 0)
|
|
62
|
+
return null;
|
|
63
|
+
if (!group.label)
|
|
64
|
+
return (_jsx(Fragment, { children: groupEntries.map(renderEntry) }, `${listId}-group-${groupIndex}`));
|
|
65
|
+
const headingId = `${listId}-group-${groupIndex}`;
|
|
66
|
+
return (_jsxs("div", { "aria-labelledby": headingId, role: "group", children: [_jsx("div", { className: "sw-combobox-heading", id: headingId, role: "presentation", children: group.label }), groupEntries.map(renderEntry)] }, headingId));
|
|
67
|
+
}), action ? renderEntry(action) : null] })) : null, options.length === 0 ? (_jsx("div", { "aria-hidden": "true", className: "sw-combobox-empty", children: emptyText })) : null, note ? (_jsx("div", { className: "sw-combobox-note", id: noteId, children: note })) : null] }));
|
|
68
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { type ComboboxAction, type ComboboxGroup, type ComboboxOption } from './types.js';
|
|
2
|
+
/** One row the arrow keys move over: an option in a group, or the action. */
|
|
3
|
+
export type ComboboxEntry = {
|
|
4
|
+
kind: 'option';
|
|
5
|
+
key: string;
|
|
6
|
+
id: string;
|
|
7
|
+
index: number;
|
|
8
|
+
option: ComboboxOption;
|
|
9
|
+
groupIndex: number;
|
|
10
|
+
} | {
|
|
11
|
+
kind: 'action';
|
|
12
|
+
key: string;
|
|
13
|
+
id: string;
|
|
14
|
+
index: number;
|
|
15
|
+
action: ComboboxAction;
|
|
16
|
+
};
|
|
17
|
+
/** The list's rows in order, each with a stable key and a DOM id under `listId`. */
|
|
18
|
+
export declare function comboboxEntries(listId: string, groups: readonly ComboboxGroup[], action: ComboboxAction | undefined): ComboboxEntry[];
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Each row's key and DOM id come from its value, not its place, so an
|
|
3
|
+
* option keeps its id (and stays the active one) when the consumer's
|
|
4
|
+
* results change under it, and a new option never inherits an old id.
|
|
5
|
+
*/
|
|
6
|
+
const encoder = new TextEncoder();
|
|
7
|
+
/** The value as hex, so any value makes a unique, valid id part. */
|
|
8
|
+
function idPart(value) {
|
|
9
|
+
return Array.from(encoder.encode(value), (byte) => byte.toString(16).padStart(2, '0')).join('');
|
|
10
|
+
}
|
|
11
|
+
/** The list's rows in order, each with a stable key and a DOM id under `listId`. */
|
|
12
|
+
export function comboboxEntries(listId, groups, action) {
|
|
13
|
+
const entries = [];
|
|
14
|
+
groups.forEach((group, groupIndex) => {
|
|
15
|
+
for (const option of group.options)
|
|
16
|
+
entries.push({
|
|
17
|
+
kind: 'option',
|
|
18
|
+
key: `option:${option.value}`,
|
|
19
|
+
id: `${listId}-option-${idPart(option.value)}`,
|
|
20
|
+
index: entries.length,
|
|
21
|
+
option,
|
|
22
|
+
groupIndex,
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
if (action)
|
|
26
|
+
entries.push({
|
|
27
|
+
kind: 'action',
|
|
28
|
+
key: 'action',
|
|
29
|
+
id: `${listId}-action`,
|
|
30
|
+
index: entries.length,
|
|
31
|
+
action,
|
|
32
|
+
});
|
|
33
|
+
return entries;
|
|
34
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { type BadgeTone } from '../../css/css-data.js';
|
|
2
|
+
import { type RequiredMark } from '../FieldLabelText.js';
|
|
3
|
+
/** A short tag after an option's label, such as "Warning" or "Blocked". */
|
|
4
|
+
export type ComboboxBadge = {
|
|
5
|
+
label: string;
|
|
6
|
+
tone?: BadgeTone;
|
|
7
|
+
};
|
|
8
|
+
/** One suggestion in a Combobox. */
|
|
9
|
+
export type ComboboxOption = {
|
|
10
|
+
value: string;
|
|
11
|
+
label: string;
|
|
12
|
+
/** A muted line of detail after the label, such as "3 items · updated Sep 28". */
|
|
13
|
+
description?: string;
|
|
14
|
+
badge?: ComboboxBadge;
|
|
15
|
+
/** Shown, read as unavailable, and never chosen, such as an archived item. */
|
|
16
|
+
disabled?: boolean;
|
|
17
|
+
};
|
|
18
|
+
/** A run of suggestions under a heading, such as "All items". */
|
|
19
|
+
export type ComboboxGroup = {
|
|
20
|
+
/** The group's heading; a group without one has no heading. */
|
|
21
|
+
label?: string;
|
|
22
|
+
options: readonly ComboboxOption[];
|
|
23
|
+
};
|
|
24
|
+
/** A command at the end of the list, such as "+ New entry “acme”". */
|
|
25
|
+
export type ComboboxAction = {
|
|
26
|
+
label: string;
|
|
27
|
+
onPress: () => void;
|
|
28
|
+
badge?: ComboboxBadge;
|
|
29
|
+
};
|
|
30
|
+
export declare const comboboxSizes: readonly ["md", "xs"];
|
|
31
|
+
export type ComboboxSize = (typeof comboboxSizes)[number];
|
|
32
|
+
export type ComboboxProps = {
|
|
33
|
+
/** The input's name, drawn above it unless `labelVisuallyHidden`. */
|
|
34
|
+
label: string;
|
|
35
|
+
labelVisuallyHidden?: boolean;
|
|
36
|
+
/** The text in the input; the consumer filters `groups` from it. */
|
|
37
|
+
inputValue: string;
|
|
38
|
+
onInputChange: (value: string) => void;
|
|
39
|
+
groups: readonly ComboboxGroup[];
|
|
40
|
+
/** Called with the chosen option's value; the consumer sets `inputValue`. */
|
|
41
|
+
onSelect: (value: string) => void;
|
|
42
|
+
action?: ComboboxAction;
|
|
43
|
+
/**
|
|
44
|
+
* Shown in the list, and announced, when no group has an option, such as
|
|
45
|
+
* "Nothing matches".
|
|
46
|
+
*/
|
|
47
|
+
emptyText: string;
|
|
48
|
+
/**
|
|
49
|
+
* The words announced for the number of options while the list is open,
|
|
50
|
+
* such as `(n) => \`${n} items\``; without it only the empty text is
|
|
51
|
+
* announced.
|
|
52
|
+
*/
|
|
53
|
+
countText?: (count: number) => string;
|
|
54
|
+
/** The list's name when it differs from the input's, such as "Results". */
|
|
55
|
+
listLabel?: string;
|
|
56
|
+
/** A muted line under the suggestions, such as how to find more. */
|
|
57
|
+
note?: string;
|
|
58
|
+
placeholder?: string;
|
|
59
|
+
description?: string;
|
|
60
|
+
error?: string;
|
|
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;
|
|
69
|
+
disabled?: boolean;
|
|
70
|
+
size?: ComboboxSize;
|
|
71
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const comboboxSizes = ['md', 'xs'];
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { badgeTones } from '../../css/css-data.js';
|
|
2
|
+
import { comboboxSizes, } from './types.js';
|
|
3
|
+
function blank(text) {
|
|
4
|
+
return typeof text !== 'string' || !text.trim();
|
|
5
|
+
}
|
|
6
|
+
function assertBadge(badge) {
|
|
7
|
+
if (badge === undefined)
|
|
8
|
+
return;
|
|
9
|
+
if (blank(badge.label))
|
|
10
|
+
throw new RangeError('A Combobox badge needs a nonempty label');
|
|
11
|
+
if (badge.tone !== undefined && !badgeTones.includes(badge.tone))
|
|
12
|
+
throw new RangeError(`Unknown Combobox badge tone: ${String(badge.tone)}`);
|
|
13
|
+
}
|
|
14
|
+
/** Fails closed on blank words, duplicate values and unknown sizes. */
|
|
15
|
+
export function assertCombobox(props) {
|
|
16
|
+
if (blank(props.label))
|
|
17
|
+
throw new RangeError('Combobox needs a nonempty label');
|
|
18
|
+
if (blank(props.emptyText))
|
|
19
|
+
throw new RangeError('Combobox needs a nonempty emptyText');
|
|
20
|
+
if (props.listLabel !== undefined && blank(props.listLabel))
|
|
21
|
+
throw new RangeError('A Combobox listLabel must not be blank');
|
|
22
|
+
if (props.size !== undefined && !comboboxSizes.includes(props.size))
|
|
23
|
+
throw new RangeError(`Unknown Combobox size: ${String(props.size)}`);
|
|
24
|
+
const values = new Set();
|
|
25
|
+
for (const group of props.groups) {
|
|
26
|
+
if (group.label !== undefined && blank(group.label))
|
|
27
|
+
throw new RangeError('A Combobox group label must not be blank');
|
|
28
|
+
for (const option of group.options) {
|
|
29
|
+
if (blank(option.value) || blank(option.label))
|
|
30
|
+
throw new RangeError('Combobox options need nonempty values and labels');
|
|
31
|
+
if (values.has(option.value))
|
|
32
|
+
throw new RangeError(`Duplicate Combobox option: ${option.value}`);
|
|
33
|
+
values.add(option.value);
|
|
34
|
+
assertBadge(option.badge);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
if (props.action) {
|
|
38
|
+
if (blank(props.action.label))
|
|
39
|
+
throw new RangeError('A Combobox action needs a nonempty label');
|
|
40
|
+
if (typeof props.action.onPress !== 'function')
|
|
41
|
+
throw new TypeError('A Combobox action needs onPress');
|
|
42
|
+
assertBadge(props.action.badge);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -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
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { type ReactNode } from 'react';
|
|
2
2
|
import { type BadgeTone } from '../../css/css-data.js';
|
|
3
|
+
import { type DenominationGridFrame, type DenominationTotalEmphasis } from '../../css/css-denomination-appearance.js';
|
|
4
|
+
import { type DenominationCellToneAppearance } from '../../css/css-denomination-cell-tones.js';
|
|
3
5
|
import { type DenominationLabelAppearance } from '../../css/css-denomination-label.js';
|
|
4
6
|
import { type DenominationLabelWidth } from '../../css/css-denomination-aligned.js';
|
|
5
|
-
|
|
7
|
+
import { type StepperLimitAppearance } from '../Stepper.js';
|
|
8
|
+
export type { DenominationCellToneAppearance, DenominationGridFrame, DenominationLabelWidth, DenominationTotalEmphasis, };
|
|
6
9
|
export type DenominationGridTone = BadgeTone;
|
|
7
10
|
export type DenominationGridLayout = 'strip' | 'tiles';
|
|
8
11
|
/**
|
|
@@ -36,6 +39,11 @@ export type DenominationGridEntry = {
|
|
|
36
39
|
errors?: readonly (string | null)[];
|
|
37
40
|
/** What an empty box shows, such as "0". */
|
|
38
41
|
placeholder?: string;
|
|
42
|
+
/**
|
|
43
|
+
* The Steppers' `limitAppearance`: `plain` keeps − at 0 (and + at a cap)
|
|
44
|
+
* in its usual look instead of faded; it is still disabled.
|
|
45
|
+
*/
|
|
46
|
+
limitAppearance?: StepperLimitAppearance;
|
|
39
47
|
};
|
|
40
48
|
export type DenominationGridRow = {
|
|
41
49
|
id: string;
|
|
@@ -79,6 +87,11 @@ export type DenominationGridProps = {
|
|
|
79
87
|
rows: readonly DenominationGridRow[];
|
|
80
88
|
layout?: DenominationGridLayout;
|
|
81
89
|
subtotal?: (count: number, column: DenominationGridColumn) => string;
|
|
90
|
+
/**
|
|
91
|
+
* What a zero or empty read-only count shows, `—` by default. An empty
|
|
92
|
+
* string leaves those counts blank, so only the counts that are there
|
|
93
|
+
* stand out (the column header still names each cell).
|
|
94
|
+
*/
|
|
82
95
|
zeroLabel?: string;
|
|
83
96
|
/**
|
|
84
97
|
* Names the strip's total column, such as "Total": a column header over
|
|
@@ -111,4 +124,25 @@ export type DenominationGridProps = {
|
|
|
111
124
|
* total.
|
|
112
125
|
*/
|
|
113
126
|
labelAppearance?: DenominationLabelAppearance;
|
|
127
|
+
/**
|
|
128
|
+
* `table` frames the strip: its surface inside a hairline with rounded
|
|
129
|
+
* corners, the heading row on the subtle fill, a rule between columns,
|
|
130
|
+
* and the counts at label size, centred under their heads. `none` (the
|
|
131
|
+
* default) leaves open rows on the page's surface. Only the strip has a
|
|
132
|
+
* frame.
|
|
133
|
+
*/
|
|
134
|
+
frame?: DenominationGridFrame;
|
|
135
|
+
/**
|
|
136
|
+
* `strong` sets the row totals in bold label type, a neutral row's in the
|
|
137
|
+
* text color and a toned row's in its tone; `default` keeps them in data
|
|
138
|
+
* type in the row's tone (a neutral row's muted).
|
|
139
|
+
*/
|
|
140
|
+
totalEmphasis?: DenominationTotalEmphasis;
|
|
141
|
+
/**
|
|
142
|
+
* How a `cellTones` tone shows on a strip's read-only count: `text` (the
|
|
143
|
+
* default) sets the count in the tone; `pill` sets it on a rounded tint of
|
|
144
|
+
* the tone in the tone's ink, such as a difference in danger or a
|
|
145
|
+
* changed count in accent. Entry counts and tiles keep their tone border.
|
|
146
|
+
*/
|
|
147
|
+
cellToneAppearance?: DenominationCellToneAppearance;
|
|
114
148
|
};
|
|
@@ -11,12 +11,14 @@ type DialogTitleProps = {
|
|
|
11
11
|
/** The close button's accessible name; without it there is no button. */
|
|
12
12
|
closeLabel?: string;
|
|
13
13
|
onClose: () => void;
|
|
14
|
+
/** A banded dialog's title is always a row, which draws the band. */
|
|
15
|
+
banded: boolean;
|
|
14
16
|
};
|
|
15
17
|
/**
|
|
16
18
|
* The dialog's title, which names it through `aria-labelledby`. Alone it is
|
|
17
19
|
* the heading, as it always was. With an icon, a description or a
|
|
18
|
-
* `closeLabel
|
|
19
|
-
* an icon-only ghost close button at the end.
|
|
20
|
+
* `closeLabel`, or in a banded dialog, it is a row: the icon, the title over
|
|
21
|
+
* its description, and an icon-only ghost close button at the end.
|
|
20
22
|
*/
|
|
21
|
-
export declare function DialogTitle({ closeLabel, description, descriptionId, icon, id, level, onClose, title, }: DialogTitleProps): import("react").JSX.Element;
|
|
23
|
+
export declare function DialogTitle({ banded, closeLabel, description, descriptionId, icon, id, level, onClose, title, }: DialogTitleProps): import("react").JSX.Element;
|
|
22
24
|
export {};
|
|
@@ -5,12 +5,12 @@ import { Text } from '../Text.js';
|
|
|
5
5
|
/**
|
|
6
6
|
* The dialog's title, which names it through `aria-labelledby`. Alone it is
|
|
7
7
|
* the heading, as it always was. With an icon, a description or a
|
|
8
|
-
* `closeLabel
|
|
9
|
-
* an icon-only ghost close button at the end.
|
|
8
|
+
* `closeLabel`, or in a banded dialog, it is a row: the icon, the title over
|
|
9
|
+
* its description, and an icon-only ghost close button at the end.
|
|
10
10
|
*/
|
|
11
|
-
export function DialogTitle({ closeLabel, description, descriptionId, icon, id, level, onClose, title, }) {
|
|
11
|
+
export function DialogTitle({ banded, closeLabel, description, descriptionId, icon, id, level, onClose, title, }) {
|
|
12
12
|
const heading = (_jsx(Text, { as: level === 2 ? 'h2' : 'h3', id: id, variant: "title", children: title }));
|
|
13
|
-
if (!closeLabel && !icon && !description)
|
|
13
|
+
if (!banded && !closeLabel && !icon && !description)
|
|
14
14
|
return heading;
|
|
15
15
|
return (_jsxs("div", { className: cx('sw-dialog-header', Boolean(icon || description) && 'sw-dialog-header-framed'), children: [icon ? (_jsx("span", { "aria-hidden": "true", className: "sw-dialog-icon", children: icon })) : null, description ? (_jsxs("div", { className: "sw-dialog-titles", children: [heading, _jsx(Text, { as: "p", className: "sw-dialog-description", color: "muted", id: descriptionId, variant: "caption", children: description })] })) : (heading), closeLabel ? (_jsxs(Button, { className: "sw-dialog-close", onPress: onClose, size: "sm", variant: "ghost", children: [_jsx("span", { className: "sw-sr-only", children: closeLabel }), _jsx("span", { "aria-hidden": "true", className: "sw-dialog-close-glyph" })] })) : null] }));
|
|
16
16
|
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
type SearchFrameProps = {
|
|
3
|
+
children: ReactNode;
|
|
4
|
+
leading?: ReactNode;
|
|
5
|
+
clearLabel: string;
|
|
6
|
+
/** Shown while the input holds text and is enabled. */
|
|
7
|
+
showClear: boolean;
|
|
8
|
+
onClear: () => void;
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* The search field's frame: the consumer's glyph at the start (hidden from
|
|
12
|
+
* assistive technology), the input, and a clear button at the end while
|
|
13
|
+
* there is text. Clearing calls `onClear` and puts focus back in the input.
|
|
14
|
+
*/
|
|
15
|
+
export declare function SearchFrame({ children, clearLabel, leading, onClear, showClear, }: SearchFrameProps): import("react").JSX.Element;
|
|
16
|
+
export {};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/*
|
|
3
|
+
* A press on the frame's chrome (the glyph or the padding) focuses the
|
|
4
|
+
* input, as on an adorned field; a press on the input keeps the browser's
|
|
5
|
+
* own caret placement, and the clear button runs its own action.
|
|
6
|
+
*/
|
|
7
|
+
function focusSearchInput(event) {
|
|
8
|
+
const input = event.currentTarget.querySelector('input');
|
|
9
|
+
const target = event.target;
|
|
10
|
+
if (!input || input.disabled || target === input)
|
|
11
|
+
return;
|
|
12
|
+
event.preventDefault();
|
|
13
|
+
// The clear button keeps focus in the input: no blur, and its click
|
|
14
|
+
// still runs.
|
|
15
|
+
if (target.closest('.sw-field-search-clear'))
|
|
16
|
+
return;
|
|
17
|
+
input.focus();
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* The search field's frame: the consumer's glyph at the start (hidden from
|
|
21
|
+
* assistive technology), the input, and a clear button at the end while
|
|
22
|
+
* there is text. Clearing calls `onClear` and puts focus back in the input.
|
|
23
|
+
*/
|
|
24
|
+
export function SearchFrame({ children, clearLabel, leading, onClear, showClear, }) {
|
|
25
|
+
return (_jsxs("span", { className: "sw-field-adorned sw-field-search", onMouseDown: focusSearchInput, children: [leading != null && leading !== false ? (_jsx("span", { "aria-hidden": "true", className: "sw-field-search-leading", children: leading })) : null, children, showClear ? (_jsx("button", { "aria-label": clearLabel, className: "sw-field-search-clear", onClick: (event) => {
|
|
26
|
+
// Focus first, so a consumer that moves focus in onClear wins.
|
|
27
|
+
event.currentTarget.parentElement
|
|
28
|
+
?.querySelector('input')
|
|
29
|
+
?.focus();
|
|
30
|
+
onClear();
|
|
31
|
+
}, type: "button", children: _jsx("span", { "aria-hidden": "true", className: "sw-field-search-cross" }) })) : null] }));
|
|
32
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export declare const actionMenuTriggerAppearances: readonly ["framed", "text"];
|
|
2
|
+
/**
|
|
3
|
+
* How an ActionMenu's trigger looks: `framed` (the default) is a small
|
|
4
|
+
* outlined control; `text` is its words in the text color with a chevron
|
|
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.
|
|
7
|
+
*/
|
|
8
|
+
export type ActionMenuTriggerAppearance = (typeof actionMenuTriggerAppearances)[number];
|
|
9
|
+
export declare const actionMenuTriggerTextClass = "sw-action-menu-trigger-text";
|
|
10
|
+
export declare function cssActionMenuTriggerClasses(): string;
|
|
11
|
+
export declare function actionMenuTriggerClassCatalog(): string[];
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { typographyVariants } from '@scalewing/tokens';
|
|
2
|
+
import { chevronStroke } from './chevron.js';
|
|
3
|
+
import { quietFill } from './quiet-fill.js';
|
|
4
|
+
import { coarsePointerQuery, touchTarget } from './touch-target.js';
|
|
5
|
+
const label = typographyVariants.label;
|
|
6
|
+
export const actionMenuTriggerAppearances = ['framed', 'text'];
|
|
7
|
+
export const actionMenuTriggerTextClass = 'sw-action-menu-trigger-text';
|
|
8
|
+
const text = `.sw-action-menu-trigger.${actionMenuTriggerTextClass}`;
|
|
9
|
+
/*
|
|
10
|
+
* A text trigger: no fill and a transparent border (kept, so forced colors
|
|
11
|
+
* can draw one), its words in label type at medium weight in the text
|
|
12
|
+
* color, the sm control height (the 44 px target on a coarse pointer), and
|
|
13
|
+
* the shared muted chevron pointing down after them, a space-2 gap apart.
|
|
14
|
+
* The quiet fill shows while the menu is open, so the open state is seen
|
|
15
|
+
* as well as announced, and on hover where the pointer can hover (a tap
|
|
16
|
+
* leaves no fill behind). Two classes, so it outranks the framed
|
|
17
|
+
* trigger's rules; the coarse-pointer target is restated after. Forced
|
|
18
|
+
* colors draw the kept border in the system's border color, the chevron
|
|
19
|
+
* in CanvasText as every chevron, and the open trigger's border in
|
|
20
|
+
* Highlight.
|
|
21
|
+
*/
|
|
22
|
+
export function cssActionMenuTriggerClasses() {
|
|
23
|
+
return `${text} {
|
|
24
|
+
align-items: center;
|
|
25
|
+
background: transparent;
|
|
26
|
+
border-color: transparent;
|
|
27
|
+
display: inline-flex;
|
|
28
|
+
font-family: var(--sw-font-sans);
|
|
29
|
+
font-size: ${label.fontSize}px;
|
|
30
|
+
font-weight: 500;
|
|
31
|
+
gap: var(--sw-space-2);
|
|
32
|
+
letter-spacing: ${label.letterSpacing}px;
|
|
33
|
+
line-height: ${label.lineHeight}px;
|
|
34
|
+
min-height: var(--sw-control-sm-min-height);
|
|
35
|
+
padding-inline: var(--sw-space-3) var(--sw-space-2);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
${text}::after {
|
|
39
|
+
${chevronStroke}
|
|
40
|
+
content: '';
|
|
41
|
+
flex: none;
|
|
42
|
+
transform: translateY(-25%) rotate(45deg);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
${text}[aria-expanded='true'] {
|
|
46
|
+
background: ${quietFill};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
@media (hover: hover) {
|
|
50
|
+
${text}:hover:not(:disabled) {
|
|
51
|
+
background: ${quietFill};
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
@media ${coarsePointerQuery} {
|
|
56
|
+
${text} { min-height: ${touchTarget}; }
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
@media (forced-colors: active) {
|
|
60
|
+
${text}::after { border-color: CanvasText; }
|
|
61
|
+
${text}[aria-expanded='true'] { border-color: Highlight; }
|
|
62
|
+
}`;
|
|
63
|
+
}
|
|
64
|
+
export function actionMenuTriggerClassCatalog() {
|
|
65
|
+
return [actionMenuTriggerTextClass];
|
|
66
|
+
}
|