@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.
- package/CHANGELOG.md +100 -0
- package/README.md +62 -7
- package/dist/anchored-position.d.ts +14 -6
- package/dist/anchored-position.js +10 -9
- package/dist/components/ActionMenu.d.ts +9 -1
- package/dist/components/ActionMenu.js +2 -2
- package/dist/components/Checkbox.js +2 -1
- package/dist/components/DateField.js +4 -2
- package/dist/components/DenominationGrid.d.ts +1 -1
- package/dist/components/DenominationGrid.js +4 -2
- package/dist/components/Field.js +9 -6
- package/dist/components/FieldErrorRegion.d.ts +14 -0
- package/dist/components/FieldErrorRegion.js +13 -0
- package/dist/components/FieldLabelText.d.ts +13 -0
- package/dist/components/FieldLabelText.js +12 -0
- package/dist/components/RadioGroup.d.ts +7 -0
- package/dist/components/RadioGroup.js +2 -1
- package/dist/components/SegmentedControl.d.ts +8 -0
- package/dist/components/SegmentedControl.js +16 -11
- package/dist/components/Select.d.ts +10 -1
- package/dist/components/Select.js +17 -86
- package/dist/components/Toast.d.ts +15 -1
- package/dist/components/Toast.js +15 -5
- package/dist/components/action-menu/ActionMenuList.d.ts +6 -4
- package/dist/components/action-menu/ActionMenuList.js +5 -4
- package/dist/components/denomination-grid/DenominationStrip.d.ts +2 -2
- package/dist/components/denomination-grid/DenominationStrip.js +13 -3
- package/dist/components/denomination-grid/DenominationTiles.js +27 -8
- package/dist/components/denomination-grid/types.d.ts +5 -0
- package/dist/components/select/SelectListbox.d.ts +20 -0
- package/dist/components/select/SelectListbox.js +16 -0
- package/dist/components/select/SelectValue.d.ts +15 -0
- package/dist/components/select/SelectValue.js +16 -0
- package/dist/components/select/use-select-list.d.ts +26 -0
- package/dist/components/select/use-select-list.js +85 -0
- package/dist/components/use-anchored-popover.d.ts +5 -2
- package/dist/components/use-anchored-popover.js +6 -5
- package/dist/css/chevron.d.ts +5 -0
- package/dist/css/chevron.js +9 -0
- package/dist/css/css-accordion.js +2 -5
- package/dist/css/css-button.d.ts +2 -0
- package/dist/css/css-button.js +28 -4
- package/dist/css/css-checkbox.js +1 -0
- package/dist/css/css-components.js +6 -0
- package/dist/css/css-data.js +17 -163
- package/dist/css/css-date-field.js +14 -6
- package/dist/css/css-denomination-grid.js +11 -1
- package/dist/css/css-document.d.ts +2 -0
- package/dist/css/css-document.js +35 -0
- package/dist/css/css-field.js +15 -2
- package/dist/css/css-filter-chips.js +8 -2
- package/dist/css/css-radio-group.js +12 -2
- package/dist/css/css-segmented.d.ts +2 -0
- package/dist/css/css-segmented.js +127 -0
- package/dist/css/css-select.js +57 -19
- package/dist/css/css-table.d.ts +2 -0
- package/dist/css/css-table.js +99 -0
- package/dist/css/css-toast.d.ts +2 -0
- package/dist/css/css-toast.js +42 -1
- package/dist/css/css-utilities.d.ts +2 -0
- package/dist/css/css-utilities.js +3 -1
- package/dist/css/stylesheet.js +4 -0
- package/dist/index.d.ts +3 -3
- package/dist/index.js +1 -1
- package/dist/styles.css +216 -48
- package/package.json +1 -1
|
@@ -1,7 +1,10 @@
|
|
|
1
|
-
|
|
2
|
-
import {
|
|
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
|
-
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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 {
|
|
3
|
+
import { useId, useRef } from 'react';
|
|
4
4
|
import { cx } from '../class-names.js';
|
|
5
|
-
import {
|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
const
|
|
23
|
-
|
|
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 ? (
|
|
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;
|
package/dist/components/Toast.js
CHANGED
|
@@ -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 =
|
|
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
|
-
},
|
|
59
|
+
}, dismissAfter);
|
|
50
60
|
return () => {
|
|
51
61
|
window.clearTimeout(timer);
|
|
52
62
|
};
|
|
53
|
-
}, [moving, open, onOpenChange
|
|
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:
|
|
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,
|
|
17
|
-
*
|
|
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,
|
|
9
|
-
*
|
|
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
|
-
|
|
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(
|
|
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) =>
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
/**
|
|
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;
|