@scalewing/react 1.11.0 → 1.13.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 (66) hide show
  1. package/CHANGELOG.md +100 -0
  2. package/README.md +62 -7
  3. package/dist/anchored-position.d.ts +14 -6
  4. package/dist/anchored-position.js +10 -9
  5. package/dist/components/ActionMenu.d.ts +9 -1
  6. package/dist/components/ActionMenu.js +2 -2
  7. package/dist/components/Checkbox.js +2 -1
  8. package/dist/components/DateField.js +4 -2
  9. package/dist/components/DenominationGrid.d.ts +1 -1
  10. package/dist/components/DenominationGrid.js +4 -2
  11. package/dist/components/Field.js +9 -6
  12. package/dist/components/FieldErrorRegion.d.ts +14 -0
  13. package/dist/components/FieldErrorRegion.js +13 -0
  14. package/dist/components/FieldLabelText.d.ts +13 -0
  15. package/dist/components/FieldLabelText.js +12 -0
  16. package/dist/components/RadioGroup.d.ts +7 -0
  17. package/dist/components/RadioGroup.js +2 -1
  18. package/dist/components/SegmentedControl.d.ts +8 -0
  19. package/dist/components/SegmentedControl.js +16 -11
  20. package/dist/components/Select.d.ts +10 -1
  21. package/dist/components/Select.js +17 -86
  22. package/dist/components/Toast.d.ts +15 -1
  23. package/dist/components/Toast.js +15 -5
  24. package/dist/components/action-menu/ActionMenuList.d.ts +6 -4
  25. package/dist/components/action-menu/ActionMenuList.js +5 -4
  26. package/dist/components/denomination-grid/DenominationStrip.d.ts +2 -2
  27. package/dist/components/denomination-grid/DenominationStrip.js +13 -3
  28. package/dist/components/denomination-grid/DenominationTiles.js +27 -8
  29. package/dist/components/denomination-grid/types.d.ts +5 -0
  30. package/dist/components/select/SelectListbox.d.ts +20 -0
  31. package/dist/components/select/SelectListbox.js +16 -0
  32. package/dist/components/select/SelectValue.d.ts +15 -0
  33. package/dist/components/select/SelectValue.js +16 -0
  34. package/dist/components/select/use-select-list.d.ts +26 -0
  35. package/dist/components/select/use-select-list.js +85 -0
  36. package/dist/components/use-anchored-popover.d.ts +5 -2
  37. package/dist/components/use-anchored-popover.js +6 -5
  38. package/dist/css/chevron.d.ts +5 -0
  39. package/dist/css/chevron.js +9 -0
  40. package/dist/css/css-accordion.js +2 -5
  41. package/dist/css/css-button.d.ts +2 -0
  42. package/dist/css/css-button.js +28 -4
  43. package/dist/css/css-checkbox.js +1 -0
  44. package/dist/css/css-components.js +6 -0
  45. package/dist/css/css-data.js +17 -163
  46. package/dist/css/css-date-field.js +14 -6
  47. package/dist/css/css-denomination-grid.js +11 -1
  48. package/dist/css/css-document.d.ts +2 -0
  49. package/dist/css/css-document.js +35 -0
  50. package/dist/css/css-field.js +15 -2
  51. package/dist/css/css-filter-chips.js +8 -2
  52. package/dist/css/css-radio-group.js +12 -2
  53. package/dist/css/css-segmented.d.ts +2 -0
  54. package/dist/css/css-segmented.js +127 -0
  55. package/dist/css/css-select.js +57 -19
  56. package/dist/css/css-table.d.ts +2 -0
  57. package/dist/css/css-table.js +99 -0
  58. package/dist/css/css-toast.d.ts +2 -0
  59. package/dist/css/css-toast.js +42 -1
  60. package/dist/css/css-utilities.d.ts +2 -0
  61. package/dist/css/css-utilities.js +3 -1
  62. package/dist/css/stylesheet.js +4 -0
  63. package/dist/index.d.ts +3 -3
  64. package/dist/index.js +1 -1
  65. package/dist/styles.css +216 -48
  66. package/package.json +1 -1
@@ -1,7 +1,10 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
2
- import { forwardRef } from 'react';
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { forwardRef, useId } from 'react';
3
4
  import { cx } from '../class-names.js';
