@scalewing/react 1.59.1 → 1.59.3

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,17 @@
1
1
  # @scalewing/react
2
2
 
3
+ ## 1.59.3
4
+
5
+ ### Patch Changes
6
+
7
+ - 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.
8
+
9
+ ## 1.59.2
10
+
11
+ ### Patch Changes
12
+
13
+ - ed35abb: `GlyphTile` letters are readable text: a tile holding letters mixes its categorical fill 25 % toward black, so white letters keep 4.5:1 (WCAG 1.4.3) on every fill in both schemes (neutral 3.26:1 before, 5.35:1 after; orange, the lightest, 5.08:1). A glyph keeps the fill as it is (3:1, WCAG 1.4.11). `Stepper`'s − and + grow to the 44 px touch target both ways on a coarse pointer (32 px wide before). No API change.
14
+
3
15
  ## 1.59.1
4
16
 
5
17
  ### Patch Changes
@@ -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,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);
@@ -3,5 +3,13 @@ export declare const glyphTileSizes: readonly ["sm", "md", "lg"];
3
3
  export type GlyphTileSize = (typeof glyphTileSizes)[number];
4
4
  /** The most letters a tile holds, such as a three-letter crest ("NSL"). */
5
5
  export declare const glyphTileLetterLimit = 3;
6
+ /**
7
+ * How far a tile holding letters mixes its fill toward black. Letters are
8
+ * text (WCAG 1.4.3, 4.5:1), and the categorical fills keep a white glyph at
9
+ * only 3:1 (neutral 3.26:1, orange 3.06:1): a 25 % mix keeps white letters
10
+ * at 5:1 or more on every fill in both schemes (orange, the lightest after
11
+ * the mix, 5.08:1). A glyph keeps the fill as it is.
12
+ */
13
+ export declare const glyphTileLetterDarken = 0.25;
6
14
  export declare function cssGlyphTileClasses(): string;
7
15
  export declare function glyphTileClassCatalog(): string[];
@@ -2,6 +2,14 @@ import { categoricalColorKeys } from '@scalewing/tokens';
2
2
  export const glyphTileSizes = ['sm', 'md', 'lg'];
3
3
  /** The most letters a tile holds, such as a three-letter crest ("NSL"). */
4
4
  export const glyphTileLetterLimit = 3;
