@scalewing/react 1.21.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.
- package/CHANGELOG.md +23 -0
- package/README.md +46 -4
- package/dist/components/ActionMenu.d.ts +13 -1
- package/dist/components/ActionMenu.js +2 -2
- package/dist/components/BarChart.d.ts +28 -0
- package/dist/components/BarChart.js +5 -4
- package/dist/components/CalendarButton.d.ts +10 -0
- package/dist/components/CalendarButton.js +4 -3
- package/dist/components/DenominationGrid.d.ts +2 -2
- package/dist/components/DenominationGrid.js +5 -2
- package/dist/components/Field.d.ts +9 -0
- package/dist/components/Field.js +3 -2
- package/dist/components/SectionNav.d.ts +44 -0
- package/dist/components/SectionNav.js +48 -0
- package/dist/components/Table.d.ts +38 -13
- package/dist/components/Table.js +7 -12
- package/dist/components/Tabs.d.ts +3 -1
- package/dist/components/Tabs.js +8 -2
- package/dist/components/Tooltip.js +19 -3
- package/dist/components/action-menu/ActionMenuList.d.ts +3 -2
- package/dist/components/action-menu/ActionMenuList.js +16 -4
- package/dist/components/date-field/CalendarDialog.d.ts +4 -1
- package/dist/components/date-field/CalendarDialog.js +2 -2
- package/dist/components/date-field/CalendarGrid.d.ts +4 -1
- package/dist/components/date-field/CalendarGrid.js +11 -3
- package/dist/components/denomination-grid/DenominationStrip.d.ts +2 -2
- package/dist/components/denomination-grid/DenominationStrip.js +17 -6
- package/dist/components/denomination-grid/types.d.ts +15 -2
- package/dist/components/table/TableCell.d.ts +42 -0
- package/dist/components/table/TableCell.js +27 -0
- package/dist/components/table/table-layout.d.ts +10 -0
- package/dist/components/table/table-layout.js +14 -0
- package/dist/components/tooltip/tooltip-open.d.ts +8 -0
- package/dist/components/tooltip/tooltip-open.js +60 -0
- package/dist/components/tooltip/use-tooltip-placement.d.ts +11 -0
- package/dist/components/tooltip/use-tooltip-placement.js +46 -0
- package/dist/components/use-anchored-popover.d.ts +2 -0
- package/dist/components/use-anchored-popover.js +2 -1
- package/dist/css/css-action-menu.js +24 -0
- package/dist/css/css-chart.js +45 -1
- package/dist/css/css-components.js +3 -0
- package/dist/css/css-date-field.js +47 -1
- package/dist/css/css-denomination-aligned.d.ts +7 -0
- package/dist/css/css-denomination-aligned.js +68 -0
- package/dist/css/css-denomination-grid.js +7 -1
- package/dist/css/css-field.js +14 -0
- package/dist/css/css-scroll-region.d.ts +8 -0
- package/dist/css/css-scroll-region.js +9 -3
- package/dist/css/css-section-nav.d.ts +4 -0
- package/dist/css/css-section-nav.js +120 -0
- package/dist/css/css-table-layout.d.ts +20 -0
- package/dist/css/css-table-layout.js +59 -0
- package/dist/css/css-table-sort.d.ts +2 -0
- package/dist/css/css-table-sort.js +105 -0
- package/dist/css/css-table.js +8 -0
- package/dist/css/css-tabs.d.ts +6 -1
- package/dist/css/css-tabs.js +9 -1
- package/dist/css/css-tooltip.js +7 -4
- package/dist/css/stylesheet.js +2 -0
- package/dist/date-only.d.ts +12 -0
- package/dist/date-only.js +24 -0
- package/dist/index.d.ts +4 -3
- package/dist/index.js +1 -0
- package/dist/styles.css +363 -6
- package/package.json +1 -1
package/dist/components/Table.js
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import { forwardRef
|
|
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
|
-
|
|
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
|
|
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;
|
package/dist/components/Tabs.js
CHANGED
|
@@ -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
|
|
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: (
|
|
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
|
|
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
|
-
|
|
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)
|
|
@@ -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:
|
|
9
|
-
* the
|
|
10
|
-
*
|
|
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-
|
|
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
|
-
|
|
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
|
|
39
|
-
*
|
|
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;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A trigger that acts when pressed: a link, a button, a form control or an
|
|
3
|
+
* element with a widget role. A tap on it does what it does and does not
|
|
4
|
+
* also open the tooltip, which would stay over the result (or, after a
|
|
5
|
+
* link, over the next page).
|
|
6
|
+
*/
|
|
7
|
+
const actingRoles = [
|
|
8
|
+
'button',
|
|
9
|
+
'checkbox',
|
|
10
|
+
'combobox',
|
|
11
|
+
'link',
|
|
12
|
+
'menuitem',
|
|
13
|
+
'menuitemcheckbox',
|
|
14
|
+
'menuitemradio',
|
|
15
|
+
'option',
|
|
16
|
+
'radio',
|
|
17
|
+
'searchbox',
|
|
18
|
+
'slider',
|
|
19
|
+
'spinbutton',
|
|
20
|
+
'switch',
|
|
21
|
+
'tab',
|
|
22
|
+
'textbox',
|
|
23
|
+
'treeitem',
|
|
24
|
+
];
|
|
25
|
+
const actingTrigger = [
|
|
26
|
+
'a[href]',
|
|
27
|
+
'area[href]',
|
|
28
|
+
'audio[controls]',
|
|
29
|
+
'button',
|
|
30
|
+
'input',
|
|
31
|
+
'label',
|
|
32
|
+
'select',
|
|
33
|
+
'summary',
|
|
34
|
+
'textarea',
|
|
35
|
+
'video[controls]',
|
|
36
|
+
'[contenteditable]:not([contenteditable="false"])',
|
|
37
|
+
...actingRoles.map((role) => `[role="${role}"]`),
|
|
38
|
+
].join(', ');
|
|
39
|
+
/** Whether a tap at `target` lands on a control inside the tooltip's anchor. */
|
|
40
|
+
export function tapActs(target, anchor) {
|
|
41
|
+
if (!(target instanceof Element))
|
|
42
|
+
return false;
|
|
43
|
+
const control = target.closest(actingTrigger);
|
|
44
|
+
return control !== null && anchor.contains(control);
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Whether the focus that just landed on `target` is visible: from the
|
|
48
|
+
* keyboard or a script, not a click or a tap, which hover and the tap's own
|
|
49
|
+
* rule already cover. A browser without `:focus-visible` counts it visible.
|
|
50
|
+
*/
|
|
51
|
+
export function focusIsVisible(target) {
|
|
52
|
+
if (!(target instanceof Element))
|
|
53
|
+
return true;
|
|
54
|
+
try {
|
|
55
|
+
return target.matches(':focus-visible');
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type RefObject } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* While `shown`, puts the tooltip on the top layer as a manual popover and
|
|
4
|
+
* keeps it beside its anchor and inside the viewport: under the anchor (over
|
|
5
|
+
* it when there is no room below), lined up with its start or, where that
|
|
6
|
+
* would leave the screen, its end, a `space-2` inset from every edge. It
|
|
7
|
+
* follows scrolling, resizing, and a change in either element's size. While
|
|
8
|
+
* hidden the tooltip stays in the page (it may name its trigger) and is not
|
|
9
|
+
* a popover. Without the popover API it keeps the same fixed placement.
|
|
10
|
+
*/
|
|
11
|
+
export declare function useTooltipPlacement(shown: boolean, tooltipRef: RefObject<HTMLElement | null>, anchorRef: RefObject<HTMLElement | null>): void;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { useLayoutEffect } from 'react';
|
|
3
|
+
import { placePopover } from '../use-anchored-popover.js';
|
|
4
|
+
/**
|
|
5
|
+
* While `shown`, puts the tooltip on the top layer as a manual popover and
|
|
6
|
+
* keeps it beside its anchor and inside the viewport: under the anchor (over
|
|
7
|
+
* it when there is no room below), lined up with its start or, where that
|
|
8
|
+
* would leave the screen, its end, a `space-2` inset from every edge. It
|
|
9
|
+
* follows scrolling, resizing, and a change in either element's size. While
|
|
10
|
+
* hidden the tooltip stays in the page (it may name its trigger) and is not
|
|
11
|
+
* a popover. Without the popover API it keeps the same fixed placement.
|
|
12
|
+
*/
|
|
13
|
+
export function useTooltipPlacement(shown, tooltipRef, anchorRef) {
|
|
14
|
+
useLayoutEffect(() => {
|
|
15
|
+
const node = tooltipRef.current;
|
|
16
|
+
const anchor = anchorRef.current;
|
|
17
|
+
if (!shown || !node || !anchor)
|
|
18
|
+
return;
|
|
19
|
+
const supportsPopover = typeof node.showPopover === 'function';
|
|
20
|
+
if (supportsPopover) {
|
|
21
|
+
node.setAttribute('popover', 'manual');
|
|
22
|
+
if (!node.matches(':popover-open'))
|
|
23
|
+
node.showPopover();
|
|
24
|
+
}
|
|
25
|
+
const place = () => {
|
|
26
|
+
if (node && anchor)
|
|
27
|
+
placePopover(node, anchor, { flipInline: true });
|
|
28
|
+
};
|
|
29
|
+
place();
|
|
30
|
+
const resizes = typeof ResizeObserver === 'function' ? new ResizeObserver(place) : null;
|
|
31
|
+
resizes?.observe(node);
|
|
32
|
+
resizes?.observe(anchor);
|
|
33
|
+
window.addEventListener('resize', place);
|
|
34
|
+
window.addEventListener('scroll', place, true);
|
|
35
|
+
return () => {
|
|
36
|
+
resizes?.disconnect();
|
|
37
|
+
window.removeEventListener('resize', place);
|
|
38
|
+
window.removeEventListener('scroll', place, true);
|
|
39
|
+
if (supportsPopover) {
|
|
40
|
+
if (node.matches(':popover-open'))
|
|
41
|
+
node.hidePopover();
|
|
42
|
+
node.removeAttribute('popover');
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
}, [anchorRef, shown, tooltipRef]);
|
|
46
|
+
}
|
|
@@ -6,6 +6,8 @@ export type AnchoredPopoverPlacement = {
|
|
|
6
6
|
/** Line up with the anchor's other edge when the preferred one overflows. */
|
|
7
7
|
flipInline?: boolean;
|
|
8
8
|
};
|
|
9
|
+
/** Places a fixed (or top-layer) `node` beside `anchor`, inside the viewport. */
|
|
10
|
+
export declare function placePopover(node: HTMLElement, anchor: HTMLElement, { align, flipInline }: AnchoredPopoverPlacement): void;
|
|
9
11
|
/**
|
|
10
12
|
* Shows a mounted element on the top layer as a manual popover, keeps it
|
|
11
13
|
* beside `anchorRef` through scroll, window resize, and a change in its own
|