@multiplatform.one/theme 7.63.4 → 7.65.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/package.json +5 -5
- package/src/keyboardFocusRing.openFocus.spec.ts +96 -0
- package/src/keyboardFocusRing.ts +38 -1
- package/src/theme/createThemes.ts +57 -6
- package/src/theme/focusState.spec.ts +15 -15
- package/src/theme/focusState.ts +18 -42
- package/src/theme/hairlineToken.spec.ts +2 -2
- package/src/theme/motion.spec.ts +137 -0
- package/src/theme/motion.ts +20 -7
- package/src/theme/mpo19Proof.spec.ts +3 -3
- package/src/theme/overlaySurfaces.tsx +9 -4
- package/src/theme/resolveKnobs.spec.ts +1 -1
- package/src/theme/resolveKnobs.ts +5 -5
- package/src/theme/transitionProps.native.ts +18 -1
- package/src/theme/transitionProps.spec.ts +2 -2
- package/src/theme/transitionProps.ts +11 -1
- package/types/keyboardFocusRing.d.ts +11 -0
- package/types/keyboardFocusRing.d.ts.map +1 -1
- package/types/theme/createThemes.d.ts.map +1 -1
- package/types/theme/focusState.d.ts +17 -41
- package/types/theme/focusState.d.ts.map +1 -1
- package/types/theme/motion.d.ts +2 -0
- package/types/theme/motion.d.ts.map +1 -1
- package/types/theme/overlaySurfaces.d.ts.map +1 -1
- package/types/theme/transitionProps.native.d.ts.map +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@multiplatform.one/theme",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.65.0",
|
|
4
4
|
"description": "Tamagui theme system for multiplatform.one",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"multiplatform",
|
|
@@ -72,8 +72,8 @@
|
|
|
72
72
|
"access": "public"
|
|
73
73
|
},
|
|
74
74
|
"dependencies": {
|
|
75
|
-
"@multiplatform.one/platform": "^7.
|
|
76
|
-
"@multiplatform.one/store": "^7.
|
|
75
|
+
"@multiplatform.one/platform": "^7.65.0",
|
|
76
|
+
"@multiplatform.one/store": "^7.65.0",
|
|
77
77
|
"@tamagui/animations-react-native": "2.7.6",
|
|
78
78
|
"@tamagui/colors": "2.7.6",
|
|
79
79
|
"@tamagui/config": "2.7.6",
|
|
@@ -89,8 +89,8 @@
|
|
|
89
89
|
},
|
|
90
90
|
"devDependencies": {
|
|
91
91
|
"@expo/prebuild-config": "55.0.18",
|
|
92
|
-
"@multiplatform.one/config": "7.
|
|
93
|
-
"@multiplatform.one/test-utils": "7.
|
|
92
|
+
"@multiplatform.one/config": "7.65.0",
|
|
93
|
+
"@multiplatform.one/test-utils": "7.65.0",
|
|
94
94
|
"@tamagui/animations-css": "2.7.6",
|
|
95
95
|
"@tamagui/animations-reanimated": "2.7.6",
|
|
96
96
|
"@tamagui/font-inter": "2.7.6",
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FZ-7944 round 2, LC-71: an overlay's open focus paints a ring only when the
|
|
3
|
+
* open came from the keyboard. Any other open lands on the frame, except a
|
|
4
|
+
* mouse open with a first text field, which takes the caret.
|
|
5
|
+
*/
|
|
6
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
7
|
+
import { ensureKeyboardModalityTracking, focusOnOpen } from "./keyboardFocusRing";
|
|
8
|
+
|
|
9
|
+
function pointer(pointerType: "mouse" | "touch" | "pen") {
|
|
10
|
+
const event = new Event("pointerdown", { bubbles: true });
|
|
11
|
+
Object.defineProperty(event, "pointerType", { value: pointerType });
|
|
12
|
+
document.body.dispatchEvent(event);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function key() {
|
|
16
|
+
document.body.dispatchEvent(new KeyboardEvent("keydown", { key: "Enter", bubbles: true }));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function overlay(tag: "button" | "input", type?: string) {
|
|
20
|
+
const frame = document.createElement("div");
|
|
21
|
+
frame.tabIndex = -1;
|
|
22
|
+
const target = document.createElement(tag);
|
|
23
|
+
if (type) target.setAttribute("type", type);
|
|
24
|
+
frame.append(target);
|
|
25
|
+
document.body.append(frame);
|
|
26
|
+
const focus = { frame: vi.spyOn(frame, "focus"), target: vi.spyOn(target, "focus") };
|
|
27
|
+
return { frame, target, focus };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
describe("focusOnOpen (LC-71, FZ-7944)", () => {
|
|
31
|
+
beforeEach(() => ensureKeyboardModalityTracking());
|
|
32
|
+
afterEach(() => {
|
|
33
|
+
document.body.replaceChildren();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("a keyboard open focuses the first control and paints its ring", () => {
|
|
37
|
+
key();
|
|
38
|
+
const { frame, target, focus } = overlay("button");
|
|
39
|
+
expect(focusOnOpen(target, frame)).toBe(target);
|
|
40
|
+
expect(focus.target).toHaveBeenCalledWith({ preventScroll: true, focusVisible: true });
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("a keyboard open still puts the caret in the first field", () => {
|
|
44
|
+
key();
|
|
45
|
+
const { frame, target, focus } = overlay("input");
|
|
46
|
+
expect(focusOnOpen(target, frame)).toBe(target);
|
|
47
|
+
expect(focus.target).toHaveBeenCalledWith({ preventScroll: true, focusVisible: true });
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("a mouse open lands on the frame, so the first button never rings", () => {
|
|
51
|
+
pointer("mouse");
|
|
52
|
+
const { frame, target, focus } = overlay("button");
|
|
53
|
+
expect(focusOnOpen(target, frame)).toBe(frame);
|
|
54
|
+
expect(focus.target).not.toHaveBeenCalled();
|
|
55
|
+
expect(focus.frame).toHaveBeenCalledWith({ preventScroll: true, focusVisible: false });
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("a mouse open puts the caret in a first text field", () => {
|
|
59
|
+
pointer("mouse");
|
|
60
|
+
const { frame, target, focus } = overlay("input");
|
|
61
|
+
expect(focusOnOpen(target, frame)).toBe(target);
|
|
62
|
+
expect(focus.target).toHaveBeenCalledWith({ preventScroll: true, focusVisible: false });
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("a touch open lands on the frame, not a text field, so no keyboard rises", () => {
|
|
66
|
+
pointer("touch");
|
|
67
|
+
const { frame, target, focus } = overlay("input");
|
|
68
|
+
expect(focusOnOpen(target, frame)).toBe(frame);
|
|
69
|
+
expect(focus.target).not.toHaveBeenCalled();
|
|
70
|
+
expect(focus.frame).toHaveBeenCalledWith({ preventScroll: true, focusVisible: false });
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("a touch open lands on the frame for a non-text control too", () => {
|
|
74
|
+
pointer("touch");
|
|
75
|
+
const { frame, target, focus } = overlay("input", "checkbox");
|
|
76
|
+
expect(focusOnOpen(target, frame)).toBe(frame);
|
|
77
|
+
expect(focus.target).not.toHaveBeenCalled();
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("a frame without a tabindex is made focusable, so it can take the open", () => {
|
|
81
|
+
pointer("mouse");
|
|
82
|
+
const { frame, target } = overlay("button");
|
|
83
|
+
frame.removeAttribute("tabindex");
|
|
84
|
+
expect(focusOnOpen(target, frame)).toBe(frame);
|
|
85
|
+
expect(frame.getAttribute("tabindex")).toBe("-1");
|
|
86
|
+
expect(document.activeElement).toBe(frame);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("with nothing focusable the frame takes focus", () => {
|
|
90
|
+
pointer("mouse");
|
|
91
|
+
const frame = document.createElement("div");
|
|
92
|
+
frame.tabIndex = -1;
|
|
93
|
+
document.body.append(frame);
|
|
94
|
+
expect(focusOnOpen(null, frame)).toBe(frame);
|
|
95
|
+
});
|
|
96
|
+
});
|
package/src/keyboardFocusRing.ts
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
let lastInputWasKeyboard = false;
|
|
14
|
+
let lastPointerWasTouch = false;
|
|
14
15
|
let hooked = false;
|
|
15
16
|
|
|
16
17
|
/**
|
|
@@ -22,7 +23,14 @@ export function ensureKeyboardModalityTracking() {
|
|
|
22
23
|
if (hooked || typeof document === "undefined") return;
|
|
23
24
|
hooked = true;
|
|
24
25
|
document.addEventListener("keydown", () => (lastInputWasKeyboard = true), true);
|
|
25
|
-
document.addEventListener(
|
|
26
|
+
document.addEventListener(
|
|
27
|
+
"pointerdown",
|
|
28
|
+
(event) => {
|
|
29
|
+
lastInputWasKeyboard = false;
|
|
30
|
+
lastPointerWasTouch = event.pointerType === "touch";
|
|
31
|
+
},
|
|
32
|
+
true,
|
|
33
|
+
);
|
|
26
34
|
}
|
|
27
35
|
|
|
28
36
|
/**
|
|
@@ -36,6 +44,35 @@ export function wasKeyboardFocus(): boolean {
|
|
|
36
44
|
return lastInputWasKeyboard;
|
|
37
45
|
}
|
|
38
46
|
|
|
47
|
+
function isTextEntry(node: HTMLElement): boolean {
|
|
48
|
+
if (node.isContentEditable || node.tagName === "TEXTAREA") return true;
|
|
49
|
+
if (node.tagName !== "INPUT") return false;
|
|
50
|
+
const type = (node as HTMLInputElement).type;
|
|
51
|
+
return !["button", "checkbox", "color", "file", "radio", "range", "reset", "submit"].includes(
|
|
52
|
+
type,
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* An overlay's open focus (LC-71, FZ-7944). Only a keyboard open lands on a
|
|
58
|
+
* control and rings it. Any other open (pointer, touch, mounted open) lands on
|
|
59
|
+
* the frame, which paints no ring: Chromium ignores `focusVisible: false` and
|
|
60
|
+
* rings a control focused before any input. A mouse open still puts the caret
|
|
61
|
+
* in a first text field, which shows LC-133's border; a touch open does not,
|
|
62
|
+
* so the keyboard does not rise over a sheet the user only looked at. A frame
|
|
63
|
+
* without a tabindex gets -1, or its focus() is a no-op and the browser's
|
|
64
|
+
* own pick stays. Returns the node that took focus.
|
|
65
|
+
*/
|
|
66
|
+
export function focusOnOpen(target: HTMLElement | null, frame: HTMLElement): HTMLElement {
|
|
67
|
+
ensureKeyboardModalityTracking();
|
|
68
|
+
const keyboard = lastInputWasKeyboard;
|
|
69
|
+
const caret = target !== null && isTextEntry(target) && !lastPointerWasTouch;
|
|
70
|
+
const node = target && (keyboard || caret) ? target : frame;
|
|
71
|
+
if (node === frame && !frame.hasAttribute("tabindex")) frame.tabIndex = -1;
|
|
72
|
+
node.focus({ preventScroll: true, focusVisible: keyboard } as FocusOptions);
|
|
73
|
+
return node;
|
|
74
|
+
}
|
|
75
|
+
|
|
39
76
|
import { FOCUS_VISIBLE_RING_INSET } from "./theme/focusState";
|
|
40
77
|
|
|
41
78
|
/**
|
|
@@ -700,6 +700,55 @@ const childrenHues = [
|
|
|
700
700
|
"gray",
|
|
701
701
|
] as const;
|
|
702
702
|
|
|
703
|
+
/**
|
|
704
|
+
* FZ-7944 round 2, LC-71: the keyboard ring is the accent, never a white or
|
|
705
|
+
* black neutral. A hue scope rings in its own accent ink. The roots, whose
|
|
706
|
+
* house accent is the neutral inverse, ring in the blue scope's ink, the
|
|
707
|
+
* default accent on Apple platforms. Solid accent sub-themes keep the on-fill
|
|
708
|
+
* ring they paint for content inside the fill.
|
|
709
|
+
*/
|
|
710
|
+
function holdFocusRingAccent(
|
|
711
|
+
themes: Record<string, Record<string, string>>,
|
|
712
|
+
): Record<string, Record<string, string>> {
|
|
713
|
+
const out: Record<string, Record<string, string>> = { ...themes };
|
|
714
|
+
for (const scheme of ["light", "dark"] as const) {
|
|
715
|
+
const base = themes[scheme];
|
|
716
|
+
if (!base) continue;
|
|
717
|
+
const grounds = [base.color1, base.color2].map(luminanceOf);
|
|
718
|
+
if (grounds.some((lum) => lum === null)) continue;
|
|
719
|
+
const holds = (color: string | undefined): color is string => {
|
|
720
|
+
const lum = luminanceOf(color);
|
|
721
|
+
return (
|
|
722
|
+
lum !== null &&
|
|
723
|
+
isChromatic(color) &&
|
|
724
|
+
grounds.every((ground) => contrastRatio(lum, ground as number) >= minContrastRatio)
|
|
725
|
+
);
|
|
726
|
+
};
|
|
727
|
+
const rootRing = [base.accentColor, themes[`${scheme}_blue`]?.accentColor].find(holds);
|
|
728
|
+
for (const [name, theme] of Object.entries(themes)) {
|
|
729
|
+
if (name !== scheme && !name.startsWith(`${scheme}_`)) continue;
|
|
730
|
+
if (theme.outlineColor === undefined) continue;
|
|
731
|
+
const parts = name.split("_");
|
|
732
|
+
if (parts.includes("accent")) continue;
|
|
733
|
+
const hue = (childrenHues as readonly string[]).includes(parts[1] ?? "")
|
|
734
|
+
? parts[1]
|
|
735
|
+
: undefined;
|
|
736
|
+
const scopeInk = hue ? themes[`${scheme}_${hue}`]?.accentColor : undefined;
|
|
737
|
+
const ring = holds(scopeInk) ? scopeInk : rootRing;
|
|
738
|
+
if (!ring || ring === theme.outlineColor) continue;
|
|
739
|
+
out[name] = { ...theme, outlineColor: ring };
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
return out;
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
function isChromatic(color: string | undefined): boolean {
|
|
746
|
+
const hex = color ? normalizeToHex(color) : null;
|
|
747
|
+
if (!hex) return false;
|
|
748
|
+
const channels = [1, 3, 5].map((index) => Number.parseInt(hex.slice(index, index + 2), 16));
|
|
749
|
+
return Math.max(...channels) - Math.min(...channels) >= 24;
|
|
750
|
+
}
|
|
751
|
+
|
|
703
752
|
export function createThemesBuilder(
|
|
704
753
|
baseTheme: BaseThemeBuilderDefinition,
|
|
705
754
|
accentTheme: ThemeBuilderDefinition,
|
|
@@ -769,12 +818,14 @@ export function createThemesBuilder(
|
|
|
769
818
|
const initialTheme = tamaguiCreateThemes(
|
|
770
819
|
createThemesProps as Parameters<typeof tamaguiCreateThemes>[0],
|
|
771
820
|
);
|
|
772
|
-
const themes =
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
821
|
+
const themes = holdFocusRingAccent(
|
|
822
|
+
holdColorThemeAccent(
|
|
823
|
+
holdSwitchTrack(
|
|
824
|
+
holdSyntaxFloor(
|
|
825
|
+
holdNamedInkFloor(
|
|
826
|
+
holdSelectedState(
|
|
827
|
+
holdAccentFloor(neutralizeSemanticEntryText(neutralizeTintText(initialTheme))),
|
|
828
|
+
),
|
|
778
829
|
),
|
|
779
830
|
),
|
|
780
831
|
),
|
|
@@ -28,13 +28,13 @@ import { defaultKnobs } from "./knobs";
|
|
|
28
28
|
import { resolveKnobs } from "./resolveKnobs";
|
|
29
29
|
|
|
30
30
|
describe("focusState tokens (DG-A11Y-01 / DG-STATE-01 / W12)", () => {
|
|
31
|
-
it("FOCUS_VISIBLE_RING meets ≥2px solid outline contract at offset
|
|
31
|
+
it("FOCUS_VISIBLE_RING meets ≥2px solid outline contract at offset +2 (FZ-7944)", () => {
|
|
32
32
|
expect(FOCUS_VISIBLE_RING.outlineWidth).toBeGreaterThanOrEqual(FOCUS_RING_MIN_WIDTH);
|
|
33
33
|
expect(FOCUS_VISIBLE_RING.outlineStyle).toBe("solid");
|
|
34
34
|
expect(FOCUS_VISIBLE_RING.outlineColor).toBe("$outlineColor");
|
|
35
|
-
//
|
|
36
|
-
//
|
|
37
|
-
expect(FOCUS_VISIBLE_RING.outlineOffset).toBe(
|
|
35
|
+
// Not −2 (band inside the fill, 1.17–1.31:1 on solid intents). FZ-7944
|
|
36
|
+
// round 2: a 2px gap, the macOS and iPadOS keyboard ring.
|
|
37
|
+
expect(FOCUS_VISIBLE_RING.outlineOffset).toBe(2);
|
|
38
38
|
expect(FOCUS_RING_MIN_CONTRAST).toBeGreaterThanOrEqual(3);
|
|
39
39
|
});
|
|
40
40
|
|
|
@@ -121,14 +121,14 @@ describe("$outlineColor token contrast (DF-03 / DG-A11Y-01)", () => {
|
|
|
121
121
|
}
|
|
122
122
|
|
|
123
123
|
// MPO-41 — assert the RATIO on the surface the band actually draws on, not
|
|
124
|
-
// just the declaration. At offset
|
|
124
|
+
// just the declaration. At offset +2 the band paints OUTSIDE the border box,
|
|
125
125
|
// on the page ground behind the control; intent Buttons pin the parent-scope
|
|
126
126
|
// `$outlineColor` literal on the ring (LC-51), so the pair that must clear
|
|
127
127
|
// the floor is base `outlineColor` × base ground — for accent and error
|
|
128
128
|
// controls alike, in both schemes.
|
|
129
129
|
for (const scheme of ["light", "dark"] as const) {
|
|
130
130
|
for (const intent of ["accent", "error"] as const) {
|
|
131
|
-
it(`${scheme} ${intent}:
|
|
131
|
+
it(`${scheme} ${intent}: the ring clears ≥3:1 on the page ground it draws on`, () => {
|
|
132
132
|
const base = themes[scheme];
|
|
133
133
|
expect(themes[`${scheme}_${intent}`]).toBeTruthy();
|
|
134
134
|
const ring = normalizeToHex(base.outlineColor);
|
|
@@ -175,21 +175,21 @@ describe("the ring still paints inside a clipped / fused group (LC-71 §4 inset
|
|
|
175
175
|
expect(keyboardFocusRingProps).toEqual(FOCUS_VISIBLE_RING_INSET);
|
|
176
176
|
});
|
|
177
177
|
|
|
178
|
-
it("ensureFocusVisibleRing honors the explicit per-call-site inset and defaults to
|
|
178
|
+
it("ensureFocusVisibleRing honors the explicit per-call-site inset and defaults to +2", () => {
|
|
179
179
|
expect(ensureFocusVisibleRing({ outlineOffset: -2 }).outlineOffset).toBe(-2);
|
|
180
|
-
expect(ensureFocusVisibleRing({ outlineWidth: 3 }).outlineOffset).toBe(
|
|
181
|
-
expect(ensureFocusVisibleRing().outlineOffset).toBe(
|
|
180
|
+
expect(ensureFocusVisibleRing({ outlineWidth: 3 }).outlineOffset).toBe(2);
|
|
181
|
+
expect(ensureFocusVisibleRing().outlineOffset).toBe(2);
|
|
182
182
|
});
|
|
183
183
|
});
|
|
184
184
|
|
|
185
185
|
describe("LC-71: keyboard-only; measure the ring / text node, not the frame", () => {
|
|
186
|
-
it("standalone ring paints a 2px band
|
|
186
|
+
it("standalone ring paints a 2px band 2px clear of the ring-carrying node's box", () => {
|
|
187
187
|
// Measure the node that paints the outline (the ring carrier / text node),
|
|
188
188
|
// never an ancestor Frame. Band = [offset, offset + width] from THAT box.
|
|
189
189
|
const { outlineOffset, outlineWidth } = FOCUS_VISIBLE_RING;
|
|
190
190
|
expect(outlineWidth).toBe(2);
|
|
191
|
-
expect(outlineOffset).toBe(
|
|
192
|
-
expect(outlineOffset + outlineWidth).toBe(
|
|
191
|
+
expect(outlineOffset).toBe(2);
|
|
192
|
+
expect(outlineOffset + outlineWidth).toBe(4);
|
|
193
193
|
});
|
|
194
194
|
|
|
195
195
|
it("chip dismiss rings the glyph node, not the 44px hit-box frame", () => {
|
|
@@ -287,10 +287,10 @@ describe("theme DEV warn (outline-none / positive-tabindex)", () => {
|
|
|
287
287
|
* shipping and being found in a later audit.
|
|
288
288
|
*/
|
|
289
289
|
describe("LC-71 §4: the sanctioned focus-ring offset set is closed (MPO-92)", () => {
|
|
290
|
-
it("is exactly three offsets — clipping inset,
|
|
290
|
+
it("is exactly three offsets — clipping inset, flush, default", () => {
|
|
291
291
|
expect([...SANCTIONED_FOCUS_RING_OFFSETS]).toEqual([-2, 0, 2]);
|
|
292
292
|
expect(FOCUS_RING_CLIPPED_OFFSET).toBe(-2);
|
|
293
|
-
expect(FOCUS_VISIBLE_RING.outlineOffset).toBe(
|
|
293
|
+
expect(FOCUS_VISIBLE_RING.outlineOffset).toBe(2);
|
|
294
294
|
expect(FOCUS_RING_HALO_OFFSET).toBe(2);
|
|
295
295
|
});
|
|
296
296
|
|
|
@@ -312,7 +312,7 @@ describe("LC-71 §4: the sanctioned focus-ring offset set is closed (MPO-92)", (
|
|
|
312
312
|
});
|
|
313
313
|
// The band spans [outlineOffset, outlineOffset + outlineWidth] from the
|
|
314
314
|
// border edge — here [2, 4]. Inner edge ≥ 0 means it paints entirely on the
|
|
315
|
-
// surface BEHIND the control, exactly like the default
|
|
315
|
+
// surface BEHIND the control, exactly like the default ring, so it
|
|
316
316
|
// measures against the page ground and cannot reproduce the 1.17–1.31:1
|
|
317
317
|
// inside-the-fill failure MPO-41 retired.
|
|
318
318
|
expect(FOCUS_VISIBLE_RING_HALO.outlineOffset).toBeGreaterThanOrEqual(0);
|
package/src/theme/focusState.ts
CHANGED
|
@@ -48,31 +48,26 @@ export function focusRingColorMeetsContrast(ringColor: string, surfaceColor: str
|
|
|
48
48
|
|
|
49
49
|
/**
|
|
50
50
|
* ONE ring geometry (DG-A11Y-01 / LC-71 §4): 2px solid `$outlineColor` at
|
|
51
|
-
* offset
|
|
52
|
-
* surface behind the control
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
* old flat inset (−2) painted
|
|
56
|
-
* 1.17–1.31:1 (
|
|
57
|
-
* the same ring on the page ground clears 5.78–6.50:1 (MPO-41 / MPO-19 X7).
|
|
51
|
+
* offset **+2**, a band clear of the border box with a 2px gap, painted on the
|
|
52
|
+
* surface behind the control. FZ-7944 round 2 moved the default from 0 to +2
|
|
53
|
+
* (the macOS and iPadOS keyboard ring); the ≥3:1 floor stays computable
|
|
54
|
+
* because the band still paints on the page ground, never inside a fill. The
|
|
55
|
+
* old flat inset (−2) painted INSIDE solid intent fills and sank to
|
|
56
|
+
* 1.17–1.31:1 (MPO-41 / MPO-19 X7).
|
|
58
57
|
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
* MPO-92 ratified a second, shape-scoped carve-out on the other side:
|
|
64
|
-
* `FOCUS_VISIBLE_RING_HALO` (+2) for pill/circular thumbs and inline text
|
|
65
|
-
* links. The F1 "not +2" reading was about the DEFAULT — a blanket +2 on
|
|
66
|
-
* every control — and it stands. The halo is a named carve-out with a written
|
|
67
|
-
* shape scope, exactly as the inset is; `SANCTIONED_FOCUS_RING_OFFSETS` below
|
|
68
|
-
* is the closed set of all three.
|
|
58
|
+
* Two carve-outs stay named: `FOCUS_VISIBLE_RING_INSET` (−2) for clipping
|
|
59
|
+
* ancestors, and `FOCUS_RING_FLUSH_OFFSET` (0) for rows and cells that sit
|
|
60
|
+
* edge to edge inside a padded overlay, where a gapped band would collide
|
|
61
|
+
* with the neighbour. `SANCTIONED_FOCUS_RING_OFFSETS` is the closed set.
|
|
69
62
|
*/
|
|
70
63
|
export const FOCUS_RING_CLIPPED_OFFSET = -2;
|
|
64
|
+
export const FOCUS_RING_FLUSH_OFFSET = 0;
|
|
65
|
+
export const FOCUS_RING_HALO_OFFSET = 2;
|
|
71
66
|
export const FOCUS_VISIBLE_RING = {
|
|
72
67
|
outlineWidth: FOCUS_RING_MIN_WIDTH,
|
|
73
68
|
outlineStyle: "solid" as const,
|
|
74
69
|
outlineColor: "$outlineColor",
|
|
75
|
-
outlineOffset:
|
|
70
|
+
outlineOffset: FOCUS_RING_HALO_OFFSET,
|
|
76
71
|
} satisfies ControlStateProps;
|
|
77
72
|
|
|
78
73
|
/**
|
|
@@ -94,29 +89,10 @@ export const FOCUS_VISIBLE_RING_INSET = {
|
|
|
94
89
|
} satisfies ControlStateProps;
|
|
95
90
|
|
|
96
91
|
/**
|
|
97
|
-
* The
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
* Scope is by SHAPE, not by instance, and it is closed at two:
|
|
102
|
-
* - **pill / circular thumbs** — a round handle the user drags (Slider
|
|
103
|
-
* thumb, Rating star, TimeRangeScrubber handle). At offset 0 the band is
|
|
104
|
-
* concentric with the glyph's own edge and reads as a thicker glyph.
|
|
105
|
-
* - **inline text links** — a link inside a run of prose (markdown/mdx
|
|
106
|
-
* `Link`, ErrorSummary's summary link, backoffice "View All"). At offset 0
|
|
107
|
-
* the band crosses the line box and collides with ascenders/descenders of
|
|
108
|
-
* the surrounding text.
|
|
109
|
-
*
|
|
110
|
-
* NOT for standalone rectangles on the page ground — cards, tiles, rows,
|
|
111
|
-
* fields. Those are the default `FOCUS_VISIBLE_RING` (offset 0), which is
|
|
112
|
-
* what makes the Axiom 12 ≥3:1 floor computable against the page ground.
|
|
113
|
-
*
|
|
114
|
-
* Contrast is unaffected: like offset 0 the halo paints OUTSIDE the border
|
|
115
|
-
* box, on the surface behind the control, so it measures against the same
|
|
116
|
-
* page ground the default does — the 1.17–1.31:1 failures MPO-41 retired were
|
|
117
|
-
* the INSET painting inside solid intent fills, a defect the halo cannot have.
|
|
92
|
+
* The halo (MPO-92): pill and circular thumbs and inline text links. Since
|
|
93
|
+
* FZ-7944 round 2 it equals the default ring; the name stays so those call
|
|
94
|
+
* sites keep saying why they hold +2 if the default ever moves again.
|
|
118
95
|
*/
|
|
119
|
-
export const FOCUS_RING_HALO_OFFSET = 2;
|
|
120
96
|
export const FOCUS_VISIBLE_RING_HALO = {
|
|
121
97
|
...FOCUS_VISIBLE_RING,
|
|
122
98
|
outlineOffset: FOCUS_RING_HALO_OFFSET,
|
|
@@ -124,7 +100,7 @@ export const FOCUS_VISIBLE_RING_HALO = {
|
|
|
124
100
|
|
|
125
101
|
/**
|
|
126
102
|
* The CLOSED set of sanctioned focus-ring offsets (LC-71 §4). Three, and only
|
|
127
|
-
* three: the
|
|
103
|
+
* three: the clipping inset −2, the flush 0, the default +2. A call site
|
|
128
104
|
* that wants any other number is a rule gap, not a preference — file it and
|
|
129
105
|
* amend §4, do not type the number. `focusState.spec.ts` asserts this set is
|
|
130
106
|
* exactly what the repo paints, so a fourth offset fails a test instead of
|
|
@@ -132,8 +108,8 @@ export const FOCUS_VISIBLE_RING_HALO = {
|
|
|
132
108
|
*/
|
|
133
109
|
export const SANCTIONED_FOCUS_RING_OFFSETS = [
|
|
134
110
|
FOCUS_RING_CLIPPED_OFFSET,
|
|
111
|
+
FOCUS_RING_FLUSH_OFFSET,
|
|
135
112
|
FOCUS_VISIBLE_RING.outlineOffset,
|
|
136
|
-
FOCUS_RING_HALO_OFFSET,
|
|
137
113
|
] as const;
|
|
138
114
|
|
|
139
115
|
/** True when `offset` is one of the three offsets LC-71 §4 sanctions. */
|
|
@@ -50,7 +50,7 @@ describe("LC-148 HAIRLINE-TOKEN: borderWidth small is the theme's hairline", ()
|
|
|
50
50
|
controlBorderOutlined: "$borderColorHairline",
|
|
51
51
|
elevatedSurface: "$borderColorHairline",
|
|
52
52
|
});
|
|
53
|
-
expect(edges({ borderWidth: "small" }, "dark").elevatedSurface).toBe("
|
|
53
|
+
expect(edges({ borderWidth: "small" }, "dark").elevatedSurface).toBe("transparent");
|
|
54
54
|
expect(edges({ borderWidth: "none", fillStyle: "outlined" }).borderRadius).toBe(
|
|
55
55
|
"$borderColorHairline",
|
|
56
56
|
);
|
|
@@ -64,7 +64,7 @@ describe("LC-148 HAIRLINE-TOKEN: borderWidth small is the theme's hairline", ()
|
|
|
64
64
|
controlBorderOutlined: "$borderColor",
|
|
65
65
|
elevatedSurface: "$borderColor",
|
|
66
66
|
});
|
|
67
|
-
expect(edges({ borderWidth }, "dark").elevatedSurface).toBe("
|
|
67
|
+
expect(edges({ borderWidth }, "dark").elevatedSurface).toBe("transparent");
|
|
68
68
|
}
|
|
69
69
|
});
|
|
70
70
|
|
package/src/theme/motion.spec.ts
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
|
+
import { readFileSync, readdirSync, statSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
1
3
|
import { describe, expect, it } from "vitest";
|
|
4
|
+
import { GAP_LG_KNOB } from "../audit/constraintAudit";
|
|
2
5
|
import { animationConfig, animationDurations, motionStops } from "./animations/css";
|
|
6
|
+
import { transitionProps } from "./transitionProps";
|
|
7
|
+
import { transitionProps as nativeTransitionProps } from "./transitionProps.native";
|
|
3
8
|
import {
|
|
9
|
+
motionExitFactor,
|
|
4
10
|
motionFor,
|
|
5
11
|
motionDurationMs,
|
|
6
12
|
motionSeconds,
|
|
@@ -82,3 +88,134 @@ describe("motion durations", () => {
|
|
|
82
88
|
expect(motionSettleMs(["quick", { opacity: "slow" }])).toBe(980);
|
|
83
89
|
});
|
|
84
90
|
});
|
|
91
|
+
|
|
92
|
+
const bouncyStops = ["bouncy", "superBouncy"];
|
|
93
|
+
const nonBouncy = motionStops.filter((stop) => !bouncyStops.includes(stop));
|
|
94
|
+
const ms = (stop: unknown) => animationDurations[stop as keyof typeof animationDurations];
|
|
95
|
+
|
|
96
|
+
function bezierYs(timing: string): number[] {
|
|
97
|
+
const match = timing.match(/cubic-bezier\(([^)]+)\)/);
|
|
98
|
+
if (!match) return [];
|
|
99
|
+
const [, y1, , y2] = match[1].split(",").map(Number);
|
|
100
|
+
return [y1, y2];
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const publicRoot = path.resolve(__dirname, "../../..");
|
|
104
|
+
|
|
105
|
+
function sources(dir: string, out: string[] = []): string[] {
|
|
106
|
+
for (const name of readdirSync(dir)) {
|
|
107
|
+
if (name === "node_modules" || name === "dist" || name === "types") continue;
|
|
108
|
+
const file = path.join(dir, name);
|
|
109
|
+
if (statSync(file).isDirectory()) sources(file, out);
|
|
110
|
+
else if (/\.tsx?$/.test(name) && !/\.(spec|test|stories)\.tsx?$|\.d\.ts$/.test(name)) {
|
|
111
|
+
out.push(file);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return out;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function enterExitObjects(source: string): string[] {
|
|
118
|
+
const objects: string[] = [];
|
|
119
|
+
for (const match of source.matchAll(
|
|
120
|
+
/\b(?:enterStyle|exitStyle)\s*(?:=\{|:)\s*(?:[^{}?]*\?\s*)?(\{[^{}]*\}|[A-Za-z_$][\w$]*)/g,
|
|
121
|
+
)) {
|
|
122
|
+
const value = match[1];
|
|
123
|
+
if (value.startsWith("{")) objects.push(value);
|
|
124
|
+
else {
|
|
125
|
+
const declared = source.match(
|
|
126
|
+
new RegExp(`\\b(?:const|let)\\s+${value}\\s*=\\s*(\\{[^{}]*\\})`),
|
|
127
|
+
);
|
|
128
|
+
if (declared) objects.push(declared[1]);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return objects;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function numbersIn(expression: string, source: string): number[] {
|
|
135
|
+
const values = [...expression.matchAll(/-?\d+(?:\.\d+)?/g)].map((m) => Math.abs(Number(m[0])));
|
|
136
|
+
for (const [identifier] of expression.matchAll(/[A-Za-z_$][\w$]*/g)) {
|
|
137
|
+
const declared = source.match(new RegExp(`\\bconst\\s+${identifier}\\s*=\\s*([^;\\n]+)`));
|
|
138
|
+
if (declared && !/[{(]/.test(declared[1])) {
|
|
139
|
+
values.push(
|
|
140
|
+
...[...declared[1].matchAll(/-?\d+(?:\.\d+)?/g)].map((m) => Math.abs(Number(m[0]))),
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return values;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
describe("LC-178 MOTION-CHARACTER: quick in, quicker out, short and still", () => {
|
|
148
|
+
it("exits every role faster than it enters, about two thirds of the enter duration", () => {
|
|
149
|
+
for (const stop of motionStops) {
|
|
150
|
+
for (const [name, role] of Object.entries(motionFor(stop))) {
|
|
151
|
+
const enter = ms(role.transition);
|
|
152
|
+
const exit = ms(role.exit);
|
|
153
|
+
const shorter = nonBouncy.filter((candidate) => ms(candidate) < enter);
|
|
154
|
+
if (shorter.length === 0) {
|
|
155
|
+
expect(role.exit, `${stop}.${name}`).toBe(role.transition);
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
expect(exit, `${stop}.${name} exit`).toBeLessThan(enter);
|
|
159
|
+
const target = enter * motionExitFactor;
|
|
160
|
+
const best = Math.min(...shorter.map((candidate) => Math.abs(ms(candidate) - target)));
|
|
161
|
+
expect(Math.abs(exit - target), `${stop}.${name} exit near 2/3`).toBe(best);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
for (const role of Object.values(motionFor("none"))) expect(role.exit).toBeUndefined();
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it("never picks a bouncy stop, so no role overshoots opacity", () => {
|
|
168
|
+
for (const stop of motionStops) {
|
|
169
|
+
for (const [name, role] of Object.entries(motionFor(stop))) {
|
|
170
|
+
const presetBounce = name === "control" && bouncyStops.includes(stop);
|
|
171
|
+
for (const leg of presetBounce ? [role.exit] : [role.transition, role.exit]) {
|
|
172
|
+
expect(bouncyStops, `${stop}.${name}`).not.toContain(leg);
|
|
173
|
+
for (const y of bezierYs(animationConfig[leg as keyof typeof animationConfig])) {
|
|
174
|
+
expect(y, `${stop}.${name} ${String(leg)} curve`).toBeGreaterThanOrEqual(0);
|
|
175
|
+
expect(y, `${stop}.${name} ${String(leg)} curve`).toBeLessThanOrEqual(1);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it("hands the exit stop to the driver, and native springs clamp their overshoot", () => {
|
|
183
|
+
const { overlay } = motionFor("quick");
|
|
184
|
+
expect(transitionProps(overlay).transition).toEqual(["quicker", { exit: "quickest" }]);
|
|
185
|
+
expect(nativeTransitionProps(overlay).transition).toEqual([
|
|
186
|
+
"quicker",
|
|
187
|
+
{ overshootClamping: true, exit: "quickest" },
|
|
188
|
+
]);
|
|
189
|
+
const shortest = motionFor("quickestLessBouncy").control;
|
|
190
|
+
expect(nativeTransitionProps(shortest).transition).toEqual([
|
|
191
|
+
"quickestLessBouncy",
|
|
192
|
+
{ overshootClamping: true },
|
|
193
|
+
]);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it("keeps every enter and exit offset in public/* within gapLg and its opacity in [0, 1]", () => {
|
|
197
|
+
const limit = GAP_LG_KNOB.gapLg;
|
|
198
|
+
expect(limit).toBeGreaterThan(0);
|
|
199
|
+
const travel: string[] = [];
|
|
200
|
+
const opacity: string[] = [];
|
|
201
|
+
let seen = 0;
|
|
202
|
+
for (const file of sources(publicRoot)) {
|
|
203
|
+
const source = readFileSync(file, "utf8");
|
|
204
|
+
for (const object of enterExitObjects(source)) {
|
|
205
|
+
seen += 1;
|
|
206
|
+
const where = `${path.relative(publicRoot, file)}: ${object}`;
|
|
207
|
+
for (const [, axis, expression] of object.matchAll(/\b([xy])\s*:\s*([^,}]+)/g)) {
|
|
208
|
+
if (numbersIn(expression, source).some((value) => value > limit)) {
|
|
209
|
+
travel.push(`${where} (${axis} past gapLg ${limit})`);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
for (const [, value] of object.matchAll(/\bopacity\s*:\s*(-?\d+(?:\.\d+)?)/g)) {
|
|
213
|
+
if (Number(value) < 0 || Number(value) > 1) opacity.push(where);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
expect(seen).toBeGreaterThan(10);
|
|
218
|
+
expect(travel).toEqual([]);
|
|
219
|
+
expect(opacity).toEqual([]);
|
|
220
|
+
});
|
|
221
|
+
});
|
package/src/theme/motion.ts
CHANGED
|
@@ -9,9 +9,12 @@ export type MotionRoleName = "control" | "overlay" | "scrim" | "reveal" | "slide
|
|
|
9
9
|
|
|
10
10
|
export interface MotionRole {
|
|
11
11
|
transition: TransitionProp | undefined;
|
|
12
|
+
exit?: TransitionProp | undefined;
|
|
12
13
|
animateOnly: readonly string[];
|
|
13
14
|
}
|
|
14
15
|
|
|
16
|
+
export const motionExitFactor = 2 / 3;
|
|
17
|
+
|
|
15
18
|
export const motionRoleProperties = {
|
|
16
19
|
control: ["backgroundColor", "borderColor", "color", "opacity", "transform"],
|
|
17
20
|
overlay: ["opacity", "transform"],
|
|
@@ -22,8 +25,8 @@ export const motionRoleProperties = {
|
|
|
22
25
|
|
|
23
26
|
const nonBouncyStops = motionStops.filter((stop) => stop !== "superBouncy" && stop !== "bouncy");
|
|
24
27
|
|
|
25
|
-
function nearestStop(duration: number): MotionStop {
|
|
26
|
-
return
|
|
28
|
+
function nearestStop(duration: number, stops: readonly MotionStop[] = nonBouncyStops): MotionStop {
|
|
29
|
+
return stops.reduce((nearest, candidate) =>
|
|
27
30
|
Math.abs(animationDurations[candidate] - duration) <
|
|
28
31
|
Math.abs(animationDurations[nearest] - duration)
|
|
29
32
|
? candidate
|
|
@@ -31,18 +34,28 @@ function nearestStop(duration: number): MotionStop {
|
|
|
31
34
|
);
|
|
32
35
|
}
|
|
33
36
|
|
|
37
|
+
function exitStop(enter: MotionStop): MotionStop {
|
|
38
|
+
const enterMs = animationDurations[enter];
|
|
39
|
+
const shorter = nonBouncyStops.filter((stop) => animationDurations[stop] < enterMs);
|
|
40
|
+
return shorter.length === 0 ? enter : nearestStop(enterMs * motionExitFactor, shorter);
|
|
41
|
+
}
|
|
42
|
+
|
|
34
43
|
export function motionFor(stop: Animation): Record<MotionRoleName, MotionRole> {
|
|
35
44
|
const transition = stop === "none" ? undefined : stop;
|
|
36
45
|
const duration = motionDurationMs(transition);
|
|
37
|
-
const role = (name: MotionRoleName, factor: number): MotionRole =>
|
|
38
|
-
|
|
46
|
+
const role = (name: MotionRoleName, factor: number): MotionRole => {
|
|
47
|
+
const enter =
|
|
39
48
|
transition === undefined
|
|
40
49
|
? undefined
|
|
41
50
|
: name === "control"
|
|
42
51
|
? transition
|
|
43
|
-
: nearestStop(duration * factor)
|
|
44
|
-
|
|
45
|
-
|
|
52
|
+
: nearestStop(duration * factor);
|
|
53
|
+
return {
|
|
54
|
+
transition: enter,
|
|
55
|
+
exit: enter === undefined ? undefined : exitStop(enter),
|
|
56
|
+
animateOnly: motionRoleProperties[name],
|
|
57
|
+
};
|
|
58
|
+
};
|
|
46
59
|
return {
|
|
47
60
|
control: role("control", 1),
|
|
48
61
|
overlay: role("overlay", 0.8),
|
|
@@ -208,9 +208,9 @@ describe("MPO-19 X5/X6 — surfaces address semantic tokens, so sub-themes work"
|
|
|
208
208
|
});
|
|
209
209
|
|
|
210
210
|
describe("MPO-19 X7 — the focus ring is inset only where a clip demands it", () => {
|
|
211
|
-
it("the default sits
|
|
212
|
-
expect(FOCUS_VISIBLE_RING.outlineOffset).toBe(
|
|
213
|
-
expect(ensureFocusVisibleRing().outlineOffset).toBe(
|
|
211
|
+
it("the default sits 2px clear of the border box (FZ-7944 round 2)", () => {
|
|
212
|
+
expect(FOCUS_VISIBLE_RING.outlineOffset).toBe(2);
|
|
213
|
+
expect(ensureFocusVisibleRing().outlineOffset).toBe(2);
|
|
214
214
|
});
|
|
215
215
|
|
|
216
216
|
it("the ring shape itself is unchanged — 2px solid, non-removable", () => {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { focusOnOpen, wasKeyboardFocus } from "../keyboardFocusRing";
|
|
1
2
|
import { transitionProps } from "./transitionProps";
|
|
2
3
|
import { Surface } from "./Surface";
|
|
3
4
|
import { useResolvedKnobs } from "./useResolvedKnobs";
|
|
@@ -40,7 +41,9 @@ const overlayFrameFocusStyle = {
|
|
|
40
41
|
// tamagui's deferred path still runs. Dialog content sits in a native
|
|
41
42
|
// <dialog> whose show() puts focus inside before the check in browsers,
|
|
42
43
|
// so there FocusScope never calls the handler; useDialogShowFocus does
|
|
43
|
-
// when show() left focus on something that is not a tab stop (MPO-470)
|
|
44
|
+
// when show() left focus on something that is not a tab stop (MPO-470), or
|
|
45
|
+
// on any control after a non-keyboard open, which Chromium would ring
|
|
46
|
+
// (LC-71, FZ-7944).
|
|
44
47
|
|
|
45
48
|
function isTabbableWithin(node: HTMLElement, container: HTMLElement): boolean {
|
|
46
49
|
if (node.tabIndex < 0 || node.hidden || node.tagName === "A") return false;
|
|
@@ -67,8 +70,10 @@ function focusFirstTabbableOnOpen(event: Event): void {
|
|
|
67
70
|
);
|
|
68
71
|
if (!first) return;
|
|
69
72
|
event.preventDefault();
|
|
70
|
-
|
|
71
|
-
if (typeof HTMLInputElement !== "undefined" &&
|
|
73
|
+
const focused = focusOnOpen(first, container);
|
|
74
|
+
if (typeof HTMLInputElement !== "undefined" && focused instanceof HTMLInputElement) {
|
|
75
|
+
focused.select();
|
|
76
|
+
}
|
|
72
77
|
}
|
|
73
78
|
|
|
74
79
|
type OpenAutoFocus = (event: Event) => void;
|
|
@@ -100,7 +105,7 @@ function useDialogShowFocus(onOpenAutoFocus: OpenAutoFocus): void {
|
|
|
100
105
|
const content = document.getElementById(contentId);
|
|
101
106
|
const focused = document.activeElement;
|
|
102
107
|
if (!content || typeof HTMLElement === "undefined" || !(focused instanceof HTMLElement)) return;
|
|
103
|
-
if (!content.contains(focused) || focused.tabIndex >= 0) return;
|
|
108
|
+
if (!content.contains(focused) || (focused.tabIndex >= 0 && wasKeyboardFocus())) return;
|
|
104
109
|
content.addEventListener(DIALOG_SHOW_FOCUS_EVENT, onOpenAutoFocus, { once: true });
|
|
105
110
|
content.dispatchEvent(new CustomEvent(DIALOG_SHOW_FOCUS_EVENT, { cancelable: true }));
|
|
106
111
|
}, [open, contentId]);
|
|
@@ -315,7 +315,7 @@ describe("resolveKnobs", () => {
|
|
|
315
315
|
scheme === "dark" ? "$color4" : "$background",
|
|
316
316
|
);
|
|
317
317
|
expect(knobProps.elevatedSurface.borderColor).toBe(
|
|
318
|
-
scheme === "dark" ? "
|
|
318
|
+
scheme === "dark" ? "transparent" : "$borderColor",
|
|
319
319
|
);
|
|
320
320
|
if (!key) {
|
|
321
321
|
expect(knobProps.elevationChrome).toEqual({});
|
|
@@ -456,10 +456,10 @@ function elevationChromeFor(key: SizeKey | undefined): ElevationChromeProps {
|
|
|
456
456
|
};
|
|
457
457
|
}
|
|
458
458
|
|
|
459
|
-
// v5's surface2 template
|
|
460
|
-
//
|
|
461
|
-
//
|
|
462
|
-
const surface2 = { fill: "$color4"
|
|
459
|
+
// v5's surface2 template step: the fill two palette steps above the base
|
|
460
|
+
// `$background` (color2), one above Card's surface1. A dark overlay separates
|
|
461
|
+
// by this lighter fill and its shadow, with no light edge (HIG, FZ-7944).
|
|
462
|
+
const surface2 = { fill: "$color4" } as const;
|
|
463
463
|
|
|
464
464
|
// `$textMuted` is the theme's secondary ink (LC-152): the most muted ramp
|
|
465
465
|
// step that clears 7:1 on the page and every card tier in its own scheme. A
|
|
@@ -801,7 +801,7 @@ export function resolveKnobs(
|
|
|
801
801
|
}
|
|
802
802
|
: {
|
|
803
803
|
backgroundColor: scheme === "dark" ? surface2.fill : "$background",
|
|
804
|
-
borderColor:
|
|
804
|
+
borderColor: scheme === "dark" ? "transparent" : edge("$borderColor", borderWidthValue),
|
|
805
805
|
borderWidth: borderWidthValue,
|
|
806
806
|
borderRadius: radiusToken,
|
|
807
807
|
elevation: platformElevation(overlayElevationToken),
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { MotionRole } from "./motion";
|
|
2
2
|
import type { TransitionProp } from "tamagui";
|
|
3
|
+
import { animationConfig as nativeAnimationConfig } from "./animations/reactNative";
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Native twin of `transitionProps.ts`.
|
|
@@ -23,12 +24,26 @@ export function transitionProps(
|
|
|
23
24
|
const properties = [...role.animateOnly, ...extra].flatMap(
|
|
24
25
|
(property) => nativeLonghands[property] ?? [property],
|
|
25
26
|
);
|
|
27
|
+
if (typeof transition !== "string") return { transition, animateOnly: properties };
|
|
28
|
+
const exit = typeof role.exit === "string" && role.exit !== transition ? role.exit : undefined;
|
|
29
|
+
const clamp = isSpring(transition) && !presetBounce.has(transition);
|
|
30
|
+
if (!exit && !clamp) return { transition, animateOnly: properties };
|
|
26
31
|
return {
|
|
27
|
-
transition
|
|
32
|
+
transition: [
|
|
33
|
+
transition,
|
|
34
|
+
{ ...(clamp ? { overshootClamping: true } : {}), ...(exit ? { exit } : {}) },
|
|
35
|
+
],
|
|
28
36
|
animateOnly: properties,
|
|
29
37
|
};
|
|
30
38
|
}
|
|
31
39
|
|
|
40
|
+
function isSpring(stop: string): boolean {
|
|
41
|
+
const config = nativeAnimationConfig[stop as keyof typeof nativeAnimationConfig] as
|
|
42
|
+
| { type?: string }
|
|
43
|
+
| undefined;
|
|
44
|
+
return config !== undefined && config.type !== "timing";
|
|
45
|
+
}
|
|
46
|
+
|
|
32
47
|
/**
|
|
33
48
|
* FZ-7919: tamagui expands `borderColor` into its four sides before the React
|
|
34
49
|
* Native driver sees the style, and the driver matches `animateOnly` by exact
|
|
@@ -36,6 +51,8 @@ export function transitionProps(
|
|
|
36
51
|
* (the bubbled focus of a field's text input) animated them. The animated
|
|
37
52
|
* copy outranked the static one and held the previous focus or theme colour.
|
|
38
53
|
*/
|
|
54
|
+
const presetBounce = new Set(["bouncy", "superBouncy"]);
|
|
55
|
+
|
|
39
56
|
const nativeLonghands: Record<string, readonly string[]> = {
|
|
40
57
|
borderColor: [
|
|
41
58
|
"borderColor",
|
|
@@ -24,7 +24,7 @@ describe("role property scopes", () => {
|
|
|
24
24
|
});
|
|
25
25
|
it("hyphenates the control scope and additional properties", () => {
|
|
26
26
|
expect(transitionProps(motionFor("quick").control, ["maxHeight", "left"])).toEqual({
|
|
27
|
-
transition: "quick",
|
|
27
|
+
transition: ["quick", { exit: "quickerLessBouncy" }],
|
|
28
28
|
animateOnly: [
|
|
29
29
|
"background-color",
|
|
30
30
|
"border-color",
|
|
@@ -41,7 +41,7 @@ describe("role property scopes", () => {
|
|
|
41
41
|
expect(nativeTransitionProps(undefined)).toEqual({});
|
|
42
42
|
expect(nativeTransitionProps("quick")).toEqual({ transition: "quick" });
|
|
43
43
|
expect(nativeTransitionProps(motionFor("quick").control, ["maxHeight"])).toEqual({
|
|
44
|
-
transition: "quick",
|
|
44
|
+
transition: ["quick", { overshootClamping: true, exit: "quickerLessBouncy" }],
|
|
45
45
|
animateOnly: [
|
|
46
46
|
"backgroundColor",
|
|
47
47
|
"borderColor",
|
|
@@ -22,13 +22,23 @@ export function transitionProps(
|
|
|
22
22
|
if (!isRole) return { transition };
|
|
23
23
|
const properties = [...role.animateOnly, ...extra];
|
|
24
24
|
return {
|
|
25
|
-
transition,
|
|
25
|
+
transition: withExit(transition, role.exit),
|
|
26
26
|
animateOnly: properties.map((property) =>
|
|
27
27
|
property.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`),
|
|
28
28
|
),
|
|
29
29
|
};
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
function withExit(
|
|
33
|
+
transition: TransitionProp | undefined,
|
|
34
|
+
exit: TransitionProp | undefined,
|
|
35
|
+
): TransitionProp | undefined {
|
|
36
|
+
if (typeof transition !== "string" || typeof exit !== "string" || exit === transition) {
|
|
37
|
+
return transition;
|
|
38
|
+
}
|
|
39
|
+
return [transition, { exit }];
|
|
40
|
+
}
|
|
41
|
+
|
|
32
42
|
function isMotionRole(value: MotionRole | TransitionProp | undefined): value is MotionRole {
|
|
33
43
|
return (
|
|
34
44
|
typeof value === "object" &&
|
|
@@ -22,6 +22,17 @@ export declare function ensureKeyboardModalityTracking(): void;
|
|
|
22
22
|
* false) and during SSR.
|
|
23
23
|
*/
|
|
24
24
|
export declare function wasKeyboardFocus(): boolean;
|
|
25
|
+
/**
|
|
26
|
+
* An overlay's open focus (LC-71, FZ-7944). Only a keyboard open lands on a
|
|
27
|
+
* control and rings it. Any other open (pointer, touch, mounted open) lands on
|
|
28
|
+
* the frame, which paints no ring: Chromium ignores `focusVisible: false` and
|
|
29
|
+
* rings a control focused before any input. A mouse open still puts the caret
|
|
30
|
+
* in a first text field, which shows LC-133's border; a touch open does not,
|
|
31
|
+
* so the keyboard does not rise over a sheet the user only looked at. A frame
|
|
32
|
+
* without a tabindex gets -1, or its focus() is a no-op and the browser's
|
|
33
|
+
* own pick stays. Returns the node that took focus.
|
|
34
|
+
*/
|
|
35
|
+
export declare function focusOnOpen(target: HTMLElement | null, frame: HTMLElement): HTMLElement;
|
|
25
36
|
/**
|
|
26
37
|
* Standard manual ring props — the INSET ring geometry
|
|
27
38
|
* (`FOCUS_VISIBLE_RING_INSET`: 2px solid `$outlineColor`, offset −2), spread
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"keyboardFocusRing.d.ts","sourceRoot":"","sources":["../src/keyboardFocusRing.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;
|
|
1
|
+
{"version":3,"file":"keyboardFocusRing.d.ts","sourceRoot":"","sources":["../src/keyboardFocusRing.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAMH;;;;GAIG;AACH,wBAAgB,8BAA8B,SAY7C;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,IAAI,OAAO,CAG1C;AAWD;;;;;;;;;GASG;AACH,wBAAgB,WAAW,CAAC,MAAM,EAAE,WAAW,GAAG,IAAI,EAAE,KAAK,EAAE,WAAW,GAAG,WAAW,CAQvF;AAID;;;;;;;;;;GAUG;AACH,eAAO,MAAM,sBAAsB;;;;;CAEzB,CAAC;AAEX;;;;;;;GAOG;AACH,eAAO,MAAM,qBAAqB,8uCA+BhC,CAAC;AAIH,uEAAuE;AACvE,wBAAgB,wBAAwB,IAAI,IAAI,CAO/C"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"createThemes.d.ts","sourceRoot":"","sources":["../../src/theme/createThemes.ts"],"names":[],"mappings":"AACA,OAAO,EAEL,YAAY,IAAI,mBAAmB,EACpC,MAAM,wBAAwB,CAAC;AAiEhC,eAAO,MAAM,cAAc;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAG1B,CAAC;AAEF,eAAO,MAAM,oBAAoB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAevB,CAAC;AAEX,eAAO,MAAM,wBAAwB;;;;;;;;;;CAI3B,CAAC;AAEX,MAAM,WAAW,OAAO;IACtB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,sBAAsB,CACrC,WAAW,SAAS,MAAM,EAAE,GAAG,MAAM,EAAE,EACvC,YAAY,SAAS,MAAM,EAAE,GAAG,MAAM,EAAE;IAExC,WAAW,EAAE,WAAW,CAAC;IACzB,YAAY,EAAE,YAAY,CAAC;CAC5B;AAED,MAAM,WAAW,0BAA0B,CACzC,WAAW,SAAS,MAAM,EAAE,GAAG,MAAM,EAAE,EACvC,YAAY,SAAS,MAAM,EAAE,GAAG,MAAM,EAAE,EACxC,WAAW,SAAS,OAAO,GAAG,OAAO,EACrC,YAAY,SAAS,OAAO,GAAG,OAAO,CACtC,SAAQ,sBAAsB,CAAC,WAAW,EAAE,YAAY,CAAC;IACzD,WAAW,CAAC,EAAE,WAAW,CAAC;IAC1B,YAAY,CAAC,EAAE,YAAY,CAAC;CAC7B;AAED,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC9B,MAAM,CAAC,EAAE,OAAO,GAAG,MAAM,CAAC;IAC1B,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;CACpB;AAED,MAAM,WAAW,0BAA0B;IACzC,eAAe,CAAC,EAAE,UAAU,CAAC,OAAO,mBAAmB,CAAC,CAAC,CAAC,CAAC,CAAC,iBAAiB,CAAC,CAAC;IAC/E,mBAAmB,CAAC,EAAE,UAAU,CAAC,OAAO,mBAAmB,CAAC,CAAC,CAAC,CAAC,CAAC,qBAAqB,CAAC,CAAC;IACvF,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,aAAa,KAAK,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC,CAAC;CACzF;AA8BD,eAAO,MAAM,YAAY,aA6BvB,CAAC;
|
|
1
|
+
{"version":3,"file":"createThemes.d.ts","sourceRoot":"","sources":["../../src/theme/createThemes.ts"],"names":[],"mappings":"AACA,OAAO,EAEL,YAAY,IAAI,mBAAmB,EACpC,MAAM,wBAAwB,CAAC;AAiEhC,eAAO,MAAM,cAAc;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAG1B,CAAC;AAEF,eAAO,MAAM,oBAAoB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAevB,CAAC;AAEX,eAAO,MAAM,wBAAwB;;;;;;;;;;CAI3B,CAAC;AAEX,MAAM,WAAW,OAAO;IACtB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,sBAAsB,CACrC,WAAW,SAAS,MAAM,EAAE,GAAG,MAAM,EAAE,EACvC,YAAY,SAAS,MAAM,EAAE,GAAG,MAAM,EAAE;IAExC,WAAW,EAAE,WAAW,CAAC;IACzB,YAAY,EAAE,YAAY,CAAC;CAC5B;AAED,MAAM,WAAW,0BAA0B,CACzC,WAAW,SAAS,MAAM,EAAE,GAAG,MAAM,EAAE,EACvC,YAAY,SAAS,MAAM,EAAE,GAAG,MAAM,EAAE,EACxC,WAAW,SAAS,OAAO,GAAG,OAAO,EACrC,YAAY,SAAS,OAAO,GAAG,OAAO,CACtC,SAAQ,sBAAsB,CAAC,WAAW,EAAE,YAAY,CAAC;IACzD,WAAW,CAAC,EAAE,WAAW,CAAC;IAC1B,YAAY,CAAC,EAAE,YAAY,CAAC;CAC7B;AAED,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC9B,MAAM,CAAC,EAAE,OAAO,GAAG,MAAM,CAAC;IAC1B,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;CACpB;AAED,MAAM,WAAW,0BAA0B;IACzC,eAAe,CAAC,EAAE,UAAU,CAAC,OAAO,mBAAmB,CAAC,CAAC,CAAC,CAAC,CAAC,iBAAiB,CAAC,CAAC;IAC/E,mBAAmB,CAAC,EAAE,UAAU,CAAC,OAAO,mBAAmB,CAAC,CAAC,CAAC,CAAC,CAAC,qBAAqB,CAAC,CAAC;IACvF,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,aAAa,KAAK,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC,CAAC;CACzF;AA8BD,eAAO,MAAM,YAAY,aA6BvB,CAAC;AA0iBH,wBAAgB,mBAAmB,CACjC,SAAS,EAAE,0BAA0B,EACrC,WAAW,EAAE,sBAAsB,EACnC,OAAO,CAAC,EAAE,0BAA0B;;aAmFzB,CAAC,SAAS,MAAM,QAAQ,CAAC,SAAS,sBAAsB,GAuB1D,MAAM,CAAC,QAAQ,CAAC,EAAE,GAAG,SAAS,CAAC,EAAE;;+BAAkC;EAG7E"}
|
|
@@ -16,26 +16,21 @@ export declare const FOCUS_RING_MIN_CONTRAST = 3;
|
|
|
16
16
|
export declare function focusRingColorMeetsContrast(ringColor: string, surfaceColor: string): boolean;
|
|
17
17
|
/**
|
|
18
18
|
* ONE ring geometry (DG-A11Y-01 / LC-71 §4): 2px solid `$outlineColor` at
|
|
19
|
-
* offset
|
|
20
|
-
* surface behind the control
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
* old flat inset (−2) painted
|
|
24
|
-
* 1.17–1.31:1 (
|
|
25
|
-
* the same ring on the page ground clears 5.78–6.50:1 (MPO-41 / MPO-19 X7).
|
|
19
|
+
* offset **+2**, a band clear of the border box with a 2px gap, painted on the
|
|
20
|
+
* surface behind the control. FZ-7944 round 2 moved the default from 0 to +2
|
|
21
|
+
* (the macOS and iPadOS keyboard ring); the ≥3:1 floor stays computable
|
|
22
|
+
* because the band still paints on the page ground, never inside a fill. The
|
|
23
|
+
* old flat inset (−2) painted INSIDE solid intent fills and sank to
|
|
24
|
+
* 1.17–1.31:1 (MPO-41 / MPO-19 X7).
|
|
26
25
|
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
* MPO-92 ratified a second, shape-scoped carve-out on the other side:
|
|
32
|
-
* `FOCUS_VISIBLE_RING_HALO` (+2) for pill/circular thumbs and inline text
|
|
33
|
-
* links. The F1 "not +2" reading was about the DEFAULT — a blanket +2 on
|
|
34
|
-
* every control — and it stands. The halo is a named carve-out with a written
|
|
35
|
-
* shape scope, exactly as the inset is; `SANCTIONED_FOCUS_RING_OFFSETS` below
|
|
36
|
-
* is the closed set of all three.
|
|
26
|
+
* Two carve-outs stay named: `FOCUS_VISIBLE_RING_INSET` (−2) for clipping
|
|
27
|
+
* ancestors, and `FOCUS_RING_FLUSH_OFFSET` (0) for rows and cells that sit
|
|
28
|
+
* edge to edge inside a padded overlay, where a gapped band would collide
|
|
29
|
+
* with the neighbour. `SANCTIONED_FOCUS_RING_OFFSETS` is the closed set.
|
|
37
30
|
*/
|
|
38
31
|
export declare const FOCUS_RING_CLIPPED_OFFSET = -2;
|
|
32
|
+
export declare const FOCUS_RING_FLUSH_OFFSET = 0;
|
|
33
|
+
export declare const FOCUS_RING_HALO_OFFSET = 2;
|
|
39
34
|
export declare const FOCUS_VISIBLE_RING: {
|
|
40
35
|
outlineWidth: number;
|
|
41
36
|
outlineStyle: "solid";
|
|
@@ -62,29 +57,10 @@ export declare const FOCUS_VISIBLE_RING_INSET: {
|
|
|
62
57
|
outlineColor: string;
|
|
63
58
|
};
|
|
64
59
|
/**
|
|
65
|
-
* The
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
* Scope is by SHAPE, not by instance, and it is closed at two:
|
|
70
|
-
* - **pill / circular thumbs** — a round handle the user drags (Slider
|
|
71
|
-
* thumb, Rating star, TimeRangeScrubber handle). At offset 0 the band is
|
|
72
|
-
* concentric with the glyph's own edge and reads as a thicker glyph.
|
|
73
|
-
* - **inline text links** — a link inside a run of prose (markdown/mdx
|
|
74
|
-
* `Link`, ErrorSummary's summary link, backoffice "View All"). At offset 0
|
|
75
|
-
* the band crosses the line box and collides with ascenders/descenders of
|
|
76
|
-
* the surrounding text.
|
|
77
|
-
*
|
|
78
|
-
* NOT for standalone rectangles on the page ground — cards, tiles, rows,
|
|
79
|
-
* fields. Those are the default `FOCUS_VISIBLE_RING` (offset 0), which is
|
|
80
|
-
* what makes the Axiom 12 ≥3:1 floor computable against the page ground.
|
|
81
|
-
*
|
|
82
|
-
* Contrast is unaffected: like offset 0 the halo paints OUTSIDE the border
|
|
83
|
-
* box, on the surface behind the control, so it measures against the same
|
|
84
|
-
* page ground the default does — the 1.17–1.31:1 failures MPO-41 retired were
|
|
85
|
-
* the INSET painting inside solid intent fills, a defect the halo cannot have.
|
|
60
|
+
* The halo (MPO-92): pill and circular thumbs and inline text links. Since
|
|
61
|
+
* FZ-7944 round 2 it equals the default ring; the name stays so those call
|
|
62
|
+
* sites keep saying why they hold +2 if the default ever moves again.
|
|
86
63
|
*/
|
|
87
|
-
export declare const FOCUS_RING_HALO_OFFSET = 2;
|
|
88
64
|
export declare const FOCUS_VISIBLE_RING_HALO: {
|
|
89
65
|
outlineOffset: number;
|
|
90
66
|
outlineWidth: number;
|
|
@@ -93,13 +69,13 @@ export declare const FOCUS_VISIBLE_RING_HALO: {
|
|
|
93
69
|
};
|
|
94
70
|
/**
|
|
95
71
|
* The CLOSED set of sanctioned focus-ring offsets (LC-71 §4). Three, and only
|
|
96
|
-
* three: the
|
|
72
|
+
* three: the clipping inset −2, the flush 0, the default +2. A call site
|
|
97
73
|
* that wants any other number is a rule gap, not a preference — file it and
|
|
98
74
|
* amend §4, do not type the number. `focusState.spec.ts` asserts this set is
|
|
99
75
|
* exactly what the repo paints, so a fourth offset fails a test instead of
|
|
100
76
|
* shipping (MPO-92 AC-3).
|
|
101
77
|
*/
|
|
102
|
-
export declare const SANCTIONED_FOCUS_RING_OFFSETS: readonly [-2,
|
|
78
|
+
export declare const SANCTIONED_FOCUS_RING_OFFSETS: readonly [-2, 0, number];
|
|
103
79
|
/** True when `offset` is one of the three offsets LC-71 §4 sanctions. */
|
|
104
80
|
export declare function isSanctionedFocusRingOffset(offset: number): boolean;
|
|
105
81
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"focusState.d.ts","sourceRoot":"","sources":["../../src/theme/focusState.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,WAAW,CAAC;AAEnD;;;;GAIG;AACH,eAAO,MAAM,oBAAoB,IAAI,CAAC;AACtC,eAAO,MAAM,uBAAuB,IAAI,CAAC;AAuBzC;;;;;;GAMG;AACH,wBAAgB,2BAA2B,CAAC,SAAS,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,OAAO,CAO5F;AAED
|
|
1
|
+
{"version":3,"file":"focusState.d.ts","sourceRoot":"","sources":["../../src/theme/focusState.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,WAAW,CAAC;AAEnD;;;;GAIG;AACH,eAAO,MAAM,oBAAoB,IAAI,CAAC;AACtC,eAAO,MAAM,uBAAuB,IAAI,CAAC;AAuBzC;;;;;;GAMG;AACH,wBAAgB,2BAA2B,CAAC,SAAS,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,OAAO,CAO5F;AAED;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,yBAAyB,KAAK,CAAC;AAC5C,eAAO,MAAM,uBAAuB,IAAI,CAAC;AACzC,eAAO,MAAM,sBAAsB,IAAI,CAAC;AACxC,eAAO,MAAM,kBAAkB;;;;;CAKF,CAAC;AAE9B;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,wBAAwB;;;;;CAGR,CAAC;AAE9B;;;;GAIG;AACH,eAAO,MAAM,uBAAuB;;;;;CAGP,CAAC;AAE9B;;;;;;;GAOG;AACH,eAAO,MAAM,6BAA6B,0BAIhC,CAAC;AAEX,yEAAyE;AACzE,wBAAgB,2BAA2B,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAEnE;AAED;;;;GAIG;AACH,eAAO,MAAM,iBAAiB;;CAED,CAAC;AAE9B,eAAO,MAAM,iBAAiB;;CAED,CAAC;AAE9B;;;;GAIG;AACH,wBAAgB,sBAAsB,CAAC,CAAC,SAAS,iBAAiB,EAChE,QAAQ,CAAC,EAAE,CAAC,CAAC,GAAG,iBAAiB,CAAC,GAAG,IAAI,GACxC,CAAC,GAAG,iBAAiB,CAyBvB;AAED,mFAAmF;AACnF,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,iBAAiB,EAC1B,KAAK,EAAE,iBAAiB,GACvB,iBAAiB,CAKnB"}
|
package/types/theme/motion.d.ts
CHANGED
|
@@ -4,8 +4,10 @@ export { motionDurationMs, motionHideDelayMs, motionSeconds, motionSettleMs } fr
|
|
|
4
4
|
export type MotionRoleName = "control" | "overlay" | "scrim" | "reveal" | "slide";
|
|
5
5
|
export interface MotionRole {
|
|
6
6
|
transition: TransitionProp | undefined;
|
|
7
|
+
exit?: TransitionProp | undefined;
|
|
7
8
|
animateOnly: readonly string[];
|
|
8
9
|
}
|
|
10
|
+
export declare const motionExitFactor: number;
|
|
9
11
|
export declare const motionRoleProperties: {
|
|
10
12
|
readonly control: readonly ["backgroundColor", "borderColor", "color", "opacity", "transform"];
|
|
11
13
|
readonly overlay: readonly ["opacity", "transform"];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"motion.d.ts","sourceRoot":"","sources":["../../src/theme/motion.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAC9C,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AAIzC,OAAO,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAEpG,MAAM,MAAM,cAAc,GAAG,SAAS,GAAG,SAAS,GAAG,OAAO,GAAG,QAAQ,GAAG,OAAO,CAAC;AAElF,MAAM,WAAW,UAAU;IACzB,UAAU,EAAE,cAAc,GAAG,SAAS,CAAC;IACvC,WAAW,EAAE,SAAS,MAAM,EAAE,CAAC;CAChC;AAED,eAAO,MAAM,oBAAoB;;;;;;CAM6B,CAAC;
|
|
1
|
+
{"version":3,"file":"motion.d.ts","sourceRoot":"","sources":["../../src/theme/motion.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAC9C,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AAIzC,OAAO,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAEpG,MAAM,MAAM,cAAc,GAAG,SAAS,GAAG,SAAS,GAAG,OAAO,GAAG,QAAQ,GAAG,OAAO,CAAC;AAElF,MAAM,WAAW,UAAU;IACzB,UAAU,EAAE,cAAc,GAAG,SAAS,CAAC;IACvC,IAAI,CAAC,EAAE,cAAc,GAAG,SAAS,CAAC;IAClC,WAAW,EAAE,SAAS,MAAM,EAAE,CAAC;CAChC;AAED,eAAO,MAAM,gBAAgB,QAAQ,CAAC;AAEtC,eAAO,MAAM,oBAAoB;;;;;;CAM6B,CAAC;AAmB/D,wBAAgB,SAAS,CAAC,IAAI,EAAE,SAAS,GAAG,MAAM,CAAC,cAAc,EAAE,UAAU,CAAC,CAuB7E"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"overlaySurfaces.d.ts","sourceRoot":"","sources":["../../src/theme/overlaySurfaces.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"overlaySurfaces.d.ts","sourceRoot":"","sources":["../../src/theme/overlaySurfaces.tsx"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAEvC,OAAO,EAML,KAAK,kBAAkB,IAAI,yBAAyB,EAEpD,KAAK,UAAU,EACf,KAAK,cAAc,EAEpB,MAAM,SAAS,CAAC;AA6DjB,KAAK,aAAa,GAAG,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;AAmC5C;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,EAAE,eAAe,EAAE,EAAE;IAAE,eAAe,CAAC,EAAE,aAAa,CAAA;CAAE,GAAG,IAAI,CAG9F;AAID,MAAM,WAAW,wBAAwB;IACvC,4EAA4E;IAC5E,cAAc,EAAE,OAAO,CAAC;CACzB;AAED;;;;GAIG;AACH,eAAO,MAAM,mBAAmB,mDAE9B,CAAC;AAEH,wBAAgB,sBAAsB,IAAI,wBAAwB,CAEjE;AAED,wBAAgB,mBAAmB,IAAI,OAAO,CAE7C;AAQD,MAAM,MAAM,kBAAkB,GAAG,IAAI,CAAC,yBAAyB,EAAE,MAAM,CAAC,GAAG;IACzE,QAAQ,CAAC,EAAE,SAAS,CAAC;IACrB,IAAI,CAAC,EAAE,UAAU,CAAC;CACnB,CAAC;AAEF,eAAO,MAAM,aAAa;eAJb,SAAS;WACb,UAAU;kDAcjB,CAAC;AA0CH;;;;;;GAMG;AACH,eAAO,MAAM,aAAa;;;;;;;;eAEiB,SAAS;kDAelD,CAAC;AAOH,eAAO,MAAM,kBAAkB;eAxFlB,SAAS;WACb,UAAU;kDAiGlB,CAAC;AAgCF,eAAO,MAAM,kBAAkB;;;;;;;;eAEY,SAAS;kDAelD,CAAC;AAiBH,eAAO,MAAM,cAAc;eALd,SAAS;IACpB,iEAAiE;sBAC/C,aAAa;kDA0BhC,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"transitionProps.native.d.ts","sourceRoot":"","sources":["../../src/theme/transitionProps.native.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAC3C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"transitionProps.native.d.ts","sourceRoot":"","sources":["../../src/theme/transitionProps.native.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAC3C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAG9C;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAC7B,IAAI,EAAE,UAAU,GAAG,cAAc,GAAG,SAAS,EAC7C,KAAK,GAAE,SAAS,MAAM,EAAO,GAC5B;IACD,UAAU,CAAC,EAAE,cAAc,CAAC;IAC5B,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;CACxB,CAmBA"}
|