@multiplatform.one/forms 7.47.4 → 7.49.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/AdaptivePopup/index.cjs +13 -7
- package/dist/cjs/AdaptivePopup/index.native.js +13 -7
- package/dist/cjs/AdaptivePopup/index.native.js.map +1 -1
- package/dist/cjs/Button/index.cjs +22 -20
- package/dist/cjs/Button/index.native.js +22 -20
- package/dist/cjs/Button/index.native.js.map +1 -1
- package/dist/cjs/ContextualSaveBar/UnsavedChangesDialog.cjs +3 -1
- package/dist/cjs/ContextualSaveBar/UnsavedChangesDialog.native.js +3 -1
- package/dist/cjs/ContextualSaveBar/UnsavedChangesDialog.native.js.map +1 -1
- package/dist/cjs/FloatingPanel/index.cjs +21 -10
- package/dist/cjs/FloatingPanel/index.native.js +116 -110
- package/dist/cjs/FloatingPanel/index.native.js.map +1 -1
- package/dist/cjs/Spinner/index.cjs +2 -1
- package/dist/cjs/Spinner/index.native.js +2 -1
- package/dist/cjs/Spinner/index.native.js.map +1 -1
- package/dist/cjs/Spinner/spinnerSize.native.js.map +1 -1
- package/dist/cjs/fields/Combobox/index.cjs +3 -2
- package/dist/cjs/fields/Combobox/index.native.js +3 -2
- package/dist/cjs/fields/Combobox/index.native.js.map +1 -1
- package/dist/cjs/fields/Geolocation/index.cjs +3 -2
- package/dist/cjs/fields/Geolocation/index.native.js +3 -2
- package/dist/cjs/fields/Geolocation/index.native.js.map +1 -1
- package/dist/cjs/fields/MentionInput/index.cjs +3 -2
- package/dist/cjs/fields/MentionInput/index.native.js +3 -2
- package/dist/cjs/fields/MentionInput/index.native.js.map +1 -1
- package/dist/cjs/fields/Select/index.cjs +3 -2
- package/dist/cjs/fields/Select/index.native.js +3 -2
- package/dist/cjs/fields/Select/index.native.js.map +1 -1
- package/dist/cjs/index.cjs +9 -0
- package/dist/cjs/index.native.js +9 -0
- package/dist/cjs/index.native.js.map +1 -1
- package/dist/cjs/shared/OverlayWindow.cjs +38 -0
- package/dist/cjs/shared/OverlayWindow.native.js +54 -0
- package/dist/cjs/shared/OverlayWindow.native.js.map +1 -0
- package/dist/cjs/shared/overlayStack.cjs +92 -0
- package/dist/cjs/shared/overlayStack.native.js +97 -0
- package/dist/cjs/shared/overlayStack.native.js.map +1 -0
- package/dist/esm/AdaptivePopup/index.mjs +13 -7
- package/dist/esm/AdaptivePopup/index.mjs.map +1 -1
- package/dist/esm/AdaptivePopup/index.native.js +13 -7
- package/dist/esm/AdaptivePopup/index.native.js.map +1 -1
- package/dist/esm/Button/index.mjs +22 -20
- package/dist/esm/Button/index.mjs.map +1 -1
- package/dist/esm/Button/index.native.js +22 -20
- package/dist/esm/Button/index.native.js.map +1 -1
- package/dist/esm/ContextualSaveBar/UnsavedChangesDialog.mjs +3 -1
- package/dist/esm/ContextualSaveBar/UnsavedChangesDialog.mjs.map +1 -1
- package/dist/esm/ContextualSaveBar/UnsavedChangesDialog.native.js +3 -1
- package/dist/esm/ContextualSaveBar/UnsavedChangesDialog.native.js.map +1 -1
- package/dist/esm/FloatingPanel/index.mjs +21 -10
- package/dist/esm/FloatingPanel/index.mjs.map +1 -1
- package/dist/esm/FloatingPanel/index.native.js +116 -110
- package/dist/esm/FloatingPanel/index.native.js.map +1 -1
- package/dist/esm/Spinner/index.mjs +2 -1
- package/dist/esm/Spinner/index.mjs.map +1 -1
- package/dist/esm/Spinner/index.native.js +2 -1
- package/dist/esm/Spinner/index.native.js.map +1 -1
- package/dist/esm/Spinner/spinnerSize.mjs.map +1 -1
- package/dist/esm/Spinner/spinnerSize.native.js.map +1 -1
- package/dist/esm/fields/Combobox/index.mjs +3 -2
- package/dist/esm/fields/Combobox/index.mjs.map +1 -1
- package/dist/esm/fields/Combobox/index.native.js +3 -2
- package/dist/esm/fields/Combobox/index.native.js.map +1 -1
- package/dist/esm/fields/Geolocation/index.mjs +3 -2
- package/dist/esm/fields/Geolocation/index.mjs.map +1 -1
- package/dist/esm/fields/Geolocation/index.native.js +3 -2
- package/dist/esm/fields/Geolocation/index.native.js.map +1 -1
- package/dist/esm/fields/MentionInput/index.mjs +3 -2
- package/dist/esm/fields/MentionInput/index.mjs.map +1 -1
- package/dist/esm/fields/MentionInput/index.native.js +3 -2
- package/dist/esm/fields/MentionInput/index.native.js.map +1 -1
- package/dist/esm/fields/Select/index.mjs +3 -2
- package/dist/esm/fields/Select/index.mjs.map +1 -1
- package/dist/esm/fields/Select/index.native.js +3 -2
- package/dist/esm/fields/Select/index.native.js.map +1 -1
- package/dist/esm/index.mjs +3 -1
- package/dist/esm/index.mjs.map +1 -1
- package/dist/esm/index.native.js +3 -1
- package/dist/esm/index.native.js.map +1 -1
- package/dist/esm/shared/OverlayWindow.mjs +13 -0
- package/dist/esm/shared/OverlayWindow.mjs.map +1 -0
- package/dist/esm/shared/OverlayWindow.native.js +26 -0
- package/dist/esm/shared/OverlayWindow.native.js.map +1 -0
- package/dist/esm/shared/overlayStack.mjs +62 -0
- package/dist/esm/shared/overlayStack.mjs.map +1 -0
- package/dist/esm/shared/overlayStack.native.js +64 -0
- package/dist/esm/shared/overlayStack.native.js.map +1 -0
- package/dist/jsx/AdaptivePopup/index.mjs +13 -7
- package/dist/jsx/AdaptivePopup/index.mjs.map +1 -1
- package/dist/jsx/AdaptivePopup/index.native.js +13 -7
- package/dist/jsx/AdaptivePopup/index.native.js.map +1 -1
- package/dist/jsx/Button/index.mjs +22 -20
- package/dist/jsx/Button/index.mjs.map +1 -1
- package/dist/jsx/Button/index.native.js +22 -20
- package/dist/jsx/Button/index.native.js.map +1 -1
- package/dist/jsx/ContextualSaveBar/UnsavedChangesDialog.mjs +3 -1
- package/dist/jsx/ContextualSaveBar/UnsavedChangesDialog.mjs.map +1 -1
- package/dist/jsx/ContextualSaveBar/UnsavedChangesDialog.native.js +3 -1
- package/dist/jsx/ContextualSaveBar/UnsavedChangesDialog.native.js.map +1 -1
- package/dist/jsx/FloatingPanel/index.mjs +21 -10
- package/dist/jsx/FloatingPanel/index.mjs.map +1 -1
- package/dist/jsx/FloatingPanel/index.native.js +116 -110
- package/dist/jsx/FloatingPanel/index.native.js.map +1 -1
- package/dist/jsx/Spinner/index.mjs +2 -1
- package/dist/jsx/Spinner/index.mjs.map +1 -1
- package/dist/jsx/Spinner/index.native.js +2 -1
- package/dist/jsx/Spinner/index.native.js.map +1 -1
- package/dist/jsx/Spinner/spinnerSize.mjs.map +1 -1
- package/dist/jsx/Spinner/spinnerSize.native.js.map +1 -1
- package/dist/jsx/fields/Combobox/index.mjs +3 -2
- package/dist/jsx/fields/Combobox/index.mjs.map +1 -1
- package/dist/jsx/fields/Combobox/index.native.js +3 -2
- package/dist/jsx/fields/Combobox/index.native.js.map +1 -1
- package/dist/jsx/fields/Geolocation/index.mjs +3 -2
- package/dist/jsx/fields/Geolocation/index.mjs.map +1 -1
- package/dist/jsx/fields/Geolocation/index.native.js +3 -2
- package/dist/jsx/fields/Geolocation/index.native.js.map +1 -1
- package/dist/jsx/fields/MentionInput/index.mjs +3 -2
- package/dist/jsx/fields/MentionInput/index.mjs.map +1 -1
- package/dist/jsx/fields/MentionInput/index.native.js +3 -2
- package/dist/jsx/fields/MentionInput/index.native.js.map +1 -1
- package/dist/jsx/fields/Select/index.mjs +3 -2
- package/dist/jsx/fields/Select/index.mjs.map +1 -1
- package/dist/jsx/fields/Select/index.native.js +3 -2
- package/dist/jsx/fields/Select/index.native.js.map +1 -1
- package/dist/jsx/index.js +3 -1
- package/dist/jsx/index.js.map +1 -1
- package/dist/jsx/index.mjs +3 -1
- package/dist/jsx/index.mjs.map +1 -1
- package/dist/jsx/index.native.js +9 -0
- package/dist/jsx/index.native.js.map +1 -1
- package/dist/jsx/shared/OverlayWindow.mjs +13 -0
- package/dist/jsx/shared/OverlayWindow.mjs.map +1 -0
- package/dist/jsx/shared/OverlayWindow.native.js +54 -0
- package/dist/jsx/shared/OverlayWindow.native.js.map +1 -0
- package/dist/jsx/shared/overlayStack.mjs +62 -0
- package/dist/jsx/shared/overlayStack.mjs.map +1 -0
- package/dist/jsx/shared/overlayStack.native.js +97 -0
- package/dist/jsx/shared/overlayStack.native.js.map +1 -0
- package/package.json +8 -8
- package/src/AdaptivePopup/index.tsx +11 -7
- package/src/Button/Button.loading.spec.tsx +63 -16
- package/src/Button/index.tsx +20 -24
- package/src/ContextualSaveBar/UnsavedChangesDialog.tsx +5 -2
- package/src/FloatingPanel/index.native.tsx +102 -97
- package/src/FloatingPanel/index.tsx +17 -12
- package/src/Spinner/index.native.tsx +2 -2
- package/src/Spinner/index.tsx +2 -2
- package/src/Spinner/spinnerSize.ts +2 -0
- package/src/fields/Combobox/index.tsx +3 -2
- package/src/fields/Geolocation/index.tsx +3 -2
- package/src/fields/MentionInput/index.tsx +3 -2
- package/src/fields/Select/index.tsx +3 -2
- package/src/index.ts +11 -0
- package/src/shared/OverlayWindow.native.tsx +28 -0
- package/src/shared/OverlayWindow.tsx +17 -0
- package/src/shared/overlayStack.spec.tsx +59 -0
- package/src/shared/overlayStack.tsx +102 -0
- package/types/AdaptivePopup/index.d.ts.map +1 -1
- package/types/Button/index.d.ts.map +1 -1
- package/types/ContextualSaveBar/UnsavedChangesDialog.d.ts.map +1 -1
- package/types/FloatingPanel/index.native.d.ts.map +1 -1
- package/types/Spinner/index.d.ts +1 -1
- package/types/Spinner/index.d.ts.map +1 -1
- package/types/Spinner/index.native.d.ts +1 -1
- package/types/Spinner/index.native.d.ts.map +1 -1
- package/types/Spinner/spinnerSize.d.ts +2 -0
- package/types/Spinner/spinnerSize.d.ts.map +1 -1
- package/types/fields/Combobox/index.d.ts.map +1 -1
- package/types/fields/Geolocation/index.d.ts.map +1 -1
- package/types/fields/MentionInput/index.d.ts.map +1 -1
- package/types/fields/Select/index.d.ts.map +1 -1
- package/types/index.d.ts +4 -0
- package/types/index.d.ts.map +1 -1
- package/types/shared/OverlayWindow.d.ts +13 -0
- package/types/shared/OverlayWindow.d.ts.map +1 -0
- package/types/shared/OverlayWindow.native.d.ts +15 -0
- package/types/shared/OverlayWindow.native.d.ts.map +1 -0
- package/types/shared/overlayStack.d.ts +42 -0
- package/types/shared/overlayStack.d.ts.map +1 -0
|
@@ -5,9 +5,10 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
|
5
5
|
import { Button } from "./index";
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
8
|
+
* FZ-7812: a loading Button keeps its label and its rest chrome; a spinner
|
|
9
|
+
* takes the leading icon slot. WP-c2's skeleton bone stays the `skeleton`
|
|
10
|
+
* placeholder (a button that has not loaded), and stops pulsing for a user
|
|
11
|
+
* who asked for reduced motion.
|
|
11
12
|
*/
|
|
12
13
|
|
|
13
14
|
let reduced = false;
|
|
@@ -32,30 +33,76 @@ afterEach(() => {
|
|
|
32
33
|
});
|
|
33
34
|
|
|
34
35
|
function bone(container: HTMLElement): HTMLElement {
|
|
35
|
-
const el = container.querySelector("[data-
|
|
36
|
-
if (!el) throw new Error("no skeleton bone in the
|
|
36
|
+
const el = container.querySelector("[data-skeleton]");
|
|
37
|
+
if (!el) throw new Error("no skeleton bone in the skeleton button");
|
|
37
38
|
return el as HTMLElement;
|
|
38
39
|
}
|
|
39
40
|
|
|
40
|
-
describe("Button loading (
|
|
41
|
-
it("
|
|
41
|
+
describe("Button loading (FZ-7812 label stays)", () => {
|
|
42
|
+
it("keeps the label and puts a spinner in the leading slot", () => {
|
|
42
43
|
const { container, getByRole } = renderWithProviders(
|
|
43
44
|
<Button loading size="$3">
|
|
44
45
|
Save
|
|
45
46
|
</Button>,
|
|
46
47
|
);
|
|
48
|
+
const button = getByRole("button");
|
|
49
|
+
expect(button.getAttribute("aria-busy")).toBe("true");
|
|
50
|
+
expect(button.textContent).toContain("Save");
|
|
51
|
+
const slot = container.querySelector("[data-button-loading]");
|
|
52
|
+
expect(slot).toBeTruthy();
|
|
53
|
+
expect(slot?.getAttribute("aria-hidden")).toBe("true");
|
|
54
|
+
expect(slot?.nextElementSibling?.textContent).toBe("Save");
|
|
55
|
+
expect(container.querySelector("[data-skeleton]")).toBeNull();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("takes an icon's slot instead of adding a second glyph", () => {
|
|
59
|
+
const Icon = () => <svg data-testid="icon" />;
|
|
60
|
+
const { container, queryByTestId } = renderWithProviders(
|
|
61
|
+
<Button loading icon={Icon}>
|
|
62
|
+
Save
|
|
63
|
+
</Button>,
|
|
64
|
+
);
|
|
65
|
+
expect(queryByTestId("icon")).toBeNull();
|
|
66
|
+
expect(container.querySelectorAll("[data-button-loading]")).toHaveLength(1);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("keeps the rest chrome: loading is not the disabled wash", () => {
|
|
70
|
+
const rest = renderWithProviders(<Button chromeless>Log in</Button>);
|
|
71
|
+
const restStyle = getComputedStyle(rest.getByRole("button"));
|
|
72
|
+
const restBg = restStyle.backgroundColor;
|
|
73
|
+
const restInk = getComputedStyle(rest.getByText("Log in")).color;
|
|
74
|
+
rest.unmount();
|
|
75
|
+
const busy = renderWithProviders(
|
|
76
|
+
<Button chromeless loading>
|
|
77
|
+
Log in
|
|
78
|
+
</Button>,
|
|
79
|
+
);
|
|
80
|
+
expect(getComputedStyle(busy.getByRole("button")).backgroundColor).toBe(restBg);
|
|
81
|
+
expect(getComputedStyle(busy.getByText("Log in")).color).toBe(restInk);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("swallows presses while busy", () => {
|
|
85
|
+
const onPress = vi.fn();
|
|
86
|
+
const { getByRole } = renderWithProviders(
|
|
87
|
+
<Button loading onPress={onPress}>
|
|
88
|
+
Save
|
|
89
|
+
</Button>,
|
|
90
|
+
);
|
|
91
|
+
getByRole("button").click();
|
|
92
|
+
expect(onPress).not.toHaveBeenCalled();
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
describe("Button skeleton (WP-c2 one Skeleton)", () => {
|
|
97
|
+
it("renders the shared rounded Skeleton at the recipe height", () => {
|
|
98
|
+
const { container } = renderWithProviders(<Button skeleton size="$3" />);
|
|
47
99
|
const el = bone(container);
|
|
48
100
|
expect(el.getAttribute("data-skeleton-variant")).toBe("rounded");
|
|
49
101
|
expect(el.getAttribute("data-height")).toBe(String(sizeRecipeForToken("$3").height));
|
|
50
|
-
expect(el.getAttribute("aria-hidden")).toBe("true");
|
|
51
|
-
const button = getByRole("button");
|
|
52
|
-
expect(button.getAttribute("aria-busy")).toBe("true");
|
|
53
|
-
// The label is swapped out for the bone (busy, not narrated).
|
|
54
|
-
expect(button.textContent).not.toContain("Save");
|
|
55
102
|
});
|
|
56
103
|
|
|
57
104
|
it("pulses with the animation knob on", () => {
|
|
58
|
-
const { container } = renderWithProviders(<Button
|
|
105
|
+
const { container } = renderWithProviders(<Button skeleton />);
|
|
59
106
|
const el = bone(container);
|
|
60
107
|
expect(el.getAttribute("data-animate")).toBe("true");
|
|
61
108
|
expect(el.className).toMatch(/mp-skeleton-pulse/);
|
|
@@ -64,7 +111,7 @@ describe("Button loading (WP-c2 one Skeleton)", () => {
|
|
|
64
111
|
|
|
65
112
|
it("respects prefers-reduced-motion: the bone goes static", () => {
|
|
66
113
|
reduced = true;
|
|
67
|
-
const { container } = renderWithProviders(<Button
|
|
114
|
+
const { container } = renderWithProviders(<Button skeleton />);
|
|
68
115
|
const el = bone(container);
|
|
69
116
|
expect(el.getAttribute("data-animate")).toBe("false");
|
|
70
117
|
expect(el.className).toMatch(/mp-skeleton-static/);
|
|
@@ -74,8 +121,8 @@ describe("Button loading (WP-c2 one Skeleton)", () => {
|
|
|
74
121
|
it("mounts one keyframe sheet that also stops motion under reduced motion", () => {
|
|
75
122
|
renderWithProviders(
|
|
76
123
|
<>
|
|
77
|
-
<Button
|
|
78
|
-
<Button
|
|
124
|
+
<Button skeleton />
|
|
125
|
+
<Button skeleton />
|
|
79
126
|
</>,
|
|
80
127
|
);
|
|
81
128
|
const sheets = document.querySelectorAll("#__mp-skeleton-pulse");
|
package/src/Button/index.tsx
CHANGED
|
@@ -52,6 +52,7 @@ import {
|
|
|
52
52
|
useTheme,
|
|
53
53
|
} from "tamagui";
|
|
54
54
|
import { Skeleton } from "../Skeleton";
|
|
55
|
+
import { Spinner } from "../Spinner";
|
|
55
56
|
import type { ThemeName } from "@tamagui/web";
|
|
56
57
|
import type { GestureResponderEvent } from "react-native";
|
|
57
58
|
import type { AnyFormApi } from "../types";
|
|
@@ -501,7 +502,10 @@ export function Button({
|
|
|
501
502
|
);
|
|
502
503
|
}
|
|
503
504
|
|
|
504
|
-
|
|
505
|
+
// Busy is not unavailable: a loading button swallows presses but keeps its
|
|
506
|
+
// rest chrome and label. Washed, it read as stuck pressed (FZ-7812).
|
|
507
|
+
const unavailable = Boolean(disabled || (action === "submit" && resolvedForm && !isValid));
|
|
508
|
+
const isDisabled = unavailable || Boolean(loading);
|
|
505
509
|
|
|
506
510
|
const handlePress = (e: GestureResponderEvent) => {
|
|
507
511
|
// Explained disable keeps the frame press-able (aria-disabled, focusable)
|
|
@@ -542,7 +546,7 @@ export function Button({
|
|
|
542
546
|
// where `$color11` misses 4.5:1 (light warning 4.48, success 4.27) and the
|
|
543
547
|
// hue's `$color12` text step clears it.
|
|
544
548
|
const washOnHost = Boolean(
|
|
545
|
-
|
|
549
|
+
unavailable && activeThemeName && solidPaletteThemes.has(activeThemeName),
|
|
546
550
|
);
|
|
547
551
|
const disabledSurface = washOnHost
|
|
548
552
|
? hostScoped(disabledState.surfaceKnobProps)
|
|
@@ -607,7 +611,7 @@ export function Button({
|
|
|
607
611
|
];
|
|
608
612
|
const isSelected =
|
|
609
613
|
selected ?? (ariaPressedProp === true || ariaPressedProp === "true" ? true : undefined);
|
|
610
|
-
const selectedFill = Boolean(isSelected) && !
|
|
614
|
+
const selectedFill = Boolean(isSelected) && !unavailable;
|
|
611
615
|
const iconSize = nested
|
|
612
616
|
? Math.min(recipe.iconSize, Math.max(12, Math.round(knobProps.nestedControl.px * 0.45)))
|
|
613
617
|
: recipe.iconSize;
|
|
@@ -647,7 +651,7 @@ export function Button({
|
|
|
647
651
|
: undefined)}
|
|
648
652
|
// LC-40: on the washed disabled chrome the label leaves the on-fill
|
|
649
653
|
// foreground for the wash's AA ink (keepLabel keeps >=4.5:1).
|
|
650
|
-
{...(
|
|
654
|
+
{...(unavailable ? disabledText : undefined)}
|
|
651
655
|
// Boards measure ink.weight on the TEXT NODE, never the frame. The
|
|
652
656
|
// prop is restated after the spreads because SizableText's size
|
|
653
657
|
// variant otherwise leaks 500/600 over it (MPO-125: a literal 400
|
|
@@ -669,29 +673,21 @@ export function Button({
|
|
|
669
673
|
: flattenedChildren?.some(isBareTextChild)
|
|
670
674
|
? wrapBareTextChildren(flattenedChildren, renderLabelText)
|
|
671
675
|
: resolvedChildren;
|
|
672
|
-
|
|
673
|
-
// label's slot at the recipe height (the label is swapped out; hosts such as
|
|
674
|
-
// Toolbar name the busy button through `aria-label`). The frame stays, so
|
|
675
|
-
// focus never leaves the button.
|
|
676
|
-
const textContent = loading ? (
|
|
677
|
-
<View data-button-loading="">
|
|
678
|
-
<Skeleton variant="rounded" height={boneHeight} width={boneHeight * 2} aria-hidden />
|
|
679
|
-
</View>
|
|
680
|
-
) : (
|
|
681
|
-
labelContent
|
|
682
|
-
);
|
|
683
|
-
|
|
684
|
-
const hasIcon = icon || iconAfter;
|
|
676
|
+
const hasIcon = icon || iconAfter || loading;
|
|
685
677
|
const nativeIconColor =
|
|
686
678
|
props.color ??
|
|
687
|
-
(
|
|
679
|
+
(unavailable ? disabledText.color : undefined) ??
|
|
688
680
|
(textColor
|
|
689
681
|
? outlinedOnHost
|
|
690
682
|
? hostScoped({ color: textColor }).color
|
|
691
683
|
: textColor
|
|
692
684
|
: outlinedInk) ??
|
|
693
685
|
"$color";
|
|
694
|
-
const iconElement =
|
|
686
|
+
const iconElement = loading ? (
|
|
687
|
+
<View flexShrink={0} data-button-loading="" aria-hidden>
|
|
688
|
+
<Spinner px={iconSize} color={nativeIconColor as string} aria-hidden />
|
|
689
|
+
</View>
|
|
690
|
+
) : icon ? (
|
|
695
691
|
<View flexShrink={0}>
|
|
696
692
|
{isWeb ? (
|
|
697
693
|
normalizeButtonIcon(icon, iconSize)
|
|
@@ -710,7 +706,7 @@ export function Button({
|
|
|
710
706
|
</View>
|
|
711
707
|
) : null;
|
|
712
708
|
|
|
713
|
-
const explainedDisable = Boolean(disabledReason?.trim()) &&
|
|
709
|
+
const explainedDisable = Boolean(disabledReason?.trim()) && unavailable;
|
|
714
710
|
const generatedId = useId();
|
|
715
711
|
const reasonId = explainedDisable
|
|
716
712
|
? `${(props as { id?: string }).id ?? generatedId}-disabled-reason`
|
|
@@ -837,7 +833,7 @@ export function Button({
|
|
|
837
833
|
// (`color` inherits into currentColor icons); dimWhole dims the
|
|
838
834
|
// button as its own assembly. Replaces the old hardcoded 0.5
|
|
839
835
|
// whole-dim that dropped label contrast under AA.
|
|
840
|
-
{...(
|
|
836
|
+
{...(unavailable
|
|
841
837
|
? {
|
|
842
838
|
...disabledSurface,
|
|
843
839
|
...disabledText,
|
|
@@ -880,7 +876,7 @@ export function Button({
|
|
|
880
876
|
onPress={handlePress}
|
|
881
877
|
{...(isWeb ? { onKeyDown: handleKeyDown as unknown as () => void } : undefined)}
|
|
882
878
|
{...(tabIndex != null ? { tabIndex } : undefined)}
|
|
883
|
-
gap={hasIcon && (resolvedChildren ||
|
|
879
|
+
gap={hasIcon && (resolvedChildren || labelContent) ? innerGap : undefined}
|
|
884
880
|
// Form actions hug their label (Clay: no stretch): a Button inside a
|
|
885
881
|
// Form never takes the column's full width. A consumer width wins.
|
|
886
882
|
{...(formFromHook && !grouped ? { alignSelf: "flex-start" as const } : undefined)}
|
|
@@ -897,7 +893,7 @@ export function Button({
|
|
|
897
893
|
{...passthroughSizeProps}
|
|
898
894
|
{...sizeRecipeEscapeOverride}
|
|
899
895
|
{...(nested ? nestedFloor : isCircular ? circularPressFloor : pageSlop)}
|
|
900
|
-
{...(selectedFill && !
|
|
896
|
+
{...(selectedFill && !unavailable
|
|
901
897
|
? {
|
|
902
898
|
backgroundColor: restFill,
|
|
903
899
|
hoverStyle: {
|
|
@@ -955,7 +951,7 @@ export function Button({
|
|
|
955
951
|
{...(selectedFill ? { "aria-pressed": true, "data-mp-selected": "true" } : undefined)}
|
|
956
952
|
>
|
|
957
953
|
{iconElement}
|
|
958
|
-
{
|
|
954
|
+
{labelContent}
|
|
959
955
|
{iconAfterElement}
|
|
960
956
|
</ButtonFrame>
|
|
961
957
|
);
|
|
@@ -10,6 +10,7 @@ import { Dialog, Paragraph, XStack } from "tamagui";
|
|
|
10
10
|
import { Button } from "../Button";
|
|
11
11
|
import { formCommonColors } from "../shared/colorRamps";
|
|
12
12
|
import { useReturnFocusOnClose } from "../shared/focusManagement";
|
|
13
|
+
import { withOverlayStack } from "../shared/overlayStack";
|
|
13
14
|
import { t } from "../shared/t";
|
|
14
15
|
|
|
15
16
|
export interface UnsavedChangesDialogProps {
|
|
@@ -19,6 +20,8 @@ export interface UnsavedChangesDialogProps {
|
|
|
19
20
|
onDiscard: () => void;
|
|
20
21
|
}
|
|
21
22
|
|
|
23
|
+
const DialogPortal = withOverlayStack(Dialog.Portal, "dropdown", "DialogPortal");
|
|
24
|
+
|
|
22
25
|
/**
|
|
23
26
|
* Opens on Keep editing, the least destructive action, as tamagui's
|
|
24
27
|
* AlertDialog.Cancel does. Without it Chromium and WebKit's `<dialog>.show()`
|
|
@@ -67,7 +70,7 @@ export function UnsavedChangesDialog({
|
|
|
67
70
|
|
|
68
71
|
return (
|
|
69
72
|
<Dialog modal open={open} onOpenChange={handleOpenChange}>
|
|
70
|
-
<
|
|
73
|
+
<DialogPortal>
|
|
71
74
|
<DialogOverlay key="overlay" />
|
|
72
75
|
<DialogContent
|
|
73
76
|
key="content"
|
|
@@ -111,7 +114,7 @@ export function UnsavedChangesDialog({
|
|
|
111
114
|
</Button>
|
|
112
115
|
</XStack>
|
|
113
116
|
</DialogContent>
|
|
114
|
-
</
|
|
117
|
+
</DialogPortal>
|
|
115
118
|
</Dialog>
|
|
116
119
|
);
|
|
117
120
|
}
|
|
@@ -83,6 +83,8 @@ import {
|
|
|
83
83
|
motionDurationMs,
|
|
84
84
|
} from "@multiplatform.one/theme";
|
|
85
85
|
import { View, YStack } from "tamagui";
|
|
86
|
+
import { useOverlayZIndex } from "../shared/overlayStack";
|
|
87
|
+
import { OverlayWindow } from "../shared/OverlayWindow";
|
|
86
88
|
import { t } from "../shared/t";
|
|
87
89
|
import { useKeyboardInset } from "../shared/useKeyboardInset";
|
|
88
90
|
import type { FloatingPanelProps } from "./index";
|
|
@@ -143,6 +145,7 @@ export function FloatingPanel({
|
|
|
143
145
|
}: FloatingPanelProps) {
|
|
144
146
|
// The panel floats over the page: it never inherits a painted ancestor.
|
|
145
147
|
const { knobProps } = useResolvedKnobs({ painted: false });
|
|
148
|
+
const windowZIndex = useOverlayZIndex("sheet");
|
|
146
149
|
const children = <Surface painted={false}>{panelChildren}</Surface>;
|
|
147
150
|
const slideTransition = knobProps.motion.slide.transition;
|
|
148
151
|
const enterDuration = motionDurationMs(slideTransition);
|
|
@@ -522,118 +525,120 @@ export function FloatingPanel({
|
|
|
522
525
|
onNativeShow?.();
|
|
523
526
|
}}
|
|
524
527
|
>
|
|
525
|
-
{
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
528
|
+
<OverlayWindow zIndex={windowZIndex}>
|
|
529
|
+
{atPoint && pressPoint ? (
|
|
530
|
+
<PointPanel
|
|
531
|
+
key={`${pressPoint.x},${pressPoint.y}`}
|
|
532
|
+
point={pressPoint}
|
|
533
|
+
bounds={{
|
|
534
|
+
top: topInset + panelViewportPadding,
|
|
535
|
+
left: (safeArea?.left ?? 0) + panelViewportPadding,
|
|
536
|
+
right: windowWidth - (safeArea?.right ?? 0) - panelViewportPadding,
|
|
537
|
+
bottom:
|
|
538
|
+
windowHeight -
|
|
539
|
+
Math.max(safeArea?.bottom ?? 0, keyboardInset) -
|
|
540
|
+
panelViewportPadding,
|
|
541
|
+
}}
|
|
542
|
+
framePadding={framePadding}
|
|
543
|
+
frameGap={frameGap}
|
|
544
|
+
onBackdropPress={handleBackdropPress}
|
|
545
|
+
onEscape={close}
|
|
546
|
+
>
|
|
547
|
+
{header}
|
|
548
|
+
{children}
|
|
549
|
+
</PointPanel>
|
|
550
|
+
) : (
|
|
551
|
+
/*
|
|
548
552
|
A plain padded column, not KeyboardAvoidingView: inside a Modal the
|
|
549
553
|
latter pads the container while the sheet keeps its own fixed height,
|
|
550
554
|
so the sheet overflows off the TOP instead of shrinking (MPO-64).
|
|
551
555
|
*/
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
556
|
+
<RNView style={{ flex: 1, justifyContent: "flex-end", paddingBottom: keyboardInset }}>
|
|
557
|
+
<Pressable
|
|
558
|
+
style={StyleSheet.absoluteFill}
|
|
559
|
+
onPress={handleBackdropPress}
|
|
560
|
+
accessibilityRole="button"
|
|
561
|
+
accessibilityLabel={t("Close")}
|
|
562
|
+
>
|
|
563
|
+
<View flex={1} backgroundColor="$shadow6" />
|
|
564
|
+
</Pressable>
|
|
565
|
+
{/*
|
|
562
566
|
Sheet sits above the absoluteFill backdrop in z-order (later sibling).
|
|
563
567
|
Do NOT claim the responder on a wrapper here — that steals presses
|
|
564
568
|
from option rows / calendar days and blocks commit.
|
|
565
569
|
*/}
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
>
|
|
588
|
-
<YStack
|
|
589
|
-
{...knobProps.elevatedSurface}
|
|
590
|
-
borderBottomLeftRadius={0}
|
|
591
|
-
borderBottomRightRadius={0}
|
|
592
|
-
maxHeight={maxHeight}
|
|
593
|
-
flexShrink={1}
|
|
594
|
-
minHeight={0}
|
|
595
|
-
// sheetFill opens at full sheet height (flexShrink keeps the
|
|
596
|
-
// keyboard from pushing the top of the sheet off-screen).
|
|
597
|
-
{...(sheetFill ? { height: maxHeight } : undefined)}
|
|
598
|
-
paddingTop="$2"
|
|
599
|
-
paddingBottom={sheetBottomPadding}
|
|
600
|
-
// VoiceOver escape gesture (two-finger Z) dismisses the sheet.
|
|
601
|
-
onAccessibilityEscape={close}
|
|
570
|
+
<Animated.View
|
|
571
|
+
testID="floating-panel-sheet"
|
|
572
|
+
onLayout={(e) => {
|
|
573
|
+
// Travel distance for a dismiss, and the basis for the detents.
|
|
574
|
+
// A content-sized picker is shorter than maxHeight, and sliding
|
|
575
|
+
// it by maxHeight would overshoot into a visible lag.
|
|
576
|
+
if (restHeightRef.current !== null) return;
|
|
577
|
+
const h = Math.round(e.nativeEvent.layout.height);
|
|
578
|
+
if (h <= 0) return;
|
|
579
|
+
naturalRef.current = h;
|
|
580
|
+
filledRef.current = h >= maxHeightRef.current - 1;
|
|
581
|
+
if (h !== measuredHeight) setMeasuredHeight(h);
|
|
582
|
+
}}
|
|
583
|
+
style={{
|
|
584
|
+
// MPO-64: a flex child defaults to flexShrink 0, so without this
|
|
585
|
+
// the sheet cannot give way to the keyboard at all.
|
|
586
|
+
flexShrink: 1,
|
|
587
|
+
minHeight: 0,
|
|
588
|
+
maxHeight: frameCap,
|
|
589
|
+
transform: [{ translateY }],
|
|
590
|
+
}}
|
|
602
591
|
>
|
|
603
|
-
|
|
592
|
+
<YStack
|
|
593
|
+
{...knobProps.elevatedSurface}
|
|
594
|
+
borderBottomLeftRadius={0}
|
|
595
|
+
borderBottomRightRadius={0}
|
|
596
|
+
maxHeight={maxHeight}
|
|
597
|
+
flexShrink={1}
|
|
598
|
+
minHeight={0}
|
|
599
|
+
// sheetFill opens at full sheet height (flexShrink keeps the
|
|
600
|
+
// keyboard from pushing the top of the sheet off-screen).
|
|
601
|
+
{...(sheetFill ? { height: maxHeight } : undefined)}
|
|
602
|
+
paddingTop="$2"
|
|
603
|
+
paddingBottom={sheetBottomPadding}
|
|
604
|
+
// VoiceOver escape gesture (two-finger Z) dismisses the sheet.
|
|
605
|
+
onAccessibilityEscape={close}
|
|
606
|
+
>
|
|
607
|
+
{/*
|
|
604
608
|
The grab ZONE, not just the 4pt pill: a 4pt tall target is
|
|
605
609
|
unhittable. The gesture lives here rather than on the whole
|
|
606
610
|
sheet so it never fights the ScrollView inside it, which is the
|
|
607
611
|
same division iOS uses.
|
|
608
612
|
*/}
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
613
|
+
<RNView
|
|
614
|
+
{...panResponder.panHandlers}
|
|
615
|
+
accessibilityRole="adjustable"
|
|
616
|
+
accessibilityLabel={t("Drag to resize")}
|
|
617
|
+
style={{
|
|
618
|
+
alignSelf: "stretch",
|
|
619
|
+
alignItems: "center",
|
|
620
|
+
paddingTop: 4,
|
|
621
|
+
paddingBottom: 12,
|
|
622
|
+
}}
|
|
623
|
+
>
|
|
624
|
+
<View width={40} height={4} borderRadius={2} backgroundColor="$color8" />
|
|
625
|
+
</RNView>
|
|
626
|
+
<YStack
|
|
627
|
+
padding={framePadding}
|
|
628
|
+
gap={frameGap}
|
|
629
|
+
flexShrink={1}
|
|
630
|
+
{...(sheetFill ? { flex: 1, minHeight: 0 } : undefined)}
|
|
631
|
+
>
|
|
632
|
+
<Surface size="$true">
|
|
633
|
+
{header}
|
|
634
|
+
{content}
|
|
635
|
+
</Surface>
|
|
636
|
+
</YStack>
|
|
632
637
|
</YStack>
|
|
633
|
-
</
|
|
634
|
-
</
|
|
635
|
-
|
|
636
|
-
|
|
638
|
+
</Animated.View>
|
|
639
|
+
</RNView>
|
|
640
|
+
)}
|
|
641
|
+
</OverlayWindow>
|
|
637
642
|
</Modal>
|
|
638
643
|
</>
|
|
639
644
|
);
|
|
@@ -28,7 +28,7 @@ import { useReturnFocusOnClose, useSheetModalFocus } from "../shared/focusManage
|
|
|
28
28
|
import { useSheetEscapeDismiss } from "../shared/useSheetEscapeDismiss";
|
|
29
29
|
import { useFloatingPanel, panelViewportPadding } from "./useFloatingPanel";
|
|
30
30
|
import type { PanelWidthMode } from "./useFloatingPanel";
|
|
31
|
-
import {
|
|
31
|
+
import { OverlayStackProvider, useOverlayZIndex } from "../shared/overlayStack";
|
|
32
32
|
import { isNestedPanelAction, type PanelActionEvent } from "./nestedAction";
|
|
33
33
|
import { PanelPortal } from "./PanelPortal";
|
|
34
34
|
import {
|
|
@@ -490,6 +490,7 @@ function FloatingPanelSheet({
|
|
|
490
490
|
escapeTarget = "window",
|
|
491
491
|
}: FloatingPanelProps & { escapeTarget?: "window" | "document" }) {
|
|
492
492
|
const { knobProps } = useResolvedKnobs({ painted: false });
|
|
493
|
+
const sheetZIndex = useOverlayZIndex("sheet");
|
|
493
494
|
// LC-24 / A-ENTEREXIT: honor animation=none (undefined) — do not hardcode "medium".
|
|
494
495
|
const slideMotion = {
|
|
495
496
|
...knobProps.motion.slide,
|
|
@@ -585,7 +586,8 @@ function FloatingPanelSheet({
|
|
|
585
586
|
dismissOnSnapToBottom
|
|
586
587
|
position={position}
|
|
587
588
|
onPositionChange={setPosition}
|
|
588
|
-
zIndex={
|
|
589
|
+
zIndex={sheetZIndex}
|
|
590
|
+
portalProps={{ zIndex: sheetZIndex }}
|
|
589
591
|
{...transitionProps(slideMotion)}
|
|
590
592
|
>
|
|
591
593
|
<Sheet.Overlay
|
|
@@ -608,14 +610,16 @@ function FloatingPanelSheet({
|
|
|
608
610
|
padding={framePadding}
|
|
609
611
|
gap={frameGap}
|
|
610
612
|
>
|
|
611
|
-
{
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
{
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
613
|
+
<OverlayStackProvider zIndex={sheetZIndex}>
|
|
614
|
+
{header}
|
|
615
|
+
{scrollable ? (
|
|
616
|
+
<Sheet.ScrollView flex={header ? 1 : undefined} {...scrollViewProps}>
|
|
617
|
+
{children}
|
|
618
|
+
</Sheet.ScrollView>
|
|
619
|
+
) : (
|
|
620
|
+
children
|
|
621
|
+
)}
|
|
622
|
+
</OverlayStackProvider>
|
|
619
623
|
</Sheet.Frame>
|
|
620
624
|
</Sheet>
|
|
621
625
|
</>
|
|
@@ -642,6 +646,7 @@ function FloatingPanelWeb({
|
|
|
642
646
|
anchorRect,
|
|
643
647
|
...sheetProps
|
|
644
648
|
}: FloatingPanelProps) {
|
|
649
|
+
const panelZIndex = useOverlayZIndex("dropdown");
|
|
645
650
|
const [collisionSheet, setCollisionSheet] = useState(false);
|
|
646
651
|
const isFill = sizing === "fill";
|
|
647
652
|
const isContextMenu = openOn === "contextmenu";
|
|
@@ -1193,7 +1198,7 @@ function FloatingPanelWeb({
|
|
|
1193
1198
|
top: overlayStyleRef.current?.top ?? panel.y ?? 0,
|
|
1194
1199
|
left: overlayStyleRef.current?.left ?? panel.x ?? 0,
|
|
1195
1200
|
scrollbarWidth: "none" as any,
|
|
1196
|
-
zIndex:
|
|
1201
|
+
zIndex: panelZIndex,
|
|
1197
1202
|
overflow: "auto",
|
|
1198
1203
|
...(cover && !isContextMenu ? { overflowX: "hidden" as const } : {}),
|
|
1199
1204
|
...panel.floatingStyleRef.current,
|
|
@@ -1261,7 +1266,7 @@ function FloatingPanelWeb({
|
|
|
1261
1266
|
{...(contentPadding !== "none" ? panel.knobProps.panelPadding : undefined)}
|
|
1262
1267
|
{...(contentPadding !== "none" ? panel.knobProps.gapLg : undefined)}
|
|
1263
1268
|
>
|
|
1264
|
-
{children}
|
|
1269
|
+
<OverlayStackProvider zIndex={panelZIndex}>{children}</OverlayStackProvider>
|
|
1265
1270
|
</View>
|
|
1266
1271
|
<ScrollArrow
|
|
1267
1272
|
direction="down"
|
|
@@ -11,10 +11,10 @@ export type { SpinnerProps, SpinnerSize } from "./spinnerSize";
|
|
|
11
11
|
const SPIN_MS = 700;
|
|
12
12
|
|
|
13
13
|
/** Native Spinner. Size follows the shared size-token scale; spin gates on transition. */
|
|
14
|
-
export function Spinner({ size, color, ...props }: SpinnerProps) {
|
|
14
|
+
export function Spinner({ size, px: pxProp, color, ...props }: SpinnerProps) {
|
|
15
15
|
const { knobProps } = useResolvedKnobs();
|
|
16
16
|
const token = spinnerSizeToken(size, knobProps.sizeToken);
|
|
17
|
-
const px = spinnerSizePx(token);
|
|
17
|
+
const px = pxProp ?? spinnerSizePx(token);
|
|
18
18
|
const stroke = useSpinnerStroke(color);
|
|
19
19
|
const [reduceMotion, setReduceMotion] = useState(false);
|
|
20
20
|
useEffect(() => {
|
package/src/Spinner/index.tsx
CHANGED
|
@@ -26,10 +26,10 @@ function ensureMotionStyles() {
|
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
/** Form-styled Spinner. Size follows the shared size-token scale; spin gates on transition. */
|
|
29
|
-
export function Spinner({ size, color, ...props }: SpinnerProps) {
|
|
29
|
+
export function Spinner({ size, px: pxProp, color, ...props }: SpinnerProps) {
|
|
30
30
|
const { knobProps } = useResolvedKnobs();
|
|
31
31
|
const token = spinnerSizeToken(size, knobProps.sizeToken);
|
|
32
|
-
const px = spinnerSizePx(token);
|
|
32
|
+
const px = pxProp ?? spinnerSizePx(token);
|
|
33
33
|
const stroke = useSpinnerStroke(color);
|
|
34
34
|
const animationsOff = !knobProps.motion.control.transition;
|
|
35
35
|
ensureMotionStyles();
|
|
@@ -6,6 +6,8 @@ export type SpinnerSize = "small" | "large";
|
|
|
6
6
|
|
|
7
7
|
export interface SpinnerProps {
|
|
8
8
|
size?: SpinnerSize;
|
|
9
|
+
/** Glyph box in px, over `size`: a host slot that already has a size (Button's icon slot). */
|
|
10
|
+
px?: number;
|
|
9
11
|
color?: string;
|
|
10
12
|
[key: string]: unknown;
|
|
11
13
|
}
|
|
@@ -67,7 +67,7 @@ import {
|
|
|
67
67
|
useViewportGtSm,
|
|
68
68
|
} from "../../FloatingPanel";
|
|
69
69
|
import { PanelPortal } from "../../FloatingPanel/PanelPortal";
|
|
70
|
-
import {
|
|
70
|
+
import { useOverlayZIndex } from "../../shared/overlayStack";
|
|
71
71
|
import { useDragScroll } from "../../shared/useDragScroll";
|
|
72
72
|
import { useIsInTableCell } from "../../shared/tableCellContext";
|
|
73
73
|
import { orderOptionsBySelected, type SelectedOrder } from "../selectedOrder";
|
|
@@ -518,6 +518,7 @@ function FloatingComboboxCore({
|
|
|
518
518
|
open: openProp,
|
|
519
519
|
onOpenChange,
|
|
520
520
|
}: FloatingComboboxCoreProps) {
|
|
521
|
+
const overlayZIndex = useOverlayZIndex("dropdown");
|
|
521
522
|
const { style: elevatedSurfaceStyle, ...elevatedSurfaceProps } = knobProps.elevatedSurface;
|
|
522
523
|
const hydrationTouch = useTouchSurface();
|
|
523
524
|
if (isWeb) ensureKeyboardModalityTracking();
|
|
@@ -1498,7 +1499,7 @@ function FloatingComboboxCore({
|
|
|
1498
1499
|
position: "fixed",
|
|
1499
1500
|
top: 0,
|
|
1500
1501
|
left: 0,
|
|
1501
|
-
zIndex:
|
|
1502
|
+
zIndex: overlayZIndex,
|
|
1502
1503
|
overflow: "hidden",
|
|
1503
1504
|
display: "flex",
|
|
1504
1505
|
flexDirection: "column",
|