@multiplatform.one/forms 7.72.0 → 7.74.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/dist/cjs/Button/IconButtonGroup.cjs +32 -0
- package/dist/cjs/Button/IconButtonGroup.native.js +32 -0
- package/dist/cjs/Button/IconButtonGroup.native.js.map +1 -1
- package/dist/cjs/Button/circleGlyph.cjs +28 -0
- package/dist/cjs/Button/circleGlyph.native.js +31 -0
- package/dist/cjs/Button/circleGlyph.native.js.map +1 -0
- package/dist/cjs/Button/glass.cjs +4 -1
- package/dist/cjs/Button/iconButtonGroupContext.native.js.map +1 -1
- package/dist/cjs/Button/index.cjs +44 -15
- package/dist/cjs/Button/index.native.js +52 -17
- package/dist/cjs/Button/index.native.js.map +1 -1
- package/dist/cjs/Button/webGlass.cjs +62 -0
- package/dist/cjs/Button/webGlass.native.js +68 -0
- package/dist/cjs/Button/webGlass.native.js.map +1 -0
- package/dist/cjs/FloatingPanel/PanelHeader.cjs +49 -0
- package/dist/cjs/FloatingPanel/PanelHeader.native.js +53 -0
- package/dist/cjs/FloatingPanel/PanelHeader.native.js.map +1 -0
- package/dist/cjs/FloatingPanel/echoContext.cjs +28 -0
- package/dist/cjs/FloatingPanel/echoContext.native.js +29 -0
- package/dist/cjs/FloatingPanel/echoContext.native.js.map +1 -1
- package/dist/cjs/FloatingPanel/iconEcho.cjs +104 -0
- package/dist/cjs/FloatingPanel/iconEcho.native.js +114 -0
- package/dist/cjs/FloatingPanel/iconEcho.native.js.map +1 -0
- package/dist/cjs/FloatingPanel/index.cjs +168 -15
- package/dist/cjs/FloatingPanel/index.native.js +77 -8
- package/dist/cjs/FloatingPanel/index.native.js.map +1 -1
- package/dist/cjs/FloatingPanel/paintedBox.cjs +80 -0
- package/dist/cjs/FloatingPanel/paintedBox.native.js +85 -0
- package/dist/cjs/FloatingPanel/paintedBox.native.js.map +1 -0
- package/dist/cjs/FloatingPanel/pointAnchor.cjs +22 -3
- package/dist/cjs/FloatingPanel/pointAnchor.native.js +26 -7
- package/dist/cjs/FloatingPanel/pointAnchor.native.js.map +1 -1
- package/dist/cjs/FloatingPanel/useFloatingPanel.cjs +2 -1
- package/dist/cjs/FloatingPanel/useFloatingPanel.native.js +3 -1
- package/dist/cjs/FloatingPanel/useFloatingPanel.native.js.map +1 -1
- package/dist/cjs/fields/FileUpload/index.cjs +8 -3
- package/dist/cjs/fields/FileUpload/index.native.js +8 -3
- package/dist/cjs/fields/FileUpload/index.native.js.map +1 -1
- package/dist/cjs/index.cjs +2 -0
- package/dist/cjs/index.native.js +2 -0
- package/dist/cjs/index.native.js.map +1 -1
- package/dist/esm/Button/IconButtonGroup.mjs +32 -0
- package/dist/esm/Button/IconButtonGroup.mjs.map +1 -1
- package/dist/esm/Button/IconButtonGroup.native.js +32 -0
- package/dist/esm/Button/IconButtonGroup.native.js.map +1 -1
- package/dist/esm/Button/circleGlyph.mjs +3 -0
- package/dist/esm/Button/circleGlyph.mjs.map +1 -0
- package/dist/esm/Button/circleGlyph.native.js +3 -0
- package/dist/esm/Button/circleGlyph.native.js.map +1 -0
- package/dist/esm/Button/glass.mjs +4 -2
- package/dist/esm/Button/glass.mjs.map +1 -1
- package/dist/esm/Button/iconButtonGroupContext.mjs.map +1 -1
- package/dist/esm/Button/iconButtonGroupContext.native.js.map +1 -1
- package/dist/esm/Button/index.mjs +44 -16
- package/dist/esm/Button/index.mjs.map +1 -1
- package/dist/esm/Button/index.native.js +52 -18
- package/dist/esm/Button/index.native.js.map +1 -1
- package/dist/esm/Button/webGlass.mjs +36 -0
- package/dist/esm/Button/webGlass.mjs.map +1 -0
- package/dist/esm/Button/webGlass.native.js +39 -0
- package/dist/esm/Button/webGlass.native.js.map +1 -0
- package/dist/esm/FloatingPanel/PanelHeader.mjs +24 -0
- package/dist/esm/FloatingPanel/PanelHeader.mjs.map +1 -0
- package/dist/esm/FloatingPanel/PanelHeader.native.js +25 -0
- package/dist/esm/FloatingPanel/PanelHeader.native.js.map +1 -0
- package/dist/esm/FloatingPanel/echoContext.mjs +25 -1
- package/dist/esm/FloatingPanel/echoContext.mjs.map +1 -1
- package/dist/esm/FloatingPanel/echoContext.native.js +26 -1
- package/dist/esm/FloatingPanel/echoContext.native.js.map +1 -1
- package/dist/esm/FloatingPanel/iconEcho.mjs +77 -0
- package/dist/esm/FloatingPanel/iconEcho.mjs.map +1 -0
- package/dist/esm/FloatingPanel/iconEcho.native.js +84 -0
- package/dist/esm/FloatingPanel/iconEcho.native.js.map +1 -0
- package/dist/esm/FloatingPanel/index.mjs +169 -16
- package/dist/esm/FloatingPanel/index.mjs.map +1 -1
- package/dist/esm/FloatingPanel/index.native.js +79 -10
- package/dist/esm/FloatingPanel/index.native.js.map +1 -1
- package/dist/esm/FloatingPanel/paintedBox.mjs +52 -0
- package/dist/esm/FloatingPanel/paintedBox.mjs.map +1 -0
- package/dist/esm/FloatingPanel/paintedBox.native.js +54 -0
- package/dist/esm/FloatingPanel/paintedBox.native.js.map +1 -0
- package/dist/esm/FloatingPanel/pointAnchor.mjs +22 -3
- package/dist/esm/FloatingPanel/pointAnchor.mjs.map +1 -1
- package/dist/esm/FloatingPanel/pointAnchor.native.js +26 -7
- package/dist/esm/FloatingPanel/pointAnchor.native.js.map +1 -1
- package/dist/esm/FloatingPanel/useFloatingPanel.mjs +2 -1
- package/dist/esm/FloatingPanel/useFloatingPanel.mjs.map +1 -1
- package/dist/esm/FloatingPanel/useFloatingPanel.native.js +3 -1
- package/dist/esm/FloatingPanel/useFloatingPanel.native.js.map +1 -1
- package/dist/esm/fields/FileUpload/index.mjs +8 -3
- package/dist/esm/fields/FileUpload/index.mjs.map +1 -1
- package/dist/esm/fields/FileUpload/index.native.js +8 -3
- package/dist/esm/fields/FileUpload/index.native.js.map +1 -1
- package/dist/esm/index.mjs +2 -1
- package/dist/esm/index.mjs.map +1 -1
- package/dist/esm/index.native.js +2 -1
- package/dist/esm/index.native.js.map +1 -1
- package/dist/jsx/Button/IconButtonGroup.mjs +32 -0
- package/dist/jsx/Button/IconButtonGroup.mjs.map +1 -1
- package/dist/jsx/Button/IconButtonGroup.native.js +32 -0
- package/dist/jsx/Button/IconButtonGroup.native.js.map +1 -1
- package/dist/jsx/Button/circleGlyph.mjs +3 -0
- package/dist/jsx/Button/circleGlyph.mjs.map +1 -0
- package/dist/jsx/Button/circleGlyph.native.js +31 -0
- package/dist/jsx/Button/circleGlyph.native.js.map +1 -0
- package/dist/jsx/Button/glass.mjs +4 -2
- package/dist/jsx/Button/glass.mjs.map +1 -1
- package/dist/jsx/Button/iconButtonGroupContext.mjs.map +1 -1
- package/dist/jsx/Button/iconButtonGroupContext.native.js.map +1 -1
- package/dist/jsx/Button/index.mjs +44 -16
- package/dist/jsx/Button/index.mjs.map +1 -1
- package/dist/jsx/Button/index.native.js +52 -17
- package/dist/jsx/Button/index.native.js.map +1 -1
- package/dist/jsx/Button/webGlass.mjs +36 -0
- package/dist/jsx/Button/webGlass.mjs.map +1 -0
- package/dist/jsx/Button/webGlass.native.js +68 -0
- package/dist/jsx/Button/webGlass.native.js.map +1 -0
- package/dist/jsx/FloatingPanel/PanelHeader.mjs +24 -0
- package/dist/jsx/FloatingPanel/PanelHeader.mjs.map +1 -0
- package/dist/jsx/FloatingPanel/PanelHeader.native.js +53 -0
- package/dist/jsx/FloatingPanel/PanelHeader.native.js.map +1 -0
- package/dist/jsx/FloatingPanel/echoContext.mjs +25 -1
- package/dist/jsx/FloatingPanel/echoContext.mjs.map +1 -1
- package/dist/jsx/FloatingPanel/echoContext.native.js +29 -0
- package/dist/jsx/FloatingPanel/echoContext.native.js.map +1 -1
- package/dist/jsx/FloatingPanel/iconEcho.mjs +77 -0
- package/dist/jsx/FloatingPanel/iconEcho.mjs.map +1 -0
- package/dist/jsx/FloatingPanel/iconEcho.native.js +114 -0
- package/dist/jsx/FloatingPanel/iconEcho.native.js.map +1 -0
- package/dist/jsx/FloatingPanel/index.mjs +169 -16
- package/dist/jsx/FloatingPanel/index.mjs.map +1 -1
- package/dist/jsx/FloatingPanel/index.native.js +77 -8
- package/dist/jsx/FloatingPanel/index.native.js.map +1 -1
- package/dist/jsx/FloatingPanel/paintedBox.mjs +52 -0
- package/dist/jsx/FloatingPanel/paintedBox.mjs.map +1 -0
- package/dist/jsx/FloatingPanel/paintedBox.native.js +85 -0
- package/dist/jsx/FloatingPanel/paintedBox.native.js.map +1 -0
- package/dist/jsx/FloatingPanel/pointAnchor.mjs +22 -3
- package/dist/jsx/FloatingPanel/pointAnchor.mjs.map +1 -1
- package/dist/jsx/FloatingPanel/pointAnchor.native.js +26 -7
- package/dist/jsx/FloatingPanel/pointAnchor.native.js.map +1 -1
- package/dist/jsx/FloatingPanel/useFloatingPanel.mjs +2 -1
- package/dist/jsx/FloatingPanel/useFloatingPanel.mjs.map +1 -1
- package/dist/jsx/FloatingPanel/useFloatingPanel.native.js +3 -1
- package/dist/jsx/FloatingPanel/useFloatingPanel.native.js.map +1 -1
- package/dist/jsx/fields/FileUpload/index.mjs +8 -3
- package/dist/jsx/fields/FileUpload/index.mjs.map +1 -1
- package/dist/jsx/fields/FileUpload/index.native.js +8 -3
- package/dist/jsx/fields/FileUpload/index.native.js.map +1 -1
- package/dist/jsx/index.js +2 -1
- package/dist/jsx/index.js.map +1 -1
- package/dist/jsx/index.mjs +2 -1
- package/dist/jsx/index.mjs.map +1 -1
- package/dist/jsx/index.native.js +2 -0
- package/dist/jsx/index.native.js.map +1 -1
- package/package.json +8 -8
- package/src/Button/Button.accentInk.spec.tsx +39 -8
- package/src/Button/Button.iconButton.spec.tsx +143 -3
- package/src/Button/Button.stories.tsx +55 -38
- package/src/Button/IconButtonGroup.spec.tsx +26 -0
- package/src/Button/IconButtonGroup.tsx +39 -0
- package/src/Button/circleGlyph.ts +6 -0
- package/src/Button/glass.spec.ts +73 -0
- package/src/Button/glass.ts +8 -3
- package/src/Button/iconButtonGroupContext.ts +2 -1
- package/src/Button/index.tsx +68 -17
- package/src/Button/webGlass.ts +42 -0
- package/src/FloatingPanel/FloatingPanel.iconEcho.spec.tsx +297 -0
- package/src/FloatingPanel/FloatingPanel.menuFace.native.spec.tsx +87 -1
- package/src/FloatingPanel/PanelHeader.tsx +26 -0
- package/src/FloatingPanel/echoContext.ts +45 -0
- package/src/FloatingPanel/iconEcho.tsx +118 -0
- package/src/FloatingPanel/index.native.tsx +115 -4
- package/src/FloatingPanel/index.tsx +238 -16
- package/src/FloatingPanel/paintedBox.spec.ts +95 -0
- package/src/FloatingPanel/paintedBox.ts +75 -0
- package/src/FloatingPanel/pointAnchor.spec.ts +47 -0
- package/src/FloatingPanel/pointAnchor.ts +28 -0
- package/src/FloatingPanel/useFloatingPanel.ts +6 -1
- package/src/fields/FileUpload/FileUpload.spec.tsx +8 -0
- package/src/fields/FileUpload/index.tsx +12 -4
- package/src/index.ts +2 -0
- package/types/Button/IconButtonGroup.d.ts.map +1 -1
- package/types/Button/circleGlyph.d.ts +7 -0
- package/types/Button/circleGlyph.d.ts.map +1 -0
- package/types/Button/glass.d.ts +5 -2
- package/types/Button/glass.d.ts.map +1 -1
- package/types/Button/iconButtonGroupContext.d.ts +2 -1
- package/types/Button/iconButtonGroupContext.d.ts.map +1 -1
- package/types/Button/index.d.ts +7 -4
- package/types/Button/index.d.ts.map +1 -1
- package/types/Button/webGlass.d.ts +32 -0
- package/types/Button/webGlass.d.ts.map +1 -0
- package/types/FloatingPanel/PanelHeader.d.ts +9 -0
- package/types/FloatingPanel/PanelHeader.d.ts.map +1 -0
- package/types/FloatingPanel/echoContext.d.ts +24 -0
- package/types/FloatingPanel/echoContext.d.ts.map +1 -1
- package/types/FloatingPanel/iconEcho.d.ts +29 -0
- package/types/FloatingPanel/iconEcho.d.ts.map +1 -0
- package/types/FloatingPanel/index.d.ts +10 -0
- package/types/FloatingPanel/index.d.ts.map +1 -1
- package/types/FloatingPanel/index.native.d.ts +1 -1
- package/types/FloatingPanel/index.native.d.ts.map +1 -1
- package/types/FloatingPanel/paintedBox.d.ts +29 -0
- package/types/FloatingPanel/paintedBox.d.ts.map +1 -0
- package/types/FloatingPanel/pointAnchor.d.ts +10 -1
- package/types/FloatingPanel/pointAnchor.d.ts.map +1 -1
- package/types/FloatingPanel/useFloatingPanel.d.ts.map +1 -1
- package/types/fields/FileUpload/index.d.ts.map +1 -1
- package/types/index.d.ts +2 -0
- package/types/index.d.ts.map +1 -1
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @vitest-environment jsdom
|
|
3
|
+
*
|
|
4
|
+
* LC-176 / LC-266: on the web only iOS Safari takes the glass face (Clay on
|
|
5
|
+
* rv_3bb3f1f65d6b981f).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { createDefaultThemeConfig } from "@multiplatform.one/theme";
|
|
9
|
+
import { cleanup, render } from "@testing-library/react";
|
|
10
|
+
import { createElement } from "react";
|
|
11
|
+
import { TamaguiProvider } from "tamagui";
|
|
12
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
13
|
+
import { isAppleTouchWeb, loadGlassView } from "./glass";
|
|
14
|
+
|
|
15
|
+
const IPHONE =
|
|
16
|
+
"Mozilla/5.0 (iPhone; CPU iPhone OS 26_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.0 Mobile/15E148 Safari/604.1";
|
|
17
|
+
const MAC_SAFARI =
|
|
18
|
+
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.0 Safari/605.1.15";
|
|
19
|
+
const houseConfig = createDefaultThemeConfig();
|
|
20
|
+
const PIXEL =
|
|
21
|
+
"Mozilla/5.0 (Linux; Android 15; Pixel 9) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Mobile Safari/537.36";
|
|
22
|
+
|
|
23
|
+
describe("isAppleTouchWeb", () => {
|
|
24
|
+
it("an iPhone's Safari takes the glass face", () => {
|
|
25
|
+
expect(isAppleTouchWeb({ userAgent: IPHONE, platform: "iPhone", maxTouchPoints: 5 })).toBe(
|
|
26
|
+
true,
|
|
27
|
+
);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("iPadOS reports a Mac with touch, and is iOS", () => {
|
|
31
|
+
expect(
|
|
32
|
+
isAppleTouchWeb({ userAgent: MAC_SAFARI, platform: "MacIntel", maxTouchPoints: 5 }),
|
|
33
|
+
).toBe(true);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("a Mac, an Android phone and no navigator draw the bare glyph", () => {
|
|
37
|
+
expect(
|
|
38
|
+
isAppleTouchWeb({ userAgent: MAC_SAFARI, platform: "MacIntel", maxTouchPoints: 0 }),
|
|
39
|
+
).toBe(false);
|
|
40
|
+
expect(isAppleTouchWeb({ userAgent: PIXEL, platform: "Linux armv8l", maxTouchPoints: 5 })).toBe(
|
|
41
|
+
false,
|
|
42
|
+
);
|
|
43
|
+
expect(isAppleTouchWeb(undefined)).toBe(false);
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
describe("loadGlassView on the web", () => {
|
|
48
|
+
afterEach(() => {
|
|
49
|
+
cleanup();
|
|
50
|
+
vi.unstubAllGlobals();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("iOS Safari's glass circle sits under the glyph it backs", () => {
|
|
54
|
+
vi.stubGlobal("navigator", { userAgent: IPHONE, platform: "iPhone", maxTouchPoints: 5 });
|
|
55
|
+
const Glass = loadGlassView();
|
|
56
|
+
expect(Glass).not.toBeNull();
|
|
57
|
+
const { container } = render(
|
|
58
|
+
createElement(
|
|
59
|
+
TamaguiProvider,
|
|
60
|
+
{ config: houseConfig.tamagui, defaultTheme: "light", disableInjectCSS: true },
|
|
61
|
+
createElement(Glass!, { style: { position: "absolute" } }),
|
|
62
|
+
),
|
|
63
|
+
);
|
|
64
|
+
const face = container.querySelector('[data-mp-glass="web"]') as HTMLElement | null;
|
|
65
|
+
expect(face).not.toBeNull();
|
|
66
|
+
expect(face!.style.zIndex).toBe("-1");
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("every other browser draws the bare glyph", () => {
|
|
70
|
+
vi.stubGlobal("navigator", { userAgent: PIXEL, platform: "Linux armv8l", maxTouchPoints: 5 });
|
|
71
|
+
expect(loadGlassView()).toBeNull();
|
|
72
|
+
});
|
|
73
|
+
});
|
package/src/Button/glass.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ComponentType, ReactNode } from "react";
|
|
2
|
+
import { isAppleTouchWeb, WebGlassFace } from "./webGlass";
|
|
2
3
|
|
|
3
4
|
/** The slice of expo-glass-effect's `GlassView` an icon button draws. */
|
|
4
5
|
export type GlassViewComponent = ComponentType<{
|
|
@@ -15,13 +16,17 @@ export type GlassContainerComponent = ComponentType<{
|
|
|
15
16
|
children?: ReactNode;
|
|
16
17
|
}>;
|
|
17
18
|
|
|
19
|
+
export { isAppleTouchWeb } from "./webGlass";
|
|
20
|
+
|
|
18
21
|
/**
|
|
19
22
|
* LC-176: the iOS 26 Liquid Glass material an icon button floats on in a bar.
|
|
20
|
-
*
|
|
21
|
-
*
|
|
23
|
+
* Native iOS draws it from expo-glass-effect (`glass.native.ts`; Android
|
|
24
|
+
* probes null). On the web only iOS Safari takes the glass face (Clay on
|
|
25
|
+
* rv_3bb3f1f65d6b981f: "Is there a way to have the buttons at the bottom be
|
|
26
|
+
* liquid glass if it's an iPhone"); every other browser draws the bare glyph.
|
|
22
27
|
*/
|
|
23
28
|
export function loadGlassView(): GlassViewComponent | null {
|
|
24
|
-
return null;
|
|
29
|
+
return isAppleTouchWeb() ? (WebGlassFace as GlassViewComponent) : null;
|
|
25
30
|
}
|
|
26
31
|
|
|
27
32
|
/**
|
|
@@ -4,7 +4,8 @@ const useIsomorphicLayoutEffect = typeof window === "undefined" ? useEffect : us
|
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* LC-176: the ground an `IconButtonGroup` paints under its buttons. `media`
|
|
7
|
-
* is one shared material capsule and `glass` one shared
|
|
7
|
+
* is one shared material capsule and `glass` one shared glass capsule (Liquid
|
|
8
|
+
* Glass on iOS 26, its web twin in iOS Safari),
|
|
8
9
|
* so a button inside draws its bare glyph instead of its own circle.
|
|
9
10
|
*/
|
|
10
11
|
export type IconButtonGroupGround = "media" | "glass" | null;
|
package/src/Button/index.tsx
CHANGED
|
@@ -34,7 +34,7 @@ import {
|
|
|
34
34
|
ensureKeyboardModalityTracking,
|
|
35
35
|
useBarSurface,
|
|
36
36
|
useElevatedSurface,
|
|
37
|
-
|
|
37
|
+
useGlyphTintOnSurface,
|
|
38
38
|
useResolvedKnobs,
|
|
39
39
|
useTouchSurface,
|
|
40
40
|
type ControlTier,
|
|
@@ -76,8 +76,11 @@ import { useControlGrouped } from "../shared/groupContext";
|
|
|
76
76
|
import { useInPanelEcho, usePanelTriggerRadiusReporter } from "../FloatingPanel/echoContext";
|
|
77
77
|
import { loadGlassView } from "./glass";
|
|
78
78
|
import { MEDIA_PLATE_WEB_STYLE } from "./mediaPlate";
|
|
79
|
+
import { ICON_BUTTON_CIRCLE_GLYPH_RATIO } from "./circleGlyph";
|
|
79
80
|
import { useIconButtonGroupGround, useIconButtonGroupMember } from "./iconButtonGroupContext";
|
|
80
81
|
|
|
82
|
+
export { ICON_BUTTON_CIRCLE_GLYPH_RATIO };
|
|
83
|
+
|
|
81
84
|
/**
|
|
82
85
|
* Coordinated size recipe (Polaris slim/medium/large, Primer s/m/l, Linear
|
|
83
86
|
* 28/32/40, Spectrum S/M, M3 XS/S). Height, pad, type, icon, gap and radius
|
|
@@ -437,32 +440,48 @@ export function iconButtonGlyphPx(rowLinePx: number | undefined): number {
|
|
|
437
440
|
return Math.max(ICON_BUTTON_GLYPH_PX, Math.round(rowLinePx / PHOSPHOR_INK_RATIO));
|
|
438
441
|
}
|
|
439
442
|
|
|
443
|
+
/** LC-176: an inline glyph is the text's size; Phosphor's 256 grid pads ~12%. */
|
|
444
|
+
const INLINE_GLYPH_RATIO = 1.2;
|
|
445
|
+
|
|
440
446
|
/**
|
|
441
|
-
* LC-176:
|
|
442
|
-
*
|
|
443
|
-
*
|
|
447
|
+
* LC-176: Phosphor bold's stem is 0.12 of its ink, an SF Symbols bold plus's
|
|
448
|
+
* 0.18 (measured at 17 and 24pt). An outline stroke this wide, in the 256-unit
|
|
449
|
+
* grid, brings a bold glyph to SF semibold-bold (0.154); a wider one closes
|
|
450
|
+
* Phosphor bold's 16-unit counters (the trash lid, copy's overlap).
|
|
444
451
|
*/
|
|
445
|
-
export const
|
|
452
|
+
export const GLYPH_BOLD_STROKE_UNITS = 8;
|
|
446
453
|
|
|
447
|
-
|
|
448
|
-
|
|
454
|
+
function boldStroke(weight: "regular" | "bold", color = "currentColor") {
|
|
455
|
+
return weight === "bold"
|
|
456
|
+
? {
|
|
457
|
+
stroke: color,
|
|
458
|
+
strokeWidth: GLYPH_BOLD_STROKE_UNITS,
|
|
459
|
+
strokeLinejoin: "round" as const,
|
|
460
|
+
}
|
|
461
|
+
: undefined;
|
|
462
|
+
}
|
|
449
463
|
|
|
450
464
|
function withGlyphWeight(
|
|
451
465
|
icon: IconLike | undefined,
|
|
452
466
|
size: number,
|
|
453
467
|
weight: "regular" | "bold",
|
|
468
|
+
floor = false,
|
|
454
469
|
): IconLike | undefined {
|
|
455
470
|
if (icon == null || icon === false) return icon;
|
|
456
|
-
if (isValidElement<{ size?: number; weight?: string }>(icon)) {
|
|
471
|
+
if (isValidElement<{ size?: number; weight?: string; color?: string }>(icon)) {
|
|
472
|
+
const resolved = (icon.props.weight ?? weight) as "regular" | "bold";
|
|
473
|
+
const own = icon.props.size;
|
|
457
474
|
return cloneElement(icon, {
|
|
458
|
-
...(
|
|
475
|
+
...(own == null || (floor && typeof own === "number" && own < size) ? { size } : undefined),
|
|
459
476
|
...(icon.props.weight == null ? { weight } : undefined),
|
|
477
|
+
...boldStroke(resolved, icon.props.color),
|
|
460
478
|
});
|
|
461
479
|
}
|
|
462
480
|
if (typeof icon === "function" || (typeof icon === "object" && "$$typeof" in icon)) {
|
|
463
481
|
return createElement(icon as ComponentType<{ size?: number; weight?: string }>, {
|
|
464
482
|
size,
|
|
465
483
|
weight,
|
|
484
|
+
...boldStroke(weight),
|
|
466
485
|
});
|
|
467
486
|
}
|
|
468
487
|
return icon;
|
|
@@ -572,7 +591,7 @@ export function Button({
|
|
|
572
591
|
const parentScopeTheme = useTheme();
|
|
573
592
|
const pageOutlineColor = parentScopeTheme.outlineColor?.val as string | undefined;
|
|
574
593
|
const hostAccentInk = useAccentOnSurface();
|
|
575
|
-
const
|
|
594
|
+
const glyphTint = useGlyphTintOnSurface();
|
|
576
595
|
const grouped = useControlGrouped();
|
|
577
596
|
// LC-176 ICON-BUTTON: with no label, the button is its glyph. It paints no
|
|
578
597
|
// plate at rest and takes the bar tint, unless the caller asked for a frame
|
|
@@ -697,7 +716,7 @@ export function Button({
|
|
|
697
716
|
// Accent and intent buttons, chromeless, and grouped segments keep their
|
|
698
717
|
// own contracts.
|
|
699
718
|
const tier: ControlTier | undefined =
|
|
700
|
-
activeThemeName || chromeless || grouped
|
|
719
|
+
activeThemeName || chromeless || grouped || mediaPlate
|
|
701
720
|
? undefined
|
|
702
721
|
: barSurface
|
|
703
722
|
? "plain"
|
|
@@ -810,7 +829,13 @@ export function Button({
|
|
|
810
829
|
// The media plate is its own state ground; a toggle over media shows its
|
|
811
830
|
// state in the glyph.
|
|
812
831
|
const selectedFill = Boolean(isSelected) && !unavailable && !mediaPlate;
|
|
813
|
-
|
|
832
|
+
// A glyph alone holds 3:1; beside a label both take the label's AA tint.
|
|
833
|
+
const barInk =
|
|
834
|
+
accentInk && chromeless && !selectedFill && !unavailable
|
|
835
|
+
? labelled
|
|
836
|
+
? glyphTint.label
|
|
837
|
+
: glyphTint.glyph
|
|
838
|
+
: null;
|
|
814
839
|
const inlineGlyph = iconButton && inline === true && !nested;
|
|
815
840
|
// LC-176: back's caret beside its label is a bar glyph too, and so is the
|
|
816
841
|
// glyph on a plate (media, or the overlay's confirm).
|
|
@@ -828,6 +853,17 @@ export function Button({
|
|
|
828
853
|
? loadGlassView()
|
|
829
854
|
: null;
|
|
830
855
|
const framedCircle = iconOnly && Boolean(circular) && !nested && !mediaPlate && !confirmPlate;
|
|
856
|
+
// LC-176: a glyph on a plate takes the plate's on-colour at full strength,
|
|
857
|
+
// never the frame's quieter default: white on the media material, the text
|
|
858
|
+
// colour on a neutral circle.
|
|
859
|
+
const plateInk =
|
|
860
|
+
unavailable || barInk || outlinedInk
|
|
861
|
+
? undefined
|
|
862
|
+
: mediaPlate || (iconButton && groupGround === "media")
|
|
863
|
+
? "$color12"
|
|
864
|
+
: framedCircle && !activeThemeName
|
|
865
|
+
? hostScoped({ color: "$color12" }).color
|
|
866
|
+
: undefined;
|
|
831
867
|
const framedCirclePx =
|
|
832
868
|
touch && typeof sizeProp !== "number"
|
|
833
869
|
? Math.max(sizeTokenPx(resolvedSize), MIN_PRESS_TARGET)
|
|
@@ -841,13 +877,22 @@ export function Button({
|
|
|
841
877
|
: barGlyph
|
|
842
878
|
? touch
|
|
843
879
|
? Math.max(glyphFloor, resolveButtonSizeRecipe("$6", recipeFamily, touch).iconSize)
|
|
844
|
-
:
|
|
845
|
-
//
|
|
846
|
-
|
|
880
|
+
: iconButton
|
|
881
|
+
? // A bare glyph has no plate to inset from, so it takes its box
|
|
882
|
+
// less a 2pt optical margin, up to the row's glyph: the SHC
|
|
883
|
+
// "+" at $3 reaches the bar's glyph, a $2 copy beside mono
|
|
884
|
+
// text stays in proportion.
|
|
885
|
+
Math.max(recipe.iconSize, Math.min(glyphFloor, recipe.height - 4))
|
|
886
|
+
: // A pointer toolbar keeps its token box (LC-54); the glyph fills
|
|
887
|
+
// it up to the same floor.
|
|
888
|
+
Math.max(recipe.iconSize, Math.min(glyphFloor, recipe.height - 8))
|
|
847
889
|
: recipe.iconSize;
|
|
890
|
+
// LC-176: a bar glyph is never smaller than its row, whatever px the caller
|
|
891
|
+
// passed (the DataTable toolbar's 14pt token icons).
|
|
892
|
+
const barGlyphFloor = barGlyph && !framedCircle && !inlineGlyph && !nested;
|
|
848
893
|
const glyph =
|
|
849
894
|
barGlyph || inlineGlyph || framedCircle
|
|
850
|
-
? withGlyphWeight(icon, iconSize, resolvedGlyphWeight)
|
|
895
|
+
? withGlyphWeight(icon, iconSize, resolvedGlyphWeight, barGlyphFloor)
|
|
851
896
|
: icon;
|
|
852
897
|
|
|
853
898
|
// LC-158: the frame keeps its recipe height under Dynamic Type, so the
|
|
@@ -912,6 +957,7 @@ export function Button({
|
|
|
912
957
|
props.color ??
|
|
913
958
|
(unavailable ? disabledText.color : undefined) ??
|
|
914
959
|
barInk ??
|
|
960
|
+
plateInk ??
|
|
915
961
|
(textColor
|
|
916
962
|
? outlinedOnHost
|
|
917
963
|
? hostScoped({ color: textColor }).color
|
|
@@ -933,7 +979,7 @@ export function Button({
|
|
|
933
979
|
) : null;
|
|
934
980
|
const glyphAfter =
|
|
935
981
|
iconAfter && (barGlyph || inlineGlyph || framedCircle)
|
|
936
|
-
? withGlyphWeight(iconAfter, iconSize, resolvedGlyphWeight)
|
|
982
|
+
? withGlyphWeight(iconAfter, iconSize, resolvedGlyphWeight, barGlyphFloor)
|
|
937
983
|
: iconAfter;
|
|
938
984
|
const iconAfterElement = glyphAfter ? (
|
|
939
985
|
<View flexShrink={0}>
|
|
@@ -1098,6 +1144,10 @@ export function Button({
|
|
|
1098
1144
|
{...(inlineGlyph ? { marginInlineStart: "$2" as const } : undefined)}
|
|
1099
1145
|
// The glass rounds itself, unclipped.
|
|
1100
1146
|
{...(GlassView ? { borderRadius: 100000 } : undefined)}
|
|
1147
|
+
// On the web the absolute glass circle needs its button as the
|
|
1148
|
+
// containing block and stacking context, or it fills the page and
|
|
1149
|
+
// paints over the glyph.
|
|
1150
|
+
{...(GlassView && isWeb ? { position: "relative" as const, zIndex: 0 } : undefined)}
|
|
1101
1151
|
{...(confirmPlate ? { borderRadius: 100000 } : undefined)}
|
|
1102
1152
|
{...(mediaPlate
|
|
1103
1153
|
? {
|
|
@@ -1117,6 +1167,7 @@ export function Button({
|
|
|
1117
1167
|
// text/icon color (currentColor) to the hue's readable text tier.
|
|
1118
1168
|
{...(outlinedInk ? { color: outlinedInk } : undefined)}
|
|
1119
1169
|
{...(barInk ? { color: barInk } : undefined)}
|
|
1170
|
+
{...(plateInk ? { color: plateInk } : undefined)}
|
|
1120
1171
|
// LC-40 DISABLED-VISIBLE: keepLabel washes the chrome (muted fill/
|
|
1121
1172
|
// border) while label/glyphs re-anchor to the lowest AA text tier
|
|
1122
1173
|
// (`color` inherits into currentColor icons); dimWhole dims the
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { isDarkThemeName } from "@multiplatform.one/theme";
|
|
2
|
+
import { createElement } from "react";
|
|
3
|
+
import { useThemeName, YStack } from "tamagui";
|
|
4
|
+
import { MEDIA_PLATE_WEB_STYLE } from "./mediaPlate";
|
|
5
|
+
|
|
6
|
+
type GlassNavigator = { userAgent?: string; platform?: string; maxTouchPoints?: number };
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* LC-266: iOS Safari, iPadOS included (it reports itself as a Mac with a
|
|
10
|
+
* touch screen). The one place web asks which platform it is on, so an iPhone
|
|
11
|
+
* user gets the iPhone's bar buttons in the browser too.
|
|
12
|
+
*/
|
|
13
|
+
export function isAppleTouchWeb(
|
|
14
|
+
nav: GlassNavigator | undefined = typeof navigator === "undefined" ? undefined : navigator,
|
|
15
|
+
): boolean {
|
|
16
|
+
if (!nav) return false;
|
|
17
|
+
if (/iPhone|iPad|iPod/.test(nav.userAgent ?? "")) return true;
|
|
18
|
+
return /Mac/.test(nav.platform ?? nav.userAgent ?? "") && (nav.maxTouchPoints ?? 0) > 1;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const webGlassMarker = { "data-mp-glass": "web" };
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* LC-176: the Liquid Glass face iOS Safari draws under a bar's icon buttons,
|
|
25
|
+
* one circle for a lone button or one capsule for an `IconButtonGroup`. It is
|
|
26
|
+
* a layer under the glyphs (absolute, z -1 in the button's stacking context):
|
|
27
|
+
* a near-white material in light and a raised grey in dark, edged by a soft
|
|
28
|
+
* drop shadow alone, with the media plate's blur.
|
|
29
|
+
*/
|
|
30
|
+
export function WebGlassFace({ style }: { style?: unknown }) {
|
|
31
|
+
const dark = isDarkThemeName(useThemeName());
|
|
32
|
+
return createElement(YStack, {
|
|
33
|
+
pointerEvents: "none",
|
|
34
|
+
backgroundColor: dark ? "$color4" : "$color1",
|
|
35
|
+
opacity: 0.92,
|
|
36
|
+
shadowColor: "$shadowColor",
|
|
37
|
+
shadowRadius: 10,
|
|
38
|
+
shadowOffset: { width: 0, height: 3 },
|
|
39
|
+
...webGlassMarker,
|
|
40
|
+
style: { ...(style as object), zIndex: -1, ...MEDIA_PLATE_WEB_STYLE },
|
|
41
|
+
});
|
|
42
|
+
}
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LC-112 icon trigger echo (FZ-8480, Clay's rv_2607879d3bdaf9bf): a panel
|
|
3
|
+
* that covers the icon button it opened from draws that button again in its
|
|
4
|
+
* exact spot, same size and plate, as an X (Round 37: every icon trigger,
|
|
5
|
+
* the + and a menu's button alike). It closes the panel and reads "Close".
|
|
6
|
+
*/
|
|
7
|
+
import { PlusIcon, XIcon } from "@phosphor-icons/react";
|
|
8
|
+
import { renderWithProviders } from "@multiplatform.one/test-utils";
|
|
9
|
+
import { fireEvent, render, waitFor, within } from "@testing-library/react";
|
|
10
|
+
import { useState } from "react";
|
|
11
|
+
import { Surface, useBarSurface } from "@multiplatform.one/theme";
|
|
12
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
13
|
+
import { Text } from "tamagui";
|
|
14
|
+
import { Button } from "../Button";
|
|
15
|
+
import { FloatingPanel } from "./index";
|
|
16
|
+
import { usePanelIconEchoBox } from "./echoContext";
|
|
17
|
+
import { PanelHeader } from "./PanelHeader";
|
|
18
|
+
import type { IconEcho } from "./iconEcho";
|
|
19
|
+
|
|
20
|
+
type Rect = { x: number; y: number; width: number; height: number };
|
|
21
|
+
|
|
22
|
+
function measure(trigger: Rect, panelWidth = 240, panelHeight = 160, surface?: Rect) {
|
|
23
|
+
const original = HTMLElement.prototype.getBoundingClientRect;
|
|
24
|
+
const rect = {
|
|
25
|
+
...trigger,
|
|
26
|
+
top: trigger.y,
|
|
27
|
+
left: trigger.x,
|
|
28
|
+
right: trigger.x + trigger.width,
|
|
29
|
+
bottom: trigger.y + trigger.height,
|
|
30
|
+
toJSON: () => ({}),
|
|
31
|
+
};
|
|
32
|
+
vi.spyOn(HTMLElement.prototype, "getBoundingClientRect").mockImplementation(
|
|
33
|
+
function (this: HTMLElement) {
|
|
34
|
+
if (this.hasAttribute("data-fp-reference")) return rect;
|
|
35
|
+
if (surface && this.hasAttribute("data-fp-cover-surface")) {
|
|
36
|
+
return {
|
|
37
|
+
...surface,
|
|
38
|
+
top: surface.y,
|
|
39
|
+
left: surface.x,
|
|
40
|
+
right: surface.x + surface.width,
|
|
41
|
+
bottom: surface.y + surface.height,
|
|
42
|
+
toJSON: () => ({}),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
return original.call(this);
|
|
46
|
+
},
|
|
47
|
+
);
|
|
48
|
+
vi.spyOn(HTMLElement.prototype, "offsetWidth", "get").mockImplementation(
|
|
49
|
+
function (this: HTMLElement) {
|
|
50
|
+
return this.hasAttribute("data-fp-panel") ? panelWidth : 0;
|
|
51
|
+
},
|
|
52
|
+
);
|
|
53
|
+
vi.spyOn(HTMLElement.prototype, "scrollHeight", "get").mockImplementation(
|
|
54
|
+
function (this: HTMLElement) {
|
|
55
|
+
return this.hasAttribute("data-fp-panel") ? panelHeight : 0;
|
|
56
|
+
},
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function EchoBoxProbe() {
|
|
61
|
+
const box = usePanelIconEchoBox();
|
|
62
|
+
return box ? (
|
|
63
|
+
<span
|
|
64
|
+
data-testid="echo-box"
|
|
65
|
+
data-height={box.height}
|
|
66
|
+
data-left={box.left}
|
|
67
|
+
data-right={box.right}
|
|
68
|
+
/>
|
|
69
|
+
) : null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function AddPanel({ kind = "close", header = false }: { kind?: IconEcho; header?: boolean }) {
|
|
73
|
+
const [open, setOpen] = useState(false);
|
|
74
|
+
return (
|
|
75
|
+
<FloatingPanel
|
|
76
|
+
open={open}
|
|
77
|
+
onOpenChange={setOpen}
|
|
78
|
+
anchorMode="cover-trigger"
|
|
79
|
+
compactFace="menu"
|
|
80
|
+
triggerEcho={false}
|
|
81
|
+
iconEcho={kind}
|
|
82
|
+
coverAlign="auto"
|
|
83
|
+
widthMode="fit-content"
|
|
84
|
+
triggerSizing="content"
|
|
85
|
+
contentPadding="none"
|
|
86
|
+
trigger={<Button icon={PlusIcon} aria-label="Add node" />}
|
|
87
|
+
>
|
|
88
|
+
{header ? (
|
|
89
|
+
<PanelHeader>
|
|
90
|
+
<Text>Add a node</Text>
|
|
91
|
+
<EchoBoxProbe />
|
|
92
|
+
</PanelHeader>
|
|
93
|
+
) : (
|
|
94
|
+
<Text>Add a node</Text>
|
|
95
|
+
)}
|
|
96
|
+
<Button>Copy</Button>
|
|
97
|
+
</FloatingPanel>
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const glyphPath = (node: Element | null | undefined) =>
|
|
102
|
+
node?.querySelector("svg path")?.getAttribute("d");
|
|
103
|
+
|
|
104
|
+
async function open(result: ReturnType<typeof renderWithProviders>) {
|
|
105
|
+
fireEvent.click(result.getByRole("button", { name: "Add node" }), { detail: 0 });
|
|
106
|
+
const panel = (await result.findByTestId("floating-panel-viewport")) as HTMLElement;
|
|
107
|
+
await waitFor(() => expect(panel.style.visibility).toBe("visible"));
|
|
108
|
+
return panel;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
afterEach(() => {
|
|
112
|
+
vi.restoreAllMocks();
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
describe("LC-112 icon trigger echo on the web (FZ-8480)", () => {
|
|
116
|
+
const viewportWidth = () => document.documentElement.clientWidth || window.innerWidth;
|
|
117
|
+
|
|
118
|
+
it("draws the + again as an X on the button's exact box at the panel's top trailing corner", async () => {
|
|
119
|
+
const button = { x: viewportWidth() - 16 - 44, y: 12, width: 44, height: 44 };
|
|
120
|
+
measure(button, 240);
|
|
121
|
+
const result = renderWithProviders(<AddPanel />);
|
|
122
|
+
const panel = await open(result);
|
|
123
|
+
const panelLeft = button.x + button.width - 240;
|
|
124
|
+
expect(panel.style.top).toBe(`${button.y}px`);
|
|
125
|
+
expect(panel.style.left).toBe(`${panelLeft}px`);
|
|
126
|
+
const row = panel.querySelector<HTMLElement>("[data-fp-icon-echo]") as HTMLElement;
|
|
127
|
+
expect(row.style.order).toBe("-1");
|
|
128
|
+
expect(row.style.height).toBe(`${button.height}px`);
|
|
129
|
+
// The X shares the content's first row, like a sheet's close button: it
|
|
130
|
+
// takes no height of its own.
|
|
131
|
+
expect(row.getAttribute("data-fp-icon-echo")).toBe("overlay");
|
|
132
|
+
const borderTop = parseFloat(getComputedStyle(panel).borderTopWidth) || 0;
|
|
133
|
+
expect(row.style.marginBottom).toBe(`${-(button.height - borderTop)}px`);
|
|
134
|
+
const box = row.querySelector<HTMLElement>("[data-fp-icon-echo-box]") as HTMLElement;
|
|
135
|
+
expect(box.style.width).toBe(`${button.width}px`);
|
|
136
|
+
expect(box.style.height).toBe(`${button.height}px`);
|
|
137
|
+
expect(box.style.marginLeft).toBe(`${button.x - panelLeft}px`);
|
|
138
|
+
const echo = within(panel).getByRole("button", { name: "Close" });
|
|
139
|
+
const x = render(<XIcon weight="bold" />);
|
|
140
|
+
expect(glyphPath(echo)).toBe(glyphPath(x.container));
|
|
141
|
+
x.unmount();
|
|
142
|
+
expect(glyphPath(echo)).not.toBe(glyphPath(result.getByRole("button", { name: "Add node" })));
|
|
143
|
+
expect(panel.querySelector("[data-fp-trigger-row]")).toBeNull();
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it("renders the echo on the trigger's bar surface, so it keeps the bar button's plate", async () => {
|
|
147
|
+
measure({ x: 16, y: 12, width: 44, height: 44 });
|
|
148
|
+
function BarProbe({ icon: _icon, ...rest }: { icon?: unknown; [key: string]: unknown }) {
|
|
149
|
+
const bar = useBarSurface();
|
|
150
|
+
const press = rest.onPress as (() => void) | undefined;
|
|
151
|
+
return (
|
|
152
|
+
<button
|
|
153
|
+
type="button"
|
|
154
|
+
aria-label={rest["aria-label"] as string}
|
|
155
|
+
data-bar={String(bar)}
|
|
156
|
+
onClick={() => press?.()}
|
|
157
|
+
/>
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
const result = renderWithProviders(
|
|
161
|
+
<Surface bar>
|
|
162
|
+
<FloatingPanel
|
|
163
|
+
open
|
|
164
|
+
onOpenChange={() => {}}
|
|
165
|
+
anchorMode="cover-trigger"
|
|
166
|
+
compactFace="menu"
|
|
167
|
+
triggerEcho={false}
|
|
168
|
+
iconEcho="close"
|
|
169
|
+
coverAlign="auto"
|
|
170
|
+
widthMode="fit-content"
|
|
171
|
+
triggerSizing="content"
|
|
172
|
+
trigger={<BarProbe icon={PlusIcon} aria-label="Add node" />}
|
|
173
|
+
>
|
|
174
|
+
<Text>Add a node</Text>
|
|
175
|
+
</FloatingPanel>
|
|
176
|
+
</Surface>,
|
|
177
|
+
);
|
|
178
|
+
const panel = (await result.findByTestId("floating-panel-viewport")) as HTMLElement;
|
|
179
|
+
expect(within(panel).getByRole("button", { name: "Close" }).getAttribute("data-bar")).toBe(
|
|
180
|
+
"true",
|
|
181
|
+
);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it("covers a grouped trigger's whole plate, only the pressed trigger showing as its X", async () => {
|
|
185
|
+
const button = { x: viewportWidth() - 16 - 44, y: 12, width: 44, height: 44 };
|
|
186
|
+
const plate = { x: button.x - 100, y: 12, width: 144, height: 44 };
|
|
187
|
+
measure(button, 120, 160, plate);
|
|
188
|
+
const result = renderWithProviders(
|
|
189
|
+
<div data-fp-cover-surface="">
|
|
190
|
+
<Button icon={PlusIcon} aria-label="Search" />
|
|
191
|
+
<AddPanel kind="menu" />
|
|
192
|
+
</div>,
|
|
193
|
+
);
|
|
194
|
+
const panel = await open(result);
|
|
195
|
+
// The plate is wider than the menu, so the menu takes the plate's width
|
|
196
|
+
// and both edges.
|
|
197
|
+
expect(panel.style.left).toBe(`${plate.x}px`);
|
|
198
|
+
expect(panel.style.top).toBe(`${plate.y}px`);
|
|
199
|
+
const row = panel.querySelector<HTMLElement>("[data-fp-icon-echo]") as HTMLElement;
|
|
200
|
+
// The band covers the whole plate; the group's other buttons are covered,
|
|
201
|
+
// not echoed.
|
|
202
|
+
expect(row.getAttribute("data-fp-icon-echo")).toBe("band");
|
|
203
|
+
expect(row.style.height).toBe(`${plate.height}px`);
|
|
204
|
+
expect(row.querySelector("[data-fp-group-echo]")).toBeNull();
|
|
205
|
+
expect(within(panel).queryByRole("button", { name: "Search" })).toBeNull();
|
|
206
|
+
const box = row.querySelector<HTMLElement>("[data-fp-icon-echo-box]") as HTMLElement;
|
|
207
|
+
expect(box.style.marginLeft).toBe(`${button.x - plate.x}px`);
|
|
208
|
+
expect(within(panel).getByRole("button", { name: "Close" })).toBeTruthy();
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it("closes the panel when the echo is pressed", async () => {
|
|
212
|
+
measure({ x: 16, y: 12, width: 44, height: 44 });
|
|
213
|
+
const result = renderWithProviders(<AddPanel />);
|
|
214
|
+
const panel = await open(result);
|
|
215
|
+
fireEvent.click(within(panel).getByRole("button", { name: "Close" }));
|
|
216
|
+
await waitFor(() => expect(result.queryByTestId("floating-panel-viewport")).toBeNull());
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it("turns a menu trigger into an X too, in its own band", async () => {
|
|
220
|
+
measure({ x: 16, y: 12, width: 44, height: 44 });
|
|
221
|
+
const result = renderWithProviders(<AddPanel kind="menu" />);
|
|
222
|
+
const panel = await open(result);
|
|
223
|
+
const echo = within(panel).getByRole("button", { name: "Close" });
|
|
224
|
+
const x = render(<XIcon weight="bold" />);
|
|
225
|
+
expect(glyphPath(echo)).toBe(glyphPath(x.container));
|
|
226
|
+
x.unmount();
|
|
227
|
+
// A menu's rows run the full width, so its X takes its own band.
|
|
228
|
+
const row = panel.querySelector<HTMLElement>("[data-fp-icon-echo]") as HTMLElement;
|
|
229
|
+
expect(row.getAttribute("data-fp-icon-echo")).toBe("band");
|
|
230
|
+
expect(row.style.marginBottom).toBe("");
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it("puts a panel header on the X's line, clear of the X", async () => {
|
|
234
|
+
const button = { x: viewportWidth() - 16 - 44, y: 12, width: 44, height: 44 };
|
|
235
|
+
measure(button, 240);
|
|
236
|
+
const result = renderWithProviders(<AddPanel header />);
|
|
237
|
+
const panel = await open(result);
|
|
238
|
+
expect(panel.querySelector("[data-fp-panel-header]")).toBeTruthy();
|
|
239
|
+
const box = await within(panel).findByTestId("echo-box");
|
|
240
|
+
// The header takes the X's height, and the X sits at the end edge, so it
|
|
241
|
+
// keeps the X's whole box clear there: from the X's start to the panel's end.
|
|
242
|
+
const panelLeft = button.x + button.width - 240;
|
|
243
|
+
expect(box.dataset.height).toBe(`${button.height}`);
|
|
244
|
+
expect(box.dataset.left).toBe("0");
|
|
245
|
+
expect(box.dataset.right).toBe(`${240 - (button.x - panelLeft)}`);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it("keeps a content-sized trigger on a centring row's line", async () => {
|
|
249
|
+
measure({ x: 16, y: 12, width: 44, height: 44 });
|
|
250
|
+
const centred = renderWithProviders(
|
|
251
|
+
<div style={{ display: "flex", flexDirection: "row", alignItems: "center", height: 60 }}>
|
|
252
|
+
<AddPanel />
|
|
253
|
+
</div>,
|
|
254
|
+
);
|
|
255
|
+
const inRow = centred.container.querySelector<HTMLElement>(
|
|
256
|
+
"[data-fp-reference]",
|
|
257
|
+
) as HTMLElement;
|
|
258
|
+
await waitFor(() => expect(getComputedStyle(inRow).alignSelf).not.toBe("flex-start"));
|
|
259
|
+
centred.unmount();
|
|
260
|
+
const column = renderWithProviders(
|
|
261
|
+
<div style={{ display: "flex", flexDirection: "column", alignItems: "stretch" }}>
|
|
262
|
+
<AddPanel />
|
|
263
|
+
</div>,
|
|
264
|
+
);
|
|
265
|
+
const inColumn = column.container.querySelector<HTMLElement>(
|
|
266
|
+
"[data-fp-reference]",
|
|
267
|
+
) as HTMLElement;
|
|
268
|
+
expect(getComputedStyle(inColumn).alignSelf).toBe("flex-start");
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
it("comes last in the document, so a keyboard open lands on the content first", async () => {
|
|
272
|
+
measure({ x: 16, y: 12, width: 44, height: 44 });
|
|
273
|
+
const result = renderWithProviders(<AddPanel />);
|
|
274
|
+
const panel = await open(result);
|
|
275
|
+
const buttons = within(panel).getAllByRole("button");
|
|
276
|
+
expect(buttons.map((b) => b.textContent || b.getAttribute("aria-label"))).toEqual([
|
|
277
|
+
"Copy",
|
|
278
|
+
"Close",
|
|
279
|
+
]);
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
it("opens up from the button's bottom edge near the bottom, the echo on the bottom row", async () => {
|
|
283
|
+
const button = { x: 16, y: window.innerHeight - 60, width: 44, height: 44 };
|
|
284
|
+
measure(button, 240, 300);
|
|
285
|
+
const result = renderWithProviders(<AddPanel />);
|
|
286
|
+
const panel = await open(result);
|
|
287
|
+
const frame = getComputedStyle(panel);
|
|
288
|
+
const borders =
|
|
289
|
+
(parseFloat(frame.borderTopWidth) || 0) + (parseFloat(frame.borderBottomWidth) || 0);
|
|
290
|
+
expect(parseFloat(panel.style.top) + 300 + borders).toBe(button.y + button.height);
|
|
291
|
+
expect(panel.style.left).toBe(`${button.x}px`);
|
|
292
|
+
const row = panel.querySelector<HTMLElement>("[data-fp-icon-echo]") as HTMLElement;
|
|
293
|
+
expect(row.style.order).toBe("1");
|
|
294
|
+
expect(panel.style.transformOrigin).toBe("bottom left");
|
|
295
|
+
expect(result.queryByTestId("floating-panel-sheet")).toBeNull();
|
|
296
|
+
});
|
|
297
|
+
});
|