@scalewing/react 1.20.0 → 1.22.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.
Files changed (69) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/README.md +47 -5
  3. package/dist/components/ActionMenu.d.ts +13 -1
  4. package/dist/components/ActionMenu.js +2 -2
  5. package/dist/components/BarChart.d.ts +28 -0
  6. package/dist/components/BarChart.js +5 -4
  7. package/dist/components/CalendarButton.d.ts +16 -0
  8. package/dist/components/CalendarButton.js +5 -3
  9. package/dist/components/DateField.d.ts +8 -1
  10. package/dist/components/DateField.js +2 -1
  11. package/dist/components/DenominationGrid.d.ts +2 -2
  12. package/dist/components/DenominationGrid.js +5 -2
  13. package/dist/components/Field.d.ts +9 -0
  14. package/dist/components/Field.js +3 -2
  15. package/dist/components/SectionNav.d.ts +44 -0
  16. package/dist/components/SectionNav.js +48 -0
  17. package/dist/components/Table.d.ts +38 -13
  18. package/dist/components/Table.js +7 -12
  19. package/dist/components/Tabs.d.ts +3 -1
  20. package/dist/components/Tabs.js +8 -2
  21. package/dist/components/Tooltip.js +19 -3
  22. package/dist/components/action-menu/ActionMenuList.d.ts +3 -2
  23. package/dist/components/action-menu/ActionMenuList.js +16 -4
  24. package/dist/components/date-field/CalendarDialog.d.ts +4 -1
  25. package/dist/components/date-field/CalendarDialog.js +2 -2
  26. package/dist/components/date-field/CalendarGrid.d.ts +4 -1
  27. package/dist/components/date-field/CalendarGrid.js +11 -3
  28. package/dist/components/date-field/use-calendar-popup.d.ts +6 -1
  29. package/dist/components/date-field/use-calendar-popup.js +5 -3
  30. package/dist/components/denomination-grid/DenominationStrip.d.ts +2 -2
  31. package/dist/components/denomination-grid/DenominationStrip.js +17 -6
  32. package/dist/components/denomination-grid/types.d.ts +15 -2
  33. package/dist/components/table/TableCell.d.ts +42 -0
  34. package/dist/components/table/TableCell.js +27 -0
  35. package/dist/components/table/table-layout.d.ts +10 -0
  36. package/dist/components/table/table-layout.js +14 -0
  37. package/dist/components/tooltip/tooltip-open.d.ts +8 -0
  38. package/dist/components/tooltip/tooltip-open.js +60 -0
  39. package/dist/components/tooltip/use-tooltip-placement.d.ts +11 -0
  40. package/dist/components/tooltip/use-tooltip-placement.js +46 -0
  41. package/dist/components/use-anchored-popover.d.ts +2 -0
  42. package/dist/components/use-anchored-popover.js +2 -1
  43. package/dist/css/css-action-menu.js +24 -0
  44. package/dist/css/css-chart.js +45 -1
  45. package/dist/css/css-components.js +3 -0
  46. package/dist/css/css-date-field.js +47 -1
  47. package/dist/css/css-denomination-aligned.d.ts +7 -0
  48. package/dist/css/css-denomination-aligned.js +68 -0
  49. package/dist/css/css-denomination-grid.js +7 -1
  50. package/dist/css/css-field.js +14 -0
  51. package/dist/css/css-scroll-region.d.ts +8 -0
  52. package/dist/css/css-scroll-region.js +9 -3
  53. package/dist/css/css-section-nav.d.ts +4 -0
  54. package/dist/css/css-section-nav.js +120 -0
  55. package/dist/css/css-table-layout.d.ts +20 -0
  56. package/dist/css/css-table-layout.js +59 -0
  57. package/dist/css/css-table-sort.d.ts +2 -0
  58. package/dist/css/css-table-sort.js +105 -0
  59. package/dist/css/css-table.js +8 -0
  60. package/dist/css/css-tabs.d.ts +6 -1
  61. package/dist/css/css-tabs.js +9 -1
  62. package/dist/css/css-tooltip.js +7 -4
  63. package/dist/css/stylesheet.js +2 -0
  64. package/dist/date-only.d.ts +12 -0
  65. package/dist/date-only.js +24 -0
  66. package/dist/index.d.ts +4 -3
  67. package/dist/index.js +1 -0
  68. package/dist/styles.css +363 -6
  69. package/package.json +1 -1
@@ -1,13 +1,50 @@
1
- import { type HTMLAttributes, type TdHTMLAttributes, type ThHTMLAttributes } from 'react';
1
+ import { type HTMLAttributes } from 'react';
2
+ import { type TableLayout, type TableVerticalAlign } from './table/table-layout.js';
3
+ export { TableCell, type TableCellProps, type TableColumnWidth, type TableSort, } from './table/TableCell.js';
4
+ export type { TableLayout, TableVerticalAlign };
2
5
  export type TableDensity = 'comfortable' | 'compact';
3
6
  export type TableProps = HTMLAttributes<HTMLTableElement> & {
4
7
  density?: TableDensity;
5
8
  stickyHeader?: boolean;
9
+ /**
10
+ * `auto` (default) sizes each column by its content. `fixed` sizes the
11
+ * columns from the header row's `width`s alone (`table-layout: fixed`),
12
+ * so no column moves when the rows change, such as when a filter hides
13
+ * one. Columns without a width share what is left equally; when every
14
+ * column has one, spare width is shared in proportion to them. When the
15
+ * widths add up to more than the container, the table scrolls in its
16
+ * region and a column without a width gets no room at all, so on a table
17
+ * that can be narrower than its widths give every column one.
18
+ */
19
+ layout?: TableLayout;
20
+ /**
21
+ * Where a cell's content sits in a row taller than it: `middle` (default)
22
+ * or `top`, for rows where one cell runs to several lines and the others
23
+ * should start on its first line.
24
+ */
25
+ verticalAlign?: TableVerticalAlign;
6
26
  };