4
- export const SegmentedControl = forwardRef(function SegmentedControl({ disabled = false, items, onChange, value, variant = 'compact', ...labelProps }, ref) {
5
+ import { FieldErrorRegion } from './FieldErrorRegion.js';
6
+ export const SegmentedControl = forwardRef(function SegmentedControl({ disabled = false, error, items, onChange, required = false, value, variant = 'compact', ...labelProps }, ref) {
7
+ const errorId = useId();
5
8
  function onKeyDown(event) {
6
9
  if (disabled || (event.key !== 'ArrowRight' && event.key !== 'ArrowLeft')) {
7
10
  return;
@@ -17,12 +20,14 @@ export const SegmentedControl = forwardRef(function SegmentedControl({ disabled
17
20
  onChange?.(next.id);
18
21
  }
19
22
  }
20
- return (_jsx("div", { ref: ref, "aria-disabled": disabled || undefined, className: cx('sw-segmented', variant === 'filled' && 'sw-segmented-filled', disabled && 'sw-segmented-disabled'), onKeyDown: onKeyDown, role: "radiogroup", ...labelProps, children: items.map((item) => {
21
- const selected = item.id === value;
22
- return (_jsx("button", { "aria-checked": selected, className: cx('sw-segmented-item', selected && 'sw-segmented-item-selected'), disabled: disabled, onClick: () => {
23
- if (!selected) {
24
- onChange?.(item.id);
25
- }
26
- }, role: "radio", type: "button", children: item.label }, item.id));
27
- }) }));
23
+ // The wrapper is always rendered, so an error that appears later swaps
24
+ // text into a live region that already exists and the group keeps focus.
25
+ return (_jsxs("div", { className: cx('sw-segmented-field', variant === 'filled' && 'sw-segmented-field-filled'), children: [_jsx("div", { ref: ref, "aria-describedby": error ? errorId : undefined, "aria-disabled": disabled || undefined, "aria-invalid": error ? true : undefined, "aria-required": required || undefined, className: cx('sw-segmented', variant === 'filled' && 'sw-segmented-filled', disabled && 'sw-segmented-disabled'), onKeyDown: onKeyDown, role: "radiogroup", ...labelProps, children: items.map((item) => {
26
+ const selected = item.id === value;
27
+ return (_jsx("button", { "aria-checked": selected, className: cx('sw-segmented-item', selected && 'sw-segmented-item-selected'), disabled: disabled, onClick: () => {
28
+ if (!selected) {
29
+ onChange?.(item.id);
30
+ }
31
+ }, role: "radio", type: "button", children: item.label }, item.id));
32
+ }) }), _jsx(FieldErrorRegion, { className: "sw-field-error", id: errorId, message: error })] }));
28
33
  });
@@ -13,5 +13,14 @@ export type SelectProps = {
13
13
  options: readonly SelectOption[];
14
14
  size?: FieldSize;
15
15
  value: string;
16
+ /**
17
+ * Shown in the closed trigger, muted, while `value` matches no option
18
+ * (such as ''). It is not an option and never becomes the value.
19
+ */
20
+ placeholder?: string;
21
+ /** Marks the label as Field does and sets `aria-required`. */
22
+ required?: boolean;
23
+ /** A validation message under the control, wired as Field's error is. */
24
+ error?: string;
16
25
  };
17
- export declare function Select({ action, label, labelVisuallyHidden, onChange, options, size, value, }: SelectProps): import("react").JSX.Element;
26
+ export declare function Select({ action, label, labelVisuallyHidden, onChange, options, size, value, placeholder, required, error, }: SelectProps): import("react").JSX.Element;
@@ -1,89 +1,28 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
- import { useEffect, useId, useRef, useState, } from 'react';
3
+ import { useId, useRef } from 'react';
4
4
  import { cx } from '../class-names.js';
5
- import { selectIndexForKey, selectedSelectIndex, } from '../select-list.js';
5
+ import { selectedSelectIndex } from '../select-list.js';
6
+ import { FieldErrorRegion } from './FieldErrorRegion.js';
7
+ import { FieldLabelText } from './FieldLabelText.js';
8
+ import { SelectListbox } from './select/SelectListbox.js';
9
+ import { SelectValue } from './select/SelectValue.js';
10
+ import { useSelectList } from './select/use-select-list.js';
6
11
  import { Stack } from './Stack.js';
7
- import { Text } from './Text.js';
8
- function SelectListOption({ action = false, active, children, id, onCommit, onHighlight, selected, }) {
9
- return (_jsx("div", { "aria-selected": selected, className: cx('sw-select-option', action && 'sw-select-action'), "data-active": active ? 'true' : undefined, id: id, onClick: onCommit, onPointerDown: (event) => {
10
- event.preventDefault();
11
- }, onPointerMove: onHighlight, role: "option", children: children }));
12
- }
13
- export function Select({ action, label, labelVisuallyHidden = false, onChange, options, size = 'md', value, }) {
12
+ export function Select({ action, label, labelVisuallyHidden = false, onChange, options, size = 'md', value, placeholder, required = false, error, }) {
13
+ if (placeholder !== undefined && !placeholder.trim())
14
+ throw new RangeError('placeholder must not be empty');
14
15
  const labelId = useId();
15
16
  const triggerId = useId();
16
17
  const listId = useId();
17
18
  const optionIdPrefix = useId();
19
+ const errorId = useId();
18
20
  const rootRef = useRef(null);
19
- const [open, setOpen] = useState(false);
20
- const [highlight, setHighlight] = useState(0);
21
- const selectedIndex = selectedSelectIndex(options, value);
22
- const selected = options[selectedIndex];
23
- const actionIndex = options.length;
24
- const itemCount = action ? actionIndex + 1 : actionIndex;
25
- useEffect(() => {
26
- if (!open) {
27
- return;
28
- }
29
- function onPointerDown(event) {
30
- if (!rootRef.current?.contains(event.target)) {
31
- setOpen(false);
32
- }
33
- }
34
- document.addEventListener('pointerdown', onPointerDown);
35
- return () => {
36
- document.removeEventListener('pointerdown', onPointerDown);
37
- };
38
- }, [open]);
39
- function openList() {
40
- if (itemCount === 0) {
41
- return;
42
- }
43
- setHighlight(selectedIndex);
44
- setOpen(true);
45
- }
46
- function commit(index) {
47
- if (action && index === actionIndex) {
48
- action.onPress();
49
- setOpen(false);
50
- return;
51
- }
52
- const option = options[index];
53
- if (option && option.value !== value) {
54
- onChange(option.value);
55
- }
56
- setOpen(false);
57
- }
58
- function onTriggerKeyDown(event) {
59
- if (event.key === 'Escape' && open) {
60
- event.preventDefault();
61
- event.stopPropagation();
62
- setOpen(false);
63
- return;
64
- }
65
- if (!open) {
66
- if (event.key === 'ArrowDown' ||
67
- event.key === 'ArrowUp' ||
68
- event.key === 'Enter' ||
69
- event.key === ' ') {
70
- event.preventDefault();
71
- openList();
72
- }
73
- return;
74
- }
75
- if (event.key === 'Enter' || event.key === ' ') {
76
- event.preventDefault();
77
- commit(highlight);
78
- return;
79
- }
80
- const next = selectIndexForKey(event.key, highlight, itemCount);
81
- if (next !== null) {
82
- event.preventDefault();
83
- setHighlight(next);
84
- }
85
- }
86
- return (_jsx("div", { ref: rootRef, className: cx('sw-select', size === 'xs' && 'sw-select-xs'), children: _jsxs(Stack, { gap: labelVisuallyHidden ? 0 : 1, children: [_jsx(Text, { as: "label", className: labelVisuallyHidden ? 'sw-sr-only' : undefined, htmlFor: triggerId, id: labelId, variant: size === 'xs' ? 'caption' : 'label', children: label }), _jsxs("div", { className: "sw-select-control", children: [_jsx("button", { "aria-activedescendant": open ? `${optionIdPrefix}-${highlight}` : undefined, "aria-controls": open ? listId : undefined, "aria-expanded": open, "aria-haspopup": "listbox", "aria-labelledby": labelId, className: "sw-select-trigger", id: triggerId, onBlur: (event) => {
21
+ const selected = options[selectedSelectIndex(options, value)];
22
+ const showsPlaceholder = placeholder !== undefined &&
23
+ !options.some((option) => option.value === value);
24
+ const { commit, highlight, onTriggerKeyDown, open, openList, setHighlight, setOpen, } = useSelectList({ action, onChange, options, rootRef, value });
25
+ return (_jsx("div", { ref: rootRef, className: cx('sw-select', size === 'xs' && 'sw-select-xs', error && 'sw-select-invalid'), children: _jsxs(Stack, { gap: labelVisuallyHidden ? 0 : 1, children: [_jsx("label", { htmlFor: triggerId, id: labelId, children: _jsx(FieldLabelText, { label: label, required: required, size: size, visuallyHidden: labelVisuallyHidden }) }), _jsxs("div", { className: "sw-select-control", children: [_jsx("button", { "aria-activedescendant": open ? `${optionIdPrefix}-${highlight}` : undefined, "aria-controls": open ? listId : undefined, "aria-describedby": error ? errorId : undefined, "aria-expanded": open, "aria-haspopup": "listbox", "aria-invalid": error ? true : undefined, "aria-labelledby": labelId, "aria-required": required || undefined, className: "sw-select-trigger", id: triggerId, onBlur: (event) => {
87
26
  const next = event.relatedTarget;
88
27
  if (next instanceof Node && rootRef.current?.contains(next)) {
89
28
  return;
@@ -95,13 +34,5 @@ export function Select({ action, label, labelVisuallyHidden = false, onChange, o
95
34
  return;
96
35
  }
97
36
  openList();
98
- }, onKeyDown: onTriggerKeyDown, role: "combobox", type: "button", children: selected?.label ?? '' }), open ? (_jsxs("div", { "aria-labelledby": labelId, className: "sw-select-list", id: listId, role: "listbox", children: [options.map((option, index) => (_jsx(SelectListOption, { active: index === highlight, id: `${optionIdPrefix}-${index}`, onCommit: () => {
99
- commit(index);
100
- }, onHighlight: () => {
101
- setHighlight(index);
102
- }, selected: option.value === value, children: option.label }, option.value))), action ? (_jsx(SelectListOption, { action: true, active: highlight === actionIndex, id: `${optionIdPrefix}-${actionIndex}`, onCommit: () => {
103
- commit(actionIndex);
104
- }, onHighlight: () => {
105
- setHighlight(actionIndex);
106
- }, selected: false, children: action.label })) : null] })) : null] })] }) }));
37
+ }, onKeyDown: onTriggerKeyDown, role: "combobox", type: "button", children: _jsx(SelectValue, { options: options, placeholder: placeholder, text: showsPlaceholder ? placeholder : (selected?.label ?? ''), textIsPlaceholder: showsPlaceholder }) }), open ? (_jsx(SelectListbox, { actionLabel: action?.label, highlight: highlight, id: listId, labelId: labelId, onCommit: commit, onHighlight: setHighlight, optionIdPrefix: optionIdPrefix, options: options, value: value })) : null] }), _jsx(FieldErrorRegion, { className: "sw-field-error", id: errorId, message: error })] }) }));
107
38
  }
@@ -1,10 +1,24 @@
1
1
  import { type HTMLAttributes, type ReactNode } from 'react';
2
+ import { toastTones, type ToastTone } from '../css/css-toast.js';
3
+ export { toastTones };
4
+ export type { ToastTone };
2
5
  export type ToastProps = Omit<HTMLAttributes<HTMLDivElement>, 'children' | 'popover'> & {
3
6
  open: boolean;
4
7
  onOpenChange: (open: boolean) => void;
8
+ /**
9
+ * How long the toast stays open, in ms. Defaults to 800 for a neutral or
10
+ * success toast and 6000 for a warning or danger one, which must be read.
11
+ */
5
12
  timeoutMs?: number;
6
13
  anchor?: Element | null;
7
14
  target?: Element | null;
15
+ /**
16
+ * `success`, `warning` and `danger` tint the border and `icon`; `danger`
17
+ * is announced as an alert, the others as a status. Default `neutral`.
18
+ */
19
+ tone?: ToastTone;
20
+ /** A consumer glyph before the message, hidden from assistive technology. */
21
+ icon?: ReactNode;
8
22
  children: ReactNode;
9
23
  };
10
- export declare function Toast({ anchor, children, className, onOpenChange, open, target, timeoutMs, ...rest }: ToastProps): import("react").JSX.Element;
24
+ export declare function Toast({ anchor, children, className, onOpenChange, open, target, timeoutMs, tone, icon, ...rest }: ToastProps): import("react").JSX.Element;
@@ -1,15 +1,25 @@
1
1
  'use client';
2
- import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { useLayoutEffect, useEffect, useRef, } from 'react';
4
4
  import { cx } from '../class-names.js';
5
+ import { toastTones } from '../css/css-toast.js';
5
6
  import { spacingClassNames } from '../spacing-classes.js';
6
7
  import { applyTravelVars, clearTravelVars, travelTranslate, } from '../toast-travel.js';
8
+ export { toastTones };
7
9
  const DEFAULT_TIMEOUT_MS = 800;
10
+ /** A warning or an error must be read, not glimpsed. */
11
+ const URGENT_TIMEOUT_MS = 6000;
12
+ function defaultTimeout(tone) {
13
+ return tone === 'warning' || tone === 'danger'
14
+ ? URGENT_TIMEOUT_MS
15
+ : DEFAULT_TIMEOUT_MS;
16
+ }
8
17
  function travels(anchor, target) {
9
18
  return Boolean(anchor && target);
10
19
  }
11
- export function Toast({ anchor = null, children, className, onOpenChange, open, target = null, timeoutMs = DEFAULT_TIMEOUT_MS, ...rest }) {
20
+ export function Toast({ anchor = null, children, className, onOpenChange, open, target = null, timeoutMs, tone = 'neutral', icon, ...rest }) {
12
21
  const nodeRef = useRef(null);
22
+ const dismissAfter = timeoutMs ?? defaultTimeout(tone);
13
23
  const moving = travels(anchor, target);
14
24
  useLayoutEffect(() => {
15
25
  const node = nodeRef.current;
@@ -46,16 +56,16 @@ export function Toast({ anchor = null, children, className, onOpenChange, open,
46
56
  }
47
57
  const timer = window.setTimeout(() => {
48
58
  onOpenChange(false);
49
- }, timeoutMs);
59
+ }, dismissAfter);
50
60
  return () => {
51
61
  window.clearTimeout(timer);
52
62
  };
53
- }, [moving, open, onOpenChange, timeoutMs]);
63
+ }, [dismissAfter, moving, open, onOpenChange]);
54
64
  function onAnimationEnd(event) {
55
65
  if (event.target !== event.currentTarget || !moving) {
56
66
  return;
57
67
  }
58
68
  onOpenChange(false);
59
69
  }
60
- return (_jsx("div", { ...rest, ref: nodeRef, className: cx('sw-toast', moving && 'sw-toast-travel', ...spacingClassNames({ padding: 3 }), className), onAnimationEnd: onAnimationEnd, popover: "manual", role: "status", children: children }));
70
+ return (_jsx("div", { ...rest, ref: nodeRef, className: cx('sw-toast', tone !== 'neutral' && `sw-toast-${tone}`, moving && 'sw-toast-travel', ...spacingClassNames({ padding: 3 }), className), onAnimationEnd: onAnimationEnd, popover: "manual", role: tone === 'danger' ? 'alert' : 'status', children: icon ? (_jsxs("div", { className: "sw-toast-row", children: [_jsx("span", { "aria-hidden": "true", className: "sw-toast-icon", children: icon }), _jsx("div", { className: "sw-toast-body", children: children })] })) : (children) }));
61
71
  }
@@ -1,7 +1,9 @@
1
1
  import { type KeyboardEvent, type RefObject } from 'react';
2
+ import { type InlineAlign } from '../../anchored-position.js';
2
3
  import type { ActionMenuItem } from '../ActionMenu.js';
3
4
  type ActionMenuListProps = {
4
5
  id: string;
6
+ align: InlineAlign;
5
7
  label: string;
6
8
  items: readonly ActionMenuItem[];
7
9
  focusIndex: number;
@@ -13,9 +15,9 @@ type ActionMenuListProps = {
13
15
  };
14
16
  /**
15
17
  * The open menu: on the popover layer beside the trigger (a gap below or
16
- * above it, inset from the viewport edges, and lined up with the trigger's
17
- * end when it would not fit from its start), with the focused command
18
- * holding DOM focus.
18
+ * above it, inset from the viewport edges, lined up with the trigger's start
19
+ * or, with `align="end"`, its end, and with the other edge when that one
20
+ * would not fit), with the focused command holding DOM focus.
19
21
  */
20
- export declare function ActionMenuList({ id, label, items, focusIndex, triggerRef, onDismiss, onFocusIndex, onKeyDown, onSelect, }: ActionMenuListProps): import("react").JSX.Element;
22
+ export declare function ActionMenuList({ id, align, label, items, focusIndex, triggerRef, onDismiss, onFocusIndex, onKeyDown, onSelect, }: ActionMenuListProps): import("react").JSX.Element;
21
23
  export {};
@@ -5,13 +5,14 @@ import { cx } from '../../class-names.js';
5
5
  import { useAnchoredPopover } from '../use-anchored-popover.js';
6
6
  /**
7
7
  * The open menu: on the popover layer beside the trigger (a gap below or
8
- * above it, inset from the viewport edges, and lined up with the trigger's
9
- * end when it would not fit from its start), with the focused command
10
- * holding DOM focus.
8
+ * above it, inset from the viewport edges, lined up with the trigger's start
9
+ * or, with `align="end"`, its end, and with the other edge when that one
10
+ * would not fit), with the focused command holding DOM focus.
11
11
  */
12
- export function ActionMenuList({ id, label, items, focusIndex, triggerRef, onDismiss, onFocusIndex, onKeyDown, onSelect, }) {
12
+ export function ActionMenuList({ id, align, label, items, focusIndex, triggerRef, onDismiss, onFocusIndex, onKeyDown, onSelect, }) {
13
13
  const menuRef = useRef(null);
14
14
  useAnchoredPopover(menuRef, triggerRef, triggerRef, onDismiss, {
15
+ align,
15
16
  flipInline: true,
16
17
  });
17
18
  useLayoutEffect(() => {
@@ -1,4 +1,4 @@
1
1
  import { type DenominationGridProps } from './types.js';
2
- type StripProps = Required<Pick<DenominationGridProps, 'label' | 'columns' | 'rows' | 'zeroLabel'>>;
3
- export declare function DenominationStrip({ label, columns, rows, zeroLabel, }: StripProps): import("react").JSX.Element;
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;
4
4
  export {};
@@ -2,10 +2,20 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { cx } from '../../class-names.js';
3
3
  import { denominationCellView } from '../../denomination-cells.js';
4
4
  import { ScrollRegion } from '../ScrollRegion.js';
5
- export function DenominationStrip({ label, columns, rows, zeroLabel, }) {
5
+ /**
6
+ * The total column's header: a visually hidden `th` when the consumer names
7
+ * the column, otherwise the empty corner it has always been. Both carry
8
+ * `sw-denomination-total-head`, so below md the column takes no width.
9
+ */
10
+ function TotalHead({ totalLabel }) {
11
+ if (totalLabel === undefined)
12
+ return _jsx("td", { className: "sw-denomination-corner sw-denomination-total-head" });
13
+ return (_jsx("th", { className: "sw-denomination-total-head", scope: "col", children: _jsx("span", { className: "sw-sr-only", children: totalLabel }) }));
14
+ }
15
+ export function DenominationStrip({ label, columns, rows, zeroLabel, totalLabel, }) {
6
16
  const hasTotals = rows.some((row) => row.total !== undefined);
7
- 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("td", { className: "sw-denomination-corner" }) : 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: [row.icon ? (_jsx("span", { "aria-hidden": "true", className: "sw-denomination-icon", children: row.icon })) : null, _jsx("span", { className: "sw-denomination-label-text", children: row.label }), row.total !== undefined ? (_jsx("span", { "aria-hidden": "true", className: "sw-denomination-total-inline", children: row.total })) : null] }) }), columns.map((column, index) => {
17
+ 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: [row.icon ? (_jsx("span", { "aria-hidden": "true", className: "sw-denomination-icon", children: row.icon })) : null, _jsx("span", { className: "sw-denomination-label-text", children: row.label }), row.total !== undefined ? (_jsx("span", { "aria-hidden": "true", className: "sw-denomination-total-inline", children: row.total })) : null] }) }), columns.map((column, index) => {
8
18
  const view = denominationCellView(row.cells[index] ?? null, row.signed ?? false, zeroLabel);
9
19
  return (_jsx("td", { className: `sw-denomination-cell sw-denomination-cell-${view.state}`, children: view.text }, column.key));
10
- }), hasTotals ? (_jsx("td", { className: "sw-denomination-total", children: row.total ?? '' })) : null] }, row.id))) })] }) }));
20
+ }), hasTotals ? (_jsx("td", { className: "sw-denomination-total", children: _jsx("span", { className: "sw-denomination-total-value", children: row.total ?? '' }) })) : null] }, row.id))) })] }) }));
11
21
  }
