@scalewing/react 1.52.0 → 1.54.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 +20 -0
- package/README.md +14 -1
- package/dist/components/Box.d.ts +26 -0
- package/dist/components/Box.js +6 -2
- package/dist/components/Checkbox.d.ts +18 -1
- package/dist/components/Checkbox.js +15 -5
- package/dist/components/Dialog.d.ts +14 -1
- package/dist/components/Dialog.js +5 -2
- package/dist/components/PasteRows.d.ts +55 -0
- package/dist/components/PasteRows.js +55 -0
- package/dist/components/Stepper.d.ts +2 -66
- package/dist/components/Stepper.js +18 -5
- package/dist/components/Table.d.ts +1 -0
- package/dist/components/Table.js +1 -0
- package/dist/components/box/external-link.d.ts +20 -0
- package/dist/components/box/external-link.js +37 -0
- package/dist/components/checkbox/CheckboxControl.d.ts +1 -1
- package/dist/components/checkbox/CheckboxControl.js +4 -3
- package/dist/components/dialog/validate.d.ts +3 -1
- package/dist/components/dialog/validate.js +3 -1
- package/dist/components/paste-rows/parse-pasted-rows.d.ts +26 -0
- package/dist/components/paste-rows/parse-pasted-rows.js +123 -0
- package/dist/components/stepper/stepper-props.d.ts +78 -0
- package/dist/components/stepper/stepper-props.js +1 -0
- package/dist/components/stepper/use-stepper-commit.d.ts +22 -0
- package/dist/components/stepper/use-stepper-commit.js +66 -0
- package/dist/components/table/TableSelectCell.d.ts +26 -0
- package/dist/components/table/TableSelectCell.js +19 -0
- package/dist/css/css-box-link.d.ts +3 -0
- package/dist/css/css-box-link.js +41 -2
- package/dist/css/css-checkbox.js +37 -3
- package/dist/css/css-components.js +3 -0
- package/dist/css/css-dialog-end.d.ts +13 -0
- package/dist/css/css-dialog-end.js +75 -0
- package/dist/css/css-select.js +10 -0
- package/dist/css/css-table-select.d.ts +3 -0
- package/dist/css/css-table-select.js +24 -0
- package/dist/css/css-table.js +4 -0
- package/dist/css/stylesheet.js +2 -0
- package/dist/index.d.ts +4 -2
- package/dist/index.js +4 -2
- package/dist/styles.css +128 -3
- package/package.json +1 -1
package/dist/components/Table.js
CHANGED
|
@@ -6,6 +6,7 @@ import { assertTableLayout, assertTableStickyHeader, assertTableVerticalAlign, }
|
|
|
6
6
|
import { assertTableMaxHeight, tableMaxHeightStyle, } from './table/table-max-height.js';
|
|
7
7
|
export { TableCell, } from './table/TableCell.js';
|
|
8
8
|
export { TableRow, } from './table/TableRow.js';
|
|
9
|
+
export { TableSelectCell, } from './table/TableSelectCell.js';
|
|
9
10
|
export { tableCellTones, tableRowVariants } from '../css/css-table-ledger.js';
|
|
10
11
|
/**
|
|
11
12
|
* A wide table scrolls inside its own `ScrollRegion`, named after the
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
/** What a `Box` passes to check and draw its external link. */
|
|
3
|
+
export type BoxExternalOptions = {
|
|
4
|
+
as: string;
|
|
5
|
+
external: boolean | undefined;
|
|
6
|
+
newTabLabel: string | undefined;
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* Refuses an external box that is not a link or has no words for its new
|
|
10
|
+
* tab, and new-tab words on a box that is not external.
|
|
11
|
+
*/
|
|
12
|
+
export declare function assertBoxExternal({ as, external, newTabLabel, }: BoxExternalOptions): void;
|
|
13
|
+
/** The consumer's `rel` words with `noopener noreferrer`, each once. */
|
|
14
|
+
export declare function externalLinkRel(rel: string | undefined): string;
|
|
15
|
+
/**
|
|
16
|
+
* An external link's content: its words, the decorative new-tab mark
|
|
17
|
+
* right after them (no space, so it never wraps alone) and the hidden
|
|
18
|
+
* words read after the name, such as "(opens in a new tab)".
|
|
19
|
+
*/
|
|
20
|
+
export declare function externalLinkContent(children: ReactNode, newTabLabel: string): ReactNode;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { externalLinkMarkClass } from '../../css/css-box-link.js';
|
|
3
|
+
/** The `rel` words a link that opens a new tab always carries. */
|
|
4
|
+
const newTabRelWords = ['noopener', 'noreferrer'];
|
|
5
|
+
/**
|
|
6
|
+
* Refuses an external box that is not a link or has no words for its new
|
|
7
|
+
* tab, and new-tab words on a box that is not external.
|
|
8
|
+
*/
|
|
9
|
+
export function assertBoxExternal({ as, external, newTabLabel, }) {
|
|
10
|
+
if (external === undefined || external === false) {
|
|
11
|
+
if (newTabLabel !== undefined)
|
|
12
|
+
throw new TypeError('Box newTabLabel is for an external link');
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
if (external !== true)
|
|
16
|
+
throw new RangeError('Box external must be a boolean');
|
|
17
|
+
if (as !== 'a')
|
|
18
|
+
throw new TypeError('Box external is for a link (as="a")');
|
|
19
|
+
if (typeof newTabLabel !== 'string' || newTabLabel.trim() === '')
|
|
20
|
+
throw new RangeError('Box external needs a non-empty newTabLabel');
|
|
21
|
+
}
|
|
22
|
+
/** The consumer's `rel` words with `noopener noreferrer`, each once. */
|
|
23
|
+
export function externalLinkRel(rel) {
|
|
24
|
+
const words = (rel ?? '').split(/\s+/).filter((word) => word !== '');
|
|
25
|
+
for (const word of newTabRelWords)
|
|
26
|
+
if (!words.includes(word))
|
|
27
|
+
words.push(word);
|
|
28
|
+
return words.join(' ');
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* An external link's content: its words, the decorative new-tab mark
|
|
32
|
+
* right after them (no space, so it never wraps alone) and the hidden
|
|
33
|
+
* words read after the name, such as "(opens in a new tab)".
|
|
34
|
+
*/
|
|
35
|
+
export function externalLinkContent(children, newTabLabel) {
|
|
36
|
+
return (_jsxs(_Fragment, { children: [children, _jsx("span", { "aria-hidden": "true", className: externalLinkMarkClass }), _jsxs("span", { className: "sw-sr-only", children: [" ", newTabLabel] })] }));
|
|
37
|
+
}
|
|
@@ -5,5 +5,5 @@ type CheckboxControlProps = Omit<InputHTMLAttributes<HTMLInputElement>, 'classNa
|
|
|
5
5
|
* generated rules style (`input + .sw-checkbox-mark`). Shared by Checkbox
|
|
6
6
|
* and MultiSelect's rows so the two cannot drift apart.
|
|
7
7
|
*/
|
|
8
|
-
export declare
|
|
8
|
+
export declare const CheckboxControl: import("react").ForwardRefExoticComponent<CheckboxControlProps & import("react").RefAttributes<HTMLInputElement>>;
|
|
9
9
|
export {};
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { forwardRef } from 'react';
|
|
2
3
|
import { tabStop } from '../../tab-stop.js';
|
|
3
4
|
/**
|
|
4
5
|
* The native checkbox and its drawn mark, the one markup Checkbox's
|
|
5
6
|
* generated rules style (`input + .sw-checkbox-mark`). Shared by Checkbox
|
|
6
7
|
* and MultiSelect's rows so the two cannot drift apart.
|
|
7
8
|
*/
|
|
8
|
-
export function CheckboxControl({ tabIndex, ...props }) {
|
|
9
|
-
return (_jsxs("span", { className: "sw-checkbox-control", children: [_jsx("input", { tabIndex: tabIndex ?? tabStop, ...props, className: "sw-checkbox-input", type: "checkbox" }), _jsx("span", { "aria-hidden": "true", className: "sw-checkbox-mark" })] }));
|
|
10
|
-
}
|
|
9
|
+
export const CheckboxControl = forwardRef(function CheckboxControl({ tabIndex, ...props }, ref) {
|
|
10
|
+
return (_jsxs("span", { className: "sw-checkbox-control", children: [_jsx("input", { ref: ref, tabIndex: tabIndex ?? tabStop, ...props, className: "sw-checkbox-input", type: "checkbox" }), _jsx("span", { "aria-hidden": "true", className: "sw-checkbox-mark" })] }));
|
|
11
|
+
});
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type Breakpoint } from '../../css/breakpoints.js';
|
|
2
2
|
import { type DialogAppearance } from '../../css/css-dialog-banded.js';
|
|
3
|
+
import { type DialogPlacement } from '../../css/css-dialog-end.js';
|
|
3
4
|
import { type ThemeCanvas } from '../../css/css-canvas-grouped.js';
|
|
4
5
|
import { type ActionBarSummary } from '../action-bar/ActionBarSummary.js';
|
|
5
6
|
export declare const dialogSizes: readonly ["md", "lg", "xl"];
|
|
@@ -20,5 +21,6 @@ export type DialogChecks = {
|
|
|
20
21
|
canvas: ThemeCanvas;
|
|
21
22
|
sheetBelow: Breakpoint | undefined;
|
|
22
23
|
sheetGrabber: boolean;
|
|
24
|
+
placement: DialogPlacement;
|
|
23
25
|
};
|
|
24
|
-
export declare function assertDialogProps({ appearance, canvas, description, role, sheetBelow, sheetGrabber, size, summary, }: DialogChecks): void;
|
|
26
|
+
export declare function assertDialogProps({ appearance, canvas, description, placement, role, sheetBelow, sheetGrabber, size, summary, }: DialogChecks): void;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { dialogAppearances, } from '../../css/css-dialog-banded.js';
|
|
2
|
+
import { dialogPlacements, } from '../../css/css-dialog-end.js';
|
|
2
3
|
import { themeCanvases, } from '../../css/css-canvas-grouped.js';
|
|
3
4
|
import { assertActionBarSummary, } from '../action-bar/ActionBarSummary.js';
|
|
4
5
|
export const dialogSizes = ['md', 'lg', 'xl'];
|
|
@@ -7,10 +8,11 @@ function oneOf(values, value, prop) {
|
|
|
7
8
|
if (!values.includes(value))
|
|
8
9
|
throw new RangeError(`Unknown Dialog ${prop}: ${String(value)}`);
|
|
9
10
|
}
|
|
10
|
-
export function assertDialogProps({ appearance, canvas, description, role, sheetBelow, sheetGrabber, size, summary, }) {
|
|
11
|
+
export function assertDialogProps({ appearance, canvas, description, placement, role, sheetBelow, sheetGrabber, size, summary, }) {
|
|
11
12
|
oneOf(dialogSizes, size, 'size');
|
|
12
13
|
oneOf(dialogAppearances, appearance, 'appearance');
|
|
13
14
|
oneOf(themeCanvases, canvas, 'canvas');
|
|
15
|
+
oneOf(dialogPlacements, placement, 'placement');
|
|
14
16
|
if (role !== undefined)
|
|
15
17
|
oneOf(dialogRoles, role, 'role');
|
|
16
18
|
if (description !== undefined &&
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How pasted cells are split: `tab` (what a spreadsheet copies), `comma`
|
|
3
|
+
* (a CSV line), or `auto`, tab when the text holds a tab and comma
|
|
4
|
+
* otherwise. A list whose one column may hold commas ("Smith, Jones") is
|
|
5
|
+
* parsed with `tab`, so a comma never splits it.
|
|
6
|
+
*/
|
|
7
|
+
export type PastedRowsDelimiter = 'auto' | 'tab' | 'comma';
|
|
8
|
+
export declare const pastedRowsDelimiters: readonly PastedRowsDelimiter[];
|
|
9
|
+
export type PastedRows = {
|
|
10
|
+
/** The rows in order, each padded with `''` to `columnCount` cells. */
|
|
11
|
+
rows: string[][];
|
|
12
|
+
/** The most cells any row has; 0 when nothing but blank lines was pasted. */
|
|
13
|
+
columnCount: number;
|
|
14
|
+
/** The delimiter used, `auto` resolved. */
|
|
15
|
+
delimiter: 'tab' | 'comma';
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* Splits pasted text into rows of trimmed cells: rows on line breaks
|
|
19
|
+
* (CR, LF or CRLF), cells on the delimiter. A cell that starts with a
|
|
20
|
+
* double quote and has a closing one later is quoted, as spreadsheets and
|
|
21
|
+
* CSV write a cell holding the delimiter, a line break or a quote; any
|
|
22
|
+
* other quote is a plain character. Rows whose cells are all blank are
|
|
23
|
+
* left out. It knows nothing of what the columns mean: the consumer maps
|
|
24
|
+
* and checks them. Linear in the text's length.
|
|
25
|
+
*/
|
|
26
|
+
export declare function parsePastedRows(text: string, delimiter?: PastedRowsDelimiter): PastedRows;
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
export const pastedRowsDelimiters = [
|
|
2
|
+
'auto',
|
|
3
|
+
'tab',
|
|
4
|
+
'comma',
|
|
5
|
+
];
|
|
6
|
+
function resolveDelimiter(text, delimiter) {
|
|
7
|
+
if (!pastedRowsDelimiters.includes(delimiter))
|
|
8
|
+
throw new RangeError(`Unknown PasteRows delimiter: ${String(delimiter)}`);
|
|
9
|
+
if (delimiter !== 'auto')
|
|
10
|
+
return delimiter;
|
|
11
|
+
return text.includes('\t') ? 'tab' : 'comma';
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Reads one quoted cell starting at the opening quote `start`: `""` is
|
|
15
|
+
* one quote, and delimiters and line breaks inside it are its words.
|
|
16
|
+
* Returns the words and the index after the closing quote, or `null` when
|
|
17
|
+
* no lone closing quote follows (the opening quote is then a character).
|
|
18
|
+
*/
|
|
19
|
+
function readQuoted(text, start) {
|
|
20
|
+
let words = '';
|
|
21
|
+
let index = start + 1;
|
|
22
|
+
while (index < text.length) {
|
|
23
|
+
const char = text[index];
|
|
24
|
+
if (char === '"') {
|
|
25
|
+
if (text[index + 1] === '"') {
|
|
26
|
+
words += '"';
|
|
27
|
+
index += 2;
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
return [words, index + 1];
|
|
31
|
+
}
|
|
32
|
+
words += char;
|
|
33
|
+
index += 1;
|
|
34
|
+
}
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* For each index, where a quoted cell's scan starting there would find its
|
|
39
|
+
* lone closing quote (`""` being one escaped quote), or -1 when none
|
|
40
|
+
* follows; one extra entry for the end. Built backwards in one pass, so
|
|
41
|
+
* each opening quote is checked in constant time and the parse stays
|
|
42
|
+
* linear: a run of quotes from i of odd length closes on its last quote,
|
|
43
|
+
* an even run is all escapes and the scan carries on after it.
|
|
44
|
+
*/
|
|
45
|
+
function closingQuoteTable(text) {
|
|
46
|
+
const closesFrom = new Int32Array(text.length + 1).fill(-1);
|
|
47
|
+
const runFrom = new Int32Array(text.length + 1);
|
|
48
|
+
for (let index = text.length - 1; index >= 0; index -= 1) {
|
|
49
|
+
if (text[index] !== '"') {
|
|
50
|
+
closesFrom[index] = closesFrom[index + 1];
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
const run = 1 + runFrom[index + 1];
|
|
54
|
+
runFrom[index] = run;
|
|
55
|
+
closesFrom[index] =
|
|
56
|
+
run % 2 === 1 ? index + run - 1 : closesFrom[index + run];
|
|
57
|
+
}
|
|
58
|
+
return closesFrom;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Splits pasted text into rows of trimmed cells: rows on line breaks
|
|
62
|
+
* (CR, LF or CRLF), cells on the delimiter. A cell that starts with a
|
|
63
|
+
* double quote and has a closing one later is quoted, as spreadsheets and
|
|
64
|
+
* CSV write a cell holding the delimiter, a line break or a quote; any
|
|
65
|
+
* other quote is a plain character. Rows whose cells are all blank are
|
|
66
|
+
* left out. It knows nothing of what the columns mean: the consumer maps
|
|
67
|
+
* and checks them. Linear in the text's length.
|
|
68
|
+
*/
|
|
69
|
+
export function parsePastedRows(text, delimiter = 'auto') {
|
|
70
|
+
const used = resolveDelimiter(text, delimiter);
|
|
71
|
+
const separator = used === 'tab' ? '\t' : ',';
|
|
72
|
+
const closesFrom = closingQuoteTable(text);
|
|
73
|
+
const rows = [];
|
|
74
|
+
let row = [];
|
|
75
|
+
let cell = '';
|
|
76
|
+
let cellStart = true;
|
|
77
|
+
const endCell = () => {
|
|
78
|
+
row.push(cell.trim());
|
|
79
|
+
cell = '';
|
|
80
|
+
cellStart = true;
|
|
81
|
+
};
|
|
82
|
+
const endRow = () => {
|
|
83
|
+
endCell();
|
|
84
|
+
if (row.some((value) => value !== ''))
|
|
85
|
+
rows.push(row);
|
|
86
|
+
row = [];
|
|
87
|
+
};
|
|
88
|
+
let index = 0;
|
|
89
|
+
while (index < text.length) {
|
|
90
|
+
const char = text[index];
|
|
91
|
+
const quoted = cellStart && char === '"' && closesFrom[index + 1] >= 0
|
|
92
|
+
? readQuoted(text, index)
|
|
93
|
+
: null;
|
|
94
|
+
if (quoted) {
|
|
95
|
+
cell += quoted[0];
|
|
96
|
+
index = quoted[1];
|
|
97
|
+
cellStart = false;
|
|
98
|
+
}
|
|
99
|
+
else if (char === separator) {
|
|
100
|
+
endCell();
|
|
101
|
+
index += 1;
|
|
102
|
+
}
|
|
103
|
+
else if (char === '\r' || char === '\n') {
|
|
104
|
+
endRow();
|
|
105
|
+
index += char === '\r' && text[index + 1] === '\n' ? 2 : 1;
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
// Spaces before a quoted cell's opening quote do not unquote it.
|
|
109
|
+
if (!(cellStart && char === ' ')) {
|
|
110
|
+
cell += char;
|
|
111
|
+
cellStart = false;
|
|
112
|
+
}
|
|
113
|
+
index += 1;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
if (cell !== '' || row.length > 0)
|
|
117
|
+
endRow();
|
|
118
|
+
const columnCount = rows.reduce((most, cells) => Math.max(most, cells.length), 0);
|
|
119
|
+
for (const cells of rows)
|
|
120
|
+
while (cells.length < columnCount)
|
|
121
|
+
cells.push('');
|
|
122
|
+
return { rows, columnCount, delimiter: used };
|
|
123
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/** `fixed` sizes the count for a few digits; `full` fills its container, such as a table cell. */
|
|
2
|
+
export type StepperWidth = 'fixed' | 'full';
|
|
3
|
+
export declare const stepperLimitAppearances: readonly ["faded", "plain"];
|
|
4
|
+
/**
|
|
5
|
+
* How − looks at `min` and + at `max`, where a press cannot move the count:
|
|
6
|
+
* `faded` (the default) fades the button as any disabled control; `plain`
|
|
7
|
+
* keeps its usual look. Either way the button is disabled.
|
|
8
|
+
*/
|
|
9
|
+
export type StepperLimitAppearance = (typeof stepperLimitAppearances)[number];
|
|
10
|
+
export type StepperProps = {
|
|
11
|
+
/** Names the count. Keep it visible unless headers around it say it (`labelVisuallyHidden`). */
|
|
12
|
+
label: string;
|
|
13
|
+
labelVisuallyHidden?: boolean;
|
|
14
|
+
/** The count, or `null` for an empty box (none). */
|
|
15
|
+
value: number | null;
|
|
16
|
+
/** A whole count, or `null` when the box is emptied. Typed counts are reported as typed, even outside the bounds. */
|
|
17
|
+
onChange: (value: number | null) => void;
|
|
18
|
+
/**
|
|
19
|
+
* The finished count, once per entry, for a page that saves as you go:
|
|
20
|
+
* on blur, on Enter, and `commitDelay` ms after the last − / + press or
|
|
21
|
+
* step key (ArrowUp, ArrowDown, Home, End). Never per typed digit, and
|
|
22
|
+
* never when the count is the one last committed (or held before the
|
|
23
|
+
* first edit). It gives the count you hold then, `null` or outside the
|
|
24
|
+
* bounds included, as `onChange` reports typed counts. `onChange` is
|
|
25
|
+
* unchanged.
|
|
26
|
+
*/
|
|
27
|
+
onCommit?: (value: number | null) => void;
|
|
28
|
+
/** How long − / + presses and step keys settle before `onCommit`, in ms; defaults to 600. */
|
|
29
|
+
commitDelay?: number;
|
|
30
|
+
/** The lowest count − reaches; defaults to 0. */
|
|
31
|
+
min?: number;
|
|
32
|
+
/** The highest count + reaches; no upper bound when left out. */
|
|
33
|
+
max?: number;
|
|
34
|
+
/** How much one press, or one arrow key, moves the count; defaults to 1. */
|
|
35
|
+
step?: number;
|
|
36
|
+
/** The − button's name, such as "One less". */
|
|
37
|
+
decrementLabel: string;
|
|
38
|
+
/** The + button's name, such as "One more". */
|
|
39
|
+
incrementLabel: string;
|
|
40
|
+
/**
|
|
41
|
+
* A message under the control, as `Field`'s: a polite live region linked by
|
|
42
|
+
* `aria-describedby`, with `aria-invalid` and the danger border. An empty
|
|
43
|
+
* string is no error.
|
|
44
|
+
*/
|
|
45
|
+
error?: string;
|
|
46
|
+
/** `Field`'s changed look (the accent border and tint) for a count someone changed, or a suggestion. `error` wins. */
|
|
47
|
+
changed?: boolean;
|
|
48
|
+
disabled?: boolean;
|
|
49
|
+
/** Readable and focusable, never changed: the buttons are hidden and keep their room. */
|
|
50
|
+
readOnly?: boolean;
|
|
51
|
+
/**
|
|
52
|
+
* Says the count in words for assistive technology (`aria-valuetext`),
|
|
53
|
+
* such as "12 straps". It wins over the count and `unit` read together.
|
|
54
|
+
*/
|
|
55
|
+
valueText?: string;
|
|
56
|
+
/**
|
|
57
|
+
* A short muted word after the count, inside its frame, such as "min".
|
|
58
|
+
* Without `valueText` the count is read with it ("90 min"), word for
|
|
59
|
+
* word: a unit that changes with the count ("players") reads "1 players",
|
|
60
|
+
* so pass `valueText` with the right form ("1 player"). Not blank.
|
|
61
|
+
*/
|
|
62
|
+
unit?: string;
|
|
63
|
+
/**
|
|
64
|
+
* Ids of elements that describe the count, such as the value it adds up
|
|
65
|
+
* to; read before an `error`'s message.
|
|
66
|
+
*/
|
|
67
|
+
'aria-describedby'?: string;
|
|
68
|
+
/** Shown in an empty box, such as "0". */
|
|
69
|
+
placeholder?: string;
|
|
70
|
+
name?: string;
|
|
71
|
+
width?: StepperWidth;
|
|
72
|
+
/**
|
|
73
|
+
* `plain` keeps − at `min` and + at `max` in their usual look instead of
|
|
74
|
+
* fading them (`faded`, the default). The button is still disabled, and a
|
|
75
|
+
* disabled or read-only Stepper is unchanged.
|
|
76
|
+
*/
|
|
77
|
+
limitAppearance?: StepperLimitAppearance;
|
|
78
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const stepperLimitAppearances = ['faded', 'plain'];
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/** How long after the last − / + press or arrow key a count commits, in ms. */
|
|
2
|
+
export declare const defaultStepperCommitDelay = 600;
|
|
3
|
+
/** Refuses a commit delay that is not a finite number of zero or more ms. */
|
|
4
|
+
export declare function assertStepperCommitDelay(delay: number): void;
|
|
5
|
+
/** What a Stepper calls as its count is edited and finished. */
|
|
6
|
+
export type StepperCommit = {
|
|
7
|
+
/** A typed change: it waits for blur or Enter. */
|
|
8
|
+
edited: () => void;
|
|
9
|
+
/** A − / + press or a step key: it commits once the presses settle. */
|
|
10
|
+
stepped: () => void;
|
|
11
|
+
/** Blur or Enter: commits now. */
|
|
12
|
+
finish: () => void;
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* Turns a Stepper's edits into one `onCommit` per finished entry: on blur,
|
|
16
|
+
* on Enter, and `delay` ms after the last − / + press or step key. A
|
|
17
|
+
* commit gives the count the consumer holds then, and only when it differs
|
|
18
|
+
* from the last committed count (or the count before the first edit). A
|
|
19
|
+
* count the consumer changes while nothing is waiting becomes the new
|
|
20
|
+
* baseline. A commit still waiting when the Stepper unmounts is made then.
|
|
21
|
+
*/
|
|
22
|
+
export declare function useStepperCommit(value: number | null, onCommit: ((value: number | null) => void) | undefined, delay: number): StepperCommit;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { useEffect, useRef } from 'react';
|
|
2
|
+
/** How long after the last − / + press or arrow key a count commits, in ms. */
|
|
3
|
+
export const defaultStepperCommitDelay = 600;
|
|
4
|
+
/** Refuses a commit delay that is not a finite number of zero or more ms. */
|
|
5
|
+
export function assertStepperCommitDelay(delay) {
|
|
6
|
+
if (!Number.isFinite(delay) || delay < 0)
|
|
7
|
+
throw new RangeError('Stepper commitDelay must be a finite number of zero or more');
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Turns a Stepper's edits into one `onCommit` per finished entry: on blur,
|
|
11
|
+
* on Enter, and `delay` ms after the last − / + press or step key. A
|
|
12
|
+
* commit gives the count the consumer holds then, and only when it differs
|
|
13
|
+
* from the last committed count (or the count before the first edit). A
|
|
14
|
+
* count the consumer changes while nothing is waiting becomes the new
|
|
15
|
+
* baseline. A commit still waiting when the Stepper unmounts is made then.
|
|
16
|
+
*/
|
|
17
|
+
export function useStepperCommit(value, onCommit, delay) {
|
|
18
|
+
const latest = useRef({ value, onCommit });
|
|
19
|
+
const baseline = useRef(value);
|
|
20
|
+
const pending = useRef(false);
|
|
21
|
+
const timer = useRef(null);
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
latest.current = { value, onCommit };
|
|
24
|
+
if (!pending.current)
|
|
25
|
+
baseline.current = value;
|
|
26
|
+
}, [value, onCommit]);
|
|
27
|
+
function clearTimer() {
|
|
28
|
+
if (timer.current === null)
|
|
29
|
+
return;
|
|
30
|
+
clearTimeout(timer.current);
|
|
31
|
+
timer.current = null;
|
|
32
|
+
}
|
|
33
|
+
function finish() {
|
|
34
|
+
clearTimer();
|
|
35
|
+
if (!pending.current)
|
|
36
|
+
return;
|
|
37
|
+
pending.current = false;
|
|
38
|
+
const { value: current, onCommit: commit } = latest.current;
|
|
39
|
+
if (current === baseline.current)
|
|
40
|
+
return;
|
|
41
|
+
baseline.current = current;
|
|
42
|
+
commit?.(current);
|
|
43
|
+
}
|
|
44
|
+
// The latest finish, for the timer and the unmount to call.
|
|
45
|
+
const finishRef = useRef(finish);
|
|
46
|
+
useEffect(() => {
|
|
47
|
+
finishRef.current = finish;
|
|
48
|
+
});
|
|
49
|
+
useEffect(() => () => finishRef.current(), []);
|
|
50
|
+
function edited() {
|
|
51
|
+
if (latest.current.onCommit === undefined)
|
|
52
|
+
return;
|
|
53
|
+
pending.current = true;
|
|
54
|
+
}
|
|
55
|
+
function stepped() {
|
|
56
|
+
if (latest.current.onCommit === undefined)
|
|
57
|
+
return;
|
|
58
|
+
pending.current = true;
|
|
59
|
+
clearTimer();
|
|
60
|
+
timer.current = setTimeout(() => {
|
|
61
|
+
timer.current = null;
|
|
62
|
+
finishRef.current();
|
|
63
|
+
}, delay);
|
|
64
|
+
}
|
|
65
|
+
return { edited, stepped, finish };
|
|
66
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export type TableSelectCellProps = {
|
|
2
|
+
/**
|
|
3
|
+
* The checkbox's name, not drawn: "Select Mallard" in a row, "Select
|
|
4
|
+
* all" in the header. Name the row by the words the row shows.
|
|
5
|
+
*/
|
|
6
|
+
label: string;
|
|
7
|
+
checked: boolean;
|
|
8
|
+
/** The mixed state, for a header's "Select all" while some rows are chosen. */
|
|
9
|
+
indeterminate?: boolean;
|
|
10
|
+
onCheckedChange: (checked: boolean) => void;
|
|
11
|
+
disabled?: boolean;
|
|
12
|
+
/**
|
|
13
|
+
* `th` in a `TableHeader` row (the column's header cell, `scope="col"`),
|
|
14
|
+
* `td` (the default) in a body row. A fixed table sizes its columns from
|
|
15
|
+
* the header row, so put one in the header too.
|
|
16
|
+
*/
|
|
17
|
+
as?: 'td' | 'th';
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* A table's select column: a cell holding one `Checkbox` whose label is
|
|
21
|
+
* visually hidden. The column is as narrow as the box, which is its own
|
|
22
|
+
* target, at least 44 × 44 px on a coarse pointer. Pair a chosen row with
|
|
23
|
+
* `TableRow selected`; a press on the box is left alone by a
|
|
24
|
+
* `TableRow pressable`.
|
|
25
|
+
*/
|
|
26
|
+
export declare const TableSelectCell: import("react").ForwardRefExoticComponent<TableSelectCellProps & import("react").RefAttributes<HTMLTableCellElement>>;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { forwardRef } from 'react';
|
|
3
|
+
import { tableSelectCellClass } from '../../css/css-table-select.js';
|
|
4
|
+
import { Checkbox } from '../Checkbox.js';
|
|
5
|
+
/**
|
|
6
|
+
* A table's select column: a cell holding one `Checkbox` whose label is
|
|
7
|
+
* visually hidden. The column is as narrow as the box, which is its own
|
|
8
|
+
* target, at least 44 × 44 px on a coarse pointer. Pair a chosen row with
|
|
9
|
+
* `TableRow selected`; a press on the box is left alone by a
|
|
10
|
+
* `TableRow pressable`.
|
|
11
|
+
*/
|
|
12
|
+
export const TableSelectCell = forwardRef(function TableSelectCell({ as = 'td', checked, disabled = false, indeterminate = false, label, onCheckedChange, }, ref) {
|
|
13
|
+
if (typeof label !== 'string' || !label.trim())
|
|
14
|
+
throw new RangeError('TableSelectCell label must not be empty');
|
|
15
|
+
if (as !== 'td' && as !== 'th')
|
|
16
|
+
throw new RangeError(`Unknown TableSelectCell as: ${String(as)}`);
|
|
17
|
+
const Component = as;
|
|
18
|
+
return (_jsx(Component, { ref: ref, className: tableSelectCellClass, scope: as === 'th' ? 'col' : undefined, children: _jsx(Checkbox, { checked: checked, disabled: disabled, indeterminate: indeterminate, label: label, labelVisuallyHidden: true, onCheckedChange: onCheckedChange }) }));
|
|
19
|
+
});
|
|
@@ -6,8 +6,11 @@
|
|
|
6
6
|
*/
|
|
7
7
|
export declare const boxLinkAppearances: readonly ["accent", "quiet"];
|
|
8
8
|
export type BoxLinkAppearance = (typeof boxLinkAppearances)[number];
|
|
9
|
+
/** The decorative new-tab mark after an external link's words. */
|
|
10
|
+
export declare const externalLinkMarkClass = "sw-link-external-mark";
|
|
9
11
|
export declare function assertBoxLink(link: unknown, as: string): void;
|
|
10
12
|
/** The generated class for a Box `link` prop, or none for the canvas link. */
|
|
11
13
|
export declare function boxLinkClass(link: BoxLinkAppearance | undefined): string | undefined;
|
|
12
14
|
export declare function cssBoxLinkClasses(): string;
|
|
15
|
+
export declare function cssExternalLinkMarkClasses(): string;
|
|
13
16
|
export declare function boxLinkClassCatalog(): string[];
|
package/dist/css/css-box-link.js
CHANGED
|
@@ -7,6 +7,8 @@ import { typographyVariants } from '@scalewing/tokens';
|
|
|
7
7
|
*/
|
|
8
8
|
export const boxLinkAppearances = ['accent', 'quiet'];
|
|
9
9
|
const quietLinkClass = 'sw-link-quiet';
|
|
10
|
+
/** The decorative new-tab mark after an external link's words. */
|
|
11
|
+
export const externalLinkMarkClass = 'sw-link-external-mark';
|
|
10
12
|
export function assertBoxLink(link, as) {
|
|
11
13
|
if (link === undefined)
|
|
12
14
|
return;
|
|
@@ -43,8 +45,45 @@ ${quiet}:hover,
|
|
|
43
45
|
${quiet}:focus-visible {
|
|
44
46
|
color: inherit;
|
|
45
47
|
text-decoration-line: underline;
|
|
46
|
-
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
${cssExternalLinkMarkClasses()}`;
|
|
51
|
+
}
|
|
52
|
+
/*
|
|
53
|
+
* An external link's new-tab mark: an arrow to the inline end and up (↗),
|
|
54
|
+
* drawn with borders in the link's colour so forced colours keep it as the
|
|
55
|
+
* system link colour. Its head is the top and inline-end borders of a
|
|
56
|
+
* space-2 square; its shaft is one border on the square's diagonal. It
|
|
57
|
+
* follows the link's colour (accent, the hover text colour, a quiet link's
|
|
58
|
+
* inherited one) and is mirrored in right-to-left text. It sits a space-1
|
|
59
|
+
* after the last word with no space between them, so it never wraps alone.
|
|
60
|
+
*/
|
|
61
|
+
export function cssExternalLinkMarkClasses() {
|
|
62
|
+
const mark = `.${externalLinkMarkClass}`;
|
|
63
|
+
return `${mark} {
|
|
64
|
+
border-right: 2px solid currentColor;
|
|
65
|
+
border-top: 2px solid currentColor;
|
|
66
|
+
box-sizing: border-box;
|
|
67
|
+
display: inline-block;
|
|
68
|
+
height: var(--sw-space-2);
|
|
69
|
+
margin-inline-start: var(--sw-space-1);
|
|
70
|
+
position: relative;
|
|
71
|
+
vertical-align: middle;
|
|
72
|
+
width: var(--sw-space-2);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
${mark}::after {
|
|
76
|
+
border-left: 2px solid currentColor;
|
|
77
|
+
content: '';
|
|
78
|
+
height: calc(var(--sw-space-2) * 1.25);
|
|
79
|
+
left: 50%;
|
|
80
|
+
position: absolute;
|
|
81
|
+
top: 50%;
|
|
82
|
+
transform: translate(-50%, -50%) rotate(45deg);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
${mark}:dir(rtl) { transform: scaleX(-1); }`;
|
|
47
86
|
}
|
|
48
87
|
export function boxLinkClassCatalog() {
|
|
49
|
-
return [quietLinkClass];
|
|
88
|
+
return [quietLinkClass, externalLinkMarkClass];
|
|
50
89
|
}
|
package/dist/css/css-checkbox.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { typographyVariants } from '@scalewing/tokens';
|
|
2
2
|
import { checkStrokeIn } from './chevron.js';
|
|
3
|
+
import { coarsePointerQuery, touchTarget } from './touch-target.js';
|
|
3
4
|
const label = typographyVariants.label;
|
|
4
5
|
const caption = typographyVariants.caption;
|
|
5
6
|
export function cssCheckboxClasses() {
|
|
@@ -68,6 +69,38 @@ export function cssCheckboxClasses() {
|
|
|
68
69
|
|
|
69
70
|
.sw-checkbox-input:checked + .sw-checkbox-mark::after { display: block; }
|
|
70
71
|
|
|
72
|
+
/* The mixed state: the accent fill with a dash in place of the check. */
|
|
73
|
+
.sw-checkbox-input:indeterminate + .sw-checkbox-mark {
|
|
74
|
+
background: var(--sw-color-accent);
|
|
75
|
+
border-color: var(--sw-color-accent);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.sw-checkbox-input:indeterminate + .sw-checkbox-mark::after {
|
|
79
|
+
border-right: 0;
|
|
80
|
+
display: block;
|
|
81
|
+
height: 0;
|
|
82
|
+
inset-block-start: calc(50% - 1px);
|
|
83
|
+
inset-inline-start: calc(50% - var(--sw-space-3) / 2);
|
|
84
|
+
transform: none;
|
|
85
|
+
width: var(--sw-space-3);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/* A visually hidden label: the box alone is the target, its own size on a
|
|
89
|
+
fine pointer (so a compact table row keeps its height), the 44 px touch
|
|
90
|
+
target both ways on a coarse one. */
|
|
91
|
+
.sw-checkbox-label-hidden {
|
|
92
|
+
justify-content: center;
|
|
93
|
+
min-height: 0;
|
|
94
|
+
position: relative;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
@media ${coarsePointerQuery} {
|
|
98
|
+
.sw-checkbox-label-hidden {
|
|
99
|
+
min-height: ${touchTarget};
|
|
100
|
+
min-width: ${touchTarget};
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
71
104
|
.sw-checkbox-input:focus-visible + .sw-checkbox-mark {
|
|
72
105
|
outline: var(--sw-focus-ring-width) solid var(--sw-color-accent);
|
|
73
106
|
outline-offset: var(--sw-focus-ring-offset);
|
|
@@ -104,13 +137,13 @@ export function cssCheckboxClasses() {
|
|
|
104
137
|
|
|
105
138
|
@media (prefers-reduced-transparency: reduce) {
|
|
106
139
|
.sw-checkbox-mark { background: var(--sw-color-surface); }
|
|
107
|
-
.sw-checkbox-input:checked + .sw-checkbox-mark { background: var(--sw-color-accent); }
|
|
140
|
+
.sw-checkbox-input:is(:checked, :indeterminate) + .sw-checkbox-mark { background: var(--sw-color-accent); }
|
|
108
141
|
}
|
|
109
142
|
|
|
110
143
|
@media (forced-colors: active) {
|
|
111
144
|
.sw-checkbox-mark { background: Canvas; border-color: CanvasText; }
|
|
112
|
-
.sw-checkbox-input:checked + .sw-checkbox-mark { background: Highlight; border-color: Highlight; }
|
|
113
|
-
.sw-checkbox-input:checked + .sw-checkbox-mark::after { border-color: HighlightText; }
|
|
145
|
+
.sw-checkbox-input:is(:checked, :indeterminate) + .sw-checkbox-mark { background: Highlight; border-color: Highlight; }
|
|
146
|
+
.sw-checkbox-input:is(:checked, :indeterminate) + .sw-checkbox-mark::after { border-color: HighlightText; }
|
|
114
147
|
.sw-checkbox-input[aria-invalid='true'] + .sw-checkbox-mark { border-color: CanvasText; }
|
|
115
148
|
}`;
|
|
116
149
|
}
|
|
@@ -118,6 +151,7 @@ export function checkboxClassCatalog() {
|
|
|
118
151
|
return [
|
|
119
152
|
'sw-checkbox',
|
|
120
153
|
'sw-checkbox-label',
|
|
154
|
+
'sw-checkbox-label-hidden',
|
|
121
155
|
'sw-checkbox-control',
|
|
122
156
|
'sw-checkbox-input',
|
|
123
157
|
'sw-checkbox-mark',
|
|
@@ -35,6 +35,7 @@ import { cssDialogBandedClasses } from './css-dialog-banded.js';
|
|
|
35
35
|
import { cssDialogFrameClasses } from './css-dialog-frame.js';
|
|
36
36
|
import { cssDialogCanvasClasses } from './css-dialog-canvas.js';
|
|
37
37
|
import { cssDialogGrabberClasses } from './css-dialog-grabber.js';
|
|
38
|
+
import { cssDialogEndClasses } from './css-dialog-end.js';
|
|
38
39
|
import { cssComboboxClasses } from './css-combobox.js';
|
|
39
40
|
import { cssSelectClasses } from './css-select.js';
|
|
40
41
|
import { cssMultiSelectClasses } from './css-multi-select.js';
|
|
@@ -224,6 +225,8 @@ ${cssDialogCanvasClasses()}
|
|
|
224
225
|
|
|
225
226
|
${cssDialogGrabberClasses()}
|
|
226
227
|
|
|
228
|
+
${cssDialogEndClasses()}
|
|
229
|
+
|
|
227
230
|
${cssAccordionClasses()}
|
|
228
231
|
|
|
229
232
|
${cssSelectClasses()}
|