@scalewing/react 1.59.2 → 1.59.4
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 +12 -0
- package/dist/components/AppShell.js +15 -4
- package/dist/components/dialog-close-requests.js +16 -1
- package/dist/css/css-action-bar.d.ts +1 -0
- package/dist/css/css-action-bar.js +15 -0
- package/dist/css/css-app-shell-page-bar.d.ts +2 -0
- package/dist/css/css-app-shell-page-bar.js +29 -0
- package/dist/css/css-context-switcher-trigger.js +8 -1
- package/dist/styles.css +31 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# @scalewing/react
|
|
2
2
|
|
|
3
|
+
## 1.59.4
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 29e682e: Focus stays clear of sticky chrome (WCAG 2.4.11). `AppShell` measures its `pageBar` (as it does the compact bar) and gives everything in `main` a `scroll-margin-block-start` past the page bar and, below `lg`, the compact bar, so a control the browser scrolls into view on focus stops under them, not behind them. A stuck `ActionBar` gives the document a `scroll-padding-block-end` of its one-row height and lift, so a control above it is not scrolled behind it. No API change.
|
|
8
|
+
|
|
9
|
+
## 1.59.3
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- f929c5c: A `Dialog` unmounted while open (a consumer that renders it only while it is open) now closes first, while it is still in the document, so the browser hands focus back to the element that had it when the dialog opened, as a dialog closed by `open={false}` does; removed while open, a modal dialog left focus on the body (WCAG 2.4.3). `onClose` is not asked. A `ContextSwitcher` trigger's muted words read as the text colour while it is open or hovered, on its quiet fill (4.14:1 before), as rows already do. No API change.
|
|
14
|
+
|
|
3
15
|
## 1.59.2
|
|
4
16
|
|
|
5
17
|
### Patch Changes
|
|
@@ -3,7 +3,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
import { useEffect, useId, useRef, useState } from 'react';
|
|
4
4
|
import { breakpointQuery } from '../css/breakpoints.js';
|
|
5
5
|
import { appShellWideQuery } from '../css/css-app-shell.js';
|
|
6
|
-
import { appShellBarHeightVariable } from '../css/css-app-shell-page-bar.js';
|
|
6
|
+
import { appShellBarHeightVariable, appShellPageBarHeightVariable, } from '../css/css-app-shell-page-bar.js';
|
|
7
7
|
import { AppShellBar } from './app-shell/AppShellBar.js';
|
|
8
8
|
import { AppShellDrawer } from './app-shell/AppShellDrawer.js';
|
|
9
9
|
import { AppShellSidebar } from './app-shell/AppShellSidebar.js';
|
|
@@ -47,9 +47,20 @@ export function AppShell({ barEnd, barTitle, children, footer, groups, header, l
|
|
|
47
47
|
// make it; the generated CSS falls back to the bar's usual height.
|
|
48
48
|
const hasPageBar = pageBar !== undefined && pageBar !== null && pageBar !== false;
|
|
49
49
|
const barHeight = useElementHeight(barRef, hasPageBar);
|
|
50
|
-
|
|
50
|
+
// The page bar's own height keeps a focused control clear of it when the
|
|
51
|
+
// browser scrolls it into view (WCAG 2.4.11), however much it wraps.
|
|
52
|
+
const pageBarRef = useRef(null);
|
|
53
|
+
const pageBarHeight = useElementHeight(pageBarRef, hasPageBar);
|
|
54
|
+
const shellStyle = barHeight === null && pageBarHeight === null
|
|
51
55
|
? undefined
|
|
52
|
-
: {
|
|
56
|
+
: {
|
|
57
|
+
...(barHeight === null
|
|
58
|
+
? {}
|
|
59
|
+
: { [appShellBarHeightVariable]: `${barHeight}px` }),
|
|
60
|
+
...(pageBarHeight === null
|
|
61
|
+
? {}
|
|
62
|
+
: { [appShellPageBarHeightVariable]: `${pageBarHeight}px` }),
|
|
63
|
+
};
|
|
53
64
|
// The drawer belongs to the bar, which is gone from `lg` up: a window
|
|
54
65
|
// widened while it is open closes it.
|
|
55
66
|
if (sidebarShown && menuOpen)
|
|
@@ -63,7 +74,7 @@ export function AppShell({ barEnd, barTitle, children, footer, groups, header, l
|
|
|
63
74
|
refocusRef.current = false;
|
|
64
75
|
menuRef.current?.focus();
|
|
65
76
|
}, [menuOpen]);
|
|
66
|
-
return (_jsxs("div", { className: "sw-app-shell", style: shellStyle, children: [skipLink ? (_jsx(SkipToMain, { label: skipLink.label, mainId: mainId, mainRef: mainRef })) : null, _jsx(AppShellBar, { barEnd: barEnd, barRef: barRef, barTitle: barTitle, menuCount: menuCount, menuCountLabel: menuCountLabel, menuCountTone: menuCountTone, menuIcon: menuIcon, menuOpen: menuOpen, menuRef: menuRef, onOpenMenu: () => setMenuOpen(true), openMenuLabel: labels.openMenu }), _jsx(AppShellSidebar, { footer: footer, groups: groups, header: header, navLabel: navLabel, onNavigate: onNavigate, rail: sidebarShown && !wide, railFooter: railFooter, railHeader: railHeader }), _jsxs("main", { ref: mainRef, className: "sw-app-shell-main", id: skipLink ? mainId : undefined, tabIndex: skipLink ? -1 : undefined, children: [hasPageBar ? (_jsx("div", { className: "sw-app-shell-page-bar", children: pageBar })) : null, children] }), _jsx(AppShellDrawer, { closeLabel: labels.closeMenu, footer: footer, groups: groups, header: header, navLabel: navLabel, onChoose: () => setMenuOpen(false), onClose: () => {
|
|
77
|
+
return (_jsxs("div", { className: "sw-app-shell", style: shellStyle, children: [skipLink ? (_jsx(SkipToMain, { label: skipLink.label, mainId: mainId, mainRef: mainRef })) : null, _jsx(AppShellBar, { barEnd: barEnd, barRef: barRef, barTitle: barTitle, menuCount: menuCount, menuCountLabel: menuCountLabel, menuCountTone: menuCountTone, menuIcon: menuIcon, menuOpen: menuOpen, menuRef: menuRef, onOpenMenu: () => setMenuOpen(true), openMenuLabel: labels.openMenu }), _jsx(AppShellSidebar, { footer: footer, groups: groups, header: header, navLabel: navLabel, onNavigate: onNavigate, rail: sidebarShown && !wide, railFooter: railFooter, railHeader: railHeader }), _jsxs("main", { ref: mainRef, className: "sw-app-shell-main", id: skipLink ? mainId : undefined, tabIndex: skipLink ? -1 : undefined, children: [hasPageBar ? (_jsx("div", { ref: pageBarRef, className: "sw-app-shell-page-bar", children: pageBar })) : null, children] }), _jsx(AppShellDrawer, { closeLabel: labels.closeMenu, footer: footer, groups: groups, header: header, navLabel: navLabel, onChoose: () => setMenuOpen(false), onClose: () => {
|
|
67
78
|
refocusRef.current = true;
|
|
68
79
|
setMenuOpen(false);
|
|
69
80
|
}, onNavigate: onNavigate, open: menuOpen })] }));
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useEffect, useRef, useState, } from 'react';
|
|
1
|
+
import { useEffect, useLayoutEffect, useRef, useState, } from 'react';
|
|
2
2
|
/**
|
|
3
3
|
* A press on the backdrop: the target is the dialog element itself (not a
|
|
4
4
|
* child, a nested dialog's backdrop, or a popover that overflows the box)
|
|
@@ -77,6 +77,21 @@ export function useDialogCloseRequests({ open, onClose, onCancel, onKeyDown, onP
|
|
|
77
77
|
if (!open && node.open)
|
|
78
78
|
node.close();
|
|
79
79
|
}, [open, browserCloses]);
|
|
80
|
+
// A consumer that renders the dialog only while it is open unmounts it
|
|
81
|
+
// instead of passing `open={false}`. Closing it first, while it is still
|
|
82
|
+
// in the document (a layout cleanup runs before React removes the node),
|
|
83
|
+
// lets the browser hand focus back to the element that had it when the
|
|
84
|
+
// dialog opened, as a dialog closed by its prop does. Removed while open,
|
|
85
|
+
// a modal dialog leaves focus on the body (WCAG 2.4.3).
|
|
86
|
+
useLayoutEffect(() => {
|
|
87
|
+
const node = nodeRef.current;
|
|
88
|
+
return () => {
|
|
89
|
+
if (!node?.open)
|
|
90
|
+
return;
|
|
91
|
+
openRef.current = false;
|
|
92
|
+
node.close();
|
|
93
|
+
};
|
|
94
|
+
}, []);
|
|
80
95
|
function handleKeyDown(event) {
|
|
81
96
|
onKeyDown?.(event);
|
|
82
97
|
// A nested control (a menu, a listbox, a tooltip, an inner dialog) or the
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type Breakpoint } from './breakpoints.js';
|
|
2
2
|
export declare function actionBarStickyClass(stickyBelow: Breakpoint | undefined): string;
|
|
3
|
+
export declare const actionBarClearance = "calc(var(--sw-control-md-min-height) + 2 * var(--sw-space-3) + 2px + 2 * var(--sw-space-2) + env(safe-area-inset-bottom, 0px))";
|
|
3
4
|
export declare function cssActionBarClasses(): string;
|
|
4
5
|
export declare function actionBarClassCatalog(): string[];
|
|
@@ -18,18 +18,33 @@ export function actionBarStickyClass(stickyBelow) {
|
|
|
18
18
|
? `sw-action-bar-sticky-below-${stickyBelow}`
|
|
19
19
|
: 'sw-action-bar-sticky';
|
|
20
20
|
}
|
|
21
|
+
/*
|
|
22
|
+
* While a bar is stuck to the bottom, the document keeps a focused control
|
|
23
|
+
* clear of it when the browser scrolls it into view (WCAG 2.4.11): the
|
|
24
|
+
* bar's one-row height (an md control in its space-3 padding and hairline
|
|
25
|
+
* border), its lift off the edge, and a space-2 gap. A bar that wraps to a
|
|
26
|
+
* second row is taller than this.
|
|
27
|
+
*/
|
|
28
|
+
export const actionBarClearance = 'calc(var(--sw-control-md-min-height) + 2 * var(--sw-space-3) + 2px + 2 * var(--sw-space-2) + env(safe-area-inset-bottom, 0px))';
|
|
29
|
+
function clearOf(stickyClass, indent) {
|
|
30
|
+
return `${indent}:root:has(.${stickyClass}) { scroll-padding-block-end: ${actionBarClearance}; }`;
|
|
31
|
+
}
|
|
21
32
|
function stickyRules() {
|
|
22
33
|
const below = breakpoints
|
|
23
34
|
.map((breakpoint) => `@media ${breakpointQuery('below', breakpoint)} {
|
|
24
35
|
.${actionBarStickyClass(breakpoint)} {
|
|
25
36
|
${stuckToBottom(' ')}
|
|
26
37
|
}
|
|
38
|
+
|
|
39
|
+
${clearOf(actionBarStickyClass(breakpoint), ' ')}
|
|
27
40
|
}`)
|
|
28
41
|
.join('\n\n');
|
|
29
42
|
return `.sw-action-bar-sticky {
|
|
30
43
|
${stuckToBottom(' ')}
|
|
31
44
|
}
|
|
32
45
|
|
|
46
|
+
${clearOf('sw-action-bar-sticky', '')}
|
|
47
|
+
|
|
33
48
|
${below}`;
|
|
34
49
|
}
|
|
35
50
|
export function cssActionBarClasses() {
|
|
@@ -1,3 +1,5 @@
|
|
|
1
1
|
export declare const appShellBarHeightVariable = "--sw-app-shell-bar-height";
|
|
2
|
+
/** The page bar's measured height, set beside the bar's. */
|
|
3
|
+
export declare const appShellPageBarHeightVariable = "--sw-app-shell-page-bar-height";
|
|
2
4
|
export declare function cssAppShellPageBarClasses(): string;
|
|
3
5
|
export declare function appShellPageBarClassCatalog(): string[];
|
|
@@ -7,6 +7,8 @@ import { coarsePointerQuery, touchTarget } from './touch-target.js';
|
|
|
7
7
|
* the bar whatever its end controls make it.
|
|
8
8
|
*/
|
|
9
9
|
export const appShellBarHeightVariable = '--sw-app-shell-bar-height';
|
|
10
|
+
/** The page bar's measured height, set beside the bar's. */
|
|
11
|
+
export const appShellPageBarHeightVariable = '--sw-app-shell-page-bar-height';
|
|
10
12
|
/*
|
|
11
13
|
* The bar's usual height before it is measured (and without script): the
|
|
12
14
|
* sm menu button, or the touch target on a coarse pointer, inside the
|
|
@@ -51,6 +53,33 @@ function pageBarRules() {
|
|
|
51
53
|
padding-inline: var(--sw-space-6);
|
|
52
54
|
top: 0;
|
|
53
55
|
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
${focusClearRules()}`;
|
|
59
|
+
}
|
|
60
|
+
/*
|
|
61
|
+
* Focus clear of the sticky chrome (WCAG 2.4.11): when the browser scrolls a
|
|
62
|
+
* focused control into view, it stops a space-2 below the page bar (and the
|
|
63
|
+
* compact bar over it below `lg`), not under them. The page bar's measured
|
|
64
|
+
* height falls back to its usual one: an sm control in its space-2 padding
|
|
65
|
+
* and hairline.
|
|
66
|
+
*/
|
|
67
|
+
function focusClearRules() {
|
|
68
|
+
const pageBar = `var(${appShellPageBarHeightVariable}, ${barHeightFallback('var(--sw-control-sm-min-height)')})`;
|
|
69
|
+
return `.sw-app-shell-main:has(> .sw-app-shell-page-bar) * {
|
|
70
|
+
scroll-margin-block-start: calc(var(${appShellBarHeightVariable}, ${barHeightFallback('var(--sw-control-sm-min-height)')}) + ${pageBar} + var(--sw-space-2));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
@media ${coarsePointerQuery} {
|
|
74
|
+
.sw-app-shell-main:has(> .sw-app-shell-page-bar) * {
|
|
75
|
+
scroll-margin-block-start: calc(var(${appShellBarHeightVariable}, ${barHeightFallback(touchTarget)}) + ${pageBar} + var(--sw-space-2));
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
@media ${fromLg} {
|
|
80
|
+
.sw-app-shell-main:has(> .sw-app-shell-page-bar) * {
|
|
81
|
+
scroll-margin-block-start: calc(${pageBar} + var(--sw-space-2));
|
|
82
|
+
}
|
|
54
83
|
}`;
|
|
55
84
|
}
|
|
56
85
|
/*
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { typographyVariants } from '@scalewing/tokens';
|
|
2
2
|
import { chevronStroke } from './chevron.js';
|
|
3
|
-
import { quietFill } from './quiet-fill.js';
|
|
3
|
+
import { mutedReadsAsText, quietFill } from './quiet-fill.js';
|
|
4
4
|
import { coarsePointerQuery, touchTarget } from './touch-target.js';
|
|
5
5
|
const label = typographyVariants.label;
|
|
6
6
|
export const contextSwitcherTriggerAppearances = [
|
|
@@ -91,6 +91,13 @@ ${trigger}[aria-expanded='true'] {
|
|
|
91
91
|
}
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
+
/* The consumer's muted words on the quiet fill read as the text colour (4.14:1 before). */
|
|
95
|
+
${mutedReadsAsText([`${trigger}[aria-expanded='true'] *`])}
|
|
96
|
+
|
|
97
|
+
@media (hover: hover) {
|
|
98
|
+
${mutedReadsAsText([`${trigger}:hover *`])}
|
|
99
|
+
}
|
|
100
|
+
|
|
94
101
|
${trigger}:focus-visible {
|
|
95
102
|
outline: var(--sw-focus-ring-width) solid var(--sw-color-accent);
|
|
96
103
|
outline-offset: var(--sw-focus-ring-offset);
|
package/dist/styles.css
CHANGED
|
@@ -6029,6 +6029,22 @@ html:has(.sw-site-header-sticky) {
|
|
|
6029
6029
|
}
|
|
6030
6030
|
}
|
|
6031
6031
|
|
|
6032
|
+
.sw-app-shell-main:has(> .sw-app-shell-page-bar) * {
|
|
6033
|
+
scroll-margin-block-start: calc(var(--sw-app-shell-bar-height, calc(var(--sw-control-sm-min-height) + 2 * var(--sw-space-2) + 1px)) + var(--sw-app-shell-page-bar-height, calc(var(--sw-control-sm-min-height) + 2 * var(--sw-space-2) + 1px)) + var(--sw-space-2));
|
|
6034
|
+
}
|
|
6035
|
+
|
|
6036
|
+
@media (pointer: coarse) {
|
|
6037
|
+
.sw-app-shell-main:has(> .sw-app-shell-page-bar) * {
|
|
6038
|
+
scroll-margin-block-start: calc(var(--sw-app-shell-bar-height, calc(var(--sw-control-md-min-height) + 2 * var(--sw-space-2) + 1px)) + var(--sw-app-shell-page-bar-height, calc(var(--sw-control-sm-min-height) + 2 * var(--sw-space-2) + 1px)) + var(--sw-space-2));
|
|
6039
|
+
}
|
|
6040
|
+
}
|
|
6041
|
+
|
|
6042
|
+
@media (min-width: 64rem) {
|
|
6043
|
+
.sw-app-shell-main:has(> .sw-app-shell-page-bar) * {
|
|
6044
|
+
scroll-margin-block-start: calc(var(--sw-app-shell-page-bar-height, calc(var(--sw-control-sm-min-height) + 2 * var(--sw-space-2) + 1px)) + var(--sw-space-2));
|
|
6045
|
+
}
|
|
6046
|
+
}
|
|
6047
|
+
|
|
6032
6048
|
.sw-app-shell-menu {
|
|
6033
6049
|
position: relative;
|
|
6034
6050
|
}
|
|
@@ -7128,6 +7144,17 @@ html:has(.sw-site-header-sticky) {
|
|
|
7128
7144
|
}
|
|
7129
7145
|
}
|
|
7130
7146
|
|
|
7147
|
+
/* The consumer's muted words on the quiet fill read as the text colour (4.14:1 before). */
|
|
7148
|
+
.sw-context-switcher-trigger[aria-expanded='true'] * {
|
|
7149
|
+
--sw-color-muted: var(--sw-color-text);
|
|
7150
|
+
}
|
|
7151
|
+
|
|
7152
|
+
@media (hover: hover) {
|
|
7153
|
+
.sw-context-switcher-trigger:hover * {
|
|
7154
|
+
--sw-color-muted: var(--sw-color-text);
|
|
7155
|
+
}
|
|
7156
|
+
}
|
|
7157
|
+
|
|
7131
7158
|
.sw-context-switcher-trigger:focus-visible {
|
|
7132
7159
|
outline: var(--sw-focus-ring-width) solid var(--sw-color-accent);
|
|
7133
7160
|
outline-offset: var(--sw-focus-ring-offset);
|
|
@@ -7913,12 +7940,16 @@ html:has(.sw-site-header-sticky) {
|
|
|
7913
7940
|
z-index: 3;
|
|
7914
7941
|
}
|
|
7915
7942
|
|
|
7943
|
+
:root:has(.sw-action-bar-sticky) { scroll-padding-block-end: calc(var(--sw-control-md-min-height) + 2 * var(--sw-space-3) + 2px + 2 * var(--sw-space-2) + env(safe-area-inset-bottom, 0px)); }
|
|
7944
|
+
|
|
7916
7945
|
@media not all and (min-width: 48rem) {
|
|
7917
7946
|
.sw-action-bar-sticky-below-md {
|
|
7918
7947
|
bottom: calc(var(--sw-space-2) + env(safe-area-inset-bottom, 0px));
|
|
7919
7948
|
position: sticky;
|
|
7920
7949
|
z-index: 3;
|
|
7921
7950
|
}
|
|
7951
|
+
|
|
7952
|
+
:root:has(.sw-action-bar-sticky-below-md) { scroll-padding-block-end: calc(var(--sw-control-md-min-height) + 2 * var(--sw-space-3) + 2px + 2 * var(--sw-space-2) + env(safe-area-inset-bottom, 0px)); }
|
|
7922
7953
|
}
|
|
7923
7954
|
|
|
7924
7955
|
.sw-switch {
|