@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
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @vitest-environment jsdom
|
|
3
|
+
*
|
|
4
|
+
* LC-176 (FZ-8711, Round 48): an icon button in a field's leading or trailing
|
|
5
|
+
* slot is part of the field. Clay, on the list search's filter and sort glass
|
|
6
|
+
* circles overhanging the field: "Liquid glass icon should not be inside of
|
|
7
|
+
* inputs though right you should do the no borders no background when you put
|
|
8
|
+
* them inside of here right". On iOS Safari and on native iOS 26 alike it is
|
|
9
|
+
* the bare tinted glyph, even when the field sits in a bar; glass stays on the
|
|
10
|
+
* bar's own buttons.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { createDefaultThemeConfig, Surface, useFieldSurface } from "@multiplatform.one/theme";
|
|
14
|
+
import { cleanup, render, screen } from "@testing-library/react";
|
|
15
|
+
import type { ReactElement } from "react";
|
|
16
|
+
import { TamaguiProvider } from "tamagui";
|
|
17
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
18
|
+
import { SearchInput } from "../fields/SearchInput";
|
|
19
|
+
import { Input as InputParts } from "../InputParts";
|
|
20
|
+
import { Button, IconButton, IconButtonGroup } from "./index";
|
|
21
|
+
|
|
22
|
+
const glass = vi.hoisted(() => ({ system: false }));
|
|
23
|
+
vi.mock("./glass", async (importOriginal) => {
|
|
24
|
+
const real = await importOriginal<typeof import("./glass")>();
|
|
25
|
+
function FakeSystemGlass({
|
|
26
|
+
glassEffectStyle: _style,
|
|
27
|
+
isInteractive: _interactive,
|
|
28
|
+
colorScheme: _scheme,
|
|
29
|
+
children,
|
|
30
|
+
...props
|
|
31
|
+
}: Record<string, unknown>) {
|
|
32
|
+
return (
|
|
33
|
+
<div data-glass="system" {...props}>
|
|
34
|
+
{children as never}
|
|
35
|
+
</div>
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
return {
|
|
39
|
+
...real,
|
|
40
|
+
// Native iOS 26 with expo-glass-effect linked, else the real web loader
|
|
41
|
+
// (iOS Safari's drawn face, nothing elsewhere).
|
|
42
|
+
loadGlassView: () => (glass.system ? FakeSystemGlass : real.loadGlassView()),
|
|
43
|
+
loadGlassContainer: () => (glass.system ? FakeSystemGlass : null),
|
|
44
|
+
};
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
afterEach(() => {
|
|
48
|
+
cleanup();
|
|
49
|
+
vi.unstubAllGlobals();
|
|
50
|
+
glass.system = false;
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const IPHONE =
|
|
54
|
+
"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";
|
|
55
|
+
|
|
56
|
+
const houseConfig = createDefaultThemeConfig();
|
|
57
|
+
|
|
58
|
+
function Glyph({ size, weight }: { size?: number; weight?: string }) {
|
|
59
|
+
return <svg data-glyph="" data-weight={weight} width={size} height={size} />;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function houseRender(ui: ReactElement, scheme: "light" | "dark") {
|
|
63
|
+
return render(
|
|
64
|
+
<TamaguiProvider config={houseConfig.tamagui} defaultTheme={scheme} disableInjectCSS>
|
|
65
|
+
{ui}
|
|
66
|
+
</TamaguiProvider>,
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** A bar holding a search field with filter and sort in its trailing slot, and a bar button. */
|
|
71
|
+
function SearchBar() {
|
|
72
|
+
return (
|
|
73
|
+
<Surface bar>
|
|
74
|
+
<SearchInput
|
|
75
|
+
value=""
|
|
76
|
+
onChange={() => {}}
|
|
77
|
+
aria-label="Search"
|
|
78
|
+
trailing={
|
|
79
|
+
<>
|
|
80
|
+
<Button chromeless accentInk icon={Glyph} aria-label="Filter" />
|
|
81
|
+
<IconButton icon={Glyph} aria-label="Sort" />
|
|
82
|
+
</>
|
|
83
|
+
}
|
|
84
|
+
/>
|
|
85
|
+
<IconButton icon={Glyph} aria-label="Add" />
|
|
86
|
+
</Surface>
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function hasGlass(label: string): boolean {
|
|
91
|
+
return screen.getByLabelText(label).querySelector("[data-glass], [data-mp-glass]") != null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const PLATFORMS = [
|
|
95
|
+
[
|
|
96
|
+
"iOS Safari",
|
|
97
|
+
() => vi.stubGlobal("navigator", { userAgent: IPHONE, platform: "iPhone", maxTouchPoints: 5 }),
|
|
98
|
+
],
|
|
99
|
+
["native iOS 26", () => (glass.system = true)],
|
|
100
|
+
] as const;
|
|
101
|
+
|
|
102
|
+
describe("LC-176: an icon button in a field's slot is the field's bare glyph (FZ-8711)", () => {
|
|
103
|
+
describe.each(PLATFORMS)("%s", (_platform, onIPhone) => {
|
|
104
|
+
it.each(["light", "dark"] as const)(
|
|
105
|
+
"the search's filter and sort draw no glass while the bar's own button keeps it (%s)",
|
|
106
|
+
(scheme) => {
|
|
107
|
+
onIPhone();
|
|
108
|
+
houseRender(<SearchBar />, scheme);
|
|
109
|
+
for (const label of ["Filter", "Sort"]) {
|
|
110
|
+
const button = screen.getByLabelText(label);
|
|
111
|
+
expect(hasGlass(label)).toBe(false);
|
|
112
|
+
expect(button.getAttribute("data-mp-icon-button")).toBe("glyph");
|
|
113
|
+
// Bold glyph, tinted, on its 44pt square.
|
|
114
|
+
expect(button.querySelector("[data-glyph]")!.getAttribute("data-weight")).toBe("bold");
|
|
115
|
+
}
|
|
116
|
+
expect(hasGlass("Add")).toBe(true);
|
|
117
|
+
expect(screen.getByLabelText("Add").getAttribute("data-mp-icon-button")).toBe("glass");
|
|
118
|
+
},
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
it("a framed, plated or toggled icon button in a slot still draws only its glyph", () => {
|
|
122
|
+
onIPhone();
|
|
123
|
+
houseRender(
|
|
124
|
+
<Surface bar>
|
|
125
|
+
<InputParts>
|
|
126
|
+
<InputParts.Box>
|
|
127
|
+
<InputParts.Area aria-label="Value" />
|
|
128
|
+
<Button outlined icon={Glyph} aria-label="Outlined" />
|
|
129
|
+
<Button circular icon={Glyph} aria-label="Circle" />
|
|
130
|
+
<Button plate="media" icon={Glyph} aria-label="Media" />
|
|
131
|
+
<Button selected icon={Glyph} aria-label="Toggle" />
|
|
132
|
+
</InputParts.Box>
|
|
133
|
+
</InputParts>
|
|
134
|
+
</Surface>,
|
|
135
|
+
"light",
|
|
136
|
+
);
|
|
137
|
+
for (const label of ["Outlined", "Circle", "Media", "Toggle"]) {
|
|
138
|
+
const button = screen.getByLabelText(label);
|
|
139
|
+
expect(hasGlass(label)).toBe(false);
|
|
140
|
+
expect(button.getAttribute("data-mp-icon-button")).toBe("glyph");
|
|
141
|
+
expect(button.getAttribute("data-mp-selected")).toBeNull();
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it("a group in a slot paints no capsule", () => {
|
|
146
|
+
onIPhone();
|
|
147
|
+
houseRender(
|
|
148
|
+
<Surface bar>
|
|
149
|
+
<InputParts>
|
|
150
|
+
<InputParts.Box>
|
|
151
|
+
<InputParts.Area aria-label="Value" />
|
|
152
|
+
<IconButtonGroup data-testid="slot-group">
|
|
153
|
+
<IconButton icon={Glyph} aria-label="Filter" />
|
|
154
|
+
<IconButton icon={Glyph} aria-label="Sort" />
|
|
155
|
+
</IconButtonGroup>
|
|
156
|
+
</InputParts.Box>
|
|
157
|
+
</InputParts>
|
|
158
|
+
</Surface>,
|
|
159
|
+
"light",
|
|
160
|
+
);
|
|
161
|
+
const group = screen.getByTestId("slot-group");
|
|
162
|
+
expect(group.querySelector("[data-glass], [data-mp-glass]")).toBeNull();
|
|
163
|
+
expect(group.getAttribute("data-mp-icon-button-group")).not.toBe("glass");
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it("an overlay opened from a slot is content again, not part of the field", () => {
|
|
168
|
+
let insideField: boolean | undefined;
|
|
169
|
+
let insideOverlay: boolean | undefined;
|
|
170
|
+
function Probe({ onRead }: { onRead: (field: boolean) => void }) {
|
|
171
|
+
onRead(useFieldSurface());
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
houseRender(
|
|
175
|
+
<Surface field>
|
|
176
|
+
<Probe onRead={(field) => (insideField = field)} />
|
|
177
|
+
<Surface painted={false} elevated>
|
|
178
|
+
<Probe onRead={(field) => (insideOverlay = field)} />
|
|
179
|
+
</Surface>
|
|
180
|
+
</Surface>,
|
|
181
|
+
"light",
|
|
182
|
+
);
|
|
183
|
+
expect(insideField).toBe(true);
|
|
184
|
+
expect(insideOverlay).toBe(false);
|
|
185
|
+
});
|
|
186
|
+
});
|
|
@@ -16,7 +16,6 @@ import {
|
|
|
16
16
|
ArchiveIcon,
|
|
17
17
|
ArrowCircleUpIcon,
|
|
18
18
|
ArrowsClockwiseIcon,
|
|
19
|
-
CaretLeftIcon,
|
|
20
19
|
CopyIcon,
|
|
21
20
|
DotsThreeIcon,
|
|
22
21
|
DownloadSimpleIcon,
|
|
@@ -26,6 +25,13 @@ import {
|
|
|
26
25
|
TrashIcon,
|
|
27
26
|
XIcon,
|
|
28
27
|
} from "@phosphor-icons/react";
|
|
28
|
+
import {
|
|
29
|
+
CodeBlock,
|
|
30
|
+
PageBackButton,
|
|
31
|
+
Popover,
|
|
32
|
+
PopoverContent,
|
|
33
|
+
ScreenToolbar,
|
|
34
|
+
} from "@multiplatform.one/components";
|
|
29
35
|
import { Button, IconButton, IconButtonGroup, type ButtonProps } from "./index";
|
|
30
36
|
import { useState } from "react";
|
|
31
37
|
|
|
@@ -447,89 +453,93 @@ export const IconButtons: Story = {
|
|
|
447
453
|
render: () => {
|
|
448
454
|
const [on, setOn] = useState(false);
|
|
449
455
|
return (
|
|
450
|
-
<YStack
|
|
451
|
-
<
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
<
|
|
461
|
-
icon={
|
|
462
|
-
aria-label="
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
456
|
+
<YStack backgroundColor="$background">
|
|
457
|
+
<ScreenToolbar
|
|
458
|
+
data-testid="icon-button-bar"
|
|
459
|
+
leading={<PageBackButton onPress={pressed("back")} />}
|
|
460
|
+
title="Inbox"
|
|
461
|
+
trailing={<IconButton icon={DotsThreeIcon} aria-label="More" onPress={pressed("more")} />}
|
|
462
|
+
/>
|
|
463
|
+
<YStack gap="$4" padding="$4">
|
|
464
|
+
<Surface bar>
|
|
465
|
+
<XStack justifyContent="center" data-testid="icon-button-toolbar">
|
|
466
|
+
<IconButtonGroup>
|
|
467
|
+
<IconButton icon={ArchiveIcon} aria-label="Archive" onPress={pressed("archive")} />
|
|
468
|
+
<IconButton icon={TrashIcon} aria-label="Delete" onPress={pressed("delete")} />
|
|
469
|
+
<IconButton
|
|
470
|
+
icon={EnvelopeSimpleIcon}
|
|
471
|
+
aria-label="Mark unread"
|
|
472
|
+
selected={on}
|
|
473
|
+
onPress={() => setOn((v) => !v)}
|
|
474
|
+
/>
|
|
475
|
+
</IconButtonGroup>
|
|
476
|
+
</XStack>
|
|
477
|
+
</Surface>
|
|
478
|
+
<Surface bar>
|
|
479
|
+
<XStack alignItems="center" data-testid="icon-button-bar-disabled">
|
|
480
|
+
<Paragraph flex={1}>Downloads</Paragraph>
|
|
481
|
+
<IconButtonGroup>
|
|
482
|
+
<IconButton
|
|
483
|
+
icon={DownloadSimpleIcon}
|
|
484
|
+
aria-label="Download"
|
|
485
|
+
onPress={pressed("download")}
|
|
486
|
+
/>
|
|
487
|
+
<IconButton
|
|
488
|
+
icon={DownloadSimpleIcon}
|
|
489
|
+
aria-label="Download (nothing to download)"
|
|
490
|
+
disabled
|
|
491
|
+
/>
|
|
492
|
+
</IconButtonGroup>
|
|
493
|
+
</XStack>
|
|
494
|
+
</Surface>
|
|
495
|
+
<XStack alignItems="center" data-testid="icon-button-content">
|
|
496
|
+
<Paragraph flex={1}>A row in content</Paragraph>
|
|
473
497
|
<IconButtonGroup>
|
|
474
498
|
<IconButton
|
|
475
499
|
icon={DownloadSimpleIcon}
|
|
476
500
|
aria-label="Download"
|
|
477
501
|
onPress={pressed("download")}
|
|
478
502
|
/>
|
|
479
|
-
<IconButton
|
|
480
|
-
icon={DownloadSimpleIcon}
|
|
481
|
-
aria-label="Download (nothing to download)"
|
|
482
|
-
disabled
|
|
483
|
-
/>
|
|
503
|
+
<IconButton icon={TrashIcon} error aria-label="Delete" onPress={pressed("delete")} />
|
|
484
504
|
</IconButtonGroup>
|
|
485
505
|
</XStack>
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
<IconButton inline icon={CopyIcon} aria-label="Copy" onPress={pressed("copy")} />
|
|
501
|
-
</XStack>
|
|
502
|
-
<YStack
|
|
503
|
-
position="relative"
|
|
504
|
-
width="$20"
|
|
505
|
-
height="$12"
|
|
506
|
-
backgroundColor="$blue9"
|
|
507
|
-
borderRadius="$4"
|
|
508
|
-
data-testid="icon-button-media"
|
|
509
|
-
>
|
|
510
|
-
<YStack position="absolute" top="$2" left="$2">
|
|
511
|
-
<IconButton
|
|
512
|
-
plate="media"
|
|
513
|
-
icon={HeartIcon}
|
|
514
|
-
aria-label="Favourite"
|
|
515
|
-
onPress={pressed("favourite")}
|
|
516
|
-
/>
|
|
517
|
-
</YStack>
|
|
518
|
-
<YStack position="absolute" top="$2" right="$2">
|
|
519
|
-
<IconButtonGroup plate="media">
|
|
520
|
-
<IconButton
|
|
521
|
-
plate="media"
|
|
522
|
-
icon={ArrowsClockwiseIcon}
|
|
523
|
-
aria-label="Replace photo"
|
|
524
|
-
onPress={pressed("replace")}
|
|
525
|
-
/>
|
|
506
|
+
<XStack alignItems="center" data-testid="icon-button-inline">
|
|
507
|
+
<Paragraph>10.64.96.1:4003</Paragraph>
|
|
508
|
+
<IconButton inline icon={CopyIcon} aria-label="Copy" onPress={pressed("copy")} />
|
|
509
|
+
</XStack>
|
|
510
|
+
<CodeBlock language="text">shc node join</CodeBlock>
|
|
511
|
+
<YStack
|
|
512
|
+
position="relative"
|
|
513
|
+
width="$20"
|
|
514
|
+
height="$12"
|
|
515
|
+
backgroundColor="$blue9"
|
|
516
|
+
borderRadius="$4"
|
|
517
|
+
data-testid="icon-button-media"
|
|
518
|
+
>
|
|
519
|
+
<YStack position="absolute" top="$2" left="$2">
|
|
526
520
|
<IconButton
|
|
527
521
|
plate="media"
|
|
528
|
-
icon={
|
|
529
|
-
aria-label="
|
|
530
|
-
onPress={pressed("
|
|
522
|
+
icon={HeartIcon}
|
|
523
|
+
aria-label="Favourite"
|
|
524
|
+
onPress={pressed("favourite")}
|
|
531
525
|
/>
|
|
532
|
-
</
|
|
526
|
+
</YStack>
|
|
527
|
+
<YStack position="absolute" top="$2" right="$2">
|
|
528
|
+
<IconButtonGroup plate="media">
|
|
529
|
+
<IconButton
|
|
530
|
+
plate="media"
|
|
531
|
+
icon={ArrowsClockwiseIcon}
|
|
532
|
+
aria-label="Replace photo"
|
|
533
|
+
onPress={pressed("replace")}
|
|
534
|
+
/>
|
|
535
|
+
<IconButton
|
|
536
|
+
plate="media"
|
|
537
|
+
icon={XIcon}
|
|
538
|
+
aria-label="Remove photo"
|
|
539
|
+
onPress={pressed("remove")}
|
|
540
|
+
/>
|
|
541
|
+
</IconButtonGroup>
|
|
542
|
+
</YStack>
|
|
533
543
|
</YStack>
|
|
534
544
|
</YStack>
|
|
535
545
|
</YStack>
|
|
@@ -560,15 +570,20 @@ export const ShcNodesScreen: TintStory = {
|
|
|
560
570
|
<Paragraph flex={1} size="$9" fontWeight="700">
|
|
561
571
|
Nodes
|
|
562
572
|
</Paragraph>
|
|
563
|
-
<
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
573
|
+
<Popover anchorMode="cover-trigger">
|
|
574
|
+
<Popover.Trigger asChild>
|
|
575
|
+
<Button size="$3" {...shcRoundGlyph} icon={PlusIcon} aria-label="Add node" />
|
|
576
|
+
</Popover.Trigger>
|
|
577
|
+
<PopoverContent>
|
|
578
|
+
<YStack padding="$4" gap="$3" width={320} maxWidth="100%">
|
|
579
|
+
<Paragraph size="$5" fontWeight="600">
|
|
580
|
+
Add a node
|
|
581
|
+
</Paragraph>
|
|
582
|
+
<Paragraph color="$color11">Run this on the new machine.</Paragraph>
|
|
583
|
+
<CodeBlock language="text">shc node join</CodeBlock>
|
|
584
|
+
</YStack>
|
|
585
|
+
</PopoverContent>
|
|
586
|
+
</Popover>
|
|
572
587
|
</XStack>
|
|
573
588
|
</YStack>
|
|
574
589
|
</GlyphTintFallbackProvider>
|
|
@@ -11,7 +11,7 @@ import { createDefaultThemeConfig, Surface } from "@multiplatform.one/theme";
|
|
|
11
11
|
import type { ReactElement } from "react";
|
|
12
12
|
import { TamaguiProvider } from "tamagui";
|
|
13
13
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
14
|
-
import { IconButton, IconButtonGroup } from "./index";
|
|
14
|
+
import { IconButton, IconButtonGroup, useGlyphOnPlate } from "./index";
|
|
15
15
|
|
|
16
16
|
const glass = vi.hoisted(() => ({
|
|
17
17
|
available: false,
|
|
@@ -99,6 +99,53 @@ describe("LC-176: IconButtonGroup", () => {
|
|
|
99
99
|
expect(screen.getByLabelText("Back").querySelector("[data-glass]")).not.toBeNull();
|
|
100
100
|
});
|
|
101
101
|
|
|
102
|
+
it("a group of one is no capsule: its button keeps its own glass circle (FZ-8442)", () => {
|
|
103
|
+
vi.stubGlobal("navigator", { userAgent: IPHONE, platform: "iPhone", maxTouchPoints: 5 });
|
|
104
|
+
glass.available = true;
|
|
105
|
+
houseRender(
|
|
106
|
+
<Surface bar>
|
|
107
|
+
<IconButtonGroup data-testid="group">
|
|
108
|
+
<>
|
|
109
|
+
<IconButton icon={Glyph} aria-label="Add node" edge="end" />
|
|
110
|
+
</>
|
|
111
|
+
</IconButtonGroup>
|
|
112
|
+
</Surface>,
|
|
113
|
+
);
|
|
114
|
+
const group = screen.getByTestId("group");
|
|
115
|
+
expect(group.querySelector("[data-mp-glass-capsule]")).toBeNull();
|
|
116
|
+
expect(screen.getByLabelText("Add node").querySelector("[data-glass]")).not.toBeNull();
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("a glyph on glass is told it is plated, so it centres instead of pulling (FZ-8442)", () => {
|
|
120
|
+
vi.stubGlobal("navigator", { userAgent: IPHONE, platform: "iPhone", maxTouchPoints: 5 });
|
|
121
|
+
glass.available = true;
|
|
122
|
+
function Probe({ size }: { size?: number }) {
|
|
123
|
+
return <svg data-plated={String(useGlyphOnPlate())} width={size} height={size} />;
|
|
124
|
+
}
|
|
125
|
+
houseRender(
|
|
126
|
+
<>
|
|
127
|
+
<Surface bar>
|
|
128
|
+
<IconButton icon={Probe} aria-label="Back" edge="start" />
|
|
129
|
+
<IconButtonGroup>
|
|
130
|
+
<IconButton icon={Probe} aria-label="Archive" />
|
|
131
|
+
<IconButton icon={Probe} aria-label="Delete" edge="end" />
|
|
132
|
+
</IconButtonGroup>
|
|
133
|
+
</Surface>
|
|
134
|
+
<IconButton icon={Probe} aria-label="Copy" />
|
|
135
|
+
</>,
|
|
136
|
+
);
|
|
137
|
+
for (const name of ["Back", "Archive", "Delete"]) {
|
|
138
|
+
const button = screen.getByLabelText(name);
|
|
139
|
+
expect(button.querySelector("svg")?.getAttribute("data-plated")).toBe("true");
|
|
140
|
+
const style = getComputedStyle(button);
|
|
141
|
+
for (const side of ["marginLeft", "marginRight", "marginInlineStart", "marginInlineEnd"])
|
|
142
|
+
expect(parseFloat(style[side as "marginLeft"] || "0") || 0).toBeGreaterThanOrEqual(0);
|
|
143
|
+
}
|
|
144
|
+
expect(screen.getByLabelText("Copy").querySelector("svg")?.getAttribute("data-plated")).toBe(
|
|
145
|
+
"false",
|
|
146
|
+
);
|
|
147
|
+
});
|
|
148
|
+
|
|
102
149
|
it("a lone glass circle fills the button's border box, as wide as the ✓ beside it (FZ-8439)", () => {
|
|
103
150
|
glass.available = true;
|
|
104
151
|
houseRender(
|
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
isDarkThemeName,
|
|
3
|
+
radiusClassProps,
|
|
4
|
+
useBarSurface,
|
|
5
|
+
useFieldSurface,
|
|
6
|
+
} from "@multiplatform.one/theme";
|
|
2
7
|
import { Children, useCallback, useMemo, useRef, useState, type ReactNode } from "react";
|
|
3
8
|
import { isWeb, Theme, useThemeName, View, XStack, type XStackProps } from "tamagui";
|
|
4
9
|
import { loadGlassContainer, loadGlassView } from "./glass";
|
|
@@ -35,7 +40,10 @@ export interface IconButtonGroupProps extends XStackProps {
|
|
|
35
40
|
* bar group with no icon button in it paints no capsule (FZ-8494).
|
|
36
41
|
*/
|
|
37
42
|
export function IconButtonGroup({ plate, children, ...rest }: IconButtonGroupProps) {
|
|
38
|
-
|
|
43
|
+
// LC-176 (FZ-8711): a group in a field's slot is part of the field, so it
|
|
44
|
+
// paints no capsule of its own: neither bar glass nor the media plate.
|
|
45
|
+
const field = useFieldSurface();
|
|
46
|
+
const bar = useBarSurface() && !field;
|
|
39
47
|
const dark = isDarkThemeName(useThemeName());
|
|
40
48
|
const [capsuleHeight, setCapsuleHeight] = useState(0);
|
|
41
49
|
const [members, setMembers] = useState(0);
|
|
@@ -59,7 +67,7 @@ export function IconButtonGroup({ plate, children, ...rest }: IconButtonGroupPro
|
|
|
59
67
|
}),
|
|
60
68
|
[],
|
|
61
69
|
);
|
|
62
|
-
if (plate === "media") {
|
|
70
|
+
if (plate === "media" && !field) {
|
|
63
71
|
return (
|
|
64
72
|
<IconButtonGroupContext.Provider value="media">
|
|
65
73
|
<Theme name="dark">
|
|
@@ -80,26 +88,34 @@ export function IconButtonGroup({ plate, children, ...rest }: IconButtonGroupPro
|
|
|
80
88
|
);
|
|
81
89
|
}
|
|
82
90
|
const GlassView = bar ? loadGlassView() : null;
|
|
91
|
+
// A group of one button is a lone button, and keeps its circle (FZ-8442).
|
|
92
|
+
const lone = members === 1 && Children.toArray(children).length === 1;
|
|
83
93
|
if (GlassView && isWeb && isAppleTouchWeb()) {
|
|
84
94
|
// The caller's layout props stay on the outer row; the capsule hugs the
|
|
85
95
|
// buttons inside it, so a stretched slot never draws a wide empty pill.
|
|
96
|
+
// A lone button keeps the same tree, so it never remounts as it registers.
|
|
97
|
+
const capsuled = members > 0 && !lone;
|
|
86
98
|
return (
|
|
87
99
|
<IconButtonGroupRegistry.Provider value={register}>
|
|
88
|
-
<IconButtonGroupContext.Provider value="glass">
|
|
100
|
+
<IconButtonGroupContext.Provider value={lone ? null : "glass"}>
|
|
89
101
|
<XStack
|
|
90
102
|
alignItems="center"
|
|
91
|
-
data-mp-icon-button-group={
|
|
103
|
+
data-mp-icon-button-group={capsuled ? "glass" : "bar"}
|
|
92
104
|
{...rest}
|
|
93
105
|
>
|
|
94
106
|
<XStack
|
|
95
107
|
alignItems="center"
|
|
96
108
|
position="relative"
|
|
97
109
|
zIndex={0}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
110
|
+
{...(lone
|
|
111
|
+
? null
|
|
112
|
+
: {
|
|
113
|
+
paddingHorizontal: "$2" as const,
|
|
114
|
+
gap: "$1" as const,
|
|
115
|
+
"data-mp-glass-capsule": "",
|
|
116
|
+
})}
|
|
101
117
|
>
|
|
102
|
-
{
|
|
118
|
+
{capsuled ? (
|
|
103
119
|
<GlassView
|
|
104
120
|
style={{
|
|
105
121
|
position: "absolute",
|
|
@@ -120,9 +136,7 @@ export function IconButtonGroup({ plate, children, ...rest }: IconButtonGroupPro
|
|
|
120
136
|
}
|
|
121
137
|
const capsule = GlassView != null && members > 0;
|
|
122
138
|
// The capsule hugs its buttons with the web twin's $2 end inset and $1 gap,
|
|
123
|
-
// so its first and last items never sit on its curve (FZ-8550).
|
|
124
|
-
// one button is a lone button, and keeps its circle.
|
|
125
|
-
const lone = members === 1 && Children.toArray(children).length === 1;
|
|
139
|
+
// so its first and last items never sit on its curve (FZ-8550).
|
|
126
140
|
const row = (
|
|
127
141
|
<XStack
|
|
128
142
|
alignItems="center"
|
|
@@ -15,6 +15,18 @@ export function useIconButtonGroupGround(): IconButtonGroupGround {
|
|
|
15
15
|
return useContext(IconButtonGroupContext);
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
/**
|
|
19
|
+
* LC-176: true while a glyph is drawn centred on a visible ground (a glass
|
|
20
|
+
* circle or capsule, the media plate, a framed circle). An icon that pulls
|
|
21
|
+
* itself toward a container edge for optical alignment, such as the back
|
|
22
|
+
* caret, keeps its ink centred instead.
|
|
23
|
+
*/
|
|
24
|
+
export const GlyphPlateContext = createContext(false);
|
|
25
|
+
|
|
26
|
+
export function useGlyphOnPlate(): boolean {
|
|
27
|
+
return useContext(GlyphPlateContext);
|
|
28
|
+
}
|
|
29
|
+
|
|
18
30
|
/**
|
|
19
31
|
* An icon button counts itself into the group around it, so a bar group of
|
|
20
32
|
* only non-icon children (a status badge in a page head) paints no capsule
|