5
+ /**
6
+ * How far a tile holding letters mixes its fill toward black. Letters are
7
+ * text (WCAG 1.4.3, 4.5:1), and the categorical fills keep a white glyph at
8
+ * only 3:1 (neutral 3.26:1, orange 3.06:1): a 25 % mix keeps white letters
9
+ * at 5:1 or more on every fill in both schemes (orange, the lightest after
10
+ * the mix, 5.08:1). A glyph keeps the fill as it is.
11
+ */
12
+ export const glyphTileLetterDarken = 0.25;
5
13
  const tileSide = {
6
14
  sm: 'var(--sw-space-5)',
7
15
  md: 'var(--sw-control-xs-min-height)',
@@ -20,7 +28,8 @@ function sizeRules() {
20
28
  /*
21
29
  * A rounded square in a categorical fill with the consumer's glyph centred
22
30
  * in white, sized to spacing step 4 whatever the glyph's own size. It is
23
- * decoration: the words beside it carry the meaning. Forced colors drop the
31
+ * decoration: the words beside it carry the meaning. Letters still have to
32
+ * be readable text, so a tile holding them takes a deeper fill. Forced colors drop the
24
33
  * fill, so a border keeps the tile's shape and the glyph takes the text
25
34
  * color.
26
35
  */
@@ -66,6 +75,11 @@ export function cssGlyphTileClasses() {
66
75
 
67
76
  ${hueRules()}
68
77
 
78
+ /* Letters are text: the fill darkens just enough for 4.5:1 (glyphTileLetterDarken). */
79
+ .sw-glyph-tile:has(> .sw-glyph-tile-text) {
80
+ background: color-mix(in srgb, var(--sw-glyph-tile-fill), black ${glyphTileLetterDarken * 100}%);
81
+ }
82
+
69
83
  ${sizeRules()}
70
84
 
71
85
  @media (forced-colors: active) {
@@ -3,8 +3,8 @@
3
3
  * digits wide by default, the track at its own width even under a wider label
4
4
  * or message (`5ch` plus its padding, so its width never follows
5
5
  * its value), or fills its container with `sw-stepper-full`. The buttons are
6
- * spacing step 6 wide on the subtle fill and every part is the `md` control
7
- * height.
6
+ * spacing step 6 wide on the subtle fill (the 44 px touch target on a coarse
7
+ * pointer) and every part is the `md` control height.
8
8
  */
9
9
  export declare function cssStepperClasses(): string;
10
10
  export declare function stepperClassCatalog(): string[];
@@ -1,5 +1,6 @@
1
1
  import { typographyVariants } from '@scalewing/tokens';
2
2
  import { changedTintLayer } from './css-field-changed.js';
3
+ import { coarsePointerQuery, touchTarget } from './touch-target.js';
3
4
  import { cssStepperUnitClasses, stepperUnitClassCatalog, } from './css-stepper-unit.js';
4
5
  const body = typographyVariants.body;
5
6
  const label = typographyVariants.label;
@@ -47,8 +48,8 @@ function stateRules() {
47
48
  * digits wide by default, the track at its own width even under a wider label
48
49
  * or message (`5ch` plus its padding, so its width never follows
49
50
  * its value), or fills its container with `sw-stepper-full`. The buttons are
50
- * spacing step 6 wide on the subtle fill and every part is the `md` control
51
- * height.
51
+ * spacing step 6 wide on the subtle fill (the 44 px touch target on a coarse
52
+ * pointer) and every part is the `md` control height.
52
53
  */
53
54
  export function cssStepperClasses() {
54
55
  return `.sw-stepper {
@@ -117,6 +118,17 @@ ${input} {
117
118
 
118
119
  .sw-stepper-full input.sw-stepper-input { inline-size: 100%; }
119
120
 
121
+ /* A coarse pointer gets the full touch target: − and + as wide as they are tall. */
122
+ @media ${coarsePointerQuery} {
123
+ .sw-stepper-track {
124
+ grid-template-columns: ${touchTarget} auto ${touchTarget};
125
+ }
126
+
127
+ .sw-stepper-full .sw-stepper-track {
128
+ grid-template-columns: ${touchTarget} minmax(0, 1fr) ${touchTarget};
129
+ }
130
+ }
131
+
120
132
  ${stateRules()}
121
133
 
122
134
  ${cssStepperUnitClasses()}`;
package/dist/styles.css CHANGED
@@ -7128,6 +7128,17 @@ html:has(.sw-site-header-sticky) {
7128
7128
  }
7129
7129
  }
7130
7130
 
7131
+ /* The consumer's muted words on the quiet fill read as the text colour (4.14:1 before). */
7132
+ .sw-context-switcher-trigger[aria-expanded='true'] * {
7133
+ --sw-color-muted: var(--sw-color-text);
7134
+ }
7135
+
7136
+ @media (hover: hover) {
7137
+ .sw-context-switcher-trigger:hover * {
7138
+ --sw-color-muted: var(--sw-color-text);
7139
+ }
7140
+ }
7141
+
7131
7142
  .sw-context-switcher-trigger:focus-visible {
7132
7143
  outline: var(--sw-focus-ring-width) solid var(--sw-color-accent);
7133
7144
  outline-offset: var(--sw-focus-ring-offset);
@@ -8433,6 +8444,11 @@ html:has(.sw-site-header-sticky) {
8433
8444
  .sw-glyph-tile-rose { --sw-glyph-tile-fill: var(--sw-categorical-rose); }
8434
8445
  .sw-glyph-tile-neutral { --sw-glyph-tile-fill: var(--sw-categorical-neutral); }
8435
8446
 
8447
+ /* Letters are text: the fill darkens just enough for 4.5:1 (glyphTileLetterDarken). */
8448
+ .sw-glyph-tile:has(> .sw-glyph-tile-text) {
8449
+ background: color-mix(in srgb, var(--sw-glyph-tile-fill), black 25%);
8450
+ }
8451
+
8436
8452
  .sw-glyph-tile-sm { --sw-glyph-tile-side: var(--sw-space-5); height: var(--sw-space-5); width: var(--sw-space-5); }
8437
8453
  .sw-glyph-tile-md { --sw-glyph-tile-side: var(--sw-control-xs-min-height); height: var(--sw-control-xs-min-height); width: var(--sw-control-xs-min-height); }
8438
8454
  .sw-glyph-tile-lg { --sw-glyph-tile-side: var(--sw-space-6); height: var(--sw-space-6); width: var(--sw-space-6); }
@@ -9996,6 +10012,17 @@ html:has(.sw-site-header-sticky) {
9996
10012
 
9997
10013
  .sw-stepper-full input.sw-stepper-input { inline-size: 100%; }
9998
10014
 
10015
+ /* A coarse pointer gets the full touch target: − and + as wide as they are tall. */
10016
+ @media (pointer: coarse) {
10017
+ .sw-stepper-track {
10018
+ grid-template-columns: var(--sw-control-md-min-height) auto var(--sw-control-md-min-height);
10019
+ }
10020
+
10021
+ .sw-stepper-full .sw-stepper-track {
10022
+ grid-template-columns: var(--sw-control-md-min-height) minmax(0, 1fr) var(--sw-control-md-min-height);
10023
+ }
10024
+ }
10025
+
9999
10026
  [data-theme] .sw-stepper-invalid input.sw-stepper-input {
10000
10027
  border-color: var(--sw-color-danger);
10001
10028
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scalewing/react",
3
- "version": "1.59.1",
3
+ "version": "1.59.3",
4
4
  "description": "Scalewing DOM components and package-owned CSS utilities.",
5
5
  "type": "module",
6
6
  "license": "MIT",