@scalewing/react 1.33.1 → 1.35.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +17 -0
- package/README.md +17 -0
- package/dist/components/Table.d.ts +16 -8
- package/dist/components/Table.js +3 -2
- package/dist/components/table/TableCell.d.ts +9 -0
- package/dist/components/table/TableCell.js +12 -2
- package/dist/components/table/TableRow.d.ts +30 -0
- package/dist/components/table/TableRow.js +24 -3
- package/dist/components/table/row-press.d.ts +17 -0
- package/dist/components/table/row-press.js +60 -0
- package/dist/components/table/table-layout.d.ts +6 -0
- package/dist/components/table/table-layout.js +4 -0
- package/dist/css/css-table-footer.js +3 -11
- package/dist/css/css-table-page-pin.d.ts +2 -0
- package/dist/css/css-table-page-pin.js +22 -0
- package/dist/css/css-table-press.d.ts +2 -0
- package/dist/css/css-table-press.js +87 -0
- package/dist/css/css-table.js +8 -0
- package/dist/index.d.ts +1 -1
- package/dist/styles.css +61 -4
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# @scalewing/react
|
|
2
2
|
|
|
3
|
+
## 1.35.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 287d933: `Table` `stickyHeader="page"` pins the header row to the page's top while the page scrolls, without a sticky `TableFooter` (`docs/requests/teisoro-table-sticky-page.md`, Teisoro F-011 task 2180). While the table fits its width its region stops being a scroll box, as it already did for a sticky footer; a table wider than its region scrolls sideways in it and pins inside it. `true` (the default) and `false` are unchanged; any other value (including an untyped `null` or `0`, which used to mean not sticky) throws a `RangeError`. New type `TableStickyHeader` and generated class `sw-table-sticky-page`. No new tokens, no new dependency.
|
|
8
|
+
|
|
9
|
+
## 1.34.0
|
|
10
|
+
|
|
11
|
+
### Minor Changes
|
|
12
|
+
|
|
13
|
+
- 7e4cbb2: A whole-row press for a table row's one action, such as a search result that is picked (`docs/requests/teisoro-table-row-press.md`, Teisoro F-011 task 2185):
|
|
14
|
+
|
|
15
|
+
- `TableRow pressable` (opt-in): a plain primary pointer press anywhere on the row activates the row's first link or button. A press with a modifier key, on another control in the row, already handled, or ending a text selection is left alone, as is a row whose action is disabled. The row is not a control (no role, no tab stop): keyboard and screen reader users use the action itself. The row takes the pointer cursor and the quiet fill while hovered, pressed, or while its action has keyboard focus (an outline in `Highlight` under forced colours), and underlines its action on hover.
|
|
16
|
+
- `TableCell onPress` (a body cell or a row header): the cell's children become a button in the cell's own text style at weight 600, underlined on hover and focus, for the row's action. A non-function value or one on a sortable header throws a `TypeError`.
|
|
17
|
+
|
|
18
|
+
New generated classes `sw-table-row-press` and `sw-table-action`. No new tokens, no new dependency.
|
|
19
|
+
|
|
3
20
|
## 1.33.1
|
|
4
21
|
|
|
5
22
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -403,10 +403,27 @@ An option's optional `description` (a `ReactNode`) is secondary text for that op
|
|
|
403
403
|
|
|
404
404
|
`Table` aligns data in rows inside its own keyboard-reachable scroll region, named after the table. While the table is wider than the region, each inline edge with columns scrolled out past it draws a soft inset shade (`sw-scroll-more-start`, `sw-scroll-more-end`, mirrored right to left), so a phone, whose scrollbars stay hidden, still shows that the table goes on; a table that fits draws none, and in forced colors the system scrollbar is the cue. `TableCell` takes `numeric` (tabular numerals, end aligned), `align` and `truncate`; `density="compact"` densifies cells. `TableRow selected` sets `aria-selected` and marks the row with a 4 px accent bar at its inline start (3:1 or more against the surface and the page), the right edge in a right-to-left table. The bar is drawn inside the first cell's padding and takes no layout space, so no column moves when a row is picked, and the row takes no fill, so text, muted and accent text keep their contrast; forced colors keep the bar in the system highlight.
|
|
405
405
|
|
|
406
|
+
`TableRow pressable` (opt-in) makes the whole row the pointer target of its one action, such as a search result that is picked: a plain primary press anywhere on it calls `click()` on the row's first link or button, so the action's own handler runs. Left alone: a modifier key or non-primary button, a press already handled, a press on another control, a press that ends a text selection (text stays selectable), and a disabled or missing action. Keep one control in the row: a control placed before the action (an icon button, say) would take the row's press. The row is not a control (no role, no tab stop), so keyboard and screen reader users use the action itself. It takes the pointer cursor and the quiet fill while hovered, pressed (a brief flash on touch) or while its action has keyboard focus (a `Highlight` outline in forced colours), and underlines its action on hover (`sw-table-row-press`). A consumer `onClick` runs once per press. Each click of a double click presses the action, as a native button's would, so keep it safe to repeat. `TableCell onPress` (a body cell or a row header in `TableBody`) makes the cell's children that action: a button in the cell's own text style at weight 600, underlined on hover and focus, with the accent focus ring (`sw-table-action`); a non-function value or one on a sortable header throws a `TypeError`.
|
|
407
|
+
|
|
408
|
+
```tsx
|
|
409
|
+
<TableRow pressable>
|
|
410
|
+
<TableCell onPress={() => pick('Red fox')}>Red fox</TableCell>
|
|
411
|
+
<TableCell>Forest</TableCell>
|
|
412
|
+
</TableRow>
|
|
413
|
+
```
|
|
414
|
+
|
|
406
415
|
A header `TableCell` (`as="th"`) with `sort` (`'ascending'`, `'descending'` or `'none'`) and `onSort` is its column's sort control: its children become a button in the header's own style with a chevron (before the name in a `numeric` or end-aligned column), and the cell carries `aria-sort` only while it is the sorted column. `sort` on a data cell, or one of the two without the other, throws a `TypeError`. `Table` `layout="fixed"` (default `auto`) sizes the columns from the header row alone, so a filter that hides a row moves no column, and `verticalAlign="top"` (default `middle`) starts every cell of a tall row on its first line. `TableCell` `width` is `min` (an auto table's column takes the least room its content allows, on one line; set it on each cell of the column) or a size, `xs` to `xl` (4, 6, 8, 12 and 16 rem); in a fixed table a column without a width shares what is left, so leave one column, such as a description, unsized. The HTML `width` attribute is not accepted. Unknown values throw a `RangeError`.
|
|
407
416
|
|
|
408
417
|
A ledger read across as well as down takes `Table` `columnRules` (a hairline at the inline start of every cell after a row's first, mirrored right to left) and `TableCell` `tone` (`success`, `danger`, `muted` or `accent`: the cell's text in the theme colour, as `Text color` uses it; never the only signal, so keep the sign or the words). `TableRow` `variant="sub"` (default `"default"`) sets a row under the one above, as a batch's items: its row header, a `TableCell as="th" scope="row"`, is indented a `space-6` step with a guide bar at the indent (a sub row without a row header is not indented). `TableRow` `struck` keeps a voided row on the record: the subtle fill and the text muted and struck through, toned cells included; it works with either variant. A line-through is not read aloud, so say why in the row, such as a `Badge` "Voided": a Badge in a line of text is not struck, but one that is itself a flex item (a direct child of `Inline`) is, so put it in the title's line. A row header in the body or footer (`th scope="row"`) reads as body text. An unknown `tone` or `variant` throws a `RangeError`.
|
|
409
418
|
|
|
419
|
+
`Table` `stickyHeader` pins the header row: `true` (default) to the top of the table's own scroll region, `false` lets it scroll away, and `'page'` pins it to the page's top as the page scrolls. With `'page'`, while the table fits its width its region stops being a scroll box (`sw-table-sticky-page`), so the header stays in view under any sticky `AppHeader` (which covers it); a table wider than its region scrolls sideways in it and pins inside it. Use it for a long table whose last row, such as a non-sticky `TableFooter` total, should scroll with the rows. An unknown value throws a `RangeError`.
|
|
420
|
+
|
|
421
|
+
```tsx
|
|
422
|
+
<Table aria-label="Site census" stickyHeader="page">
|
|
423
|
+
…
|
|
424
|
+
</Table>
|
|
425
|
+
```
|
|
426
|
+
|
|
410
427
|
`TableFooter` is the table's `tfoot`, for a total: its cells are set at weight 600 under a 2 px rule across its first row (drawn on each cell's `::before`, so forced colors keep it), and a `TableCell as="th" scope="row"` in it names the total. `sticky` pins the footer to the bottom edge on the header's glass (solid under Reduce Transparency, the system canvas in forced colors), on its own layer over the cells and the pinned header. While the table fits its width, a table with a sticky footer stops being a scroll box, so the footer pins to the page's bottom and the sticky header to its top as the page scrolls; a sticky `AppHeader` covers that header. A table wider than its region scrolls sideways in it and pins to the region instead. A sticky `ActionBar` covers a pinned footer, and so does a `Dialog`'s pinned footer, so do not pair them.
|
|
411
428
|
|
|
412
429
|
`BarChart` `formatValue` writes the axis's values and each value without its own `valueLabel`, such as a currency. `diverging` puts zero in the middle of each track behind a hairline: a positive bar grows toward the inline end and a negative one toward the start, on one scale, and the axis reads minus the maximum, zero and the maximum. Without it every bar grows from the start. An item's `tone` (`BarChartTone`: `'accent'`, `'success'`, `'warning'` or `'danger'`, listed in the exported `barChartTones`) colors its bar by what it means in place of its sign's default (the accent, or danger for a negative value), such as a diverging chart's overage in `'warning'` and shortage in `'danger'`; a toned bar keeps its sign's place, every tone keeps 3:1 against the track, and an unknown tone throws a `RangeError`. Forced colors draw every bar in the system text color, so the value and its side of zero carry the meaning there.
|
|
@@ -1,19 +1,23 @@
|
|
|
1
1
|
import { type HTMLAttributes } from 'react';
|
|
2
|
-
import { type TableLayout, type TableVerticalAlign } from './table/table-layout.js';
|
|
2
|
+
import { type TableLayout, type TableStickyHeader, type TableVerticalAlign } from './table/table-layout.js';
|
|
3
3
|
export { TableCell, type TableCellProps, type TableColumnWidth, type TableSort, } from './table/TableCell.js';
|
|
4
4
|
export { TableRow, type TableRowProps, type TableRowVariant, } from './table/TableRow.js';
|
|
5
5
|
export type { TableCellTone } from '../css/css-table-ledger.js';
|
|
6
6
|
export { tableCellTones, tableRowVariants } from '../css/css-table-ledger.js';
|
|
7
|
-
export type { TableLayout, TableVerticalAlign };
|
|
7
|
+
export type { TableLayout, TableStickyHeader, TableVerticalAlign };
|
|
8
8
|
export type TableDensity = 'comfortable' | 'compact';
|
|
9
9
|
export type TableProps = HTMLAttributes<HTMLTableElement> & {
|
|
10
10
|
density?: TableDensity;
|
|
11
11
|
/**
|
|
12
12
|
* Pins the header row to the top of the table's own scroll region
|
|
13
|
-
* (default).
|
|
14
|
-
*
|
|
13
|
+
* (`true`, default). `'page'` pins it to the page's top while the page
|
|
14
|
+
* scrolls: while the table fits its width its region stops being a
|
|
15
|
+
* scroll box (under any sticky AppHeader, which covers it); a table
|
|
16
|
+
* wider than its region scrolls sideways in it and pins to the region.
|
|
17
|
+
* A table with a sticky `TableFooter` that fits its width pins its
|
|
18
|
+
* header to the page with `true` too. `false` lets it scroll away.
|
|
15
19
|
*/
|
|
16
|
-
stickyHeader?:
|
|
20
|
+
stickyHeader?: TableStickyHeader;
|
|
17
21
|
/**
|
|
18
22
|
* `auto` (default) sizes each column by its content. `fixed` sizes the
|
|
19
23
|
* columns from the header row's `width`s alone (`table-layout: fixed`),
|
|
@@ -42,10 +46,14 @@ export declare const Table: import("react").ForwardRefExoticComponent<HTMLAttrib
|
|
|
42
46
|
density?: TableDensity;
|
|
43
47
|
/**
|
|
44
48
|
* Pins the header row to the top of the table's own scroll region
|
|
45
|
-
* (default).
|
|
46
|
-
*
|
|
49
|
+
* (`true`, default). `'page'` pins it to the page's top while the page
|
|
50
|
+
* scrolls: while the table fits its width its region stops being a
|
|
51
|
+
* scroll box (under any sticky AppHeader, which covers it); a table
|
|
52
|
+
* wider than its region scrolls sideways in it and pins to the region.
|
|
53
|
+
* A table with a sticky `TableFooter` that fits its width pins its
|
|
54
|
+
* header to the page with `true` too. `false` lets it scroll away.
|
|
47
55
|
*/
|
|
48
|
-
stickyHeader?:
|
|
56
|
+
stickyHeader?: TableStickyHeader;
|
|
49
57
|
/**
|
|
50
58
|
* `auto` (default) sizes each column by its content. `fixed` sizes the
|
|
51
59
|
* columns from the header row's `width`s alone (`table-layout: fixed`),
|
package/dist/components/Table.js
CHANGED
|
@@ -2,15 +2,16 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
2
2
|
import { forwardRef } from 'react';
|
|
3
3
|
import { cx } from '../class-names.js';
|
|
4
4
|
import { ScrollRegion } from './ScrollRegion.js';
|
|
5
|
-
import { assertTableLayout, assertTableVerticalAlign, } from './table/table-layout.js';
|
|
5
|
+
import { assertTableLayout, assertTableStickyHeader, assertTableVerticalAlign, } from './table/table-layout.js';
|
|
6
6
|
export { TableCell, } from './table/TableCell.js';
|
|
7
7
|
export { TableRow, } from './table/TableRow.js';
|
|
8
8
|
export { tableCellTones, tableRowVariants } from '../css/css-table-ledger.js';
|
|
9
9
|
/** A wide table scrolls inside its own `ScrollRegion`, named after the table. */
|
|
10
10
|
export const Table = forwardRef(function Table({ children, className, columnRules = false, density = 'comfortable', layout = 'auto', stickyHeader = true, verticalAlign = 'middle', ...rest }, ref) {
|
|
11
11
|
assertTableLayout(layout);
|
|
12
|
+
assertTableStickyHeader(stickyHeader);
|
|
12
13
|
assertTableVerticalAlign(verticalAlign);
|
|
13
|
-
return (_jsx(ScrollRegion, { "aria-label": rest['aria-label'], "aria-labelledby": rest['aria-labelledby'], className: "sw-table-wrap", markFits: true, children: _jsx("table", { ref: ref, className: cx('sw-table', density === 'compact' && 'sw-table-compact', layout === 'fixed' && 'sw-table-fixed', stickyHeader && 'sw-table-sticky', verticalAlign === 'top' && 'sw-table-top', columnRules && 'sw-table-column-rules', className), ...rest, children: children }) }));
|
|
14
|
+
return (_jsx(ScrollRegion, { "aria-label": rest['aria-label'], "aria-labelledby": rest['aria-labelledby'], className: "sw-table-wrap", markFits: true, children: _jsx("table", { ref: ref, className: cx('sw-table', density === 'compact' && 'sw-table-compact', layout === 'fixed' && 'sw-table-fixed', stickyHeader !== false && 'sw-table-sticky', stickyHeader === 'page' && 'sw-table-sticky-page', verticalAlign === 'top' && 'sw-table-top', columnRules && 'sw-table-column-rules', className), ...rest, children: children }) }));
|
|
14
15
|
});
|
|
15
16
|
export const TableHeader = forwardRef(function TableHeader({ className, ...rest }, ref) {
|
|
16
17
|
return _jsx("thead", { ref: ref, className: className, ...rest });
|
|
@@ -46,5 +46,14 @@ export type TableCellProps = ((Omit<TdHTMLAttributes<HTMLTableCellElement>, 'wid
|
|
|
46
46
|
* is never the only signal: keep the sign or the words.
|
|
47
47
|
*/
|
|
48
48
|
tone?: TableCellTone;
|
|
49
|
+
/**
|
|
50
|
+
* Makes the cell's children a button in the cell's own text style at
|
|
51
|
+
* weight 600, underlined while hovered or focused: a row's one action,
|
|
52
|
+
* such as the name of a search result that picks it. Pair it with
|
|
53
|
+
* `TableRow pressable` so a press anywhere on the row does the same. A
|
|
54
|
+
* body cell's (`td`, or a row header `th` in `TableBody`; not for a
|
|
55
|
+
* column header in `TableHeader`), never with `sort`.
|
|
56
|
+
*/
|
|
57
|
+
onPress?: () => void;
|
|
49
58
|
};
|
|
50
59
|
export declare const TableCell: import("react").ForwardRefExoticComponent<TableCellProps & import("react").RefAttributes<HTMLTableCellElement>>;
|
|
@@ -4,6 +4,15 @@ import { cx } from '../../class-names.js';
|
|
|
4
4
|
import { assertTableCellTone, } from '../../css/css-table-ledger.js';
|
|
5
5
|
import { assertTableColumnWidth, tableColumnWidthClass, } from './table-layout.js';
|
|
6
6
|
const tableSorts = ['ascending', 'descending', 'none'];
|
|
7
|
+
/** A cell's press is a function, and never on a sortable header. */
|
|
8
|
+
function assertPress(onPress, sort) {
|
|
9
|
+
if (onPress === undefined)
|
|
10
|
+
return;
|
|
11
|
+
if (typeof onPress !== 'function')
|
|
12
|
+
throw new TypeError('TableCell onPress is a function');
|
|
13
|
+
if (sort !== undefined)
|
|
14
|
+
throw new TypeError('TableCell onPress is not for a sortable header');
|
|
15
|
+
}
|
|
7
16
|
/** `sort` and `onSort` come together, on a header cell, with a known value. */
|
|
8
17
|
function assertSort(as, sort, onSort) {
|
|
9
18
|
if (sort === undefined && onSort === undefined)
|
|
@@ -15,8 +24,9 @@ function assertSort(as, sort, onSort) {
|
|
|
15
24
|
if (!tableSorts.includes(sort))
|
|
16
25
|
throw new RangeError(`Unknown TableCell sort: ${String(sort)}`);
|
|
17
26
|
}
|
|
18
|
-
export const TableCell = forwardRef(function TableCell({ align = 'start', as = 'td', children, className, numeric = false, onSort, sort, tone, truncate = false, width, ...rest }, ref) {
|
|
27
|
+
export const TableCell = forwardRef(function TableCell({ align = 'start', as = 'td', children, className, numeric = false, onPress, onSort, sort, tone, truncate = false, width, ...rest }, ref) {
|
|
19
28
|
assertSort(as, sort, onSort);
|
|
29
|
+
assertPress(onPress, sort);
|
|
20
30
|
assertTableColumnWidth(width);
|
|
21
31
|
assertTableCellTone(tone);
|
|
22
32
|
const Component = as;
|
|
@@ -25,5 +35,5 @@ export const TableCell = forwardRef(function TableCell({ align = 'start', as = '
|
|
|
25
35
|
: align === 'end'
|
|
26
36
|
? 'sw-table-end'
|
|
27
37
|
: undefined;
|
|
28
|
-
return (_jsx(Component, { ref: ref, "aria-sort": sort === undefined || sort === 'none' ? undefined : sort, className: cx(alignmentClass, truncate && 'sw-table-clip', tone && `sw-table-tone-${tone}`, width && tableColumnWidthClass(width), className), ...rest, children: sort !== undefined && onSort ? (_jsxs("button", { className: "sw-table-sort", onClick: onSort, type: "button", children: [children, _jsx("span", { "aria-hidden": "true", className: cx('sw-table-sort-glyph', `sw-table-sort-${sort}`) })] })) : (children) }));
|
|
38
|
+
return (_jsx(Component, { ref: ref, "aria-sort": sort === undefined || sort === 'none' ? undefined : sort, className: cx(alignmentClass, truncate && 'sw-table-clip', tone && `sw-table-tone-${tone}`, width && tableColumnWidthClass(width), className), ...rest, children: sort !== undefined && onSort ? (_jsxs("button", { className: "sw-table-sort", onClick: onSort, type: "button", children: [children, _jsx("span", { "aria-hidden": "true", className: cx('sw-table-sort-glyph', `sw-table-sort-${sort}`) })] })) : onPress ? (_jsx("button", { className: "sw-table-action", onClick: onPress, type: "button", children: children })) : (children) }));
|
|
29
39
|
});
|
|
@@ -18,6 +18,21 @@ export type TableRowProps = HTMLAttributes<HTMLTableRowElement> & {
|
|
|
18
18
|
* flex item, a direct child of `Inline`, is). Works with either variant.
|
|
19
19
|
*/
|
|
20
20
|
struck?: boolean;
|
|
21
|
+
/**
|
|
22
|
+
* Makes the whole row a press target for its one action, such as a
|
|
23
|
+
* search result that is picked: the row's first link or button (a
|
|
24
|
+
* `TableCell onPress`, or a `Box as="a"`), so keep one control in the
|
|
25
|
+
* row. A plain pointer press anywhere else on the row activates that
|
|
26
|
+
* control, unless it is disabled; a press with a modifier key, on another
|
|
27
|
+
* control, or one that ends a text selection is left alone. The row takes
|
|
28
|
+
* the pointer cursor and the quiet fill while hovered, pressed, or while
|
|
29
|
+
* its action has keyboard focus. Keyboard and assistive technology use
|
|
30
|
+
* the action itself: the row is not a control and adds no tab stop.
|
|
31
|
+
* A consumer `onClick` runs once per press, not again for the click the
|
|
32
|
+
* row hands its action. Each click of a double click presses the action,
|
|
33
|
+
* as a native button's would, so keep the action safe to repeat.
|
|
34
|
+
*/
|
|
35
|
+
pressable?: boolean;
|
|
21
36
|
};
|
|
22
37
|
export declare const TableRow: import("react").ForwardRefExoticComponent<HTMLAttributes<HTMLTableRowElement> & {
|
|
23
38
|
selected?: boolean;
|
|
@@ -36,4 +51,19 @@ export declare const TableRow: import("react").ForwardRefExoticComponent<HTMLAtt
|
|
|
36
51
|
* flex item, a direct child of `Inline`, is). Works with either variant.
|
|
37
52
|
*/
|
|
38
53
|
struck?: boolean;
|
|
54
|
+
/**
|
|
55
|
+
* Makes the whole row a press target for its one action, such as a
|
|
56
|
+
* search result that is picked: the row's first link or button (a
|
|
57
|
+
* `TableCell onPress`, or a `Box as="a"`), so keep one control in the
|
|
58
|
+
* row. A plain pointer press anywhere else on the row activates that
|
|
59
|
+
* control, unless it is disabled; a press with a modifier key, on another
|
|
60
|
+
* control, or one that ends a text selection is left alone. The row takes
|
|
61
|
+
* the pointer cursor and the quiet fill while hovered, pressed, or while
|
|
62
|
+
* its action has keyboard focus. Keyboard and assistive technology use
|
|
63
|
+
* the action itself: the row is not a control and adds no tab stop.
|
|
64
|
+
* A consumer `onClick` runs once per press, not again for the click the
|
|
65
|
+
* row hands its action. Each click of a double click presses the action,
|
|
66
|
+
* as a native button's would, so keep the action safe to repeat.
|
|
67
|
+
*/
|
|
68
|
+
pressable?: boolean;
|
|
39
69
|
} & import("react").RefAttributes<HTMLTableRowElement>>;
|
|
@@ -1,8 +1,29 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import { forwardRef } from 'react';
|
|
2
|
+
import { forwardRef, useRef, } from 'react';
|
|
3
3
|
import { cx } from '../../class-names.js';
|
|
4
4
|
import { assertTableRowVariant, } from '../../css/css-table-ledger.js';
|
|
5
|
-
|
|
5
|
+
import { rowPressAction } from './row-press.js';
|
|
6
|
+
export const TableRow = forwardRef(function TableRow({ className, onClick, pressable = false, selected = false, struck = false, variant = 'default', ...rest }, ref) {
|
|
6
7
|
assertTableRowVariant(variant);
|
|
7
|
-
|
|
8
|
+
// The click the row hands its action bubbles back to the row; it is
|
|
9
|
+
// the action's, so neither the row nor a consumer onClick takes it.
|
|
10
|
+
const forwarding = useRef(false);
|
|
11
|
+
const press = (event) => {
|
|
12
|
+
if (forwarding.current)
|
|
13
|
+
return;
|
|
14
|
+
onClick?.(event);
|
|
15
|
+
if (!pressable)
|
|
16
|
+
return;
|
|
17
|
+
const action = rowPressAction(event.currentTarget, event);
|
|
18
|
+
if (action === null)
|
|
19
|
+
return;
|
|
20
|
+
forwarding.current = true;
|
|
21
|
+
try {
|
|
22
|
+
action.click();
|
|
23
|
+
}
|
|
24
|
+
finally {
|
|
25
|
+
forwarding.current = false;
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
return (_jsx("tr", { ref: ref, "aria-selected": selected || undefined, className: cx(selected && 'sw-table-row-selected', variant === 'sub' && 'sw-table-row-sub', struck && 'sw-table-row-struck', pressable && 'sw-table-row-press', className), onClick: pressable || onClick ? press : undefined, ...rest }));
|
|
8
29
|
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** What `rowPressAction` reads of a click. */
|
|
2
|
+
export type RowPress = {
|
|
3
|
+
button: number;
|
|
4
|
+
altKey: boolean;
|
|
5
|
+
ctrlKey: boolean;
|
|
6
|
+
metaKey: boolean;
|
|
7
|
+
shiftKey: boolean;
|
|
8
|
+
defaultPrevented: boolean;
|
|
9
|
+
target: EventTarget | null;
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* The control a press on a pressable row activates, or none: a plain
|
|
13
|
+
* primary press (no modifier key, not already handled) that lands on the
|
|
14
|
+
* row but not on a control in it, and does not end a text selection, goes
|
|
15
|
+
* to the row's first link or button, unless that is disabled.
|
|
16
|
+
*/
|
|
17
|
+
export declare function rowPressAction(row: HTMLElement, press: RowPress): HTMLElement | null;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The controls a press on a pressable row leaves alone: a press on one of
|
|
3
|
+
* them is that control's own, never the row's.
|
|
4
|
+
*/
|
|
5
|
+
const controlSelector = [
|
|
6
|
+
'a[href]',
|
|
7
|
+
'button',
|
|
8
|
+
'input',
|
|
9
|
+
'select',
|
|
10
|
+
'textarea',
|
|
11
|
+
'label',
|
|
12
|
+
'summary',
|
|
13
|
+
'[contenteditable]:not([contenteditable="false"])',
|
|
14
|
+
'[role="button"]',
|
|
15
|
+
'[role="link"]',
|
|
16
|
+
'[role="checkbox"]',
|
|
17
|
+
'[role="switch"]',
|
|
18
|
+
'[role="menuitem"]',
|
|
19
|
+
'[role="option"]',
|
|
20
|
+
'[tabindex]:not([tabindex="-1"])',
|
|
21
|
+
].join(', ');
|
|
22
|
+
/** The row's action: its first link or button. */
|
|
23
|
+
const actionSelector = 'a[href], button';
|
|
24
|
+
function isDisabled(action) {
|
|
25
|
+
return ((action instanceof HTMLButtonElement && action.disabled) ||
|
|
26
|
+
action.getAttribute('aria-disabled') === 'true');
|
|
27
|
+
}
|
|
28
|
+
/** A selection the press ended, such as text dragged over in the row. */
|
|
29
|
+
function endsSelection(row) {
|
|
30
|
+
const selection = row.ownerDocument.getSelection();
|
|
31
|
+
if (selection === null || selection.isCollapsed)
|
|
32
|
+
return false;
|
|
33
|
+
for (let index = 0; index < selection.rangeCount; index += 1)
|
|
34
|
+
if (selection.getRangeAt(index).intersectsNode(row))
|
|
35
|
+
return true;
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* The control a press on a pressable row activates, or none: a plain
|
|
40
|
+
* primary press (no modifier key, not already handled) that lands on the
|
|
41
|
+
* row but not on a control in it, and does not end a text selection, goes
|
|
42
|
+
* to the row's first link or button, unless that is disabled.
|
|
43
|
+
*/
|
|
44
|
+
export function rowPressAction(row, press) {
|
|
45
|
+
if (press.defaultPrevented || press.button !== 0)
|
|
46
|
+
return null;
|
|
47
|
+
if (press.altKey || press.ctrlKey || press.metaKey || press.shiftKey)
|
|
48
|
+
return null;
|
|
49
|
+
if (!(press.target instanceof Element) || !row.contains(press.target))
|
|
50
|
+
return null;
|
|
51
|
+
const control = press.target.closest(controlSelector);
|
|
52
|
+
if (control !== null && control !== row && row.contains(control))
|
|
53
|
+
return null;
|
|
54
|
+
if (endsSelection(row))
|
|
55
|
+
return null;
|
|
56
|
+
const action = row.querySelector(actionSelector);
|
|
57
|
+
if (action === null || isDisabled(action))
|
|
58
|
+
return null;
|
|
59
|
+
return action;
|
|
60
|
+
}
|
|
@@ -5,6 +5,12 @@ export type { TableColumnWidth };
|
|
|
5
5
|
export type TableLayout = 'auto' | 'fixed';
|
|
6
6
|
/** Where a cell's content sits in a row taller than it. */
|
|
7
7
|
export type TableVerticalAlign = 'middle' | 'top';
|
|
8
|
+
/**
|
|
9
|
+
* Whether the header row pins: `true` to the top of the table's own scroll
|
|
10
|
+
* region, `'page'` to the page's top while the table fits its width.
|
|
11
|
+
*/
|
|
12
|
+
export type TableStickyHeader = boolean | 'page';
|
|
13
|
+
export declare function assertTableStickyHeader(sticky: TableStickyHeader): void;
|
|
8
14
|
export declare function assertTableLayout(layout: TableLayout): void;
|
|
9
15
|
export declare function assertTableVerticalAlign(align: TableVerticalAlign): void;
|
|
10
16
|
export declare function assertTableColumnWidth(width: TableColumnWidth | undefined): void;
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { tableColumnWidths, } from '../../css/css-table-layout.js';
|
|
2
2
|
export { tableColumnWidthClass } from '../../css/css-table-layout.js';
|
|
3
|
+
export function assertTableStickyHeader(sticky) {
|
|
4
|
+
if (typeof sticky !== 'boolean' && sticky !== 'page')
|
|
5
|
+
throw new RangeError(`Unknown Table stickyHeader: ${String(sticky)}`);
|
|
6
|
+
}
|
|
3
7
|
export function assertTableLayout(layout) {
|
|
4
8
|
if (layout !== 'auto' && layout !== 'fixed')
|
|
5
9
|
throw new RangeError(`Unknown Table layout: ${String(layout)}`);
|
|
@@ -10,12 +10,8 @@ import { zIndex } from './stacking.js';
|
|
|
10
10
|
* (solid surface under reduced transparency, the system canvas in forced
|
|
11
11
|
* colours, as the pinned header cells), on the `stickyFooter` layer: over
|
|
12
12
|
* the cells and the pinned header cells, under the ActionBar, which covers
|
|
13
|
-
* it, so a page does not pair the two.
|
|
14
|
-
*
|
|
15
|
-
* width (`sw-scroll-fits`) a region that holds a sticky footer stops being
|
|
16
|
-
* one, so the footer pins to the page's bottom and the sticky header to
|
|
17
|
-
* its top, under any sticky top chrome (an AppHeader covers it). A table
|
|
18
|
-
* wider than its region scrolls sideways in it and pins to the region.
|
|
13
|
+
* it, so a page does not pair the two. Where it pins (the page or the
|
|
14
|
+
* table's own region) is `css-table-page-pin.ts`.
|
|
19
15
|
*/
|
|
20
16
|
export function cssTableFooterClasses() {
|
|
21
17
|
return `.sw-table tfoot :is(th, td) {
|
|
@@ -42,10 +38,6 @@ export function cssTableFooterClasses() {
|
|
|
42
38
|
${zIndex('stickyFooter')}
|
|
43
39
|
}
|
|
44
40
|
|
|
45
|
-
.sw-table-wrap.sw-scroll-fits:has(> .sw-table > .sw-table-footer-sticky) {
|
|
46
|
-
overflow: visible;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
41
|
@media (forced-colors: active) {
|
|
50
42
|
.sw-table tfoot > tr:first-child > :is(th, td)::before { border-color: CanvasText; }
|
|
51
43
|
.sw-table .sw-table-footer-sticky :is(th, td),
|
|
@@ -53,5 +45,5 @@ export function cssTableFooterClasses() {
|
|
|
53
45
|
}`;
|
|
54
46
|
}
|
|
55
47
|
export function tableFooterClassCatalog() {
|
|
56
|
-
return ['sw-table-footer-sticky'
|
|
48
|
+
return ['sw-table-footer-sticky'];
|
|
57
49
|
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Where a table's pinned rows pin. A table's region is a scroll box (it
|
|
3
|
+
* scrolls a wide table sideways), and a sticky cell pins to its nearest
|
|
4
|
+
* scroll box: the region, which never scrolls vertically, so on a page
|
|
5
|
+
* that scrolls nothing would stay in view. While the table fits its width
|
|
6
|
+
* (`sw-scroll-fits`, set once the region is measured) a region that holds
|
|
7
|
+
* a sticky footer, or a table whose header pins to the page
|
|
8
|
+
* (`stickyHeader="page"`, `sw-table-sticky-page`), stops being a scroll
|
|
9
|
+
* box, so the footer pins to the page's bottom edge and the sticky header
|
|
10
|
+
* to its top, under any sticky top chrome (an AppHeader covers it). A
|
|
11
|
+
* table wider than its region scrolls sideways in it and pins to the
|
|
12
|
+
* region instead.
|
|
13
|
+
*/
|
|
14
|
+
export function cssTablePagePinClasses() {
|
|
15
|
+
return `.sw-table-wrap.sw-scroll-fits:has(> .sw-table > .sw-table-footer-sticky),
|
|
16
|
+
.sw-table-wrap.sw-scroll-fits:has(> .sw-table-sticky-page) {
|
|
17
|
+
overflow: visible;
|
|
18
|
+
}`;
|
|
19
|
+
}
|
|
20
|
+
export function tablePagePinClassCatalog() {
|
|
21
|
+
return ['sw-scroll-fits', 'sw-table-sticky-page'];
|
|
22
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { typographyVariants } from '@scalewing/tokens';
|
|
2
|
+
import { quietFill } from './quiet-fill.js';
|
|
3
|
+
const row = '.sw-table-row-press';
|
|
4
|
+
const action = '.sw-table-action';
|
|
5
|
+
/*
|
|
6
|
+
* A pressable row: the pointer cursor over all of it, and the quiet fill
|
|
7
|
+
* on its cells while it is hovered (on a pointer that hovers), pressed, or
|
|
8
|
+
* while its action has keyboard focus, so a keyboard user sees the row the
|
|
9
|
+
* focused name belongs to. The fill sits under the cells' text, so text,
|
|
10
|
+
* muted text and toned figures keep their contrast as on any quiet-filled
|
|
11
|
+
* row (an open menu's). Its action is underlined with it, so the row reads
|
|
12
|
+
* as that one action. Forced colours drop fills, so the row is outlined in
|
|
13
|
+
* the system highlight instead.
|
|
14
|
+
*/
|
|
15
|
+
function pressableRowRules() {
|
|
16
|
+
return `${row} {
|
|
17
|
+
cursor: pointer;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
@media (hover: hover) {
|
|
21
|
+
${row}:hover > :is(td, th) {
|
|
22
|
+
background: ${quietFill};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
${row}:hover ${action} {
|
|
26
|
+
text-decoration-line: underline;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
${row}:active > :is(td, th),
|
|
31
|
+
${row}:has(:focus-visible) > :is(td, th) {
|
|
32
|
+
background: ${quietFill};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
@media (forced-colors: active) {
|
|
36
|
+
${row}:hover,
|
|
37
|
+
${row}:has(:focus-visible) {
|
|
38
|
+
outline: var(--sw-focus-ring-width) solid Highlight;
|
|
39
|
+
outline-offset: calc(-1 * var(--sw-focus-ring-width));
|
|
40
|
+
}
|
|
41
|
+
}`;
|
|
42
|
+
}
|
|
43
|
+
/*
|
|
44
|
+
* A cell's action keeps the cell's own text (its colour, size and
|
|
45
|
+
* alignment) at weight 600, the label weight, as a quiet link does: the
|
|
46
|
+
* weight, not a colour, sets the name apart from the row's other text.
|
|
47
|
+
* Hover and keyboard focus underline it; focus draws the accent ring.
|
|
48
|
+
* Forced colours paint it as any button's text.
|
|
49
|
+
*/
|
|
50
|
+
function actionRules() {
|
|
51
|
+
return `${action} {
|
|
52
|
+
appearance: none;
|
|
53
|
+
background: transparent;
|
|
54
|
+
border: 0;
|
|
55
|
+
border-radius: var(--sw-radius-sm);
|
|
56
|
+
color: inherit;
|
|
57
|
+
cursor: pointer;
|
|
58
|
+
font: inherit;
|
|
59
|
+
font-weight: ${typographyVariants.label.fontWeight};
|
|
60
|
+
letter-spacing: inherit;
|
|
61
|
+
margin: 0;
|
|
62
|
+
padding: 0;
|
|
63
|
+
text-align: inherit;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
${action}:hover,
|
|
67
|
+
${action}:focus-visible {
|
|
68
|
+
text-decoration-line: underline;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
${action}:focus-visible {
|
|
72
|
+
outline: var(--sw-focus-ring-width) solid var(--sw-color-accent);
|
|
73
|
+
outline-offset: var(--sw-focus-ring-offset);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
@media (forced-colors: active) {
|
|
77
|
+
${action} { color: ButtonText; }
|
|
78
|
+
}`;
|
|
79
|
+
}
|
|
80
|
+
export function cssTablePressClasses() {
|
|
81
|
+
return `${pressableRowRules()}
|
|
82
|
+
|
|
83
|
+
${actionRules()}`;
|
|
84
|
+
}
|
|
85
|
+
export function tablePressClassCatalog() {
|
|
86
|
+
return ['sw-table-row-press', 'sw-table-action'];
|
|
87
|
+
}
|
package/dist/css/css-table.js
CHANGED
|
@@ -3,6 +3,8 @@ import { scrollRegionRules } from './css-scroll-region.js';
|
|
|
3
3
|
import { cssTableLayoutClasses, tableLayoutClassCatalog, } from './css-table-layout.js';
|
|
4
4
|
import { cssTableLedgerClasses, tableLedgerClassCatalog, } from './css-table-ledger.js';
|
|
5
5
|
import { cssTableFooterClasses, tableFooterClassCatalog, } from './css-table-footer.js';
|
|
6
|
+
import { cssTablePagePinClasses, tablePagePinClassCatalog, } from './css-table-page-pin.js';
|
|
7
|
+
import { cssTablePressClasses, tablePressClassCatalog, } from './css-table-press.js';
|
|
6
8
|
import { cssTableSortClasses, tableSortClassCatalog, } from './css-table-sort.js';
|
|
7
9
|
import { zIndex } from './stacking.js';
|
|
8
10
|
const caption = typographyVariants.caption;
|
|
@@ -95,6 +97,10 @@ ${cssTableLedgerClasses()}
|
|
|
95
97
|
|
|
96
98
|
${cssTableFooterClasses()}
|
|
97
99
|
|
|
100
|
+
${cssTablePagePinClasses()}
|
|
101
|
+
|
|
102
|
+
${cssTablePressClasses()}
|
|
103
|
+
|
|
98
104
|
${selectedRowRules()}`;
|
|
99
105
|
}
|
|
100
106
|
export function tableClassCatalog() {
|
|
@@ -111,5 +117,7 @@ export function tableClassCatalog() {
|
|
|
111
117
|
...tableSortClassCatalog(),
|
|
112
118
|
...tableLedgerClassCatalog(),
|
|
113
119
|
...tableFooterClassCatalog(),
|
|
120
|
+
...tablePagePinClassCatalog(),
|
|
121
|
+
...tablePressClassCatalog(),
|
|
114
122
|
];
|
|
115
123
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -42,7 +42,7 @@ export { Select } from './components/Select.js';
|
|
|
42
42
|
export type { SelectAction, SelectOption, SelectProps, SelectWidth, } from './components/Select.js';
|
|
43
43
|
export { MultiSelect, type MultiSelectAlign, type MultiSelectOption, type MultiSelectProps, } from './components/MultiSelect.js';
|
|
44
44
|
export { Stack, type Align, type Justify, type StackProps, } from './components/Stack.js';
|
|
45
|
-
export { Table, TableBody, TableCell, TableFooter, TableHeader, TableRow, tableCellTones, tableRowVariants, type TableBodyProps, type TableCellProps, type TableCellTone, type TableColumnWidth, type TableDensity, type TableFooterProps, type TableHeaderProps, type TableLayout, type TableProps, type TableRowProps, type TableRowVariant, type TableSort, type TableVerticalAlign, } from './components/Table.js';
|
|
45
|
+
export { Table, TableBody, TableCell, TableFooter, TableHeader, TableRow, tableCellTones, tableRowVariants, type TableBodyProps, type TableCellProps, type TableCellTone, type TableColumnWidth, type TableDensity, type TableFooterProps, type TableHeaderProps, type TableLayout, type TableProps, type TableRowProps, type TableRowVariant, type TableSort, type TableStickyHeader, type TableVerticalAlign, } from './components/Table.js';
|
|
46
46
|
export { Text, type TextAlign, type TextElement, type TextProps, } from './components/Text.js';
|
|
47
47
|
export { ThemeProvider, useTheme, type ThemeProviderProps, } from './theme/ThemeProvider.js';
|
|
48
48
|
export { utilityClassCatalog } from './css/stylesheet.js';
|
package/dist/styles.css
CHANGED
|
@@ -1831,16 +1831,73 @@ body {
|
|
|
1831
1831
|
z-index: 2;
|
|
1832
1832
|
}
|
|
1833
1833
|
|
|
1834
|
-
.sw-table-wrap.sw-scroll-fits:has(> .sw-table > .sw-table-footer-sticky) {
|
|
1835
|
-
overflow: visible;
|
|
1836
|
-
}
|
|
1837
|
-
|
|
1838
1834
|
@media (forced-colors: active) {
|
|
1839
1835
|
.sw-table tfoot > tr:first-child > :is(th, td)::before { border-color: CanvasText; }
|
|
1840
1836
|
.sw-table .sw-table-footer-sticky :is(th, td),
|
|
1841
1837
|
.sw-table-sticky thead th { background: Canvas; backdrop-filter: none; -webkit-backdrop-filter: none; }
|
|
1842
1838
|
}
|
|
1843
1839
|
|
|
1840
|
+
.sw-table-wrap.sw-scroll-fits:has(> .sw-table > .sw-table-footer-sticky),
|
|
1841
|
+
.sw-table-wrap.sw-scroll-fits:has(> .sw-table-sticky-page) {
|
|
1842
|
+
overflow: visible;
|
|
1843
|
+
}
|
|
1844
|
+
|
|
1845
|
+
.sw-table-row-press {
|
|
1846
|
+
cursor: pointer;
|
|
1847
|
+
}
|
|
1848
|
+
|
|
1849
|
+
@media (hover: hover) {
|
|
1850
|
+
.sw-table-row-press:hover > :is(td, th) {
|
|
1851
|
+
background: color-mix(in srgb, var(--sw-color-muted) 16%, transparent);
|
|
1852
|
+
}
|
|
1853
|
+
|
|
1854
|
+
.sw-table-row-press:hover .sw-table-action {
|
|
1855
|
+
text-decoration-line: underline;
|
|
1856
|
+
}
|
|
1857
|
+
}
|
|
1858
|
+
|
|
1859
|
+
.sw-table-row-press:active > :is(td, th),
|
|
1860
|
+
.sw-table-row-press:has(:focus-visible) > :is(td, th) {
|
|
1861
|
+
background: color-mix(in srgb, var(--sw-color-muted) 16%, transparent);
|
|
1862
|
+
}
|
|
1863
|
+
|
|
1864
|
+
@media (forced-colors: active) {
|
|
1865
|
+
.sw-table-row-press:hover,
|
|
1866
|
+
.sw-table-row-press:has(:focus-visible) {
|
|
1867
|
+
outline: var(--sw-focus-ring-width) solid Highlight;
|
|
1868
|
+
outline-offset: calc(-1 * var(--sw-focus-ring-width));
|
|
1869
|
+
}
|
|
1870
|
+
}
|
|
1871
|
+
|
|
1872
|
+
.sw-table-action {
|
|
1873
|
+
appearance: none;
|
|
1874
|
+
background: transparent;
|
|
1875
|
+
border: 0;
|
|
1876
|
+
border-radius: var(--sw-radius-sm);
|
|
1877
|
+
color: inherit;
|
|
1878
|
+
cursor: pointer;
|
|
1879
|
+
font: inherit;
|
|
1880
|
+
font-weight: 600;
|
|
1881
|
+
letter-spacing: inherit;
|
|
1882
|
+
margin: 0;
|
|
1883
|
+
padding: 0;
|
|
1884
|
+
text-align: inherit;
|
|
1885
|
+
}
|
|
1886
|
+
|
|
1887
|
+
.sw-table-action:hover,
|
|
1888
|
+
.sw-table-action:focus-visible {
|
|
1889
|
+
text-decoration-line: underline;
|
|
1890
|
+
}
|
|
1891
|
+
|
|
1892
|
+
.sw-table-action:focus-visible {
|
|
1893
|
+
outline: var(--sw-focus-ring-width) solid var(--sw-color-accent);
|
|
1894
|
+
outline-offset: var(--sw-focus-ring-offset);
|
|
1895
|
+
}
|
|
1896
|
+
|
|
1897
|
+
@media (forced-colors: active) {
|
|
1898
|
+
.sw-table-action { color: ButtonText; }
|
|
1899
|
+
}
|
|
1900
|
+
|
|
1844
1901
|
.sw-table-row-selected > :first-child {
|
|
1845
1902
|
position: relative;
|
|
1846
1903
|
}
|