@multiplatform.one/components 7.80.0 → 7.81.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/CheckGlyph/MenuCheckColumn.cjs +56 -0
- package/dist/cjs/CheckGlyph/MenuCheckColumn.native.js +60 -0
- package/dist/cjs/CheckGlyph/MenuCheckColumn.native.js.map +1 -0
- package/dist/cjs/CheckGlyph/index.cjs +40 -0
- package/dist/cjs/CheckGlyph/index.native.js +44 -0
- package/dist/cjs/CheckGlyph/index.native.js.map +1 -0
- package/dist/cjs/ContextMenu/index.cjs +5 -22
- package/dist/cjs/ContextMenu/index.native.js +7 -25
- package/dist/cjs/ContextMenu/index.native.js.map +1 -1
- package/dist/cjs/DropdownMenu/index.cjs +5 -17
- package/dist/cjs/DropdownMenu/index.native.js +7 -19
- package/dist/cjs/DropdownMenu/index.native.js.map +1 -1
- package/dist/cjs/Popover.cjs +2 -2
- package/dist/cjs/Popover.native.js +2 -2
- package/dist/cjs/Popover.native.js.map +1 -1
- package/dist/cjs/SchemeToggle/index.cjs +2 -2
- package/dist/cjs/SchemeToggle/index.native.js +2 -2
- package/dist/cjs/SchemeToggle/index.native.js.map +1 -1
- package/dist/cjs/SheetModal/index.cjs +4 -1
- package/dist/cjs/SheetModal/index.native.js +1 -1
- package/dist/cjs/SheetModal/index.native.js.map +1 -1
- package/dist/cjs/TreeSelect/index.cjs +2 -6
- package/dist/cjs/TreeSelect/index.native.js +2 -6
- package/dist/cjs/TreeSelect/index.native.js.map +1 -1
- package/dist/cjs/appShell/AppShell.cjs +23 -17
- package/dist/cjs/appShell/AppShell.native.js +28 -22
- package/dist/cjs/appShell/AppShell.native.js.map +1 -1
- package/dist/cjs/appShell/ScopeMenu.cjs +39 -16
- package/dist/cjs/appShell/ScopeMenu.native.js +43 -17
- package/dist/cjs/appShell/ScopeMenu.native.js.map +1 -1
- package/dist/cjs/appShell/TabHost.native.js +41 -10
- package/dist/cjs/appShell/TabHost.native.js.map +1 -1
- package/dist/cjs/layouts/sidebar.cjs +29 -8
- package/dist/cjs/layouts/sidebar.native.js +30 -8
- package/dist/cjs/layouts/sidebar.native.js.map +1 -1
- package/dist/esm/CheckGlyph/MenuCheckColumn.mjs +31 -0
- package/dist/esm/CheckGlyph/MenuCheckColumn.mjs.map +1 -0
- package/dist/esm/CheckGlyph/MenuCheckColumn.native.js +32 -0
- package/dist/esm/CheckGlyph/MenuCheckColumn.native.js.map +1 -0
- package/dist/esm/CheckGlyph/index.mjs +15 -0
- package/dist/esm/CheckGlyph/index.mjs.map +1 -0
- package/dist/esm/CheckGlyph/index.native.js +16 -0
- package/dist/esm/CheckGlyph/index.native.js.map +1 -0
- package/dist/esm/ContextMenu/index.mjs +6 -23
- package/dist/esm/ContextMenu/index.mjs.map +1 -1
- package/dist/esm/ContextMenu/index.native.js +8 -26
- package/dist/esm/ContextMenu/index.native.js.map +1 -1
- package/dist/esm/DropdownMenu/index.mjs +6 -18
- package/dist/esm/DropdownMenu/index.mjs.map +1 -1
- package/dist/esm/DropdownMenu/index.native.js +8 -20
- package/dist/esm/DropdownMenu/index.native.js.map +1 -1
- package/dist/esm/Popover.mjs +2 -2
- package/dist/esm/Popover.mjs.map +1 -1
- package/dist/esm/Popover.native.js +2 -2
- package/dist/esm/Popover.native.js.map +1 -1
- package/dist/esm/SchemeToggle/index.mjs +3 -3
- package/dist/esm/SchemeToggle/index.mjs.map +1 -1
- package/dist/esm/SchemeToggle/index.native.js +3 -3
- package/dist/esm/SchemeToggle/index.native.js.map +1 -1
- package/dist/esm/SheetModal/index.mjs +5 -2
- package/dist/esm/SheetModal/index.mjs.map +1 -1
- package/dist/esm/SheetModal/index.native.js +2 -2
- package/dist/esm/SheetModal/index.native.js.map +1 -1
- package/dist/esm/TreeSelect/index.mjs +4 -8
- package/dist/esm/TreeSelect/index.mjs.map +1 -1
- package/dist/esm/TreeSelect/index.native.js +4 -8
- package/dist/esm/TreeSelect/index.native.js.map +1 -1
- package/dist/esm/appShell/AppShell.mjs +23 -17
- package/dist/esm/appShell/AppShell.mjs.map +1 -1
- package/dist/esm/appShell/AppShell.native.js +28 -22
- package/dist/esm/appShell/AppShell.native.js.map +1 -1
- package/dist/esm/appShell/ScopeMenu.mjs +43 -20
- package/dist/esm/appShell/ScopeMenu.mjs.map +1 -1
- package/dist/esm/appShell/ScopeMenu.native.js +47 -21
- package/dist/esm/appShell/ScopeMenu.native.js.map +1 -1
- package/dist/esm/appShell/TabBar.mjs.map +1 -1
- package/dist/esm/appShell/TabHost.native.js +41 -12
- package/dist/esm/appShell/TabHost.native.js.map +1 -1
- package/dist/esm/layouts/sidebar.mjs +31 -10
- package/dist/esm/layouts/sidebar.mjs.map +1 -1
- package/dist/esm/layouts/sidebar.native.js +32 -10
- package/dist/esm/layouts/sidebar.native.js.map +1 -1
- package/dist/jsx/CheckGlyph/MenuCheckColumn.mjs +31 -0
- package/dist/jsx/CheckGlyph/MenuCheckColumn.mjs.map +1 -0
- package/dist/jsx/CheckGlyph/MenuCheckColumn.native.js +60 -0
- package/dist/jsx/CheckGlyph/MenuCheckColumn.native.js.map +1 -0
- package/dist/jsx/CheckGlyph/index.mjs +15 -0
- package/dist/jsx/CheckGlyph/index.mjs.map +1 -0
- package/dist/jsx/CheckGlyph/index.native.js +44 -0
- package/dist/jsx/CheckGlyph/index.native.js.map +1 -0
- package/dist/jsx/ContextMenu/index.mjs +6 -23
- package/dist/jsx/ContextMenu/index.mjs.map +1 -1
- package/dist/jsx/ContextMenu/index.native.js +7 -25
- package/dist/jsx/ContextMenu/index.native.js.map +1 -1
- package/dist/jsx/DropdownMenu/index.mjs +6 -18
- package/dist/jsx/DropdownMenu/index.mjs.map +1 -1
- package/dist/jsx/DropdownMenu/index.native.js +7 -19
- package/dist/jsx/DropdownMenu/index.native.js.map +1 -1
- package/dist/jsx/Popover.mjs +2 -2
- package/dist/jsx/Popover.mjs.map +1 -1
- package/dist/jsx/Popover.native.js +2 -2
- package/dist/jsx/Popover.native.js.map +1 -1
- package/dist/jsx/SchemeToggle/index.mjs +3 -3
- package/dist/jsx/SchemeToggle/index.mjs.map +1 -1
- package/dist/jsx/SchemeToggle/index.native.js +2 -2
- package/dist/jsx/SchemeToggle/index.native.js.map +1 -1
- package/dist/jsx/SheetModal/index.mjs +5 -2
- package/dist/jsx/SheetModal/index.mjs.map +1 -1
- package/dist/jsx/SheetModal/index.native.js +1 -1
- package/dist/jsx/SheetModal/index.native.js.map +1 -1
- package/dist/jsx/TreeSelect/index.mjs +4 -8
- package/dist/jsx/TreeSelect/index.mjs.map +1 -1
- package/dist/jsx/TreeSelect/index.native.js +2 -6
- package/dist/jsx/TreeSelect/index.native.js.map +1 -1
- package/dist/jsx/appShell/AppShell.mjs +23 -17
- package/dist/jsx/appShell/AppShell.mjs.map +1 -1
- package/dist/jsx/appShell/AppShell.native.js +28 -22
- package/dist/jsx/appShell/AppShell.native.js.map +1 -1
- package/dist/jsx/appShell/ScopeMenu.mjs +43 -20
- package/dist/jsx/appShell/ScopeMenu.mjs.map +1 -1
- package/dist/jsx/appShell/ScopeMenu.native.js +43 -17
- package/dist/jsx/appShell/ScopeMenu.native.js.map +1 -1
- package/dist/jsx/appShell/TabBar.mjs.map +1 -1
- package/dist/jsx/appShell/TabBar.native.js.map +1 -1
- package/dist/jsx/appShell/TabHost.native.js +41 -10
- package/dist/jsx/appShell/TabHost.native.js.map +1 -1
- package/dist/jsx/layouts/sidebar.mjs +31 -10
- package/dist/jsx/layouts/sidebar.mjs.map +1 -1
- package/dist/jsx/layouts/sidebar.native.js +30 -8
- package/dist/jsx/layouts/sidebar.native.js.map +1 -1
- package/package.json +9 -9
- package/src/CheckGlyph/CheckGlyph.spec.tsx +46 -0
- package/src/CheckGlyph/MenuCheckColumn.tsx +32 -0
- package/src/CheckGlyph/index.tsx +12 -0
- package/src/ContextMenu/ContextMenu.spec.tsx +46 -1
- package/src/ContextMenu/index.tsx +5 -22
- package/src/DropdownMenu/DropdownMenu.avatarTrigger.spec.tsx +6 -9
- package/src/DropdownMenu/index.tsx +5 -18
- package/src/Popover.coverTrigger.spec.tsx +3 -3
- package/src/Popover.tsx +5 -5
- package/src/SchemeToggle/SchemeToggle.spec.tsx +5 -11
- package/src/SchemeToggle/SchemeToggle.stories.tsx +21 -0
- package/src/SchemeToggle/index.tsx +2 -1
- package/src/SheetModal/index.native.tsx +2 -1
- package/src/SheetModal/index.tsx +5 -2
- package/src/TreeSelect/index.tsx +2 -4
- package/src/appShell/AppShell.spec.tsx +30 -0
- package/src/appShell/AppShell.tsx +22 -13
- package/src/appShell/ScopeMenu.spec.tsx +22 -0
- package/src/appShell/ScopeMenu.tsx +57 -18
- package/src/appShell/TabBar.tsx +13 -3
- package/src/appShell/TabHost.native.spec.tsx +53 -6
- package/src/appShell/TabHost.native.tsx +50 -14
- package/src/appShell/TabHost.stories.tsx +72 -0
- package/src/layouts/PageHeader.stories.tsx +3 -3
- package/src/layouts/Sidebar.spec.tsx +61 -1
- package/src/layouts/pageActionButton.spec.tsx +7 -3
- package/src/layouts/sidebar.tsx +22 -8
- package/types/CheckGlyph/MenuCheckColumn.d.ts +11 -0
- package/types/CheckGlyph/MenuCheckColumn.d.ts.map +1 -0
- package/types/CheckGlyph/index.d.ts +10 -0
- package/types/CheckGlyph/index.d.ts.map +1 -0
- package/types/ContextMenu/index.d.ts.map +1 -1
- package/types/DropdownMenu/index.d.ts.map +1 -1
- package/types/Popover.d.ts +1 -1
- package/types/Popover.d.ts.map +1 -1
- package/types/SchemeToggle/index.d.ts.map +1 -1
- package/types/SheetModal/index.d.ts.map +1 -1
- package/types/SheetModal/index.native.d.ts.map +1 -1
- package/types/TreeSelect/index.d.ts.map +1 -1
- package/types/appShell/AppShell.d.ts.map +1 -1
- package/types/appShell/ScopeMenu.d.ts +0 -18
- package/types/appShell/ScopeMenu.d.ts.map +1 -1
- package/types/appShell/TabBar.d.ts +12 -6
- package/types/appShell/TabBar.d.ts.map +1 -1
- package/types/appShell/TabHost.native.d.ts +23 -5
- package/types/appShell/TabHost.native.d.ts.map +1 -1
- package/types/layouts/sidebar.d.ts +7 -1
- package/types/layouts/sidebar.d.ts.map +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { CircleHalfIcon, MoonIcon, SunIcon
|
|
1
|
+
import { CircleHalfIcon, MoonIcon, SunIcon } from "@phosphor-icons/react";
|
|
2
2
|
import { fireEvent, render, waitFor } from "@testing-library/react";
|
|
3
3
|
import { describe, expect, it, vi } from "vitest";
|
|
4
4
|
import { renderWithProviders } from "@multiplatform.one/test-utils";
|
|
@@ -43,7 +43,7 @@ describe("SchemeToggle (FZ-8158)", () => {
|
|
|
43
43
|
expect(set).toHaveBeenCalledWith("system");
|
|
44
44
|
});
|
|
45
45
|
|
|
46
|
-
it("opens its menu on the button
|
|
46
|
+
it("opens its menu on the button with no X, a press outside closing it (FZ-8665)", async () => {
|
|
47
47
|
const { useUserScheme } = preference("dark", "dark");
|
|
48
48
|
const { getByRole } = renderWithProviders(<SchemeToggle useUserScheme={useUserScheme} />);
|
|
49
49
|
const live = getByRole("button", { name: "Appearance: Dark" });
|
|
@@ -53,15 +53,9 @@ describe("SchemeToggle (FZ-8158)", () => {
|
|
|
53
53
|
expect(found?.querySelector('[role="menu"]')).toBeTruthy();
|
|
54
54
|
return found as HTMLElement;
|
|
55
55
|
});
|
|
56
|
-
|
|
57
|
-
expect(
|
|
58
|
-
|
|
59
|
-
node?.querySelector("svg path")?.getAttribute("d");
|
|
60
|
-
const x = render(<XIcon weight="bold" />);
|
|
61
|
-
expect(pathOf(echo)).toBe(pathOf(x.container));
|
|
62
|
-
expect(pathOf(echo)).not.toBe(pathOf(live));
|
|
63
|
-
x.unmount();
|
|
64
|
-
fireEvent.click(echo as HTMLElement);
|
|
56
|
+
expect(panel.querySelector('[aria-label="Close"]')).toBeNull();
|
|
57
|
+
expect(panel.querySelector(`[aria-label="${live.getAttribute("aria-label")}"]`)).toBeNull();
|
|
58
|
+
fireEvent.click(document.body);
|
|
65
59
|
await waitFor(() => expect(document.querySelector('[role="menu"]')).toBeFalsy());
|
|
66
60
|
});
|
|
67
61
|
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react-native-web-vite";
|
|
2
|
+
import { XStack } from "tamagui";
|
|
3
|
+
import { SchemeToggle } from "./index";
|
|
4
|
+
|
|
5
|
+
const meta: Meta<typeof SchemeToggle> = {
|
|
6
|
+
title: "Components/SchemeToggle",
|
|
7
|
+
component: SchemeToggle,
|
|
8
|
+
parameters: { status: { type: "beta" } },
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export default meta;
|
|
12
|
+
type Story = StoryObj<typeof SchemeToggle>;
|
|
13
|
+
|
|
14
|
+
/** A tap flips light and dark; a right-click (long-press on native) opens System, Light and Dark. */
|
|
15
|
+
export const Default: Story = {
|
|
16
|
+
render: () => (
|
|
17
|
+
<XStack padding="$4" justifyContent="flex-end">
|
|
18
|
+
<SchemeToggle />
|
|
19
|
+
</XStack>
|
|
20
|
+
),
|
|
21
|
+
};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { CircleHalfIcon, MoonIcon, SunIcon } from "@phosphor-icons/react";
|
|
2
2
|
import { IconButton, type IconButtonProps } from "@multiplatform.one/forms";
|
|
3
3
|
import {
|
|
4
|
+
menuRowIconGlyph,
|
|
4
5
|
useSchemePreference,
|
|
5
6
|
type SchemePreference,
|
|
6
7
|
type ThemeScheme,
|
|
@@ -60,7 +61,7 @@ export function SchemeToggle({
|
|
|
60
61
|
const Glyph = schemeGlyphs[value];
|
|
61
62
|
return {
|
|
62
63
|
label: name(value),
|
|
63
|
-
icon: <Glyph size={
|
|
64
|
+
icon: <Glyph size={menuRowIconGlyph.size} weight={menuRowIconGlyph.weight} />,
|
|
64
65
|
checked: scheme.setting === value,
|
|
65
66
|
onSelect: () => scheme.set(value),
|
|
66
67
|
};
|
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
useResolvedKnobs,
|
|
29
29
|
type MotionStop,
|
|
30
30
|
nativeAnimationConfig as animationConfig,
|
|
31
|
+
overlayScrim,
|
|
31
32
|
} from "@multiplatform.one/theme";
|
|
32
33
|
import {
|
|
33
34
|
useCallback,
|
|
@@ -424,7 +425,7 @@ export function SheetModal({
|
|
|
424
425
|
accessibilityRole="button"
|
|
425
426
|
accessibilityLabel={t("Close")}
|
|
426
427
|
>
|
|
427
|
-
<View flex={1} backgroundColor=
|
|
428
|
+
<View flex={1} backgroundColor={overlayScrim} />
|
|
428
429
|
</Pressable>
|
|
429
430
|
<Animated.View
|
|
430
431
|
onLayout={(e) => {
|
package/src/SheetModal/index.tsx
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* on device). Dismissal: overlay tap / snap-to-bottom / Escape (LC-21).
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
import { transitionProps, useResolvedKnobs } from "@multiplatform.one/theme";
|
|
12
|
+
import { overlayScrim, transitionProps, useResolvedKnobs } from "@multiplatform.one/theme";
|
|
13
13
|
import { OverlayStackProvider, useOverlayZIndex } from "@multiplatform.one/forms";
|
|
14
14
|
import { useEffect, useId, useRef, type ReactNode } from "react";
|
|
15
15
|
import { isWeb, Sheet, View, useWindowDimensions } from "tamagui";
|
|
@@ -128,7 +128,7 @@ export function SheetModal({
|
|
|
128
128
|
portalProps={{ zIndex: sheetZIndex }}
|
|
129
129
|
>
|
|
130
130
|
<Sheet.Overlay
|
|
131
|
-
backgroundColor=
|
|
131
|
+
backgroundColor={overlayScrim}
|
|
132
132
|
{...transitionProps(knobProps.motion.scrim)}
|
|
133
133
|
enterStyle={{ opacity: 0 }}
|
|
134
134
|
exitStyle={{ opacity: 0 }}
|
|
@@ -160,6 +160,9 @@ export function SheetModal({
|
|
|
160
160
|
: null)}
|
|
161
161
|
borderBottomLeftRadius={0}
|
|
162
162
|
borderBottomRightRadius={0}
|
|
163
|
+
borderLeftWidth={0}
|
|
164
|
+
borderRightWidth={0}
|
|
165
|
+
borderBottomWidth={0}
|
|
163
166
|
overflow="hidden"
|
|
164
167
|
{...frameRingReset}
|
|
165
168
|
>
|
package/src/TreeSelect/index.tsx
CHANGED
|
@@ -11,11 +11,11 @@ import {
|
|
|
11
11
|
panelViewportPadding,
|
|
12
12
|
useOverlayZIndex,
|
|
13
13
|
} from "@multiplatform.one/forms";
|
|
14
|
+
import { CheckGlyph } from "../CheckGlyph";
|
|
14
15
|
import {
|
|
15
16
|
CaretDownIcon,
|
|
16
17
|
CaretLeftIcon,
|
|
17
18
|
CaretRightIcon,
|
|
18
|
-
CheckIcon,
|
|
19
19
|
FileIcon,
|
|
20
20
|
FolderIcon,
|
|
21
21
|
FolderOpenIcon,
|
|
@@ -27,7 +27,6 @@ import {
|
|
|
27
27
|
ensureKeyboardModalityTracking,
|
|
28
28
|
keyboardFocusRingProps,
|
|
29
29
|
listHeightForRows,
|
|
30
|
-
useMenuRowCheckInk,
|
|
31
30
|
menuRowChecked,
|
|
32
31
|
useResolvedKnobs,
|
|
33
32
|
} from "@multiplatform.one/theme";
|
|
@@ -110,7 +109,6 @@ function TreeSelectRows({
|
|
|
110
109
|
}) {
|
|
111
110
|
const { t } = useTranslation();
|
|
112
111
|
const { knobProps } = useResolvedKnobs();
|
|
113
|
-
const checkInk = useMenuRowCheckInk();
|
|
114
112
|
const checkedFill = menuRowChecked(knobProps.scheme);
|
|
115
113
|
const nestedPx = knobProps.nestedControl.px;
|
|
116
114
|
const indentStep = 16;
|
|
@@ -271,7 +269,7 @@ function TreeSelectRows({
|
|
|
271
269
|
) : null}
|
|
272
270
|
{isSelected ? (
|
|
273
271
|
<View flexShrink={0} width={14} height={14}>
|
|
274
|
-
<
|
|
272
|
+
<CheckGlyph />
|
|
275
273
|
</View>
|
|
276
274
|
) : null}
|
|
277
275
|
</XStack>
|
|
@@ -184,6 +184,36 @@ describe("AppShell on a phone", () => {
|
|
|
184
184
|
expect(screen.queryByRole("tablist")).toBeNull();
|
|
185
185
|
expect(screen.getByTestId("page")).toBeTruthy();
|
|
186
186
|
});
|
|
187
|
+
|
|
188
|
+
it("keeps the tab bar around an own-chrome route that is a tab, and on a route below it", () => {
|
|
189
|
+
const withDesk = [...destinations, { key: "/desk", label: "Desk", icon: HouseIcon }];
|
|
190
|
+
for (const path of ["/desk", "/desk/Pokemon"]) {
|
|
191
|
+
const { onNavigate } = shell(path, { destinations: withDesk });
|
|
192
|
+
expect(screen.getByTestId("app-shell").getAttribute("data-chrome")).toBe("own");
|
|
193
|
+
expect(screen.queryByTestId("app-shell-bar")).toBeNull();
|
|
194
|
+
const tabs = screen.getAllByRole("tab");
|
|
195
|
+
expect(tabs.map((tab) => tab.getAttribute("aria-label"))).toEqual([
|
|
196
|
+
"Home",
|
|
197
|
+
"Pokemon",
|
|
198
|
+
"Desk",
|
|
199
|
+
]);
|
|
200
|
+
expect(tabs[2]!.getAttribute("aria-selected")).toBe("true");
|
|
201
|
+
fireEvent.click(tabs[0]!);
|
|
202
|
+
expect(onNavigate).toHaveBeenCalledWith("/");
|
|
203
|
+
expect(screen.getByTestId("page")).toBeTruthy();
|
|
204
|
+
cleanup();
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it("draws no tab bar around an own-chrome route at regular width", () => {
|
|
209
|
+
size.value = "expanded";
|
|
210
|
+
shell("/desk", {
|
|
211
|
+
destinations: [...destinations, { key: "/desk", label: "Desk", icon: HouseIcon }],
|
|
212
|
+
});
|
|
213
|
+
expect(screen.getByTestId("app-shell").getAttribute("data-chrome")).toBe("own");
|
|
214
|
+
expect(screen.queryByRole("tablist")).toBeNull();
|
|
215
|
+
expect(screen.queryByTestId("app-shell-sidebar")).toBeNull();
|
|
216
|
+
});
|
|
187
217
|
});
|
|
188
218
|
|
|
189
219
|
describe("AppShell at regular width", () => {
|
|
@@ -17,7 +17,8 @@
|
|
|
17
17
|
* (LC-101 ONE-SIDEBAR).
|
|
18
18
|
*
|
|
19
19
|
* A route listed in `ownChrome` mounts its own full chrome (a desk), so the
|
|
20
|
-
* shell draws
|
|
20
|
+
* shell draws no bar or sidebar around it. On a phone, one that is also a tab
|
|
21
|
+
* destination keeps the tab bar, the only way to the other tabs (FZ-8105). A page whose `Screen` pins its own top bar
|
|
21
22
|
* (LC-157) owns the nav bar on a phone, so the shell's bar steps aside there.
|
|
22
23
|
*/
|
|
23
24
|
import { zIndex } from "@multiplatform.one/forms";
|
|
@@ -305,6 +306,18 @@ export function AppShell({
|
|
|
305
306
|
[destinations],
|
|
306
307
|
);
|
|
307
308
|
|
|
309
|
+
const showTabs = compact && Boolean(destination && tabs.includes(destination));
|
|
310
|
+
const tabItems: TabBarItem[] = tabs.map(({ key, label, icon, nativeIcon }) => ({
|
|
311
|
+
key,
|
|
312
|
+
label,
|
|
313
|
+
icon,
|
|
314
|
+
nativeIcon,
|
|
315
|
+
}));
|
|
316
|
+
const onTabPress = (key: string) => {
|
|
317
|
+
const item = tabs.find((tab) => tab.key === key);
|
|
318
|
+
if (item) go(item);
|
|
319
|
+
};
|
|
320
|
+
|
|
308
321
|
if (ownsChrome) {
|
|
309
322
|
return (
|
|
310
323
|
<YStack
|
|
@@ -315,7 +328,13 @@ export function AppShell({
|
|
|
315
328
|
data-testid={testID}
|
|
316
329
|
data-chrome="own"
|
|
317
330
|
>
|
|
318
|
-
{
|
|
331
|
+
{showTabs ? (
|
|
332
|
+
<TabHost items={tabItems} activeKey={destination?.key} onTabPress={onTabPress}>
|
|
333
|
+
{children}
|
|
334
|
+
</TabHost>
|
|
335
|
+
) : (
|
|
336
|
+
children
|
|
337
|
+
)}
|
|
319
338
|
</YStack>
|
|
320
339
|
);
|
|
321
340
|
}
|
|
@@ -401,13 +420,6 @@ export function AppShell({
|
|
|
401
420
|
</YStack>
|
|
402
421
|
);
|
|
403
422
|
|
|
404
|
-
const showTabs = compact && Boolean(destination && tabs.includes(destination));
|
|
405
|
-
const tabItems: TabBarItem[] = tabs.map(({ key, label, icon, nativeIcon }) => ({
|
|
406
|
-
key,
|
|
407
|
-
label,
|
|
408
|
-
icon,
|
|
409
|
-
nativeIcon,
|
|
410
|
-
}));
|
|
411
423
|
// The floating TabBar takes the accessory into its own capsule. Otherwise
|
|
412
424
|
// it renders in place, and on native it clears the system tab bar, which
|
|
413
425
|
// the content runs under.
|
|
@@ -430,10 +442,7 @@ export function AppShell({
|
|
|
430
442
|
<TabHost
|
|
431
443
|
items={tabItems}
|
|
432
444
|
activeKey={showTabs ? destination?.key : undefined}
|
|
433
|
-
onTabPress={
|
|
434
|
-
const item = tabs.find((tab) => tab.key === key);
|
|
435
|
-
if (item) go(item);
|
|
436
|
-
}}
|
|
445
|
+
onTabPress={onTabPress}
|
|
437
446
|
>
|
|
438
447
|
{content}
|
|
439
448
|
{pageAccessory}
|
|
@@ -135,3 +135,25 @@ describe("ScopeMenu (LC-105 COVER-TRIGGER)", () => {
|
|
|
135
135
|
expect(screen.getByLabelText("Environment: Production")).toBeTruthy();
|
|
136
136
|
});
|
|
137
137
|
});
|
|
138
|
+
|
|
139
|
+
describe("ScopeMenu as a phone sheet (FZ-8557)", () => {
|
|
140
|
+
it("titles the sheet with the picker's label instead of echoing the trigger", async () => {
|
|
141
|
+
const width = window.innerWidth;
|
|
142
|
+
Object.defineProperty(window, "innerWidth", { configurable: true, writable: true, value: 390 });
|
|
143
|
+
try {
|
|
144
|
+
renderWithProviders(<Tenant />);
|
|
145
|
+
fireEvent.click(screen.getByTestId("tenant-trigger"));
|
|
146
|
+
const header = await screen.findByTestId("tenant-sheet-header");
|
|
147
|
+
expect(screen.getByRole("heading", { name: "Tenant" })).toBeTruthy();
|
|
148
|
+
expect(header.closest('[data-testid="floating-panel-sheet"]')).not.toBeNull();
|
|
149
|
+
expect(screen.queryByTestId("tenant-first-row")).toBeNull();
|
|
150
|
+
expect(screen.getAllByRole("menuitemradio")).toHaveLength(2);
|
|
151
|
+
} finally {
|
|
152
|
+
Object.defineProperty(window, "innerWidth", {
|
|
153
|
+
configurable: true,
|
|
154
|
+
writable: true,
|
|
155
|
+
value: width,
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
});
|
|
@@ -14,13 +14,16 @@
|
|
|
14
14
|
* side, never one menu with two sections.
|
|
15
15
|
*
|
|
16
16
|
* Narrow viewports and native get FloatingPanel's sheet (phones take
|
|
17
|
-
* sheets)
|
|
17
|
+
* sheets). A sheet isn't over its trigger, so it drops the first row for a
|
|
18
|
+
* sheet title, the picker's label (FZ-8557).
|
|
18
19
|
*/
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
20
|
+
import { CheckGlyph } from "../CheckGlyph";
|
|
21
|
+
import { FloatingPanel, type PanelWidth, useInPanelSheet } from "@multiplatform.one/forms";
|
|
22
|
+
import { menuRowCheckGlyph, menuRowChecked, useResolvedKnobs } from "@multiplatform.one/theme";
|
|
23
|
+
import { CaretDownIcon, CaretUpIcon } from "@phosphor-icons/react";
|
|
22
24
|
import { useState } from "react";
|
|
23
|
-
import { XStack, YStack } from "tamagui";
|
|
25
|
+
import { View, XStack, YStack } from "tamagui";
|
|
26
|
+
import { OverlayHeader } from "../actions/StandardAction";
|
|
24
27
|
import { SizableText } from "../Text";
|
|
25
28
|
|
|
26
29
|
export interface ScopeMenuOption {
|
|
@@ -60,8 +63,8 @@ function ScopeRow({
|
|
|
60
63
|
ariaLabel?: string;
|
|
61
64
|
}) {
|
|
62
65
|
const { knobProps, control } = useResolvedKnobs();
|
|
66
|
+
const inSheet = useInPanelSheet();
|
|
63
67
|
const iconSize = Number(knobProps.controlIcon?.width) || undefined;
|
|
64
|
-
const checkInk = useMenuRowCheckInk();
|
|
65
68
|
const Chevron = chevron === "up" ? CaretUpIcon : CaretDownIcon;
|
|
66
69
|
return (
|
|
67
70
|
<XStack
|
|
@@ -71,8 +74,11 @@ function ScopeRow({
|
|
|
71
74
|
{...(role === "menuitemradio" ? { "aria-checked": !!checked } : {})}
|
|
72
75
|
alignItems="center"
|
|
73
76
|
gap="$2"
|
|
74
|
-
|
|
75
|
-
|
|
77
|
+
// A menu row is the menu row recipe's height, a touch target on a phone
|
|
78
|
+
// (44pt, HIG); the bar trigger and its echo keep the bar's size.
|
|
79
|
+
minHeight={role === "menuitemradio" ? knobProps.menuRow.minHeight : "$3"}
|
|
80
|
+
// A phone sheet's rows keep HIG's 16pt gutter; a bar menu's hug the trigger.
|
|
81
|
+
paddingHorizontal={inSheet ? "$4" : "$2.5"}
|
|
76
82
|
cursor="pointer"
|
|
77
83
|
onPress={onPress}
|
|
78
84
|
// The echo row is the trigger itself, laid on the panel (LC-112): the
|
|
@@ -91,8 +97,8 @@ function ScopeRow({
|
|
|
91
97
|
{...(chevron === "down" ? { borderRadius: knobProps.borderRadius.borderRadius } : undefined)}
|
|
92
98
|
>
|
|
93
99
|
{role === "menuitemradio" ? (
|
|
94
|
-
<YStack width={
|
|
95
|
-
{checked ? <
|
|
100
|
+
<YStack width={menuRowCheckGlyph.size} alignItems="center" aria-hidden>
|
|
101
|
+
{checked ? <CheckGlyph /> : null}
|
|
96
102
|
</YStack>
|
|
97
103
|
) : null}
|
|
98
104
|
<SizableText size="$2" fontWeight={chevron ? "500" : "400"} numberOfLines={1}>
|
|
@@ -103,6 +109,42 @@ function ScopeRow({
|
|
|
103
109
|
);
|
|
104
110
|
}
|
|
105
111
|
|
|
112
|
+
/**
|
|
113
|
+
* Over its trigger, the menu's first row is the pressed item with its chevron
|
|
114
|
+
* flipped (LC-112). A phone sheet isn't over its trigger, so it takes a sheet
|
|
115
|
+
* title, the picker's label, and a close (HIG, FZ-8557 Round 40).
|
|
116
|
+
*/
|
|
117
|
+
function ScopeMenuHead({
|
|
118
|
+
label,
|
|
119
|
+
currentLabel,
|
|
120
|
+
onClose,
|
|
121
|
+
testID,
|
|
122
|
+
}: {
|
|
123
|
+
label: string;
|
|
124
|
+
currentLabel: string;
|
|
125
|
+
onClose: () => void;
|
|
126
|
+
testID: string;
|
|
127
|
+
}) {
|
|
128
|
+
const inSheet = useInPanelSheet();
|
|
129
|
+
if (inSheet) {
|
|
130
|
+
return (
|
|
131
|
+
<View paddingHorizontal="$4" paddingBottom="$2">
|
|
132
|
+
<OverlayHeader title={label} onClose={onClose} testID={`${testID}-sheet-header`} />
|
|
133
|
+
</View>
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
return (
|
|
137
|
+
<ScopeRow
|
|
138
|
+
role="button"
|
|
139
|
+
label={currentLabel}
|
|
140
|
+
chevron="up"
|
|
141
|
+
ariaLabel={`${label}: ${currentLabel}`}
|
|
142
|
+
onPress={onClose}
|
|
143
|
+
testID={`${testID}-first-row`}
|
|
144
|
+
/>
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
|
|
106
148
|
export function ScopeMenu({
|
|
107
149
|
label,
|
|
108
150
|
options,
|
|
@@ -144,14 +186,11 @@ export function ScopeMenu({
|
|
|
144
186
|
}
|
|
145
187
|
>
|
|
146
188
|
<YStack role="menu" aria-label={label} data-testid={`${testID}-panel`}>
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
ariaLabel={`${label}: ${currentLabel}`}
|
|
153
|
-
onPress={() => setOpen(false)}
|
|
154
|
-
testID={`${testID}-first-row`}
|
|
189
|
+
<ScopeMenuHead
|
|
190
|
+
label={label}
|
|
191
|
+
currentLabel={currentLabel}
|
|
192
|
+
onClose={() => setOpen(false)}
|
|
193
|
+
testID={testID}
|
|
155
194
|
/>
|
|
156
195
|
{options.map((option) => (
|
|
157
196
|
<ScopeRow
|
package/src/appShell/TabBar.tsx
CHANGED
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
import { zIndex } from "@multiplatform.one/forms";
|
|
22
22
|
import { isWeb } from "@multiplatform.one/platform";
|
|
23
23
|
import { type ComponentType, type ReactNode, useEffect, useState } from "react";
|
|
24
|
+
import type { ImageSourcePropType } from "react-native";
|
|
24
25
|
import { XStack, YStack } from "tamagui";
|
|
25
26
|
import { materials, useMaterial } from "../Material";
|
|
26
27
|
import { MaterialBlurFill } from "../Material/blur";
|
|
@@ -41,10 +42,19 @@ export interface TabBarItem {
|
|
|
41
42
|
label: string;
|
|
42
43
|
icon: TabBarIcon;
|
|
43
44
|
/**
|
|
44
|
-
* The
|
|
45
|
-
* on
|
|
45
|
+
* The glyphs the system tab bar draws (TabHost): SF Symbols on iOS, a
|
|
46
|
+
* raster template image on Android (Material's bar draws no SF Symbol or
|
|
47
|
+
* SVG; a bundled PNG, so an OTA can carry it). A platform keeps the MPO
|
|
48
|
+
* TabBar unless every item names its glyph.
|
|
46
49
|
*/
|
|
47
|
-
nativeIcon?:
|
|
50
|
+
nativeIcon?: TabBarNativeIcon;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface TabBarNativeIcon {
|
|
54
|
+
sfSymbol?: string;
|
|
55
|
+
sfSymbolSelected?: string;
|
|
56
|
+
/** A monochrome PNG (24dp, @2x and @3x beside it); the bar tints it. */
|
|
57
|
+
android?: ImageSourcePropType;
|
|
48
58
|
}
|
|
49
59
|
|
|
50
60
|
export interface TabBarProps {
|
|
@@ -16,23 +16,36 @@ import {
|
|
|
16
16
|
NativeTabHost,
|
|
17
17
|
TabHost,
|
|
18
18
|
itemsHaveNativeIcons,
|
|
19
|
+
nativeIconFor,
|
|
19
20
|
nativeTabBarHeight,
|
|
21
|
+
nativeTabsPlatforms,
|
|
20
22
|
resolveNativeTabs,
|
|
21
23
|
} from "./TabHost.native";
|
|
22
24
|
|
|
23
25
|
afterEach(cleanup);
|
|
24
26
|
|
|
25
27
|
const Glyph = () => null;
|
|
28
|
+
const png = (name: string) => ({ uri: `${name}.png`, width: 24, height: 24 });
|
|
26
29
|
|
|
27
30
|
const items: TabBarItem[] = [
|
|
28
31
|
{
|
|
29
32
|
key: "/",
|
|
30
33
|
label: "Home",
|
|
31
34
|
icon: Glyph,
|
|
32
|
-
nativeIcon: { sfSymbol: "house", sfSymbolSelected: "house.fill" },
|
|
35
|
+
nativeIcon: { sfSymbol: "house", sfSymbolSelected: "house.fill", android: png("house") },
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
key: "/pokemon",
|
|
39
|
+
label: "Pokemon",
|
|
40
|
+
icon: Glyph,
|
|
41
|
+
nativeIcon: { sfSymbol: "sparkles", android: png("sparkle") },
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
key: "/moves",
|
|
45
|
+
label: "Moves",
|
|
46
|
+
icon: Glyph,
|
|
47
|
+
nativeIcon: { sfSymbol: "bolt", android: png("lightning") },
|
|
33
48
|
},
|
|
34
|
-
{ key: "/pokemon", label: "Pokemon", icon: Glyph, nativeIcon: { sfSymbol: "sparkles" } },
|
|
35
|
-
{ key: "/moves", label: "Moves", icon: Glyph, nativeIcon: { sfSymbol: "bolt" } },
|
|
36
49
|
];
|
|
37
50
|
|
|
38
51
|
const fakeTabs: NativeTabs = {
|
|
@@ -58,7 +71,8 @@ const fakeTabs: NativeTabs = {
|
|
|
58
71
|
data-testid={`native-screen-${tabKey}`}
|
|
59
72
|
data-focused={String(Boolean(isFocused))}
|
|
60
73
|
data-inset-adjust={String(overrideScrollViewContentInsetAdjustmentBehavior)}
|
|
61
|
-
data-symbol={icon?.ios
|
|
74
|
+
data-symbol={icon?.ios?.name}
|
|
75
|
+
data-android-icon={(icon?.android?.imageSource as { uri?: string } | undefined)?.uri}
|
|
62
76
|
aria-label={title}
|
|
63
77
|
>
|
|
64
78
|
{children}
|
|
@@ -113,19 +127,49 @@ describe("native tabs detection", () => {
|
|
|
113
127
|
expect(resolveNativeTabs(sources(["RNSTabsHost", "RNSTabsScreen"], throws), "ios")).toBeNull();
|
|
114
128
|
});
|
|
115
129
|
|
|
116
|
-
it("keeps the MPO TabBar on Android and the web", () => {
|
|
130
|
+
it("keeps the MPO TabBar on Android and the web by default", () => {
|
|
117
131
|
const screens = vi.fn(() => ({ Tabs: fakeTabs }));
|
|
118
132
|
const all = sources(["RNSTabsHost", "RNSTabsScreen"], screens);
|
|
133
|
+
expect(nativeTabsPlatforms).toEqual({ ios: true, android: false });
|
|
119
134
|
expect(resolveNativeTabs(all, "android")).toBeNull();
|
|
120
135
|
expect(resolveNativeTabs(all, "web")).toBeNull();
|
|
121
136
|
expect(screens).not.toHaveBeenCalled();
|
|
122
137
|
});
|
|
123
138
|
|
|
124
|
-
it("
|
|
139
|
+
it("picks the Material bar on Android once the platform is switched on (FZ-8140)", () => {
|
|
140
|
+
const on = { ios: true, android: true };
|
|
141
|
+
expect(
|
|
142
|
+
resolveNativeTabs(
|
|
143
|
+
sources(["RNSTabsHost", "RNSTabsScreen"], () => ({ Tabs: fakeTabs })),
|
|
144
|
+
"android",
|
|
145
|
+
on,
|
|
146
|
+
),
|
|
147
|
+
).toBe(fakeTabs);
|
|
148
|
+
const screens = vi.fn(() => ({ Tabs: fakeTabs }));
|
|
149
|
+
expect(resolveNativeTabs(sources([], screens), "android", on)).toBeNull();
|
|
150
|
+
expect(screens).not.toHaveBeenCalled();
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it("needs this platform's glyph on every item: an SF Symbol on iOS, a PNG on Android", () => {
|
|
125
154
|
expect(itemsHaveNativeIcons(items)).toBe(true);
|
|
155
|
+
expect(itemsHaveNativeIcons(items, "android")).toBe(true);
|
|
126
156
|
expect(itemsHaveNativeIcons([...items, { key: "/x", label: "X", icon: Glyph }])).toBe(false);
|
|
157
|
+
const symbolsOnly = items.map(({ nativeIcon, ...item }) => ({
|
|
158
|
+
...item,
|
|
159
|
+
nativeIcon: { sfSymbol: nativeIcon?.sfSymbol },
|
|
160
|
+
}));
|
|
161
|
+
expect(itemsHaveNativeIcons(symbolsOnly, "ios")).toBe(true);
|
|
162
|
+
expect(itemsHaveNativeIcons(symbolsOnly, "android")).toBe(false);
|
|
127
163
|
expect(itemsHaveNativeIcons([])).toBe(false);
|
|
128
164
|
});
|
|
165
|
+
|
|
166
|
+
it("hands react-native-screens each platform's glyph from the one item", () => {
|
|
167
|
+
expect(nativeIconFor(items[0]!)).toEqual({
|
|
168
|
+
ios: { type: "sfSymbol", name: "house" },
|
|
169
|
+
android: { type: "imageSource", imageSource: png("house") },
|
|
170
|
+
});
|
|
171
|
+
expect(nativeIconFor({ key: "/x", label: "X", icon: Glyph })).toEqual({});
|
|
172
|
+
});
|
|
129
173
|
});
|
|
130
174
|
|
|
131
175
|
describe("NativeTabHost", () => {
|
|
@@ -136,6 +180,9 @@ describe("NativeTabHost", () => {
|
|
|
136
180
|
expect(pokemon.contains(screen.getByTestId("page"))).toBe(true);
|
|
137
181
|
expect(screen.getByTestId("native-screen-/").getAttribute("data-focused")).toBe("false");
|
|
138
182
|
expect(screen.getByTestId("native-screen-/").getAttribute("data-symbol")).toBe("house");
|
|
183
|
+
expect(screen.getByTestId("native-screen-/").getAttribute("data-android-icon")).toBe(
|
|
184
|
+
"house.png",
|
|
185
|
+
);
|
|
139
186
|
expect(screen.getByTestId("native-host").getAttribute("data-hidden")).toBe("false");
|
|
140
187
|
expect(screen.queryByTestId("tab-bar")).toBeNull();
|
|
141
188
|
});
|