@@ -1,13 +1,32 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { cx } from '../../class-names.js';
3
3
  import { denominationCellView } from '../../denomination-cells.js';
4
+ /**
5
+ * A lone row is named by the grid's own label, so its label line shows only
6
+ * when it adds something: an icon or a total. Several rows always show it.
7
+ */
8
+ function showsRowLabel(rowCount, row) {
9
+ return rowCount > 1 || Boolean(row.icon) || row.total !== undefined;
10
+ }
11
+ /**
12
+ * A row is a region named by its label, except a lone row without its label
13
+ * line whose label repeats the grid's: that region would only say the
14
+ * group's name a second time (Teisoro SDAY-31). A lone row with its own
15
+ * words keeps them, since nothing else on screen says them.
16
+ */
17
+ function namesRow(rowCount, row, gridLabel) {
18
+ return showsRowLabel(rowCount, row) || row.label !== gridLabel;
19
+ }
4
20
  export function DenominationTiles({ label, columns, rows, subtotal, zeroLabel, }) {
5
- return (_jsx("div", { "aria-label": label, className: "sw-denomination-grid sw-denomination-tiles", role: "group", children: rows.map((row) => (_jsxs("section", { "aria-label": row.label, className: cx('sw-denomination-row', `sw-denomination-row-${row.tone ?? 'neutral'}`), children: [rows.length > 1 || row.icon ? (_jsxs("span", { className: "sw-denomination-label", children: [row.icon ? (_jsx("span", { "aria-hidden": "true", className: "sw-denomination-icon", children: row.icon })) : null, _jsx("span", { className: "sw-denomination-label-text", children: row.label }), row.total !== undefined ? (_jsx("span", { className: "sw-denomination-total", children: row.total })) : null] })) : null, _jsx("ul", { className: "sw-denomination-tile-list", children: columns.map((column, index) => {
6
- const cell = row.cells[index] ?? null;
7
- const view = denominationCellView(cell, row.signed ?? false, zeroLabel);
8
- const amount = subtotal && cell !== null && cell !== 0
9
- ? subtotal(cell, column)
10
- : null;
11
- return (_jsxs("li", { className: "sw-denomination-tile", children: [_jsx("span", { className: "sw-denomination-head", children: column.label }), _jsx("span", { className: `sw-denomination-cell sw-denomination-cell-${view.state}`, children: view.text }), subtotal ? (_jsx("span", { className: "sw-denomination-subtotal", children: amount ?? zeroLabel })) : null] }, column.key));
12
- }) })] }, row.id))) }));
21
+ return (_jsx("div", { "aria-label": label, className: "sw-denomination-grid sw-denomination-tiles", role: "group", children: rows.map((row) => {
22
+ const labelled = showsRowLabel(rows.length, row);
23
+ return (_jsxs("section", { "aria-label": namesRow(rows.length, row, label) ? row.label : undefined, className: cx('sw-denomination-row', `sw-denomination-row-${row.tone ?? 'neutral'}`), children: [labelled ? (_jsxs("span", { className: "sw-denomination-label", children: [row.icon ? (_jsx("span", { "aria-hidden": "true", className: "sw-denomination-icon", children: row.icon })) : null, _jsx("span", { className: "sw-denomination-label-text", children: row.label }), row.total !== undefined ? (_jsx("span", { className: "sw-denomination-total", children: row.total })) : null] })) : null, _jsx("ul", { className: "sw-denomination-tile-list", children: columns.map((column, index) => {
24
+ const cell = row.cells[index] ?? null;
25
+ const view = denominationCellView(cell, row.signed ?? false, zeroLabel);
26
+ const amount = subtotal && cell !== null && cell !== 0
27
+ ? subtotal(cell, column)
28
+ : null;
29
+ return (_jsxs("li", { className: "sw-denomination-tile", children: [_jsx("span", { className: "sw-denomination-head", children: column.label }), _jsx("span", { className: `sw-denomination-cell sw-denomination-cell-${view.state}`, children: view.text }), subtotal ? (_jsx("span", { className: "sw-denomination-subtotal", children: amount ?? zeroLabel })) : null] }, column.key));
30
+ }) })] }, row.id));
31
+ }) }));
13
32
  }
