@multiplatform.one/forms 7.81.2 → 7.82.1
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 +13 -9
- package/dist/cjs/Button/IconButtonGroup.native.js +13 -9
- package/dist/cjs/Button/IconButtonGroup.native.js.map +1 -1
- package/dist/cjs/Button/iconButtonGroupContext.cjs +6 -0
- package/dist/cjs/Button/iconButtonGroupContext.native.js +6 -0
- package/dist/cjs/Button/iconButtonGroupContext.native.js.map +1 -1
- package/dist/cjs/Button/index.cjs +38 -26
- package/dist/cjs/Button/index.native.js +38 -26
- package/dist/cjs/Button/index.native.js.map +1 -1
- package/dist/cjs/FloatingPanel/RegularPanel.cjs +2 -2
- package/dist/cjs/FloatingPanel/RegularPanel.native.js +2 -2
- package/dist/cjs/FloatingPanel/RegularPanel.native.js.map +1 -1
- package/dist/cjs/FloatingPanel/index.native.js +11 -10
- package/dist/cjs/FloatingPanel/index.native.js.map +1 -1
- package/dist/cjs/InputParts/index.cjs +12 -6
- package/dist/cjs/InputParts/index.native.js +12 -6
- package/dist/cjs/InputParts/index.native.js.map +1 -1
- package/dist/cjs/index.cjs +1 -0
- package/dist/cjs/index.native.js +1 -0
- package/dist/cjs/index.native.js.map +1 -1
- package/dist/esm/Button/IconButtonGroup.mjs +14 -10
- package/dist/esm/Button/IconButtonGroup.mjs.map +1 -1
- package/dist/esm/Button/IconButtonGroup.native.js +14 -10
- package/dist/esm/Button/IconButtonGroup.native.js.map +1 -1
- package/dist/esm/Button/iconButtonGroupContext.mjs +5 -1
- package/dist/esm/Button/iconButtonGroupContext.mjs.map +1 -1
- package/dist/esm/Button/iconButtonGroupContext.native.js +5 -1
- package/dist/esm/Button/iconButtonGroupContext.native.js.map +1 -1
- package/dist/esm/Button/index.mjs +39 -28
- package/dist/esm/Button/index.mjs.map +1 -1
- package/dist/esm/Button/index.native.js +39 -28
- package/dist/esm/Button/index.native.js.map +1 -1
- package/dist/esm/FloatingPanel/RegularPanel.mjs +2 -2
- package/dist/esm/FloatingPanel/RegularPanel.mjs.map +1 -1
- package/dist/esm/FloatingPanel/RegularPanel.native.js +2 -2
- package/dist/esm/FloatingPanel/RegularPanel.native.js.map +1 -1
- package/dist/esm/FloatingPanel/index.native.js +11 -10
- package/dist/esm/FloatingPanel/index.native.js.map +1 -1
- package/dist/esm/InputParts/index.mjs +13 -7
- package/dist/esm/InputParts/index.mjs.map +1 -1
- package/dist/esm/InputParts/index.native.js +13 -7
- package/dist/esm/InputParts/index.native.js.map +1 -1
- package/dist/esm/index.mjs +2 -2
- package/dist/esm/index.mjs.map +1 -1
- package/dist/esm/index.native.js +2 -2
- package/dist/esm/index.native.js.map +1 -1
- package/dist/jsx/Button/IconButtonGroup.mjs +14 -10
- package/dist/jsx/Button/IconButtonGroup.mjs.map +1 -1
- package/dist/jsx/Button/IconButtonGroup.native.js +13 -9
- package/dist/jsx/Button/IconButtonGroup.native.js.map +1 -1
- package/dist/jsx/Button/iconButtonGroupContext.mjs +5 -1
- package/dist/jsx/Button/iconButtonGroupContext.mjs.map +1 -1
- package/dist/jsx/Button/iconButtonGroupContext.native.js +6 -0
- package/dist/jsx/Button/iconButtonGroupContext.native.js.map +1 -1
- package/dist/jsx/Button/index.mjs +39 -28
- package/dist/jsx/Button/index.mjs.map +1 -1
- package/dist/jsx/Button/index.native.js +38 -26
- package/dist/jsx/Button/index.native.js.map +1 -1
- package/dist/jsx/FloatingPanel/RegularPanel.mjs +2 -2
- package/dist/jsx/FloatingPanel/RegularPanel.mjs.map +1 -1
- package/dist/jsx/FloatingPanel/RegularPanel.native.js +2 -2
- package/dist/jsx/FloatingPanel/RegularPanel.native.js.map +1 -1
- package/dist/jsx/FloatingPanel/index.native.js +11 -10
- package/dist/jsx/FloatingPanel/index.native.js.map +1 -1
- package/dist/jsx/InputParts/index.mjs +13 -7
- package/dist/jsx/InputParts/index.mjs.map +1 -1
- package/dist/jsx/InputParts/index.native.js +12 -6
- package/dist/jsx/InputParts/index.native.js.map +1 -1
- package/dist/jsx/index.js +2 -2
- package/dist/jsx/index.js.map +1 -1
- package/dist/jsx/index.mjs +2 -2
- package/dist/jsx/index.mjs.map +1 -1
- package/dist/jsx/index.native.js +1 -0
- package/dist/jsx/index.native.js.map +1 -1
- package/package.json +8 -8
- package/src/Button/Button.fieldGlyph.spec.tsx +186 -0
- package/src/Button/Button.stories.tsx +97 -82
- package/src/Button/IconButtonGroup.spec.tsx +48 -1
- package/src/Button/IconButtonGroup.tsx +26 -12
- package/src/Button/iconButtonGroupContext.ts +12 -0
- package/src/Button/index.tsx +73 -35
- package/src/FloatingPanel/FloatingPanel.menuFace.native.spec.tsx +2 -2
- package/src/FloatingPanel/FloatingPanel.openOnMount.native.spec.tsx +198 -0
- package/src/FloatingPanel/RegularPanel.tsx +8 -6
- package/src/FloatingPanel/index.native.tsx +29 -18
- package/src/InputParts/index.tsx +40 -27
- package/src/index.ts +1 -0
- package/types/Button/IconButtonGroup.d.ts.map +1 -1
- package/types/Button/iconButtonGroupContext.d.ts +8 -0
- package/types/Button/iconButtonGroupContext.d.ts.map +1 -1
- package/types/Button/index.d.ts +1 -0
- package/types/Button/index.d.ts.map +1 -1
- package/types/FloatingPanel/RegularPanel.d.ts.map +1 -1
- package/types/FloatingPanel/index.native.d.ts +5 -0
- package/types/FloatingPanel/index.native.d.ts.map +1 -1
- package/types/InputParts/index.d.ts.map +1 -1
- package/types/index.d.ts +1 -1
- package/types/index.d.ts.map +1 -1
package/src/Button/index.tsx
CHANGED
|
@@ -37,6 +37,7 @@ import {
|
|
|
37
37
|
normalizeToHex,
|
|
38
38
|
relativeLuminance,
|
|
39
39
|
useBarSurface,
|
|
40
|
+
useFieldSurface,
|
|
40
41
|
useElevatedSurface,
|
|
41
42
|
useGlyphTintOnSurface,
|
|
42
43
|
useResolvedKnobs,
|
|
@@ -80,7 +81,11 @@ import { useControlGrouped } from "../shared/groupContext";
|
|
|
80
81
|
import { useInPanelEcho, usePanelTriggerRadiusReporter } from "../FloatingPanel/echoContext";
|
|
81
82
|
import { loadGlassView } from "./glass";
|
|
82
83
|
import { boldStroke, GLYPH_BOLD_STROKE_UNITS, ICON_BUTTON_CIRCLE_GLYPH_RATIO } from "./circleGlyph";
|
|
83
|
-
import {
|
|
84
|
+
import {
|
|
85
|
+
GlyphPlateContext,
|
|
86
|
+
useIconButtonGroupGround,
|
|
87
|
+
useIconButtonGroupMember,
|
|
88
|
+
} from "./iconButtonGroupContext";
|
|
84
89
|
|
|
85
90
|
export { GLYPH_BOLD_STROKE_UNITS, ICON_BUTTON_CIRCLE_GLYPH_RATIO };
|
|
86
91
|
|
|
@@ -614,14 +619,24 @@ export function Button({
|
|
|
614
619
|
// (`outlined`, `circular`, `chromeless={false}`) or a segment owns it.
|
|
615
620
|
const labelled = verb != null || (children != null && children !== false && children !== "");
|
|
616
621
|
const iconOnly = !labelled && Boolean(icon || iconAfter);
|
|
622
|
+
// LC-176 (FZ-8711): an icon button in a field's leading or trailing slot is
|
|
623
|
+
// part of the field: the bare tinted glyph on every platform, with no glass,
|
|
624
|
+
// plate, frame or fill of its own, whatever the caller or the bar asked for.
|
|
625
|
+
const fieldSurface = useFieldSurface();
|
|
626
|
+
const fieldGlyph = iconOnly && fieldSurface;
|
|
627
|
+
if (fieldGlyph) {
|
|
628
|
+
outlined = false;
|
|
629
|
+
if (chromeless === false) chromeless = undefined;
|
|
630
|
+
}
|
|
617
631
|
// LC-176: `circular chromeless` paints no plate, so an icon-only one is an
|
|
618
632
|
// icon button like any other (the SHC console's add and copy buttons).
|
|
619
|
-
const circular =
|
|
633
|
+
const circular =
|
|
634
|
+
circularProp && !fieldGlyph && !(iconOnly && chromeless === true && plate == null);
|
|
620
635
|
// LC-176: inside a group's shared capsule a button draws its glyph alone.
|
|
621
636
|
const groupGround = useIconButtonGroupGround();
|
|
622
637
|
useIconButtonGroupMember(iconOnly);
|
|
623
|
-
const mediaPlate = iconOnly && plate === "media" && groupGround !== "media";
|
|
624
|
-
const confirmPlate = iconOnly && primaryPlate;
|
|
638
|
+
const mediaPlate = iconOnly && plate === "media" && groupGround !== "media" && !fieldGlyph;
|
|
639
|
+
const confirmPlate = iconOnly && primaryPlate && !fieldGlyph;
|
|
625
640
|
const iconButton =
|
|
626
641
|
iconOnly &&
|
|
627
642
|
!mediaPlate &&
|
|
@@ -642,10 +657,15 @@ export function Button({
|
|
|
642
657
|
// An action inline with a value (copy beside an address) is tinted like a
|
|
643
658
|
// link, so it reads as the one pressable thing on the line.
|
|
644
659
|
const neutralGlyph =
|
|
645
|
-
iconButton &&
|
|
660
|
+
iconButton &&
|
|
661
|
+
!(theme ?? intent) &&
|
|
662
|
+
!barSurface &&
|
|
663
|
+
!fieldGlyph &&
|
|
664
|
+
accentInk == null &&
|
|
665
|
+
inline !== true;
|
|
646
666
|
if (iconButton) {
|
|
647
667
|
chromeless = true;
|
|
648
|
-
accentInk = accentInk ?? (!(theme ?? intent) && (barSurface || inline === true));
|
|
668
|
+
accentInk = accentInk ?? (!(theme ?? intent) && (barSurface || fieldGlyph || inline === true));
|
|
649
669
|
}
|
|
650
670
|
// LC-176: a glyph that is the whole button is bold, so it reads as one.
|
|
651
671
|
const resolvedGlyphWeight = glyphWeight ?? (iconOnly ? "bold" : "regular");
|
|
@@ -845,7 +865,7 @@ export function Button({
|
|
|
845
865
|
selected ?? (ariaPressedProp === true || ariaPressedProp === "true" ? true : undefined);
|
|
846
866
|
// The media plate is its own state ground; a toggle over media shows its
|
|
847
867
|
// state in the glyph.
|
|
848
|
-
const selectedFill = Boolean(isSelected) && !unavailable && !mediaPlate;
|
|
868
|
+
const selectedFill = Boolean(isSelected) && !unavailable && !mediaPlate && !fieldGlyph;
|
|
849
869
|
// A glyph alone holds 3:1; beside a label both take the label's AA tint.
|
|
850
870
|
const barInk =
|
|
851
871
|
accentInk && chromeless && !selectedFill && !unavailable && !confirmPlate
|
|
@@ -863,10 +883,18 @@ export function Button({
|
|
|
863
883
|
const glyphFloor = iconButtonGlyphPx(labelLine);
|
|
864
884
|
// LC-176: on iOS 26 a bar's icon button sits in system Liquid Glass, the
|
|
865
885
|
// native bar button, and iOS Safari draws its web face. Elsewhere, and on a
|
|
866
|
-
// binary without the module, it stays the bare tinted glyph (FZ-8666).
|
|
886
|
+
// binary without the module, it stays the bare tinted glyph (FZ-8666). In a
|
|
887
|
+
// field's slot it is always that glyph (FZ-8711).
|
|
867
888
|
const glassGroup = groupGround === "glass";
|
|
868
889
|
const GlassView =
|
|
869
|
-
iconButton &&
|
|
890
|
+
iconButton &&
|
|
891
|
+
!inlineGlyph &&
|
|
892
|
+
!nested &&
|
|
893
|
+
accentInk &&
|
|
894
|
+
barSurface &&
|
|
895
|
+
!fieldGlyph &&
|
|
896
|
+
!selectedFill &&
|
|
897
|
+
!glassGroup
|
|
870
898
|
? loadGlassView()
|
|
871
899
|
: null;
|
|
872
900
|
const framedCircle = iconOnly && Boolean(circular) && !nested && !mediaPlate && !confirmPlate;
|
|
@@ -1125,11 +1153,18 @@ export function Button({
|
|
|
1125
1153
|
: mediaPlate
|
|
1126
1154
|
? { backgroundColor: "$shadow5" }
|
|
1127
1155
|
: undefined;
|
|
1156
|
+
// LC-176: a glyph on a visible ground sits at that ground's centre, and
|
|
1157
|
+
// the ground, not the glyph, takes the gutter (FZ-8442).
|
|
1158
|
+
const glyphPlated = Boolean(
|
|
1159
|
+
GlassView || groupGround || mediaPlate || confirmPlate || framedCircle,
|
|
1160
|
+
);
|
|
1128
1161
|
// A square box, so an `edge` pull puts the glyph itself on the gutter.
|
|
1129
|
-
// On glass the circle, not the glyph, sits on the margin.
|
|
1130
1162
|
const glassInset = typeof resolvedBorderWidth === "number" ? -resolvedBorderWidth : 0;
|
|
1131
|
-
const glyphEdgePull =
|
|
1132
|
-
|
|
1163
|
+
const glyphEdgePull = glyphPlated
|
|
1164
|
+
? 0
|
|
1165
|
+
: iconButton
|
|
1166
|
+
? -Math.max(0, (visualPx - iconSize) / 2)
|
|
1167
|
+
: edgePull;
|
|
1133
1168
|
const buttonElement = (
|
|
1134
1169
|
<ButtonFrame
|
|
1135
1170
|
// DG-ST-03: an explained disable is aria-disabled (focusable, press
|
|
@@ -1355,29 +1390,31 @@ export function Button({
|
|
|
1355
1390
|
data-size-token={knobProps.sizeToken}
|
|
1356
1391
|
{...(selectedFill ? { "aria-pressed": true, "data-mp-selected": "true" } : undefined)}
|
|
1357
1392
|
>
|
|
1358
|
-
{
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1393
|
+
<GlyphPlateContext.Provider value={glyphPlated}>
|
|
1394
|
+
{GlassView ? (
|
|
1395
|
+
<GlassView
|
|
1396
|
+
glassEffectStyle="regular"
|
|
1397
|
+
isInteractive={!unavailable}
|
|
1398
|
+
colorScheme={darkScheme ? "dark" : "light"}
|
|
1399
|
+
style={{
|
|
1400
|
+
position: "absolute",
|
|
1401
|
+
top: glassInset,
|
|
1402
|
+
right: glassInset,
|
|
1403
|
+
bottom: glassInset,
|
|
1404
|
+
left: glassInset,
|
|
1405
|
+
borderRadius: visualPx / 2,
|
|
1406
|
+
alignItems: "center",
|
|
1407
|
+
justifyContent: "center",
|
|
1408
|
+
}}
|
|
1409
|
+
>
|
|
1410
|
+
{iconElement}
|
|
1411
|
+
</GlassView>
|
|
1412
|
+
) : (
|
|
1413
|
+
iconElement
|
|
1414
|
+
)}
|
|
1415
|
+
{labelContent}
|
|
1416
|
+
{iconAfterElement}
|
|
1417
|
+
</GlyphPlateContext.Provider>
|
|
1381
1418
|
</ButtonFrame>
|
|
1382
1419
|
);
|
|
1383
1420
|
|
|
@@ -1445,4 +1482,5 @@ export function IconButton(props: IconButtonProps) {
|
|
|
1445
1482
|
}
|
|
1446
1483
|
|
|
1447
1484
|
export { IconButtonGroup } from "./IconButtonGroup";
|
|
1485
|
+
export { useGlyphOnPlate } from "./iconButtonGroupContext";
|
|
1448
1486
|
export type { IconButtonGroupProps } from "./IconButtonGroup";
|
|
@@ -631,10 +631,10 @@ describe("LC-112 grouped glass capsule on native (FZ-8550, Round 37 second pass)
|
|
|
631
631
|
</IconButtonGroupSurfaceContext.Provider>,
|
|
632
632
|
);
|
|
633
633
|
tap(result.getAllByRole("button", { name: "Add node" })[0]!);
|
|
634
|
+
expect(menu()).toBeNull();
|
|
635
|
+
act(() => answer?.(plate));
|
|
634
636
|
const panel = menu() as HTMLElement;
|
|
635
637
|
layout(panel, MENU_MIN_WIDTH, 180);
|
|
636
|
-
expect(panel.style.opacity).toBe("0");
|
|
637
|
-
act(() => answer?.(plate));
|
|
638
638
|
expect(panel.style.opacity).toBe("1");
|
|
639
639
|
expect(panel.style.left).toBe(`${plate.x + plate.width - MENU_MIN_WIDTH}px`);
|
|
640
640
|
});
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FZ-8725: a menu opened on mount, before its trigger is measured, presents
|
|
3
|
+
* its Modal only once the measure lands, and no backdrop ever stands without
|
|
4
|
+
* a placed panel. The Kitchen Sink Account Menu deep link froze the screen:
|
|
5
|
+
* the previous story's menu presented in its mount commit and was torn down
|
|
6
|
+
* mid-presentation, leaving an invisible modal over every tap.
|
|
7
|
+
*/
|
|
8
|
+
import { renderWithProviders } from "@multiplatform.one/test-utils";
|
|
9
|
+
import { act } from "@testing-library/react";
|
|
10
|
+
import { PlusIcon } from "@phosphor-icons/react";
|
|
11
|
+
import { useState, type ReactNode } from "react";
|
|
12
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
13
|
+
import { Text } from "tamagui";
|
|
14
|
+
import { Button } from "../Button";
|
|
15
|
+
import { MENU_MIN_WIDTH } from "./compactFace";
|
|
16
|
+
import { FloatingPanel } from "./index.native";
|
|
17
|
+
|
|
18
|
+
const modal = vi.hoisted(() => ({ visible: [] as boolean[] }));
|
|
19
|
+
|
|
20
|
+
vi.mock("react-native-web", async (importOriginal) => {
|
|
21
|
+
const actual = await importOriginal<typeof import("react-native")>();
|
|
22
|
+
return {
|
|
23
|
+
...actual,
|
|
24
|
+
ActionSheetIOS: undefined,
|
|
25
|
+
Modal: ({ children, visible }: { children: ReactNode; visible: boolean }) => {
|
|
26
|
+
modal.visible.push(visible);
|
|
27
|
+
return visible ? children : null;
|
|
28
|
+
},
|
|
29
|
+
useWindowDimensions: () => ({ width: 390, height: 844, scale: 3, fontScale: 1 }),
|
|
30
|
+
};
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const trigger = { x: 334, y: 292, width: 44, height: 44 };
|
|
34
|
+
beforeEach(() => {
|
|
35
|
+
vi.useFakeTimers();
|
|
36
|
+
modal.visible = [];
|
|
37
|
+
// RN-web's measureInWindow reads the node's client rect on a timer.
|
|
38
|
+
vi.spyOn(HTMLElement.prototype, "getBoundingClientRect").mockImplementation(() => ({
|
|
39
|
+
x: trigger.x,
|
|
40
|
+
y: trigger.y,
|
|
41
|
+
left: trigger.x,
|
|
42
|
+
top: trigger.y,
|
|
43
|
+
width: trigger.width,
|
|
44
|
+
height: trigger.height,
|
|
45
|
+
right: trigger.x + trigger.width,
|
|
46
|
+
bottom: trigger.y + trigger.height,
|
|
47
|
+
toJSON: () => ({}),
|
|
48
|
+
}));
|
|
49
|
+
});
|
|
50
|
+
afterEach(() => {
|
|
51
|
+
vi.restoreAllMocks();
|
|
52
|
+
vi.useRealTimers();
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
function AccountMenu({ label = "Account" }: { label?: string }) {
|
|
56
|
+
const [open, setOpen] = useState(true);
|
|
57
|
+
return (
|
|
58
|
+
<FloatingPanel
|
|
59
|
+
open={open}
|
|
60
|
+
onOpenChange={setOpen}
|
|
61
|
+
compactFace="menu"
|
|
62
|
+
anchorMode="cover-trigger"
|
|
63
|
+
triggerEcho={false}
|
|
64
|
+
iconCover
|
|
65
|
+
coverAlign="auto"
|
|
66
|
+
triggerSizing="content"
|
|
67
|
+
contentPadding="none"
|
|
68
|
+
trigger={<Button icon={PlusIcon} aria-label={label} />}
|
|
69
|
+
triggerA11y={{ label }}
|
|
70
|
+
>
|
|
71
|
+
<Text>Sign out</Text>
|
|
72
|
+
</FloatingPanel>
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const layer = () => document.querySelector('[data-testid="floating-panel-menu-layer"]');
|
|
77
|
+
const menu = () => document.querySelector<HTMLElement>('[data-testid="floating-panel-menu"]');
|
|
78
|
+
const backdrop = (result: ReturnType<typeof renderWithProviders>) =>
|
|
79
|
+
result.queryByRole("button", { name: "Close" });
|
|
80
|
+
|
|
81
|
+
function tap(target: Element) {
|
|
82
|
+
act(() => {
|
|
83
|
+
target.dispatchEvent(new MouseEvent("mousedown", { bubbles: true, button: 0 }));
|
|
84
|
+
target.dispatchEvent(new MouseEvent("mouseup", { bubbles: true, button: 0 }));
|
|
85
|
+
target.dispatchEvent(new MouseEvent("click", { bubbles: true, button: 0 }));
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
type FiberLike = { memoizedProps?: Record<string, unknown>; return?: FiberLike };
|
|
90
|
+
|
|
91
|
+
/** The live prop a component took: RN-web drops the ones it has no DOM use for. */
|
|
92
|
+
function propOf<T>(node: Element, name: string): T | undefined {
|
|
93
|
+
const key = Object.keys(node).find((candidate) => candidate.startsWith("__reactFiber$"));
|
|
94
|
+
let fiber = (node as unknown as Record<string, FiberLike | undefined>)[key ?? ""];
|
|
95
|
+
while (fiber) {
|
|
96
|
+
const value = fiber.memoizedProps?.[name];
|
|
97
|
+
if (value !== undefined) return value as T;
|
|
98
|
+
fiber = fiber.return;
|
|
99
|
+
}
|
|
100
|
+
return undefined;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function measure() {
|
|
104
|
+
act(() => {
|
|
105
|
+
vi.advanceTimersByTime(1);
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** RN-web parks onLayout on the node for a ResizeObserver happy-dom never fires. */
|
|
110
|
+
function layout(node: Element, width: number, height: number) {
|
|
111
|
+
const onLayout = (node as unknown as { __reactLayoutHandler?: (event: unknown) => void })
|
|
112
|
+
.__reactLayoutHandler;
|
|
113
|
+
expect(onLayout).toBeTypeOf("function");
|
|
114
|
+
act(() => {
|
|
115
|
+
onLayout?.({ nativeEvent: { layout: { x: 0, y: 0, width, height } } });
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Never a backdrop on screen without the panel it closes. */
|
|
120
|
+
function expectNoBareBackdrop(result: ReturnType<typeof renderWithProviders>) {
|
|
121
|
+
if (backdrop(result)) expect(menu()?.style.opacity).toBe("1");
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
describe("FloatingPanel opened on mount (FZ-8725)", () => {
|
|
125
|
+
it("presents nothing until the trigger is measured", () => {
|
|
126
|
+
const result = renderWithProviders(<AccountMenu />);
|
|
127
|
+
expect(modal.visible.length).toBeGreaterThan(0);
|
|
128
|
+
expect(modal.visible.every((visible) => !visible)).toBe(true);
|
|
129
|
+
expect(layer()).toBeNull();
|
|
130
|
+
expect(backdrop(result)).toBeNull();
|
|
131
|
+
expect(result.getAllByRole("button", { name: "Account" }).length).toBeGreaterThan(0);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it("shows the panel over its trigger once the measure lands", () => {
|
|
135
|
+
const result = renderWithProviders(<AccountMenu />);
|
|
136
|
+
measure();
|
|
137
|
+
expect(modal.visible.at(-1)).toBe(true);
|
|
138
|
+
expect(menu()).not.toBeNull();
|
|
139
|
+
expectNoBareBackdrop(result);
|
|
140
|
+
layout(menu() as HTMLElement, MENU_MIN_WIDTH, 120);
|
|
141
|
+
expect(menu()?.style.opacity).toBe("1");
|
|
142
|
+
expect(menu()?.style.top).toBe(`${trigger.y}px`);
|
|
143
|
+
expect(menu()?.style.left).toBe(`${trigger.x + trigger.width - MENU_MIN_WIDTH}px`);
|
|
144
|
+
expect(backdrop(result)).not.toBeNull();
|
|
145
|
+
expectNoBareBackdrop(result);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it("keeps the backdrop and the modal flag off while the panel is still unplaced", () => {
|
|
149
|
+
const result = renderWithProviders(<AccountMenu />);
|
|
150
|
+
measure();
|
|
151
|
+
expect(menu()?.style.opacity).toBe("0");
|
|
152
|
+
expect(backdrop(result)).toBeNull();
|
|
153
|
+
expect(propOf<boolean>(layer() as Element, "accessibilityViewIsModal")).toBe(false);
|
|
154
|
+
layout(menu() as HTMLElement, MENU_MIN_WIDTH, 120);
|
|
155
|
+
act(() => {
|
|
156
|
+
vi.advanceTimersByTime(1000);
|
|
157
|
+
});
|
|
158
|
+
expect(backdrop(result)).not.toBeNull();
|
|
159
|
+
expect(propOf<boolean>(layer() as Element, "accessibilityViewIsModal")).toBe(true);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it("closes from the backdrop and leaves the screen tappable", () => {
|
|
163
|
+
const result = renderWithProviders(<AccountMenu />);
|
|
164
|
+
measure();
|
|
165
|
+
layout(menu() as HTMLElement, MENU_MIN_WIDTH, 120);
|
|
166
|
+
act(() => {
|
|
167
|
+
vi.advanceTimersByTime(1000);
|
|
168
|
+
});
|
|
169
|
+
tap(backdrop(result) as HTMLElement);
|
|
170
|
+
expect(layer()).toBeNull();
|
|
171
|
+
expect(modal.visible.at(-1)).toBe(false);
|
|
172
|
+
tap(result.getAllByRole("button", { name: "Account" })[0]!);
|
|
173
|
+
measure();
|
|
174
|
+
expect(menu()).not.toBeNull();
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it("never presents a menu that is swapped out before its measure lands", () => {
|
|
178
|
+
let swap = () => {};
|
|
179
|
+
function Stories() {
|
|
180
|
+
const [second, setSecond] = useState(false);
|
|
181
|
+
swap = () => setSecond(true);
|
|
182
|
+
return second ? (
|
|
183
|
+
<AccountMenu key="b" label="Account" />
|
|
184
|
+
) : (
|
|
185
|
+
<AccountMenu key="a" label="Main" />
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
renderWithProviders(<Stories />);
|
|
189
|
+
act(() => swap());
|
|
190
|
+
const beforeMeasure = modal.visible.slice();
|
|
191
|
+
expect(beforeMeasure.every((visible) => !visible)).toBe(true);
|
|
192
|
+
measure();
|
|
193
|
+
expect(modal.visible.at(-1)).toBe(true);
|
|
194
|
+
layout(menu() as HTMLElement, MENU_MIN_WIDTH, 120);
|
|
195
|
+
expect(menu()?.style.opacity).toBe("1");
|
|
196
|
+
expect(document.querySelectorAll('[data-testid="floating-panel-menu-layer"]')).toHaveLength(1);
|
|
197
|
+
});
|
|
198
|
+
});
|
|
@@ -89,12 +89,14 @@ export function RegularPanel({
|
|
|
89
89
|
) : null;
|
|
90
90
|
return (
|
|
91
91
|
<RNView style={StyleSheet.absoluteFill}>
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
92
|
+
{placement ? (
|
|
93
|
+
<Pressable
|
|
94
|
+
style={StyleSheet.absoluteFill}
|
|
95
|
+
onPress={onBackdropPress}
|
|
96
|
+
accessibilityRole="button"
|
|
97
|
+
accessibilityLabel={t("Close")}
|
|
98
|
+
/>
|
|
99
|
+
) : null}
|
|
98
100
|
<RNView
|
|
99
101
|
testID="floating-panel-regular"
|
|
100
102
|
onLayout={(e) => {
|
|
@@ -43,6 +43,11 @@
|
|
|
43
43
|
* and the sheet never appears. Fields should not also toggle open on native.
|
|
44
44
|
* - Opening a Modal under the same finger can synthesize a backdrop press;
|
|
45
45
|
* backdrop dismiss is armed a short delay after the present begins.
|
|
46
|
+
* - An anchored panel opened before its trigger is measured (`open` on
|
|
47
|
+
* mount) presents its Modal only once the measure lands (FZ-8725). A Modal
|
|
48
|
+
* presented in the mount commit and torn down mid-presentation (a remount,
|
|
49
|
+
* a story switch) left UIKit an invisible view controller over the screen:
|
|
50
|
+
* no panel, and every tap under it dead.
|
|
46
51
|
*
|
|
47
52
|
* Menu face (`compactFace`, FZ-7830): instead of the sheet, a menu opens
|
|
48
53
|
* over the trigger (LC-112), its top start corner on the trigger's, the
|
|
@@ -318,6 +323,8 @@ export function FloatingPanel({
|
|
|
318
323
|
};
|
|
319
324
|
}, [anchored, open, iconCover, anchorMode, groupSurface]);
|
|
320
325
|
|
|
326
|
+
const shown = open && (!anchored || (anchor !== null && group !== undefined));
|
|
327
|
+
|
|
321
328
|
const handleBackdropPress = useCallback(() => {
|
|
322
329
|
if (backdropArmed) close();
|
|
323
330
|
}, [backdropArmed, close]);
|
|
@@ -568,10 +575,10 @@ export function FloatingPanel({
|
|
|
568
575
|
|
|
569
576
|
useEffect(() => {
|
|
570
577
|
presentationRef.current += 1;
|
|
571
|
-
activeRef.current =
|
|
578
|
+
activeRef.current = shown;
|
|
572
579
|
restHeightRef.current = null;
|
|
573
580
|
frameCap.setValue(maxHeightRef.current);
|
|
574
|
-
if (
|
|
581
|
+
if (shown) {
|
|
575
582
|
enter();
|
|
576
583
|
} else {
|
|
577
584
|
setBackdropArmed(false);
|
|
@@ -590,7 +597,7 @@ export function FloatingPanel({
|
|
|
590
597
|
}
|
|
591
598
|
translateY.stopAnimation();
|
|
592
599
|
};
|
|
593
|
-
}, [
|
|
600
|
+
}, [shown, enter, frameCap, translateY]);
|
|
594
601
|
|
|
595
602
|
const content = scrollable ? (
|
|
596
603
|
<RNScrollView
|
|
@@ -658,7 +665,7 @@ export function FloatingPanel({
|
|
|
658
665
|
<TrailingActionOverlay>{triggerEnd}</TrailingActionOverlay>
|
|
659
666
|
</RNView>
|
|
660
667
|
<Modal
|
|
661
|
-
visible={
|
|
668
|
+
visible={shown}
|
|
662
669
|
transparent
|
|
663
670
|
animationType={knobProps.motion.scrim.transition ? "fade" : "none"}
|
|
664
671
|
statusBarTranslucent
|
|
@@ -1018,14 +1025,16 @@ function TriggerMenuPanel({
|
|
|
1018
1025
|
<RNView
|
|
1019
1026
|
style={StyleSheet.absoluteFill}
|
|
1020
1027
|
testID="floating-panel-menu-layer"
|
|
1021
|
-
accessibilityViewIsModal
|
|
1028
|
+
accessibilityViewIsModal={placement != null}
|
|
1022
1029
|
>
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1030
|
+
{placement ? (
|
|
1031
|
+
<Pressable
|
|
1032
|
+
style={StyleSheet.absoluteFill}
|
|
1033
|
+
onPress={onBackdropPress}
|
|
1034
|
+
accessibilityRole="button"
|
|
1035
|
+
accessibilityLabel={t("Close")}
|
|
1036
|
+
/>
|
|
1037
|
+
) : null}
|
|
1029
1038
|
<RNView
|
|
1030
1039
|
testID="floating-panel-menu"
|
|
1031
1040
|
onLayout={(e) => {
|
|
@@ -1223,14 +1232,16 @@ function PointPanel({
|
|
|
1223
1232
|
<RNView
|
|
1224
1233
|
style={StyleSheet.absoluteFill}
|
|
1225
1234
|
testID="floating-panel-point-layer"
|
|
1226
|
-
accessibilityViewIsModal
|
|
1235
|
+
accessibilityViewIsModal={placement != null}
|
|
1227
1236
|
>
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1237
|
+
{placement ? (
|
|
1238
|
+
<Pressable
|
|
1239
|
+
style={StyleSheet.absoluteFill}
|
|
1240
|
+
onPress={onBackdropPress}
|
|
1241
|
+
accessibilityRole="button"
|
|
1242
|
+
accessibilityLabel={t("Close")}
|
|
1243
|
+
/>
|
|
1244
|
+
) : null}
|
|
1234
1245
|
<RNView
|
|
1235
1246
|
testID="floating-panel-point"
|
|
1236
1247
|
onLayout={(e) => {
|
package/src/InputParts/index.tsx
CHANGED
|
@@ -47,6 +47,7 @@ import {
|
|
|
47
47
|
resolveControlRadius,
|
|
48
48
|
resolveGlyphPaint,
|
|
49
49
|
SIZE_RECIPE_RADIUS_TOKEN_TO_STOP,
|
|
50
|
+
Surface,
|
|
50
51
|
sizeRecipeBoxVariants,
|
|
51
52
|
sizeRecipeForToken,
|
|
52
53
|
sizeRecipeIconVariants,
|
|
@@ -530,7 +531,7 @@ const InputGroupImpl = InputGroupFrame.styleable<{
|
|
|
530
531
|
|
|
531
532
|
// Chromeless rendering inside table cells
|
|
532
533
|
if (inTableCell) {
|
|
533
|
-
return (
|
|
534
|
+
return inFieldFrame(
|
|
534
535
|
<FocusContext.Provider focused={focused} setFocused={setFocused}>
|
|
535
536
|
{wrapNativePressable(
|
|
536
537
|
onPress,
|
|
@@ -569,38 +570,40 @@ const InputGroupImpl = InputGroupFrame.styleable<{
|
|
|
569
570
|
{trailingSlot}
|
|
570
571
|
</InputGroupFrame>,
|
|
571
572
|
)}
|
|
572
|
-
</FocusContext.Provider
|
|
573
|
+
</FocusContext.Provider>,
|
|
573
574
|
);
|
|
574
575
|
}
|
|
575
576
|
|
|
576
577
|
if (grouped) {
|
|
577
|
-
return
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
578
|
+
return inFieldFrame(
|
|
579
|
+
wrapNativePressable(
|
|
580
|
+
onPress,
|
|
581
|
+
disabled,
|
|
582
|
+
<InputGroupFrame
|
|
583
|
+
touch={hydrationTouch}
|
|
584
|
+
ref={forwardedRef}
|
|
585
|
+
borderWidth={0}
|
|
586
|
+
borderRadius={0}
|
|
587
|
+
backgroundColor="transparent"
|
|
588
|
+
{...(groupedVertical
|
|
589
|
+
? { flexGrow: 0, flexShrink: 0, flexBasis: "auto", height: controlFragment.height }
|
|
590
|
+
: { flex: 1 })}
|
|
591
|
+
elevation={undefined}
|
|
592
|
+
{...props}
|
|
593
|
+
{...passthroughSizeProps}
|
|
594
|
+
{...insetPadProps}
|
|
595
|
+
{...sizeRecipeEscapeOverride}
|
|
596
|
+
{...framePressProps}
|
|
597
|
+
{...boxRingAttrs}
|
|
598
|
+
overflow={boxOverflow}
|
|
599
|
+
>
|
|
600
|
+
{children}
|
|
601
|
+
</InputGroupFrame>,
|
|
602
|
+
),
|
|
600
603
|
);
|
|
601
604
|
}
|
|
602
605
|
|
|
603
|
-
return (
|
|
606
|
+
return inFieldFrame(
|
|
604
607
|
<FocusContext.Provider focused={focused} setFocused={setFocused}>
|
|
605
608
|
{wrapNativePressable(
|
|
606
609
|
onPress,
|
|
@@ -651,10 +654,20 @@ const InputGroupImpl = InputGroupFrame.styleable<{
|
|
|
651
654
|
{trailingSlot}
|
|
652
655
|
</InputGroupFrame>,
|
|
653
656
|
)}
|
|
654
|
-
</FocusContext.Provider
|
|
657
|
+
</FocusContext.Provider>,
|
|
655
658
|
);
|
|
656
659
|
});
|
|
657
660
|
|
|
661
|
+
/**
|
|
662
|
+
* LC-176 (FZ-8711): everything inside a field's frame is part of the field,
|
|
663
|
+
* so an icon button in its leading or trailing slot draws the bare tinted
|
|
664
|
+
* glyph, never bar glass or a plate. Wrapped outside the XGroup frame so the
|
|
665
|
+
* group's own children stay its items.
|
|
666
|
+
*/
|
|
667
|
+
function inFieldFrame(frame: ReactNode) {
|
|
668
|
+
return <Surface field>{frame}</Surface>;
|
|
669
|
+
}
|
|
670
|
+
|
|
658
671
|
// ---------------------------------------------------------------------------
|
|
659
672
|
// Input size variant helper
|
|
660
673
|
// ---------------------------------------------------------------------------
|
package/src/index.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"IconButtonGroup.d.ts","sourceRoot":"","sources":["../../src/Button/IconButtonGroup.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"IconButtonGroup.d.ts","sourceRoot":"","sources":["../../src/Button/IconButtonGroup.tsx"],"names":[],"mappings":"AAMA,OAAO,EAAoD,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AACzF,OAAO,EAA4C,KAAK,WAAW,EAAE,MAAM,SAAS,CAAC;AAmBrF,MAAM,WAAW,oBAAqB,SAAQ,WAAW;IACvD,gEAAgE;IAChE,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,QAAQ,CAAC,EAAE,SAAS,CAAC;CACtB;AAED;;;;;;;;GAQG;AACH,wBAAgB,eAAe,CAAC,EAAE,KAAK,EAAE,QAAQ,EAAE,GAAG,IAAI,EAAE,EAAE,oBAAoB,2CAkJjF"}
|
|
@@ -6,6 +6,14 @@
|
|
|
6
6
|
export type IconButtonGroupGround = "media" | "glass" | null;
|
|
7
7
|
export declare const IconButtonGroupContext: import("react").Context<IconButtonGroupGround>;
|
|
8
8
|
export declare function useIconButtonGroupGround(): IconButtonGroupGround;
|
|
9
|
+
/**
|
|
10
|
+
* LC-176: true while a glyph is drawn centred on a visible ground (a glass
|
|
11
|
+
* circle or capsule, the media plate, a framed circle). An icon that pulls
|
|
12
|
+
* itself toward a container edge for optical alignment, such as the back
|
|
13
|
+
* caret, keeps its ink centred instead.
|
|
14
|
+
*/
|
|
15
|
+
export declare const GlyphPlateContext: import("react").Context<boolean>;
|
|
16
|
+
export declare function useGlyphOnPlate(): boolean;
|
|
9
17
|
/**
|
|
10
18
|
* An icon button counts itself into the group around it, so a bar group of
|
|
11
19
|
* only non-icon children (a status badge in a page head) paints no capsule
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"iconButtonGroupContext.d.ts","sourceRoot":"","sources":["../../src/Button/iconButtonGroupContext.ts"],"names":[],"mappings":"AAIA;;;;GAIG;AACH,MAAM,MAAM,qBAAqB,GAAG,OAAO,GAAG,OAAO,GAAG,IAAI,CAAC;AAE7D,eAAO,MAAM,sBAAsB,gDAA6C,CAAC;AAEjF,wBAAgB,wBAAwB,IAAI,qBAAqB,CAEhE;AAED;;;;GAIG;AACH,eAAO,MAAM,uBAAuB,iCAAwB,MAAM,IAAI,SAAe,CAAC;AAEtF,wBAAgB,wBAAwB,CAAC,QAAQ,EAAE,OAAO,GAAG,IAAI,CAMhE;AAED;;;;;GAKG;AACH,MAAM,WAAW,oBAAoB;IACnC,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;IACV,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,sBAAsB;IACrC,mEAAmE;IACnE,OAAO,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,oBAAoB,GAAG,IAAI,KAAK,IAAI,GAAG,IAAI,CAAC;CACvE;AAED,eAAO,MAAM,6BAA6B,wDAAqD,CAAC;AAEhG,wBAAgB,yBAAyB,IAAI,sBAAsB,GAAG,IAAI,CAEzE"}
|
|
1
|
+
{"version":3,"file":"iconButtonGroupContext.d.ts","sourceRoot":"","sources":["../../src/Button/iconButtonGroupContext.ts"],"names":[],"mappings":"AAIA;;;;GAIG;AACH,MAAM,MAAM,qBAAqB,GAAG,OAAO,GAAG,OAAO,GAAG,IAAI,CAAC;AAE7D,eAAO,MAAM,sBAAsB,gDAA6C,CAAC;AAEjF,wBAAgB,wBAAwB,IAAI,qBAAqB,CAEhE;AAED;;;;;GAKG;AACH,eAAO,MAAM,iBAAiB,kCAAuB,CAAC;AAEtD,wBAAgB,eAAe,IAAI,OAAO,CAEzC;AAED;;;;GAIG;AACH,eAAO,MAAM,uBAAuB,iCAAwB,MAAM,IAAI,SAAe,CAAC;AAEtF,wBAAgB,wBAAwB,CAAC,QAAQ,EAAE,OAAO,GAAG,IAAI,CAMhE;AAED;;;;;GAKG;AACH,MAAM,WAAW,oBAAoB;IACnC,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;IACV,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,sBAAsB;IACrC,mEAAmE;IACnE,OAAO,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,oBAAoB,GAAG,IAAI,KAAK,IAAI,GAAG,IAAI,CAAC;CACvE;AAED,eAAO,MAAM,6BAA6B,wDAAqD,CAAC;AAEhG,wBAAgB,yBAAyB,IAAI,sBAAsB,GAAG,IAAI,CAEzE"}
|
package/types/Button/index.d.ts
CHANGED
|
@@ -197,5 +197,6 @@ export interface IconButtonProps extends Omit<ButtonProps, "children" | "icon" |
|
|
|
197
197
|
*/
|
|
198
198
|
export declare function IconButton(props: IconButtonProps): import("react/jsx-runtime").JSX.Element;
|
|
199
199
|
export { IconButtonGroup } from "./IconButtonGroup";
|
|
200
|
+
export { useGlyphOnPlate } from "./iconButtonGroupContext";
|
|
200
201
|
export type { IconButtonGroupProps } from "./IconButtonGroup";
|
|
201
202
|
//# sourceMappingURL=index.d.ts.map
|