7
27
  /** A wide table scrolls inside its own `ScrollRegion`, named after the table. */
8
28
  export declare const Table: import("react").ForwardRefExoticComponent<HTMLAttributes<HTMLTableElement> & {
9
29
  density?: TableDensity;
10
30
  stickyHeader?: boolean;
31
+ /**
32
+ * `auto` (default) sizes each column by its content. `fixed` sizes the
33
+ * columns from the header row's `width`s alone (`table-layout: fixed`),
34
+ * so no column moves when the rows change, such as when a filter hides
35
+ * one. Columns without a width share what is left equally; when every
36
+ * column has one, spare width is shared in proportion to them. When the
37
+ * widths add up to more than the container, the table scrolls in its
38
+ * region and a column without a width gets no room at all, so on a table
39
+ * that can be narrower than its widths give every column one.
40
+ */
41
+ layout?: TableLayout;
42
+ /**
43
+ * Where a cell's content sits in a row taller than it: `middle` (default)
44
+ * or `top`, for rows where one cell runs to several lines and the others
45
+ * should start on its first line.
46
+ */
47
+ verticalAlign?: TableVerticalAlign;
11
48
  } & import("react").RefAttributes<HTMLTableElement>>;
12
49
  export type TableHeaderProps = HTMLAttributes<HTMLTableSectionElement>;
13
50
  export declare const TableHeader: import("react").ForwardRefExoticComponent<TableHeaderProps & import("react").RefAttributes<HTMLTableSectionElement>>;