@@ -22,4 +22,9 @@ export type DenominationGridProps = {
22
22
  layout?: DenominationGridLayout;
23
23
  subtotal?: (count: number, column: DenominationGridColumn) => string;
24
24
  zeroLabel?: string;
25
+ /**
26
+ * Names the strip's total column for assistive technology (a visually
27
+ * hidden column header), such as "Total". Only the strip has that column.
28
+ */
29
+ totalLabel?: string;
25
30
  };
@@ -0,0 +1,20 @@
1
+ import { type SelectOption } from '../../select-list.js';
2
+ type SelectListboxProps = {
3
+ /** The trailing command's label, after the options; never the value. */
4
+ actionLabel?: string;
5
+ highlight: number;
6
+ id: string;
7
+ labelId: string;
8
+ optionIdPrefix: string;
9
+ options: readonly SelectOption[];
10
+ value: string;
11
+ onCommit: (index: number) => void;
12
+ onHighlight: (index: number) => void;
13
+ };
14
+ /**
15
+ * The open list: one option per value, then the optional action as the last
16
+ * item (index `options.length`). The trigger keeps DOM focus and points at
17
+ * the highlighted item with `aria-activedescendant`.
18
+ */
19
+ export declare function SelectListbox({ actionLabel, highlight, id, labelId, optionIdPrefix, options, value, onCommit, onHighlight, }: SelectListboxProps): import("react").JSX.Element;
20
+ export {};
@@ -0,0 +1,16 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { cx } from '../../class-names.js';
3
+ function SelectListOption({ action = false, active, children, id, onCommit, onHighlight, selected, }) {
4
+ return (_jsx("div", { "aria-selected": selected, className: cx('sw-select-option', action && 'sw-select-action'), "data-active": active ? 'true' : undefined, id: id, onClick: onCommit, onPointerDown: (event) => {
5
+ event.preventDefault();
6
+ }, onPointerMove: onHighlight, role: "option", children: children }));
7
+ }
8
+ /**
9
+ * The open list: one option per value, then the optional action as the last
10
+ * item (index `options.length`). The trigger keeps DOM focus and points at
11
+ * the highlighted item with `aria-activedescendant`.
12
+ */
13
+ export function SelectListbox({ actionLabel, highlight, id, labelId, optionIdPrefix, options, value, onCommit, onHighlight, }) {
14
+ const actionIndex = options.length;
15
+ return (_jsxs("div", { "aria-labelledby": labelId, className: "sw-select-list", id: id, role: "listbox", children: [options.map((option, index) => (_jsx(SelectListOption, { active: index === highlight, id: `${optionIdPrefix}-${index}`, onCommit: () => onCommit(index), onHighlight: () => onHighlight(index), selected: option.value === value, children: option.label }, option.value))), actionLabel === undefined ? null : (_jsx(SelectListOption, { action: true, active: highlight === actionIndex, id: `${optionIdPrefix}-${actionIndex}`, onCommit: () => onCommit(actionIndex), onHighlight: () => onHighlight(actionIndex), selected: false, children: actionLabel }))] }));
16
+ }
@@ -0,0 +1,15 @@
1
+ import { type SelectOption } from '../../select-list.js';
2
+ /**
3
+ * The closed trigger's text: the current label, stacked in one grid cell
4
+ * over a hidden copy of every option's label, so the trigger is as wide as
5
+ * its longest option, as a native select is, and does not change width when
6
+ * the value changes, including to or from the placeholder. The copies are
7
+ * drawn from `data-label` by generated CSS, so they are neither text content
8
+ * nor in the accessibility tree.
9
+ */
10
+ export declare function SelectValue({ options, placeholder, text, textIsPlaceholder, }: {
11
+ options: readonly SelectOption[];
12
+ placeholder?: string;
13
+ text: string;
14
+ textIsPlaceholder: boolean;
15
+ }): import("react").JSX.Element;
@@ -0,0 +1,16 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { cx } from '../../class-names.js';
3
+ /**
4
+ * The closed trigger's text: the current label, stacked in one grid cell
5
+ * over a hidden copy of every option's label, so the trigger is as wide as
6
+ * its longest option, as a native select is, and does not change width when
7
+ * the value changes, including to or from the placeholder. The copies are
8
+ * drawn from `data-label` by generated CSS, so they are neither text content
9
+ * nor in the accessibility tree.
10
+ */
11
+ export function SelectValue({ options, placeholder, text, textIsPlaceholder, }) {
12
+ const labels = options.map((option) => option.label);
13
+ if (placeholder !== undefined)
14
+ labels.push(placeholder);
15
+ return (_jsxs("span", { className: "sw-select-value", children: [_jsx("span", { className: cx('sw-select-value-text', textIsPlaceholder && 'sw-select-placeholder'), children: text }), labels.map((sizerLabel, index) => (_jsx("span", { "aria-hidden": "true", className: "sw-select-value-sizer", "data-label": sizerLabel }, index)))] }));
16
+ }
@@ -0,0 +1,26 @@
1
+ import { type KeyboardEvent, type RefObject } from 'react';
2
+ import { type SelectOption } from '../../select-list.js';
3
+ import type { SelectAction } from '../Select.js';
4
+ type SelectListOptions = {
5
+ action?: SelectAction;
6
+ onChange: (value: string) => void;
7
+ options: readonly SelectOption[];
8
+ rootRef: RefObject<HTMLElement | null>;
9
+ value: string;
10
+ };
11
+ /**
12
+ * The open state, highlighted item and keyboard model of a Select: the
13
+ * trigger keeps focus, arrows move the highlight over the options and the
14
+ * trailing action (index `options.length`), Enter or Space commits, Escape
15
+ * and a press outside the root close.
16
+ */
17
+ export declare function useSelectList({ action, onChange, options, rootRef, value, }: SelectListOptions): {
18
+ commit: (index: number) => void;
19
+ highlight: number;
20
+ onTriggerKeyDown: (event: KeyboardEvent<HTMLButtonElement>) => void;
21
+ open: boolean;
22
+ openList: () => void;
23
+ setHighlight: import("react").Dispatch<import("react").SetStateAction<number>>;
24
+ setOpen: import("react").Dispatch<import("react").SetStateAction<boolean>>;
25
+ };
26
+ export {};
@@ -0,0 +1,85 @@
1
+ import { useEffect, useState } from 'react';
2
+ import { selectIndexForKey, selectedSelectIndex, } from '../../select-list.js';
3
+ /**
4
+ * The open state, highlighted item and keyboard model of a Select: the
5
+ * trigger keeps focus, arrows move the highlight over the options and the
6
+ * trailing action (index `options.length`), Enter or Space commits, Escape
7
+ * and a press outside the root close.
8
+ */
9
+ export function useSelectList({ action, onChange, options, rootRef, value, }) {
10
+ const [open, setOpen] = useState(false);
11
+ const [highlight, setHighlight] = useState(0);
12
+ const selectedIndex = selectedSelectIndex(options, value);
13
+ const actionIndex = options.length;
14
+ const itemCount = action ? actionIndex + 1 : actionIndex;
15
+ useEffect(() => {
16
+ if (!open) {
17
+ return;
18
+ }
19
+ function onPointerDown(event) {
20
+ if (!rootRef.current?.contains(event.target)) {
21
+ setOpen(false);
22
+ }
23
+ }
24
+ document.addEventListener('pointerdown', onPointerDown);
25
+ return () => {
26
+ document.removeEventListener('pointerdown', onPointerDown);
27
+ };
28
+ }, [open, rootRef]);
29
+ function openList() {
30
+ if (itemCount === 0) {
31
+ return;
32
+ }
33
+ setHighlight(selectedIndex);
34
+ setOpen(true);
35
+ }
36
+ function commit(index) {
37
+ if (action && index === actionIndex) {
38
+ action.onPress();
39
+ setOpen(false);
40
+ return;
41
+ }
42
+ const option = options[index];
43
+ if (option && option.value !== value) {
44
+ onChange(option.value);
45
+ }
46
+ setOpen(false);
47
+ }
48
+ function onTriggerKeyDown(event) {
49
+ if (event.key === 'Escape' && open) {
50
+ event.preventDefault();
51
+ event.stopPropagation();
52
+ setOpen(false);
53
+ return;
54
+ }
55
+ if (!open) {
56
+ if (event.key === 'ArrowDown' ||
57
+ event.key === 'ArrowUp' ||
58
+ event.key === 'Enter' ||
59
+ event.key === ' ') {
60
+ event.preventDefault();
61
+ openList();
62
+ }
63
+ return;
64
+ }
65
+ if (event.key === 'Enter' || event.key === ' ') {
66
+ event.preventDefault();
67
+ commit(highlight);
68
+ return;
69
+ }
70
+ const next = selectIndexForKey(event.key, highlight, itemCount);
71
+ if (next !== null) {
72
+ event.preventDefault();
73
+ setHighlight(next);
74
+ }
75
+ }
76
+ return {
77
+ commit,
78
+ highlight,
79
+ onTriggerKeyDown,
80
+ open,
81
+ openList,
82
+ setHighlight,
83
+ setOpen,
84
+ };
85
+ }
@@ -1,6 +1,9 @@
1
1
  import { type RefObject } from 'react';
