@scalewing/react 1.59.3 → 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 CHANGED
@@ -1,5 +1,11 @@
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
+
3
9
  ## 1.59.3
4
10
 
5
11
  ### 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
- const shellStyle = barHeight === null
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
- : { [appShellBarHeightVariable]: `${barHeight}px` };
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,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
  /*
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
  }
@@ -7924,12 +7940,16 @@ html:has(.sw-site-header-sticky) {
7924
7940
  z-index: 3;
7925
7941
  }
7926
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
+
7927
7945
  @media not all and (min-width: 48rem) {
7928
7946
  .sw-action-bar-sticky-below-md {
7929
7947
  bottom: calc(var(--sw-space-2) + env(safe-area-inset-bottom, 0px));
7930
7948
  position: sticky;
7931
7949
  z-index: 3;
7932
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)); }
7933
7953
  }
7934
7954
 
7935
7955
  .sw-switch {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scalewing/react",
3
- "version": "1.59.3",
3
+ "version": "1.59.4",
4
4
  "description": "Scalewing DOM components and package-owned CSS utilities.",
5
5
  "type": "module",
6
6
  "license": "MIT",