@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
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
* so only inside another overlay, so a sheet never stacks on a sheet.
|
|
5
5
|
*/
|
|
6
6
|
import { renderWithProviders } from "@multiplatform.one/test-utils";
|
|
7
|
-
import { act } from "@testing-library/react";
|
|
7
|
+
import { act, render } from "@testing-library/react";
|
|
8
|
+
import { PlusIcon, XIcon } from "@phosphor-icons/react";
|
|
8
9
|
import { useState, type ReactNode } from "react";
|
|
9
10
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
10
11
|
import { Text, XStack, YStack, getTokens, getVariableValue } from "tamagui";
|
|
@@ -454,3 +455,88 @@ describe("FloatingPanel native menu face panelWidth (LC-112, FZ-8438 r3)", () =>
|
|
|
454
455
|
expect(open(900).style.width).toBe(`${room}px`);
|
|
455
456
|
});
|
|
456
457
|
});
|
|
458
|
+
|
|
459
|
+
function EchoMenu({ kind }: { kind: "close" | "menu" }) {
|
|
460
|
+
const [open, setOpen] = useState(false);
|
|
461
|
+
return (
|
|
462
|
+
<FloatingPanel
|
|
463
|
+
open={open}
|
|
464
|
+
onOpenChange={setOpen}
|
|
465
|
+
compactFace="menu"
|
|
466
|
+
anchorMode="cover-trigger"
|
|
467
|
+
triggerEcho={false}
|
|
468
|
+
iconEcho={kind}
|
|
469
|
+
coverAlign="auto"
|
|
470
|
+
triggerSizing="content"
|
|
471
|
+
contentPadding="none"
|
|
472
|
+
trigger={<Button icon={PlusIcon} aria-label="Add node" />}
|
|
473
|
+
triggerA11y={{ label: "Add node" }}
|
|
474
|
+
>
|
|
475
|
+
<Text>Add a node</Text>
|
|
476
|
+
</FloatingPanel>
|
|
477
|
+
);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
const iconEcho = () =>
|
|
481
|
+
document.querySelector<HTMLElement>('[data-testid="floating-panel-icon-echo"]');
|
|
482
|
+
const iconBand = () =>
|
|
483
|
+
document.querySelector<HTMLElement>('[data-testid="floating-panel-icon-echo-band"]');
|
|
484
|
+
const glyphPath = (node: Element | null) => node?.querySelector("svg path")?.getAttribute("d");
|
|
485
|
+
|
|
486
|
+
describe("LC-112 icon trigger echo on native (FZ-8480, rv_2607879d3bdaf9bf)", () => {
|
|
487
|
+
it("draws the + again as an X in the button's own box, at the menu's top trailing corner", () => {
|
|
488
|
+
trigger = { x: 330, y: 60, width: 44, height: 44 };
|
|
489
|
+
const result = renderWithProviders(<EchoMenu kind="close" />);
|
|
490
|
+
tap(result.getAllByRole("button", { name: "Add node" })[0]!);
|
|
491
|
+
const panel = menu() as HTMLElement;
|
|
492
|
+
layout(panel, MENU_MIN_WIDTH, 180);
|
|
493
|
+
expect(panel.style.top).toBe(`${trigger.y}px`);
|
|
494
|
+
expect(panel.style.left).toBe(`${trigger.x + trigger.width - MENU_MIN_WIDTH}px`);
|
|
495
|
+
const band = iconBand() as HTMLElement;
|
|
496
|
+
expect(band.style.height).toBe("44px");
|
|
497
|
+
const box = band.firstElementChild as HTMLElement;
|
|
498
|
+
expect(box.style.width).toBe("44px");
|
|
499
|
+
expect(box.style.height).toBe("44px");
|
|
500
|
+
// The box sits where the trigger is: its start offset is the trigger's x
|
|
501
|
+
// less the menu's left edge.
|
|
502
|
+
expect(box.style.marginInlineStart || box.style.marginLeft).toBe(
|
|
503
|
+
`${trigger.x - (trigger.x + trigger.width - MENU_MIN_WIDTH)}px`,
|
|
504
|
+
);
|
|
505
|
+
const echoButton = iconEcho() as HTMLElement;
|
|
506
|
+
expect(echoButton.getAttribute("aria-label")).toBe("Close");
|
|
507
|
+
const x = render(<XIcon weight="bold" />);
|
|
508
|
+
expect(glyphPath(echoButton)).toBe(glyphPath(x.container));
|
|
509
|
+
x.unmount();
|
|
510
|
+
expect(echo()).toBeNull();
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
it("closes the menu when the echo is pressed", () => {
|
|
514
|
+
trigger = { x: 330, y: 60, width: 44, height: 44 };
|
|
515
|
+
const result = renderWithProviders(<EchoMenu kind="close" />);
|
|
516
|
+
tap(result.getAllByRole("button", { name: "Add node" })[0]!);
|
|
517
|
+
layout(menu() as HTMLElement, MENU_MIN_WIDTH, 180);
|
|
518
|
+
act(() => {
|
|
519
|
+
vi.advanceTimersByTime(1000);
|
|
520
|
+
});
|
|
521
|
+
tap(iconEcho() as HTMLElement);
|
|
522
|
+
expect(menu()).toBeNull();
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
it("turns a menu trigger into an X too, and opens up from the button's bottom edge near the bottom", () => {
|
|
526
|
+
trigger = { x: 20, y: 760, width: 44, height: 44 };
|
|
527
|
+
const result = renderWithProviders(<EchoMenu kind="menu" />);
|
|
528
|
+
const live = result.getAllByRole("button", { name: "Add node" })[0]!;
|
|
529
|
+
tap(live);
|
|
530
|
+
const panel = menu() as HTMLElement;
|
|
531
|
+
layout(panel, MENU_MIN_WIDTH, 300);
|
|
532
|
+
expect(parseFloat(panel.style.top) + 300).toBe(trigger.y + trigger.height);
|
|
533
|
+
const surface = frame() as HTMLElement;
|
|
534
|
+
const band = iconBand() as HTMLElement;
|
|
535
|
+
const clip = surface.querySelector('[data-testid="floating-panel-menu-clip"]') as HTMLElement;
|
|
536
|
+
expect(clip.lastElementChild).toBe(band);
|
|
537
|
+
const x = render(<XIcon weight="bold" />);
|
|
538
|
+
expect(glyphPath(iconEcho())).toBe(glyphPath(x.container));
|
|
539
|
+
x.unmount();
|
|
540
|
+
expect(glyphPath(iconEcho())).not.toBe(glyphPath(live));
|
|
541
|
+
});
|
|
542
|
+
});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { XStack, type XStackProps } from "tamagui";
|
|
2
|
+
import { usePanelIconEchoBox } from "./echoContext";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A floating panel's header row (LC-112, FZ-8480 Round 37). Over an
|
|
6
|
+
* icon-trigger panel whose X shares the header's row, it takes the X's
|
|
7
|
+
* height and centres its content on the X's line, keeping clear of the X;
|
|
8
|
+
* anywhere else it is the panel's padded top row.
|
|
9
|
+
*/
|
|
10
|
+
export function PanelHeader({ children, ...rest }: XStackProps) {
|
|
11
|
+
const echo = usePanelIconEchoBox();
|
|
12
|
+
return (
|
|
13
|
+
<XStack
|
|
14
|
+
alignItems="center"
|
|
15
|
+
gap="$2"
|
|
16
|
+
paddingLeft={echo?.left || "$4"}
|
|
17
|
+
paddingRight={echo?.right || "$4"}
|
|
18
|
+
paddingTop={echo ? 0 : "$4"}
|
|
19
|
+
{...(echo ? { minHeight: echo.height } : undefined)}
|
|
20
|
+
data-fp-panel-header=""
|
|
21
|
+
{...rest}
|
|
22
|
+
>
|
|
23
|
+
{children}
|
|
24
|
+
</XStack>
|
|
25
|
+
);
|
|
26
|
+
}
|
|
@@ -20,3 +20,48 @@ export const PanelTriggerRadiusContext = createContext<((radius: number) => void
|
|
|
20
20
|
export function usePanelTriggerRadiusReporter(): ((radius: number) => void) | null {
|
|
21
21
|
return useContext(PanelTriggerRadiusContext);
|
|
22
22
|
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The X a panel lays over its content's header row (LC-112, FZ-8480 Round
|
|
26
|
+
* 37): its height, and how far the header keeps clear of it on each side.
|
|
27
|
+
* A panel header sits on the X's line and never runs under it.
|
|
28
|
+
*/
|
|
29
|
+
export interface PanelIconEchoBox {
|
|
30
|
+
height: number;
|
|
31
|
+
left: number;
|
|
32
|
+
right: number;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export const PanelIconEchoBoxContext = createContext<PanelIconEchoBox | null>(null);
|
|
36
|
+
|
|
37
|
+
export function usePanelIconEchoBox(): PanelIconEchoBox | null {
|
|
38
|
+
return useContext(PanelIconEchoBoxContext);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* The header's clearance around an X `offset` in from the panel's start edge:
|
|
43
|
+
* the X's side keeps clear of it, the other side keeps none of its own.
|
|
44
|
+
*/
|
|
45
|
+
export function echoHeaderBox({
|
|
46
|
+
offset,
|
|
47
|
+
width,
|
|
48
|
+
height,
|
|
49
|
+
panelWidth,
|
|
50
|
+
rtl = false,
|
|
51
|
+
}: {
|
|
52
|
+
offset: number;
|
|
53
|
+
width: number;
|
|
54
|
+
height: number;
|
|
55
|
+
panelWidth: number;
|
|
56
|
+
rtl?: boolean;
|
|
57
|
+
}): PanelIconEchoBox {
|
|
58
|
+
const atStart = offset + width / 2 < panelWidth / 2;
|
|
59
|
+
const start = atStart ? Math.max(0, offset + width) : 0;
|
|
60
|
+
const end = atStart ? 0 : Math.max(0, panelWidth - offset);
|
|
61
|
+
return { height, left: rtl ? end : start, right: rtl ? start : end };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function sameEchoBox(a: PanelIconEchoBox | null, b: PanelIconEchoBox | null): boolean {
|
|
65
|
+
if (a == null || b == null) return a === b;
|
|
66
|
+
return a.height === b.height && a.left === b.left && a.right === b.right;
|
|
67
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { XIcon } from "@phosphor-icons/react";
|
|
2
|
+
import { cloneElement, isValidElement, useState, type ReactElement, type ReactNode } from "react";
|
|
3
|
+
import { SurfaceContext, type SurfaceIntent } from "@multiplatform.one/theme";
|
|
4
|
+
import { useTheme, View } from "tamagui";
|
|
5
|
+
import { ICON_BUTTON_CIRCLE_GLYPH_RATIO } from "../Button/circleGlyph";
|
|
6
|
+
import { t } from "../shared/t";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* How a panel covering an icon-only trigger shows the pressed button (LC-112,
|
|
10
|
+
* FZ-8480 Round 37): in its exact spot, turned into an X that closes the
|
|
11
|
+
* panel, the way iOS morphs a + into an X. `"close"` lays the X over the
|
|
12
|
+
* content's header row (a panel's title shares the X's line); `"menu"` gives
|
|
13
|
+
* the X its own band above the rows, which run the panel's full width.
|
|
14
|
+
*/
|
|
15
|
+
export type IconEcho = "close" | "menu";
|
|
16
|
+
|
|
17
|
+
export const ICON_ECHO_TEST_ID = "floating-panel-icon-echo";
|
|
18
|
+
|
|
19
|
+
const DROPPED_PROPS = [
|
|
20
|
+
"id",
|
|
21
|
+
"onLongPress",
|
|
22
|
+
"onKeyDown",
|
|
23
|
+
"onContextMenu",
|
|
24
|
+
"aria-haspopup",
|
|
25
|
+
"aria-expanded",
|
|
26
|
+
"aria-controls",
|
|
27
|
+
"aria-describedby",
|
|
28
|
+
"tooltip",
|
|
29
|
+
] as const;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* An X sits in the panel's corner over the content's header row, the way a
|
|
33
|
+
* sheet's close button shares the row with its title; a menu's X takes its
|
|
34
|
+
* own band above the rows. A panel that opened upward always gives the echo
|
|
35
|
+
* its own band.
|
|
36
|
+
*/
|
|
37
|
+
export function iconEchoOverlays(kind: IconEcho, up: boolean): boolean {
|
|
38
|
+
return kind === "close" && !up;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* The pressed trigger drawn again over its panel as an X: the same element
|
|
43
|
+
* with the same size and plate and an X for its glyph, a fresh render that
|
|
44
|
+
* shares none of the consumer's refs. It renders on the trigger's own
|
|
45
|
+
* surface, so a bar button keeps its bar treatment (on iOS its own Liquid
|
|
46
|
+
* Glass circle) inside the panel. Pressing it closes the panel, and it reads
|
|
47
|
+
* "Close". A button (anything with an `icon`) is the control itself; any
|
|
48
|
+
* other trigger (an avatar) becomes an X in a circle of its size.
|
|
49
|
+
*/
|
|
50
|
+
export function iconEchoElement(
|
|
51
|
+
trigger: ReactNode,
|
|
52
|
+
kind: IconEcho,
|
|
53
|
+
onClose: () => void,
|
|
54
|
+
surface?: SurfaceIntent,
|
|
55
|
+
): ReactNode {
|
|
56
|
+
const echo = echoOf(trigger, kind, onClose);
|
|
57
|
+
if (echo == null || !surface) return echo;
|
|
58
|
+
return <SurfaceContext.Provider {...surface}>{echo}</SurfaceContext.Provider>;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function echoOf(trigger: ReactNode, _kind: IconEcho, onClose: () => void): ReactNode {
|
|
62
|
+
if (!isValidElement(trigger)) return null;
|
|
63
|
+
const props = trigger.props as { icon?: unknown };
|
|
64
|
+
const label = t("Close");
|
|
65
|
+
const dropped = Object.fromEntries(DROPPED_PROPS.map((name) => [name, undefined]));
|
|
66
|
+
if (props.icon != null) {
|
|
67
|
+
return cloneElement(trigger as ReactElement<Record<string, unknown>>, {
|
|
68
|
+
...dropped,
|
|
69
|
+
ref: null,
|
|
70
|
+
icon: XIcon,
|
|
71
|
+
"aria-label": label,
|
|
72
|
+
onPress: onClose,
|
|
73
|
+
testID: ICON_ECHO_TEST_ID,
|
|
74
|
+
"data-testid": ICON_ECHO_TEST_ID,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
return <CircleClose label={label} onClose={onClose} />;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** The plate an avatar draws by default, so its X keeps the avatar's circle. */
|
|
81
|
+
const avatarPlate = "$selectedBackground";
|
|
82
|
+
|
|
83
|
+
/** The X's glyph before the circle has measured itself. */
|
|
84
|
+
const circleGlyphFallbackPx = 18;
|
|
85
|
+
|
|
86
|
+
/** An X in a circle the size of the trigger it replaces (an avatar). */
|
|
87
|
+
function CircleClose({ label, onClose }: { label: string; onClose: () => void }) {
|
|
88
|
+
const theme = useTheme();
|
|
89
|
+
const [side, setSide] = useState(0);
|
|
90
|
+
return (
|
|
91
|
+
<View
|
|
92
|
+
role="button"
|
|
93
|
+
aria-label={label}
|
|
94
|
+
tabIndex={0}
|
|
95
|
+
cursor="pointer"
|
|
96
|
+
width="100%"
|
|
97
|
+
height="100%"
|
|
98
|
+
borderRadius={100000}
|
|
99
|
+
backgroundColor={avatarPlate}
|
|
100
|
+
alignItems="center"
|
|
101
|
+
justifyContent="center"
|
|
102
|
+
pressStyle={{ opacity: 0.6 }}
|
|
103
|
+
onPress={onClose}
|
|
104
|
+
onLayout={(event) =>
|
|
105
|
+
setSide(Math.min(event.nativeEvent.layout.width, event.nativeEvent.layout.height))
|
|
106
|
+
}
|
|
107
|
+
testID={ICON_ECHO_TEST_ID}
|
|
108
|
+
data-testid={ICON_ECHO_TEST_ID}
|
|
109
|
+
>
|
|
110
|
+
<XIcon
|
|
111
|
+
size={side > 0 ? Math.round(side * ICON_BUTTON_CIRCLE_GLYPH_RATIO) : circleGlyphFallbackPx}
|
|
112
|
+
weight="bold"
|
|
113
|
+
color={theme.selectedColor?.val ?? theme.color?.val}
|
|
114
|
+
aria-hidden
|
|
115
|
+
/>
|
|
116
|
+
</View>
|
|
117
|
+
);
|
|
118
|
+
}
|
|
@@ -90,7 +90,12 @@ import {
|
|
|
90
90
|
type GestureResponderEvent,
|
|
91
91
|
} from "react-native";
|
|
92
92
|
import { SafeAreaInsetsContext, initialWindowMetrics } from "react-native-safe-area-context";
|
|
93
|
-
import {
|
|
93
|
+
import {
|
|
94
|
+
Surface,
|
|
95
|
+
SurfaceContext,
|
|
96
|
+
useResolvedKnobs,
|
|
97
|
+
motionDurationMs,
|
|
98
|
+
} from "@multiplatform.one/theme";
|
|
94
99
|
import { View, YStack } from "tamagui";
|
|
95
100
|
import { useInsideOverlay, useOverlayZIndex } from "../shared/overlayStack";
|
|
96
101
|
import { OverlayWindow } from "../shared/OverlayWindow";
|
|
@@ -100,7 +105,12 @@ import { useKeyboardInset } from "../shared/useKeyboardInset";
|
|
|
100
105
|
import type { FloatingPanelProps } from "./index";
|
|
101
106
|
import { MENU_MIN_WIDTH, resolveCompactFace } from "./compactFace";
|
|
102
107
|
import { echoOffset, type PanelWidth, panelWidthBox, screenBounds } from "./panelWidth";
|
|
103
|
-
import {
|
|
108
|
+
import {
|
|
109
|
+
echoHeaderBox,
|
|
110
|
+
PanelEchoContext,
|
|
111
|
+
PanelIconEchoBoxContext,
|
|
112
|
+
PanelTriggerRadiusContext,
|
|
113
|
+
} from "./echoContext";
|
|
104
114
|
import {
|
|
105
115
|
placeAtPoint,
|
|
106
116
|
placeBesideTrigger,
|
|
@@ -112,6 +122,7 @@ import {
|
|
|
112
122
|
import { RegularPanel } from "./RegularPanel";
|
|
113
123
|
import { panelViewportPadding, resolvePanelWidthMode } from "./useFloatingPanel";
|
|
114
124
|
import { TrailingActionOverlay, TriggerWithTrailingAction } from "./trailingAction";
|
|
125
|
+
import { iconEchoElement, iconEchoOverlays, type IconEcho } from "./iconEcho";
|
|
115
126
|
|
|
116
127
|
// Re-export the hook + constants so Select / Combobox / external consumers
|
|
117
128
|
// resolve identically on native (their floating cores are unused on device
|
|
@@ -167,6 +178,7 @@ export function FloatingPanel({
|
|
|
167
178
|
triggerRow,
|
|
168
179
|
triggerEcho = true,
|
|
169
180
|
coverAlign = "start",
|
|
181
|
+
iconEcho,
|
|
170
182
|
anchorMode,
|
|
171
183
|
panelWidth = "content",
|
|
172
184
|
sheet,
|
|
@@ -175,6 +187,7 @@ export function FloatingPanel({
|
|
|
175
187
|
}: FloatingPanelProps) {
|
|
176
188
|
// The panel floats over the page: it never inherits a painted ancestor.
|
|
177
189
|
const { knobProps } = useResolvedKnobs({ painted: false });
|
|
190
|
+
const triggerSurface = SurfaceContext.useStyledContext();
|
|
178
191
|
const windowZIndex = useOverlayZIndex("sheet");
|
|
179
192
|
const children = (
|
|
180
193
|
<Surface painted={false} elevated>
|
|
@@ -634,8 +647,14 @@ export function FloatingPanel({
|
|
|
634
647
|
Math.max(safeArea?.bottom ?? 0, keyboardInset) -
|
|
635
648
|
panelViewportPadding,
|
|
636
649
|
}}
|
|
637
|
-
echo={triggerEcho ? (triggerRow ?? trigger) : null}
|
|
650
|
+
echo={triggerEcho && !iconEcho ? (triggerRow ?? trigger) : null}
|
|
638
651
|
echoLabel={triggerA11y?.label}
|
|
652
|
+
iconEcho={
|
|
653
|
+
iconEcho && anchorMode !== "attach"
|
|
654
|
+
? iconEchoElement(trigger, iconEcho, close, triggerSurface)
|
|
655
|
+
: null
|
|
656
|
+
}
|
|
657
|
+
iconEchoKind={iconEcho}
|
|
639
658
|
coverAlign={anchorMode === "attach" ? "start" : coverAlign}
|
|
640
659
|
panelWidth={panelWidth}
|
|
641
660
|
triggerRadius={anchorMode === "attach" ? null : triggerRadius}
|
|
@@ -803,6 +822,8 @@ function TriggerMenuPanel({
|
|
|
803
822
|
bounds,
|
|
804
823
|
echo,
|
|
805
824
|
echoLabel,
|
|
825
|
+
iconEcho = null,
|
|
826
|
+
iconEchoKind = "menu",
|
|
806
827
|
coverAlign,
|
|
807
828
|
panelWidth,
|
|
808
829
|
triggerRadius,
|
|
@@ -817,6 +838,9 @@ function TriggerMenuPanel({
|
|
|
817
838
|
bounds: PanelBounds;
|
|
818
839
|
echo: ReactNode;
|
|
819
840
|
echoLabel?: string;
|
|
841
|
+
/** LC-112 (FZ-8480): the icon trigger drawn again in its own spot. */
|
|
842
|
+
iconEcho?: ReactNode;
|
|
843
|
+
iconEchoKind?: IconEcho;
|
|
820
844
|
coverAlign: "start" | "auto";
|
|
821
845
|
panelWidth: PanelWidth;
|
|
822
846
|
triggerRadius: number | null;
|
|
@@ -858,6 +882,7 @@ function TriggerMenuPanel({
|
|
|
858
882
|
bounds: room,
|
|
859
883
|
rtl: I18nManager.isRTL,
|
|
860
884
|
align: coverAlign,
|
|
885
|
+
pinned: iconEcho != null,
|
|
861
886
|
})
|
|
862
887
|
: null;
|
|
863
888
|
// LC-112 seamless cover: the menu is its trigger grown open, so it draws the
|
|
@@ -871,6 +896,7 @@ function TriggerMenuPanel({
|
|
|
871
896
|
? half
|
|
872
897
|
: undefined;
|
|
873
898
|
const borderWidth = Number(knobProps.elevatedSurface.borderWidth) || 0;
|
|
899
|
+
const echoOverlays = iconEchoOverlays(iconEchoKind, placement?.up ?? false);
|
|
874
900
|
// A panel slid along the screen (a `"screen"` or fixed width, or one pushed
|
|
875
901
|
// back inside the margins) keeps its echo on the trigger.
|
|
876
902
|
const echoShift =
|
|
@@ -954,14 +980,43 @@ function TriggerMenuPanel({
|
|
|
954
980
|
</RNView>
|
|
955
981
|
</Pressable>
|
|
956
982
|
) : null}
|
|
983
|
+
{iconEcho != null && anchor && !placement?.up && !echoOverlays ? (
|
|
984
|
+
<IconEchoBand anchor={anchor} shift={echoShift} borderWidth={borderWidth} edge="top">
|
|
985
|
+
{iconEcho}
|
|
986
|
+
</IconEchoBand>
|
|
987
|
+
) : null}
|
|
957
988
|
<RNScrollView
|
|
958
989
|
style={{ flexGrow: 0, flexShrink: 1 }}
|
|
959
990
|
keyboardShouldPersistTaps="handled"
|
|
960
991
|
>
|
|
961
992
|
<YStack padding={framePadding} gap={frameGap}>
|
|
962
|
-
|
|
993
|
+
<PanelIconEchoBoxContext.Provider
|
|
994
|
+
value={
|
|
995
|
+
iconEcho != null && anchor && placement && echoOverlays
|
|
996
|
+
? echoHeaderBox({
|
|
997
|
+
offset: echoShift,
|
|
998
|
+
width: anchor.width,
|
|
999
|
+
height: anchor.height,
|
|
1000
|
+
panelWidth: placement.width,
|
|
1001
|
+
rtl: I18nManager.isRTL,
|
|
1002
|
+
})
|
|
1003
|
+
: null
|
|
1004
|
+
}
|
|
1005
|
+
>
|
|
1006
|
+
{children}
|
|
1007
|
+
</PanelIconEchoBoxContext.Provider>
|
|
963
1008
|
</YStack>
|
|
964
1009
|
</RNScrollView>
|
|
1010
|
+
{iconEcho != null && anchor && (placement?.up || echoOverlays) ? (
|
|
1011
|
+
<IconEchoBand
|
|
1012
|
+
anchor={anchor}
|
|
1013
|
+
shift={echoShift}
|
|
1014
|
+
borderWidth={borderWidth}
|
|
1015
|
+
edge={echoOverlays ? "overlay" : "bottom"}
|
|
1016
|
+
>
|
|
1017
|
+
{iconEcho}
|
|
1018
|
+
</IconEchoBand>
|
|
1019
|
+
) : null}
|
|
965
1020
|
</YStack>
|
|
966
1021
|
</YStack>
|
|
967
1022
|
</RNView>
|
|
@@ -969,6 +1024,62 @@ function TriggerMenuPanel({
|
|
|
969
1024
|
);
|
|
970
1025
|
}
|
|
971
1026
|
|
|
1027
|
+
/**
|
|
1028
|
+
* The band an icon trigger's echo rides (LC-112, FZ-8480): the trigger's
|
|
1029
|
+
* height on the edge the menu shares with it, outside the scroll view so the
|
|
1030
|
+
* rows scroll under it, and the button in the trigger's own box, pulled back
|
|
1031
|
+
* over the frame's border so it lands exactly where the trigger is.
|
|
1032
|
+
*/
|
|
1033
|
+
function IconEchoBand({
|
|
1034
|
+
anchor,
|
|
1035
|
+
shift,
|
|
1036
|
+
borderWidth,
|
|
1037
|
+
edge,
|
|
1038
|
+
children,
|
|
1039
|
+
}: {
|
|
1040
|
+
anchor: TriggerRect;
|
|
1041
|
+
shift: number;
|
|
1042
|
+
borderWidth: number;
|
|
1043
|
+
/** `"overlay"`: an X laid over the content's top corner, taking no height. */
|
|
1044
|
+
edge: "top" | "bottom" | "overlay";
|
|
1045
|
+
children: ReactNode;
|
|
1046
|
+
}) {
|
|
1047
|
+
return (
|
|
1048
|
+
<RNView
|
|
1049
|
+
testID="floating-panel-icon-echo-band"
|
|
1050
|
+
pointerEvents="box-none"
|
|
1051
|
+
style={
|
|
1052
|
+
edge === "overlay"
|
|
1053
|
+
? {
|
|
1054
|
+
position: "absolute",
|
|
1055
|
+
top: -borderWidth,
|
|
1056
|
+
start: -borderWidth,
|
|
1057
|
+
end: -borderWidth,
|
|
1058
|
+
height: anchor.height,
|
|
1059
|
+
zIndex: 1,
|
|
1060
|
+
}
|
|
1061
|
+
: {
|
|
1062
|
+
height: anchor.height,
|
|
1063
|
+
marginHorizontal: -borderWidth,
|
|
1064
|
+
...(edge === "top" ? { marginTop: -borderWidth } : { marginBottom: -borderWidth }),
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
>
|
|
1068
|
+
<RNView
|
|
1069
|
+
style={{
|
|
1070
|
+
width: anchor.width,
|
|
1071
|
+
height: anchor.height,
|
|
1072
|
+
marginStart: shift,
|
|
1073
|
+
alignItems: "flex-start",
|
|
1074
|
+
justifyContent: "flex-start",
|
|
1075
|
+
}}
|
|
1076
|
+
>
|
|
1077
|
+
{children}
|
|
1078
|
+
</RNView>
|
|
1079
|
+
</RNView>
|
|
1080
|
+
);
|
|
1081
|
+
}
|
|
1082
|
+
|
|
972
1083
|
/**
|
|
973
1084
|
* The menu at the long-press point: measured once per open at its natural
|
|
974
1085
|
* size, then placed by the same rule as the web pointer menu. Later layouts
|