2
+ import { type InlineAlign } from '../anchored-position.js';
2
3
  export type AnchoredPopoverPlacement = {
3
- /** Line up with the anchor's end when the start-aligned popover overflows. */
4
+ /** The anchor edge the popover prefers to line up with (default start). */
5
+ align?: InlineAlign;
6
+ /** Line up with the anchor's other edge when the preferred one overflows. */
4
7
  flipInline?: boolean;
5
8
  };
6
9
  /**
@@ -11,4 +14,4 @@ export type AnchoredPopoverPlacement = {
11
14
  * outside it and outside `ignoreRef` (the button that toggles it).
12
15
  * Browsers without the popover API get the same fixed placement.
13
16
  */
14
- export declare function useAnchoredPopover(popoverRef: RefObject<HTMLElement | null>, anchorRef: RefObject<HTMLElement | null>, ignoreRef: RefObject<HTMLElement | null>, onDismiss: () => void, { flipInline }?: AnchoredPopoverPlacement): void;
17
+ export declare function useAnchoredPopover(popoverRef: RefObject<HTMLElement | null>, anchorRef: RefObject<HTMLElement | null>, ignoreRef: RefObject<HTMLElement | null>, onDismiss: () => void, { align, flipInline }?: AnchoredPopoverPlacement): void;