@@ -19,15 +56,3 @@ export type TableRowProps = HTMLAttributes<HTMLTableRowElement> & {
19
56
  export declare const TableRow: import("react").ForwardRefExoticComponent<HTMLAttributes<HTMLTableRowElement> & {
20
57
  selected?: boolean;
21
58
  } & import("react").RefAttributes<HTMLTableRowElement>>;
22
- type TableCellAlign = 'start' | 'end';
23
- export type TableCellProps = ((TdHTMLAttributes<HTMLTableCellElement> & {
24
- as?: 'td';
25
- }) | (ThHTMLAttributes<HTMLTableCellElement> & {
26
- as: 'th';
27
- })) & {
28
- align?: TableCellAlign;
29
- numeric?: boolean;
30
- truncate?: boolean;
31
- };
32
- export declare const TableCell: import("react").ForwardRefExoticComponent<TableCellProps & import("react").RefAttributes<HTMLTableCellElement>>;
33
- export {};
@@ -1,10 +1,14 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
- import { forwardRef, } from 'react';
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';
6
+ export { TableCell, } from './table/TableCell.js';
5
7
  /** A wide table scrolls inside its own `ScrollRegion`, named after the table. */
6
- export const Table = forwardRef(function Table({ children, className, density = 'comfortable', stickyHeader = true, ...rest }, ref) {
7
- return (_jsx(ScrollRegion, { "aria-label": rest['aria-label'], "aria-labelledby": rest['aria-labelledby'], className: "sw-table-wrap", children: _jsx("table", { ref: ref, className: cx('sw-table', density === 'compact' && 'sw-table-compact', stickyHeader && 'sw-table-sticky', className), ...rest, children: children }) }));
8
+ export const Table = forwardRef(function Table({ children, className, density = 'comfortable', layout = 'auto', stickyHeader = true, verticalAlign = 'middle', ...rest }, ref) {
9
+ assertTableLayout(layout);
10
+ assertTableVerticalAlign(verticalAlign);
11
+ return (_jsx(ScrollRegion, { "aria-label": rest['aria-label'], "aria-labelledby": rest['aria-labelledby'], className: "sw-table-wrap", 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', className), ...rest, children: children }) }));
8
12
  });
9
13
  export const TableHeader = forwardRef(function TableHeader({ className, ...rest }, ref) {
10
14
  return _jsx("thead", { ref: ref, className: className, ...rest });
@@ -15,12 +19,3 @@ export const TableBody = forwardRef(function TableBody({ className, ...rest }, r
15
19
  export const TableRow = forwardRef(function TableRow({ className, selected = false, ...rest }, ref) {
16
20
  return (_jsx("tr", { ref: ref, "aria-selected": selected || undefined, className: cx(selected && 'sw-table-row-selected', className), ...rest }));
17
21
  });
18
- export const TableCell = forwardRef(function TableCell({ align = 'start', as = 'td', className, numeric = false, truncate = false, ...rest }, ref) {
19
- const Component = as;
20
- const alignmentClass = numeric
21
- ? 'sw-table-numeric'
22
- : align === 'end'
23
- ? 'sw-table-end'
24
- : undefined;
25
- return (_jsx(Component, { ref: ref, className: cx(alignmentClass, truncate && 'sw-table-clip', className), ...rest }));
26
- });
@@ -41,7 +41,9 @@ export declare function tabPanelId(tabsId: string, id: string): string;
41
41
  /**
42
42
  * A tab strip with `tablist` semantics for the sections of one page. Arrow
43
43
  * keys, Home and End move the focus and select at once; the strip scrolls
44
- * sideways when it overflows. Pair each item with a `TabPanel`. `sticky`
44
+ * sideways when it overflows, and an edge with labels past it draws a shade
45
+ * (`sw-scroll-more-start`, `sw-scroll-more-end`), as a wide `Table` does.
46
+ * Pair each item with a `TabPanel`. `sticky`
45
47
  * keeps it at the top of the viewport over a long panel.
46
48
  */
47
49
  export declare function Tabs({ id, items, onChange, sticky, value, ...labelProps }: TabsProps): import("react").JSX.Element;
@@ -1,6 +1,8 @@
1
+ 'use client';
1
2
  import { jsx as _jsx } from "react/jsx-runtime";
2
3
  import { useRef, } from 'react';
3
4
  import { cx } from '../class-names.js';
5
+ import { useScrollOverflow } from './use-scroll-overflow.js';
4
6
  export function tabId(tabsId, id) {
5
7
  return `${tabsId}-tab-${id}`;
6
8
  }
@@ -10,11 +12,15 @@ export function tabPanelId(tabsId, id) {
10
12
  /**
11
13
  * A tab strip with `tablist` semantics for the sections of one page. Arrow
12
14
  * keys, Home and End move the focus and select at once; the strip scrolls
13
- * sideways when it overflows. Pair each item with a `TabPanel`. `sticky`
15
+ * sideways when it overflows, and an edge with labels past it draws a shade
16
+ * (`sw-scroll-more-start`, `sw-scroll-more-end`), as a wide `Table` does.
17
+ * Pair each item with a `TabPanel`. `sticky`
14
18
  * keeps it at the top of the viewport over a long panel.
15
19
  */
16
20
  export function Tabs({ id, items, onChange, sticky = false, value, ...labelProps }) {
17
21
  const tabs = useRef(new Map());
22
+ const strip = useRef(null);
23
+ const overflow = useScrollOverflow(strip);
18
24
  function select(next) {
19
25
  if (!next || next.id === value) {
20
26
  return;
@@ -43,7 +49,7 @@ export function Tabs({ id, items, onChange, sticky = false, value, ...labelProps
43
49
  event.preventDefault();
44
50
  select(target);
45
51
  }
46
- return (_jsx("div", { className: cx('sw-tabs', sticky && 'sw-tabs-sticky'), onKeyDown: onKeyDown, role: "tablist", ...labelProps, children: items.map((item) => {
52
+ return (_jsx("div", { className: cx('sw-tabs', sticky && 'sw-tabs-sticky', overflow.start && 'sw-scroll-more-start', overflow.end && 'sw-scroll-more-end'), onKeyDown: onKeyDown, ref: strip, role: "tablist", ...labelProps, children: items.map((item) => {
47
53
  const selected = item.id === value;
48
54
  return (_jsx("button", { "aria-controls": tabPanelId(id, item.id), "aria-selected": selected, className: cx('sw-tab', selected && 'sw-tab-selected'), id: tabId(id, item.id), onClick: () => select(item), ref: (node) => {
49
55
  if (node) {
@@ -1,6 +1,8 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { cloneElement, useEffect, useId, useRef, useState, } from 'react';
4
+ import { focusIsVisible, tapActs } from './tooltip/tooltip-open.js';
5
+ import { useTooltipPlacement } from './tooltip/use-tooltip-placement.js';
4
6
  import { tooltipTriggerAria, } from './tooltip-trigger-aria.js';
5
7
  export function Tooltip({ content, disabled = false, relationship = 'description', trigger, }) {
6
8
  if (!content.trim())
@@ -11,6 +13,8 @@ export function Tooltip({ content, disabled = false, relationship = 'description
11
13
  const shown = open && !disabled;
12
14
  const id = useId();
13
15
  const wrapperRef = useRef(null);
16
+ const tooltipRef = useRef(null);
17
+ useTooltipPlacement(shown, tooltipRef, wrapperRef);
14
18
  const triggerAria = tooltipTriggerAria(trigger.props, disabled ? undefined : id, relationship);
15
19
  useEffect(() => {
16
20
  if (!open)
@@ -26,7 +30,13 @@ export function Tooltip({ content, disabled = false, relationship = 'description
26
30
  return (_jsxs("span", { className: "sw-tooltip-anchor", onBlurCapture: (event) => {
27
31
  if (!event.currentTarget.contains(event.relatedTarget))
28
32
  setOpen(false);
29
- }, onFocusCapture: () => setOpen(true), onKeyDownCapture: (event) => {
33
+ }, onFocusCapture: (event) => {
34
+ // A click or a tap focuses the trigger too; only a visible focus
35
+ // (the keyboard) opens the tooltip, so a clicked control does not
36
+ // keep its help shown until the focus moves.
37
+ if (focusIsVisible(event.target))
38
+ setOpen(true);
39
+ }, onKeyDownCapture: (event) => {
30
40
  // Escape hides a visible tooltip and nothing else, so a surrounding
31
41
  // Dialog does not also close. A hidden tooltip leaves Escape alone.
32
42
  if (event.key !== 'Escape' || !shown)
@@ -40,7 +50,13 @@ export function Tooltip({ content, disabled = false, relationship = 'description
40
50
  if (event.pointerType === 'mouse' || event.pointerType === 'pen')
41
51
  setOpen(false);
42
52
  }, onPointerDownCapture: (event) => {
43
- if (event.pointerType === 'touch')
53
+ if (event.pointerType !== 'touch')
54
+ return;
55
+ // A tap on a control does what the control does; only a trigger
56
+ // that does nothing else on a tap toggles its help.
57
+ if (tapActs(event.target, event.currentTarget))
58
+ setOpen(false);
59
+ else
44
60
  setOpen((current) => !current);
45
- }, ref: wrapperRef, children: [cloneElement(trigger, triggerAria), disabled ? null : (_jsx("span", { className: "sw-tooltip", hidden: !shown, id: id, role: "tooltip", children: content }))] }));
61
+ }, ref: wrapperRef, children: [cloneElement(trigger, triggerAria), disabled ? null : (_jsx("span", { className: "sw-tooltip", hidden: !shown, id: id, ref: tooltipRef, role: "tooltip", children: content }))] }));
46
62
  }
@@ -1,9 +1,10 @@
1
- import { type KeyboardEvent, type RefObject } from 'react';
1
+ import { type KeyboardEvent, type ReactNode, type RefObject } from 'react';
2
2
  import { type InlineAlign } from '../../anchored-position.js';
3
3
  import type { ActionMenuItem } from '../ActionMenu.js';
4
4
  type ActionMenuListProps = {
5
5
  id: string;
6
6
  align: InlineAlign;
7
+ header?: ReactNode;
7
8
  label: string;
8
9
  items: readonly ActionMenuItem[];
9
10
  focusIndex: number;
@@ -19,5 +20,5 @@ type ActionMenuListProps = {
19
20
  * or, with `align="end"`, its end, and with the other edge when that one
20
21
  * would not fit), with the focused command holding DOM focus.
21
22
  */
22
- export declare function ActionMenuList({ id, align, label, items, focusIndex, triggerRef, onDismiss, onFocusIndex, onKeyDown, onSelect, }: ActionMenuListProps): import("react").JSX.Element;
23
+ export declare function ActionMenuList({ id, align, header, label, items, focusIndex, triggerRef, onDismiss, onFocusIndex, onKeyDown, onSelect, }: ActionMenuListProps): import("react").JSX.Element;
23
24
  export {};
@@ -1,6 +1,6 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
- import { useLayoutEffect, useRef, } from 'react';
3
+ import { useId, useLayoutEffect, useRef, } from 'react';
4
4
  import { cx } from '../../class-names.js';
5
5
  import { useAnchoredPopover } from '../use-anchored-popover.js';
6
6
  /**
@@ -9,7 +9,7 @@ import { useAnchoredPopover } from '../use-anchored-popover.js';
9
9
  * or, with `align="end"`, its end, and with the other edge when that one
10
10
  * would not fit), with the focused command holding DOM focus.
11
11
  */
12
- export function ActionMenuList({ id, align, label, items, focusIndex, triggerRef, onDismiss, onFocusIndex, onKeyDown, onSelect, }) {
12
+ export function ActionMenuList({ id, align, header, label, items, focusIndex, triggerRef, onDismiss, onFocusIndex, onKeyDown, onSelect, }) {
13
13
  const menuRef = useRef(null);
14
14
  useAnchoredPopover(menuRef, triggerRef, triggerRef, onDismiss, {
15
15
  align,
@@ -19,10 +19,22 @@ export function ActionMenuList({ id, align, label, items, focusIndex, triggerRef
19
19
  const commands = menuRef.current?.querySelectorAll('[role="menuitem"]');
20
20
  commands?.[focusIndex]?.focus();
21
21
  }, [focusIndex]);
22
- return (_jsx("div", { "aria-label": label, className: "sw-action-menu-list", id: id, onKeyDown: onKeyDown, ref: menuRef, role: "menu", children: items.map((item, index) => (_jsxs("button", { className: cx('sw-action-menu-item', item.destructive && 'sw-action-menu-item-danger'), disabled: item.disabled, onClick: () => onSelect(item), onFocus: () => onFocusIndex(index), onKeyDown: (event) => {
22
+ const headerId = useId();
23
+ // An empty header (nothing, false or '') is no header, so the menu is never
24
+ // described by an empty element.
25
+ const hasHeader = header !== undefined &&
26
+ header !== null &&
27
+ header !== false &&
28
+ header !== '';
29
+ const menu = (_jsx("div", { "aria-describedby": hasHeader ? headerId : undefined, "aria-label": label, className: hasHeader ? 'sw-action-menu-items' : 'sw-action-menu-list', id: hasHeader ? undefined : id, onKeyDown: onKeyDown, ref: hasHeader ? undefined : menuRef, role: "menu", children: items.map((item, index) => (_jsxs("button", { className: cx('sw-action-menu-item', item.destructive && 'sw-action-menu-item-danger'), disabled: item.disabled, onClick: () => onSelect(item), onFocus: () => onFocusIndex(index), onKeyDown: (event) => {
23
30
  if (event.key === 'Enter' || event.key === ' ') {
24
31
  event.preventDefault();
25
32
  onSelect(item);
26
33
  }
27
- }, role: "menuitem", tabIndex: index === focusIndex ? 0 : -1, type: "button", children: [item.icon ? _jsx("span", { "aria-hidden": "true", children: item.icon }) : null, _jsx("span", { children: item.label })] }, item.id))) }));
34
+ }, role: "menuitem", tabIndex: index === focusIndex ? 0 : -1, type: "button", children: [item.icon ? _jsx("span", { "aria-hidden": "true", children: item.icon }) : null, _jsx("span", { lang: item.lang, children: item.label })] }, item.id))) }));
35
+ if (!hasHeader)
36
+ return menu;
37
+ // The header is not a menu item: it sits above the menu in the same
38
+ // popover, outside the arrow-key order, and describes the menu.
39
+ return (_jsxs("div", { className: "sw-action-menu-list", id: id, ref: menuRef, children: [_jsx("div", { className: "sw-action-menu-header", id: headerId, children: header }), menu] }));
28
40
  }
@@ -1,6 +1,7 @@
1
1
  import { type RefObject } from 'react';
2
2
  import { type WeekStart } from '../../calendar-month.js';
3
3
  import { type DateFieldLabels } from '../../date-field-labels.js';
4
+ import { type DateOnlyRange } from '../../date-only.js';
4
5
  export type CalendarDialogProps = {
5
6
  anchorRef: RefObject<HTMLElement | null>;
6
7
  buttonRef: RefObject<HTMLElement | null>;
@@ -13,6 +14,8 @@ export type CalendarDialogProps = {
13
14
  /** `true` returns focus to the calendar button. */
14
15
  onClose: (restoreFocus: boolean) => void;
15
16
  onSelect: (value: string) => void;
17
+ /** Days to tint as a span; the selection stays `value`. */
18
+ range?: DateOnlyRange;
16
19
  required: boolean;
17
20
  today: string;
18
21
  value: string;
@@ -23,4 +26,4 @@ export type CalendarDialogProps = {
23
26
  * following the WAI-ARIA date picker dialog pattern. It opens on the
24
27
  * selected day, or today, kept inside `[min, max]`.
25
28
  */
26
- export declare function CalendarDialog({ anchorRef, buttonRef, id, labelId, labels, locale, max, min, onClose, onSelect, required, today, value, weekStartsOn, }: CalendarDialogProps): import("react").JSX.Element;
29
+ export declare function CalendarDialog({ anchorRef, buttonRef, id, labelId, labels, locale, max, min, onClose, onSelect, range, required, today, value, weekStartsOn, }: CalendarDialogProps): import("react").JSX.Element;
@@ -43,7 +43,7 @@ function sameDayIn(month, focused) {
43
43
  * following the WAI-ARIA date picker dialog pattern. It opens on the
44
44
  * selected day, or today, kept inside `[min, max]`.
45
45
  */
46
- export function CalendarDialog({ anchorRef, buttonRef, id, labelId, labels, locale, max, min, onClose, onSelect, required, today, value, weekStartsOn, }) {
46
+ export function CalendarDialog({ anchorRef, buttonRef, id, labelId, labels, locale, max, min, onClose, onSelect, range, required, today, value, weekStartsOn, }) {
47
47
  const titleId = useId();
48
48
  const dialogRef = useRef(null);
49
49
  const [rawFocusDate, setFocusDate] = useState(value || today);
@@ -89,5 +89,5 @@ export function CalendarDialog({ anchorRef, buttonRef, id, labelId, labels, loca
89
89
  }, onKeyDown: onKeyDown, ref: dialogRef, role: "dialog", children: [_jsx(CalendarHeader, { labels: labels, locale: locale, max: max, min: min, month: month, onMonthChange: (next) => setFocusDate(sameDayIn(next, focusDate)), titleId: titleId, todayYear: monthOf(today).year }), _jsx(CalendarGrid, { focusDate: focusDate, locale: locale, max: max, min: min, month: month, onFocusDate: setFocusDate, onMoveFocus: (next) => {
90
90
  focusGrid.current = true;
91
91
  setFocusDate(next);
92
- }, onSelect: onSelect, selected: value, titleId: titleId, today: today, weekStartsOn: weekStartsOn }), _jsxs("div", { className: "sw-date-field-footer", children: [required ? null : (_jsx(Button, { disabled: value === '', onPress: () => onSelect(''), size: "sm", variant: "ghost", children: labels.clear })), _jsx(Button, { disabled: todayUnavailable, onPress: () => onSelect(today), size: "sm", variant: "ghost", children: labels.today })] })] }));
92
+ }, onSelect: onSelect, range: range, selected: value, titleId: titleId, today: today, weekStartsOn: weekStartsOn }), _jsxs("div", { className: "sw-date-field-footer", children: [required ? null : (_jsx(Button, { disabled: value === '', onPress: () => onSelect(''), size: "sm", variant: "ghost", children: labels.clear })), _jsx(Button, { disabled: todayUnavailable, onPress: () => onSelect(today), size: "sm", variant: "ghost", children: labels.today })] })] }));
93
93
  }
@@ -1,4 +1,5 @@
1
1
  import { type CalendarMonth, type WeekStart } from '../../calendar-month.js';
2
+ import { type DateOnlyRange } from '../../date-only.js';
2
3
  export type CalendarGridProps = {
3
4
  focusDate: string;
4
5
  locale: string;
@@ -10,6 +11,8 @@ export type CalendarGridProps = {
10
11
  /** A grid key chose a day: focus it. */
11
12
  onMoveFocus: (value: string) => void;
12
13
  onSelect: (value: string) => void;
14
+ /** Days to tint as a span, such as the period a page shows. */
15
+ range?: DateOnlyRange;
13
16
  selected: string;
14
17
  titleId: string;
15
18
  today: string;
@@ -20,4 +23,4 @@ export type CalendarGridProps = {
20
23
  * `[min, max]` stay focusable for orientation but are `aria-disabled`;
21
24
  * padding past the four-digit years is blank and inert.
22
25
  */
23
- export declare function CalendarGrid({ focusDate, locale, max, min, month, onFocusDate, onMoveFocus, onSelect, selected, titleId, today, weekStartsOn, }: CalendarGridProps): import("react").JSX.Element;
26
+ export declare function CalendarGrid({ focusDate, locale, max, min, month, onFocusDate, onMoveFocus, onSelect, range, selected, titleId, today, weekStartsOn, }: CalendarGridProps): import("react").JSX.Element;
@@ -3,13 +3,21 @@ import { cx } from '../../class-names.js';
3
3
  import { calendarKeyTarget } from '../../calendar-keys.js';
4
4
  import { formatDateLabel, weekdayLabels } from '../../calendar-labels.js';
5
5
  import { monthGrid, } from '../../calendar-month.js';
6
- import { isOutsideDateRange } from '../../date-only.js';
6
+ import { dayInRange, isOutsideDateRange, } from '../../date-only.js';
7
+ /** A day's range classes: in the span, and its start or end. */
8
+ function rangeClassNames(place) {
9
+ return [
10
+ place !== null && 'sw-date-field-day-in-range',
11
+ (place === 'start' || place === 'only') && 'sw-date-field-day-range-start',
12
+ (place === 'end' || place === 'only') && 'sw-date-field-day-range-end',
13
+ ];
14
+ }
7
15
  /**
8
16
  * The month as a WAI-ARIA grid with one roving tab stop. Days outside
9
17
  * `[min, max]` stay focusable for orientation but are `aria-disabled`;
10
18
  * padding past the four-digit years is blank and inert.
11
19
  */
12
- export function CalendarGrid({ focusDate, locale, max, min, month, onFocusDate, onMoveFocus, onSelect, selected, titleId, today, weekStartsOn, }) {
20
+ export function CalendarGrid({ focusDate, locale, max, min, month, onFocusDate, onMoveFocus, onSelect, range, selected, titleId, today, weekStartsOn, }) {
13
21
  function choose(value) {
14
22
  if (!isOutsideDateRange(value, min, max))
15
23
  onSelect(value);
@@ -27,7 +35,7 @@ export function CalendarGrid({ focusDate, locale, max, min, month, onFocusDate,
27
35
  event.preventDefault();
28
36
  onMoveFocus(target);
29
37
  }
30
- return (_jsxs("table", { "aria-labelledby": titleId, className: "sw-date-field-grid", onKeyDown: onKeyDown, role: "grid", children: [_jsx("thead", { children: _jsx("tr", { children: weekdayLabels(locale, weekStartsOn).map((weekday) => (_jsx("th", { abbr: weekday.long, scope: "col", children: weekday.short }, weekday.long))) }) }), _jsx("tbody", { children: monthGrid(month, weekStartsOn).map((row, rowIndex) => (_jsx("tr", { children: row.map((cell, column) => cell === null ? (_jsx("td", { "aria-disabled": "true", className: "sw-date-field-day sw-date-field-day-outside", role: "gridcell" }, `blank-${column}`)) : (_jsx("td", { "aria-current": cell.value === today ? 'date' : undefined, "aria-disabled": isOutsideDateRange(cell.value, min, max) || undefined, "aria-label": formatDateLabel(cell.value, locale), "aria-selected": cell.value === selected, className: cx('sw-date-field-day', !cell.inMonth && 'sw-date-field-day-outside'), "data-date": cell.value, onClick: () => choose(cell.value), onFocus: () => {
38
+ return (_jsxs("table", { "aria-labelledby": titleId, className: "sw-date-field-grid", onKeyDown: onKeyDown, role: "grid", children: [_jsx("thead", { children: _jsx("tr", { children: weekdayLabels(locale, weekStartsOn).map((weekday) => (_jsx("th", { abbr: weekday.long, scope: "col", children: weekday.short }, weekday.long))) }) }), _jsx("tbody", { children: monthGrid(month, weekStartsOn).map((row, rowIndex) => (_jsx("tr", { children: row.map((cell, column) => cell === null ? (_jsx("td", { "aria-disabled": "true", className: "sw-date-field-day sw-date-field-day-outside", role: "gridcell" }, `blank-${column}`)) : (_jsx("td", { "aria-current": cell.value === today ? 'date' : undefined, "aria-disabled": isOutsideDateRange(cell.value, min, max) || undefined, "aria-label": formatDateLabel(cell.value, locale), "aria-selected": cell.value === selected, className: cx('sw-date-field-day', !cell.inMonth && 'sw-date-field-day-outside', ...rangeClassNames(dayInRange(cell.value, range))), "data-date": cell.value, onClick: () => choose(cell.value), onFocus: () => {
31
39
  // A press on a neighboring month's day selects it; moving
32
40
  // the grid first would pull the day out from under it.
33
41
  if (cell.inMonth && cell.value !== focusDate)
@@ -3,6 +3,11 @@ export type CalendarPopupOptions = {
3
3
  value: string;
4
4
  disabled: boolean;
5
5
  onChange: (value: string) => void;
6
+ /**
7
+ * The calendar's today (`YYYY-MM-DD`); the device's local date when left
8
+ * out. An empty or malformed date throws a RangeError.
9
+ */
10
+ today?: string;
6
11
  /** Runs before a chosen date closes the calendar, such as a text reset. */
7
12
  beforeSelect?: () => void;
8
13
  };
@@ -33,4 +38,4 @@ export type CalendarPopup = {
33
38
  * DateField and CalendarButton. Disabling closes it for good; choosing a
34
39
  * date closes it, returns focus to the button, and reports a changed date.
35
40
  */
36
- export declare function useCalendarPopup({ value, disabled, onChange, beforeSelect, }: CalendarPopupOptions): CalendarPopup;
41
+ export declare function useCalendarPopup({ value, disabled, onChange, today, beforeSelect, }: CalendarPopupOptions): CalendarPopup;
@@ -1,11 +1,13 @@
1
1
  import { useId, useRef, useState } from 'react';
2
- import { todayDateOnly } from '../../date-only.js';
2
+ import { assertDateOnly, todayDateOnly } from '../../date-only.js';
3
3
  /**
4
4
  * The open state of a calendar dialog toggled by one button, shared by
5
5
  * DateField and CalendarButton. Disabling closes it for good; choosing a
6
6
  * date closes it, returns focus to the button, and reports a changed date.
7
7
  */
8
- export function useCalendarPopup({ value, disabled, onChange, beforeSelect, }) {
8
+ export function useCalendarPopup({ value, disabled, onChange, today, beforeSelect, }) {
9
+ if (today !== undefined)
10
+ assertDateOnly('today', today);
9
11
  const buttonRef = useRef(null);
10
12
  const [open, setOpen] = useState(false);
11
13
  // Disabling closes the calendar for good, not just while disabled.
@@ -37,7 +39,7 @@ export function useCalendarPopup({ value, disabled, onChange, beforeSelect, }) {
37
39
  id,
38
40
  onClose: close,
39
41
  onSelect: select,
40
- today: todayDateOnly(),
42
+ today: today ?? todayDateOnly(),
41
43
  },
42
44
  };
43
45
  }
@@ -1,4 +1,4 @@
1
1
  import { type DenominationGridProps } from './types.js';
2
- type StripProps = Required<Pick<DenominationGridProps, 'label' | 'columns' | 'rows' | 'zeroLabel'>> & Pick<DenominationGridProps, 'totalLabel'>;
3
- export declare function DenominationStrip({ label, columns, rows, zeroLabel, totalLabel, }: StripProps): import("react").JSX.Element;
2
+ type StripProps = Required<Pick<DenominationGridProps, 'label' | 'columns' | 'rows' | 'zeroLabel'>> & Pick<DenominationGridProps, 'totalLabel' | 'labelWidth'>;
3
+ export declare function DenominationStrip({ label, columns, rows, zeroLabel, totalLabel, labelWidth, }: StripProps): import("react").JSX.Element;
4
4
  export {};
@@ -1,22 +1,33 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { cx } from '../../class-names.js';
3
+ import { denominationLabelWidthClass } from '../../css/css-denomination-aligned.js';
3
4
  import { denominationCellToneClassNames } from '../../css/css-denomination-cell-tones.js';
4
5
  import { denominationCellView } from '../../denomination-cells.js';
5
6
  import { ScrollRegion } from '../ScrollRegion.js';
6
7
  import { RowLabelLine } from './RowLabelLine.js';
7
8
  /**
8
- * The total column's header: a visually hidden `th` when the consumer names
9
- * the column, otherwise the empty corner it has always been. Both carry
10
- * `sw-denomination-total-head`, so below md the column takes no width.
9
+ * The total column's header: the consumer's name for the column, shown over
10
+ * the totals from md up in the column heads' style, and visually hidden
11
+ * below md, where each total sits under its row's label and the column
12
+ * takes no width. Without a name it is the empty corner it has always been.
13
+ * Both carry `sw-denomination-total-head`.
11
14
  */
12
15
  function TotalHead({ totalLabel }) {
13
16
  if (totalLabel === undefined)
14
17
  return _jsx("td", { className: "sw-denomination-corner sw-denomination-total-head" });
15
- return (_jsx("th", { className: "sw-denomination-total-head", scope: "col", children: _jsx("span", { className: "sw-sr-only", children: totalLabel }) }));
18
+ return (_jsx("th", { className: "sw-denomination-head sw-denomination-total-head", scope: "col", children: _jsx("span", { className: "sw-denomination-total-label", children: totalLabel }) }));
16
19
  }
17
- export function DenominationStrip({ label, columns, rows, zeroLabel, totalLabel, }) {
20
+ export function DenominationStrip({ label, columns, rows, zeroLabel, totalLabel, labelWidth, }) {
18
21
  const hasTotals = rows.some((row) => row.total !== undefined);
19
- return (_jsx(ScrollRegion, { "aria-label": label, className: "sw-denomination-scroll", children: _jsxs("table", { className: "sw-denomination-grid sw-denomination-strip", 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: column.label }, column.key))), hasTotals ? _jsx(TotalHead, { totalLabel: totalLabel }) : null] }) }), _jsx("tbody", { children: rows.map((row) => (_jsxs("tr", { className: cx('sw-denomination-row', `sw-denomination-row-${row.tone ?? 'neutral'}`, row.signed && 'sw-denomination-row-signed'), children: [_jsx("th", { className: "sw-denomination-label", scope: "row", children: _jsxs("span", { className: "sw-denomination-label-body", children: [_jsx(RowLabelLine, { row: row }), row.total !== undefined ? (_jsx("span", { "aria-hidden": "true", className: "sw-denomination-total-inline", children: row.total })) : null] }) }), columns.map((column, index) => {
22
+ // An aligned strip sizes its columns from the region's width; it needs
23
+ // the column count and whether a total column takes a share.
24
+ const aligned = labelWidth === undefined
25
+ ? undefined
26
+ : {
27
+ '--sw-denomination-columns': columns.length,
28
+ '--sw-denomination-total': hasTotals ? 1 : 0,
29
+ };
30
+ 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', labelWidth && denominationLabelWidthClass(labelWidth)), 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: column.label }, column.key))), hasTotals ? _jsx(TotalHead, { totalLabel: totalLabel }) : null] }) }), _jsx("tbody", { children: rows.map((row) => (_jsxs("tr", { className: cx('sw-denomination-row', `sw-denomination-row-${row.tone ?? 'neutral'}`, row.signed && 'sw-denomination-row-signed'), children: [_jsx("th", { className: "sw-denomination-label", scope: "row", children: _jsxs("span", { className: "sw-denomination-label-body", children: [_jsx(RowLabelLine, { row: row }), row.total !== undefined ? (_jsx("span", { "aria-hidden": "true", className: "sw-denomination-total-inline", children: row.total })) : null] }) }), columns.map((column, index) => {
20
31
  const view = denominationCellView(row.cells[index] ?? null, row.signed ?? false, zeroLabel);
21
32
  return (_jsx("td", { className: cx('sw-denomination-cell', `sw-denomination-cell-${view.state}`, ...denominationCellToneClassNames(row.cellTones?.[index])), children: view.text }, column.key));
22
33
  }), hasTotals ? (_jsx("td", { className: "sw-denomination-total", children: _jsx("span", { className: "sw-denomination-total-value", children: row.total ?? '' }) })) : null] }, row.id))) })] }) }));
@@ -1,5 +1,7 @@
1
1
  import { type ReactNode } from 'react';
2
2
  import { type BadgeTone } from '../../css/css-data.js';
3
+ import { type DenominationLabelWidth } from '../../css/css-denomination-aligned.js';
4
+ export type { DenominationLabelWidth };
3
5
  export type DenominationGridTone = BadgeTone;
4
6
  export type DenominationGridLayout = 'strip' | 'tiles';
5
7
  /**
@@ -35,8 +37,9 @@ export type DenominationGridProps = {
35
37
  subtotal?: (count: number, column: DenominationGridColumn) => string;
36
38
  zeroLabel?: string;
37
39
  /**
38
- * Names the strip's total column for assistive technology (a visually
39
- * hidden column header), such as "Total". Only the strip has that column.
40
+ * Names the strip's total column, such as "Total": a column header over
41
+ * the totals from md up, visually hidden below md, where each total sits
42
+ * under its row's label. Only the strip has that column.
40
43
  */
41
44
  totalLabel?: string;
42
45
  /**
@@ -46,4 +49,14 @@ export type DenominationGridProps = {
46
49
  * landmark list. Only the tiles layout has row containers.
47
50
  */
48
51
  rowRole?: DenominationGridRowRole;
52
+ /**
53
+ * Lines the strip's columns up with every other strip of the same columns
54
+ * in a container of the same width, such as the cards of a feed: the row
55
+ * labels take this width (`xs` to `xl`, 4 to 16 rem, the `Table` column
56
+ * sizes; pick the narrowest that holds the longest label's longest word)
57
+ * and the count columns share the rest equally, whatever the counts are.
58
+ * Without it each strip sizes its columns by its own content. Only the
59
+ * strip has a label column.
60
+ */
61
+ labelWidth?: DenominationLabelWidth;
49
62
  };
@@ -0,0 +1,42 @@
1
+ import { type TdHTMLAttributes, type ThHTMLAttributes } from 'react';
2
+ import { type TableColumnWidth } from './table-layout.js';
3
+ export type { TableColumnWidth };
4
+ type TableCellAlign = 'start' | 'end';
5
+ /**
6
+ * A sortable column's state: sorted `ascending` or `descending`, or `none`
7
+ * (sortable, not the column the table is sorted by).
8
+ */
9
+ export type TableSort = 'ascending' | 'descending' | 'none';
10
+ type TableHeaderSort = {
11
+ /**
12
+ * Makes a header cell (`as="th"`) a sort control: its children become a
13
+ * button in the header's own text style, with a sort glyph after them (an
14
+ * up chevron when `ascending`, a down chevron when `descending`, a muted
15
+ * pair when `none`), and the cell carries `aria-sort` while it is the
16
+ * sorted column. Pass it with `onSort`, on `th` cells only.
17
+ */
18
+ sort?: TableSort;
19
+ /** Called when the header's sort button is pressed; the consumer sorts. */
20
+ onSort?: () => void;
21
+ };
22
+ export type TableCellProps = ((Omit<TdHTMLAttributes<HTMLTableCellElement>, 'width'> & {
23
+ as?: 'td';
24
+ sort?: never;
25
+ onSort?: never;
26
+ }) | (Omit<ThHTMLAttributes<HTMLTableCellElement>, 'width'> & {
27
+ as: 'th';
28
+ } & TableHeaderSort)) & {
29
+ align?: TableCellAlign;
30
+ numeric?: boolean;
31
+ truncate?: boolean;
32
+ /**
33
+ * The column's width, on its header cell: `xs` to `xl` are widths in rem
34
+ * (4, 6, 8, 12 and 16), which a `fixed` table keeps whatever the rows
35
+ * hold. `min` gives an `auto` table's column the least room its content
36
+ * allows and keeps this cell's text on one line, so set it on every cell
37
+ * of that column; a `fixed` table, which never measures content, treats
38
+ * it as no width.
39
+ */
40
+ width?: TableColumnWidth;
41
+ };
42
+ export declare const TableCell: import("react").ForwardRefExoticComponent<TableCellProps & import("react").RefAttributes<HTMLTableCellElement>>;
@@ -0,0 +1,27 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { forwardRef, } from 'react';
3
+ import { cx } from '../../class-names.js';
4
+ import { assertTableColumnWidth, tableColumnWidthClass, } from './table-layout.js';
5
+ const tableSorts = ['ascending', 'descending', 'none'];
6
+ /** `sort` and `onSort` come together, on a header cell, with a known value. */
7
+ function assertSort(as, sort, onSort) {
8
+ if (sort === undefined && onSort === undefined)
9
+ return;
10
+ if (as !== 'th')
11
+ throw new TypeError('TableCell sort is for a header cell (as="th")');
12
+ if (sort === undefined || typeof onSort !== 'function')
13
+ throw new TypeError('TableCell sort and onSort are passed together');
14
+ if (!tableSorts.includes(sort))
15
+ throw new RangeError(`Unknown TableCell sort: ${String(sort)}`);
16
+ }
17
+ export const TableCell = forwardRef(function TableCell({ align = 'start', as = 'td', children, className, numeric = false, onSort, sort, truncate = false, width, ...rest }, ref) {
18
+ assertSort(as, sort, onSort);
19
+ assertTableColumnWidth(width);
20
+ const Component = as;
21
+ const alignmentClass = numeric
22
+ ? 'sw-table-numeric'
23
+ : align === 'end'
24
+ ? 'sw-table-end'
25
+ : undefined;
26
+ return (_jsx(Component, { ref: ref, "aria-sort": sort === undefined || sort === 'none' ? undefined : sort, className: cx(alignmentClass, truncate && 'sw-table-clip', 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) }));
27
+ });
@@ -0,0 +1,10 @@
1
+ import { type TableColumnWidth } from '../../css/css-table-layout.js';
2
+ export { tableColumnWidthClass } from '../../css/css-table-layout.js';
3
+ export type { TableColumnWidth };
4
+ /** How a table sizes its columns: by content, or from the header row only. */
5
+ export type TableLayout = 'auto' | 'fixed';
6
+ /** Where a cell's content sits in a row taller than it. */
7
+ export type TableVerticalAlign = 'middle' | 'top';
8
+ export declare function assertTableLayout(layout: TableLayout): void;
9
+ export declare function assertTableVerticalAlign(align: TableVerticalAlign): void;
10
+ export declare function assertTableColumnWidth(width: TableColumnWidth | undefined): void;
@@ -0,0 +1,14 @@
1
+ import { tableColumnWidths, } from '../../css/css-table-layout.js';
2
+ export { tableColumnWidthClass } from '../../css/css-table-layout.js';
3
+ export function assertTableLayout(layout) {
4
+ if (layout !== 'auto' && layout !== 'fixed')
5
+ throw new RangeError(`Unknown Table layout: ${String(layout)}`);
6
+ }
7
+ export function assertTableVerticalAlign(align) {
8
+ if (align !== 'middle' && align !== 'top')
9
+ throw new RangeError(`Unknown Table verticalAlign: ${String(align)}`);
10
+ }
11
+ export function assertTableColumnWidth(width) {
12
+ if (width !== undefined && !tableColumnWidths.includes(width))
13
+ throw new RangeError(`Unknown TableCell width: ${String(width)}`);
14
+ }
@@ -0,0 +1,8 @@
1
+ /** Whether a tap at `target` lands on a control inside the tooltip's anchor. */
2
+ export declare function tapActs(target: EventTarget | null, anchor: Element): boolean;
3
+ /**
4
+ * Whether the focus that just landed on `target` is visible: from the
5
+ * keyboard or a script, not a click or a tap, which hover and the tap's own
6
+ * rule already cover. A browser without `:focus-visible` counts it visible.
7
+ */
8
+ export declare function focusIsVisible(target: EventTarget | null): boolean;