@multiplatform.one/components 7.7.3 → 7.7.6

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.
Files changed (117) hide show
  1. package/dist/cjs/Accordion/index.cjs +4 -0
  2. package/dist/cjs/Accordion/index.native.js +4 -0
  3. package/dist/cjs/Accordion/index.native.js.map +1 -1
  4. package/dist/cjs/AnimateHeight/index.cjs +29 -2
  5. package/dist/cjs/AnimateHeight/index.native.js +30 -2
  6. package/dist/cjs/AnimateHeight/index.native.js.map +1 -1
  7. package/dist/cjs/Collapsible.cjs +48 -0
  8. package/dist/cjs/Collapsible.native.js +56 -0
  9. package/dist/cjs/Collapsible.native.js.map +1 -0
  10. package/dist/cjs/Pagination.cjs +28 -9
  11. package/dist/cjs/Pagination.native.js +28 -9
  12. package/dist/cjs/Pagination.native.js.map +1 -1
  13. package/dist/cjs/SheetModal/index.native.js +49 -25
  14. package/dist/cjs/SheetModal/index.native.js.map +1 -1
  15. package/dist/cjs/Tabs/index.cjs +144 -91
  16. package/dist/cjs/Tabs/index.native.js +186 -123
  17. package/dist/cjs/Tabs/index.native.js.map +1 -1
  18. package/dist/cjs/knobImmuneExports.cjs +0 -1
  19. package/dist/cjs/knobImmuneExports.native.js +0 -1
  20. package/dist/cjs/knobImmuneExports.native.js.map +1 -1
  21. package/dist/cjs/tamagui.cjs +3 -2
  22. package/dist/cjs/tamagui.native.js +2 -1
  23. package/dist/cjs/tamagui.native.js.map +1 -1
  24. package/dist/esm/Accordion/index.mjs +4 -0
  25. package/dist/esm/Accordion/index.mjs.map +1 -1
  26. package/dist/esm/Accordion/index.native.js +4 -0
  27. package/dist/esm/Accordion/index.native.js.map +1 -1
  28. package/dist/esm/AnimateHeight/index.mjs +29 -2
  29. package/dist/esm/AnimateHeight/index.mjs.map +1 -1
  30. package/dist/esm/AnimateHeight/index.native.js +30 -2
  31. package/dist/esm/AnimateHeight/index.native.js.map +1 -1
  32. package/dist/esm/Collapsible.mjs +23 -0
  33. package/dist/esm/Collapsible.mjs.map +1 -0
  34. package/dist/esm/Collapsible.native.js +28 -0
  35. package/dist/esm/Collapsible.native.js.map +1 -0
  36. package/dist/esm/Pagination.mjs +28 -9
  37. package/dist/esm/Pagination.mjs.map +1 -1
  38. package/dist/esm/Pagination.native.js +28 -9
  39. package/dist/esm/Pagination.native.js.map +1 -1
  40. package/dist/esm/SheetModal/index.native.js +49 -25
  41. package/dist/esm/SheetModal/index.native.js.map +1 -1
  42. package/dist/esm/Tabs/index.mjs +144 -91
  43. package/dist/esm/Tabs/index.mjs.map +1 -1
  44. package/dist/esm/Tabs/index.native.js +186 -123
  45. package/dist/esm/Tabs/index.native.js.map +1 -1
  46. package/dist/esm/knobImmuneExports.mjs +0 -1
  47. package/dist/esm/knobImmuneExports.mjs.map +1 -1
  48. package/dist/esm/knobImmuneExports.native.js +0 -1
  49. package/dist/esm/knobImmuneExports.native.js.map +1 -1
  50. package/dist/esm/tamagui.mjs +2 -1
  51. package/dist/esm/tamagui.mjs.map +1 -1
  52. package/dist/esm/tamagui.native.js +2 -1
  53. package/dist/esm/tamagui.native.js.map +1 -1
  54. package/dist/jsx/Accordion/index.mjs +4 -0
  55. package/dist/jsx/Accordion/index.mjs.map +1 -1
  56. package/dist/jsx/Accordion/index.native.js +4 -0
  57. package/dist/jsx/Accordion/index.native.js.map +1 -1
  58. package/dist/jsx/AnimateHeight/index.mjs +29 -2
  59. package/dist/jsx/AnimateHeight/index.mjs.map +1 -1
  60. package/dist/jsx/AnimateHeight/index.native.js +30 -2
  61. package/dist/jsx/AnimateHeight/index.native.js.map +1 -1
  62. package/dist/jsx/Collapsible.mjs +23 -0
  63. package/dist/jsx/Collapsible.mjs.map +1 -0
  64. package/dist/jsx/Collapsible.native.js +56 -0
  65. package/dist/jsx/Collapsible.native.js.map +1 -0
  66. package/dist/jsx/Pagination.mjs +28 -9
  67. package/dist/jsx/Pagination.mjs.map +1 -1
  68. package/dist/jsx/Pagination.native.js +28 -9
  69. package/dist/jsx/Pagination.native.js.map +1 -1
  70. package/dist/jsx/SheetModal/index.native.js +49 -25
  71. package/dist/jsx/SheetModal/index.native.js.map +1 -1
  72. package/dist/jsx/Tabs/index.mjs +144 -91
  73. package/dist/jsx/Tabs/index.mjs.map +1 -1
  74. package/dist/jsx/Tabs/index.native.js +186 -123
  75. package/dist/jsx/Tabs/index.native.js.map +1 -1
  76. package/dist/jsx/knobImmuneExports.mjs +0 -1
  77. package/dist/jsx/knobImmuneExports.mjs.map +1 -1
  78. package/dist/jsx/knobImmuneExports.native.js +0 -1
  79. package/dist/jsx/knobImmuneExports.native.js.map +1 -1
  80. package/dist/jsx/tamagui.mjs +2 -1
  81. package/dist/jsx/tamagui.mjs.map +1 -1
  82. package/dist/jsx/tamagui.native.js +2 -1
  83. package/dist/jsx/tamagui.native.js.map +1 -1
  84. package/package.json +9 -9
  85. package/src/Accordion/index.tsx +7 -1
  86. package/src/AnimateHeight/AnimateHeight.native.spec.tsx +51 -0
  87. package/src/AnimateHeight/index.tsx +30 -5
  88. package/src/Collapsible.tsx +23 -0
  89. package/src/Pagination.simple.spec.tsx +23 -0
  90. package/src/Pagination.stories.tsx +8 -0
  91. package/src/Pagination.tsx +27 -10
  92. package/src/SheetModal/SheetModal.native.spec.tsx +138 -0
  93. package/src/SheetModal/index.native.tsx +59 -26
  94. package/src/Tabs/Tabs.bandColor.spec.tsx +23 -0
  95. package/src/Tabs/Tabs.native.spec.tsx +35 -0
  96. package/src/Tabs/Tabs.spec.tsx +51 -1
  97. package/src/Tabs/Tabs.stories.tsx +27 -1
  98. package/src/Tabs/index.tsx +266 -187
  99. package/src/disclosureForm.spec.tsx +57 -0
  100. package/src/disclosureNativeProps.spec.tsx +35 -0
  101. package/src/knobImmuneExports.ts +0 -2
  102. package/src/primitives.spec.tsx +19 -4
  103. package/src/tamagui.ts +3 -1
  104. package/types/Accordion/index.d.ts +2 -0
  105. package/types/Accordion/index.d.ts.map +1 -1
  106. package/types/AnimateHeight/index.d.ts +3 -4
  107. package/types/AnimateHeight/index.d.ts.map +1 -1
  108. package/types/Collapsible.d.ts +19 -0
  109. package/types/Collapsible.d.ts.map +1 -0
  110. package/types/Pagination.d.ts +1 -1
  111. package/types/Pagination.d.ts.map +1 -1
  112. package/types/SheetModal/index.native.d.ts.map +1 -1
  113. package/types/Tabs/index.d.ts +7 -3
  114. package/types/Tabs/index.d.ts.map +1 -1
  115. package/types/knobImmuneExports.d.ts.map +1 -1
  116. package/types/tamagui.d.ts +2 -1
  117. package/types/tamagui.d.ts.map +1 -1
@@ -62,19 +62,6 @@ export function SheetModal({
62
62
  if (backdropArmed) close();
63
63
  }, [backdropArmed, close]);
64
64
 
65
- useEffect(() => {
66
- if (!open) {
67
- setBackdropArmed(false);
68
- if (armTimerRef.current) {
69
- clearTimeout(armTimerRef.current);
70
- armTimerRef.current = null;
71
- }
72
- }
73
- return () => {
74
- if (armTimerRef.current) clearTimeout(armTimerRef.current);
75
- };
76
- }, [open]);
77
-
78
65
  // MPO-64: the sheet ends above the keyboard rather than running under it.
79
66
  const available = Math.max(0, windowHeight - keyboardInset);
80
67
  const maxHeight = Math.max(
@@ -127,11 +114,9 @@ export function SheetModal({
127
114
  travelRef.current = travel;
128
115
  detentsRef.current = detents;
129
116
 
130
- useEffect(() => {
131
- if (open) return;
132
- translateY.setValue(travelRef.current);
133
- restRef.current = travelRef.current;
134
- }, [open, translateY]);
117
+ // Async callbacks belong to one committed presentation, never its successor.
118
+ const presentationRef = useRef(0);
119
+ const activeRef = useRef(false);
135
120
 
136
121
  const settleTo = useCallback(
137
122
  (toValue: number, velocity = 0) => {
@@ -143,7 +128,10 @@ export function SheetModal({
143
128
  stiffness: 320,
144
129
  mass: 0.9,
145
130
  overshootClamping: true,
146
- useNativeDriver: true,
131
+ // MPO-256: JS driver. A native-driven Animated node inside a core RN
132
+ // Modal does not advance on the New Architecture, and the drag path
133
+ // already writes this value from JS.
134
+ useNativeDriver: false,
147
135
  }).start();
148
136
  },
149
137
  [translateY],
@@ -151,6 +139,8 @@ export function SheetModal({
151
139
 
152
140
  const dismiss = useCallback(
153
141
  (velocity = 0) => {
142
+ if (!activeRef.current) return;
143
+ const presentation = presentationRef.current;
154
144
  restRef.current = travelRef.current;
155
145
  Animated.spring(translateY, {
156
146
  toValue: travelRef.current,
@@ -159,9 +149,9 @@ export function SheetModal({
159
149
  stiffness: 400,
160
150
  mass: 0.9,
161
151
  overshootClamping: true,
162
- useNativeDriver: true,
152
+ useNativeDriver: false,
163
153
  }).start(({ finished }) => {
164
- if (finished) close();
154
+ if (finished && activeRef.current && presentation === presentationRef.current) close();
165
155
  });
166
156
  },
167
157
  [close, translateY],
@@ -207,9 +197,25 @@ export function SheetModal({
207
197
  [dismiss, settleTo, translateY],
208
198
  );
209
199
 
210
- const handleModalShow = useCallback(() => {
200
+ /*
201
+ * MPO-256 — the entrance runs from an effect on `open` as well as from
202
+ * `onShow`, whichever gets here first, guarded so the second is a no-op.
203
+ * Wired to `onShow` ALONE, the sheet stays parked at `travel` for good
204
+ * whenever that callback does not arrive: one sheet-height below the
205
+ * screen, every row in the accessibility tree and none of them reachable.
206
+ */
207
+ const enteredRef = useRef(false);
208
+ const enter = useCallback(() => {
209
+ if (!activeRef.current || enteredRef.current) return;
210
+ const presentation = presentationRef.current;
211
+ enteredRef.current = true;
211
212
  if (armTimerRef.current) clearTimeout(armTimerRef.current);
212
- armTimerRef.current = setTimeout(() => setBackdropArmed(true), 280);
213
+ armTimerRef.current = setTimeout(() => {
214
+ if (activeRef.current && presentation === presentationRef.current) {
215
+ armTimerRef.current = null;
216
+ setBackdropArmed(true);
217
+ }
218
+ }, 280);
213
219
  // MPO-62: the modal fades (backdrop dims IN PLACE); only the panel slides.
214
220
  // MPO-71: the sheet is already parked off-screen before the present, so
215
221
  // this setValue only normalises the start to exactly `travel` once the
@@ -221,10 +227,37 @@ export function SheetModal({
221
227
  toValue: 0,
222
228
  duration: 280,
223
229
  easing: Easing.out(Easing.cubic),
224
- useNativeDriver: true,
225
- }).start();
230
+ useNativeDriver: false,
231
+ }).start(({ finished }) => {
232
+ if (finished && activeRef.current && presentation === presentationRef.current)
233
+ translateY.setValue(0);
234
+ });
226
235
  }, [translateY]);
227
236
 
237
+ useEffect(() => {
238
+ presentationRef.current += 1;
239
+ activeRef.current = open;
240
+ if (open) {
241
+ enter();
242
+ } else {
243
+ setBackdropArmed(false);
244
+ const parked = travelRef.current || 10000;
245
+ translateY.setValue(parked);
246
+ restRef.current = parked;
247
+ }
248
+ return () => {
249
+ // Invalidate first: stopping an animation can synchronously finish it.
250
+ activeRef.current = false;
251
+ presentationRef.current += 1;
252
+ enteredRef.current = false;
253
+ if (armTimerRef.current) {
254
+ clearTimeout(armTimerRef.current);
255
+ armTimerRef.current = null;
256
+ }
257
+ translateY.stopAnimation();
258
+ };
259
+ }, [open, enter, translateY]);
260
+
228
261
  return (
229
262
  <Modal
230
263
  visible={open}
@@ -232,7 +265,7 @@ export function SheetModal({
232
265
  animationType="fade"
233
266
  statusBarTranslucent
234
267
  onRequestClose={close}
235
- onShow={handleModalShow}
268
+ onShow={enter}
236
269
  >
237
270
  <RNView style={{ flex: 1, justifyContent: "flex-end", paddingBottom: keyboardInset }}>
238
271
  <Pressable
@@ -0,0 +1,23 @@
1
+ import { renderWithProviders } from "@multiplatform.one/test-utils";
2
+ import { describe, expect, it, vi } from "vitest";
3
+ import { Tabs } from "./index";
4
+
5
+ vi.mock("@multiplatform.one/theme", async (importOriginal) => ({
6
+ ...(await importOriginal<typeof import("@multiplatform.one/theme")>()),
7
+ useReadableTextOn: () => "#fff",
8
+ }));
9
+
10
+ describe("selected tab ink follows its painted surface", () => {
11
+ it("uses page-surface ink for the band even when contained tint needs white ink", () => {
12
+ const result = renderWithProviders(
13
+ <Tabs variant="band" value="one" items={[{ value: "one", label: "Selected" }]} />,
14
+ );
15
+ expect(result.getByText("Selected").className).toContain("_col-color");
16
+ });
17
+ it("keeps contrasting tint ink for the contained indicator", () => {
18
+ const result = renderWithProviders(
19
+ <Tabs variant="contained" value="one" items={[{ value: "one", label: "Selected" }]} />,
20
+ );
21
+ expect(result.getByText("Selected").className).not.toContain("_col-color");
22
+ });
23
+ });
@@ -0,0 +1,35 @@
1
+ import { renderWithProviders } from "@multiplatform.one/test-utils";
2
+ import { describe, expect, it, vi } from "vitest";
3
+ import { Tabs } from "./index";
4
+
5
+ // Exercise the native accessibility branch while retaining the test host renderer.
6
+ // Geometry still requires a native runtime; this test asserts role/name/state only.
7
+ vi.mock("tamagui", async (importOriginal) => ({
8
+ ...(await importOriginal<typeof import("tamagui")>()),
9
+ isWeb: false,
10
+ }));
11
+
12
+ describe("native Tabs accessibility contract", () => {
13
+ it("names each button and exposes selected and disabled states", () => {
14
+ const result = renderWithProviders(
15
+ <Tabs
16
+ variant="band"
17
+ value="one"
18
+ items={[
19
+ { value: "one", label: "One" },
20
+ { value: "two", label: "Two", disabled: true },
21
+ { value: "three", label: "Three", accessibilityLabel: "Third destination" },
22
+ ]}
23
+ />,
24
+ );
25
+ const selected = result.getByRole("button", { name: "One" });
26
+ expect(selected).toHaveAttribute("aria-selected", "true");
27
+ const disabled = result.getByRole("button", { name: "Two" });
28
+ expect(disabled).toHaveAttribute("aria-disabled", "true");
29
+ const third = result.getByRole("button", { name: "Three" });
30
+ expect(third).toHaveAttribute("accessibilitylabel", "Third destination");
31
+ expect(third).toHaveAttribute("aria-selected", "false");
32
+ expect(selected).toHaveAttribute("accessibilityrole", "button");
33
+ expect(selected).toHaveAttribute("accessibilitylabel", "One");
34
+ });
35
+ });
@@ -1,4 +1,4 @@
1
- import { fireEvent, screen } from "@testing-library/react";
1
+ import { fireEvent, screen, waitFor } from "@testing-library/react";
2
2
  import { Text } from "tamagui";
3
3
  import { renderWithProviders } from "@multiplatform.one/test-utils";
4
4
  import { afterEach, describe, expect, it, vitest } from "vitest";
@@ -220,6 +220,56 @@ describe("Tabs", () => {
220
220
  expect(screen.getByRole("tab", { name: "Three" }).tabIndex).toBe(-1);
221
221
  });
222
222
 
223
+ it("navigates the band by keyboard while skipping disabled tabs", async () => {
224
+ renderWithProviders(
225
+ <Tabs items={[...items, { value: "four", label: "Four" }]} variant="band" />,
226
+ );
227
+ const one = screen.getByRole("tab", { name: "One" });
228
+ one.focus();
229
+ fireEvent.keyDown(one, { key: "End" });
230
+ await waitFor(() => expect(screen.getByRole("tab", { name: "Four" })).toHaveFocus());
231
+ fireEvent.keyDown(document.activeElement!, { key: "ArrowLeft" });
232
+ await waitFor(() => expect(screen.getByRole("tab", { name: "Two" })).toHaveFocus());
233
+ fireEvent.keyDown(document.activeElement!, { key: "Home" });
234
+ await waitFor(() => expect(one).toHaveFocus());
235
+ });
236
+
237
+ it("keeps manual activation distinct from keyboard focus in a band", async () => {
238
+ renderWithProviders(<Tabs items={items} variant="band" activationMode="manual" />);
239
+ const one = screen.getByRole("tab", { name: "One" });
240
+ const two = screen.getByRole("tab", { name: "Two" });
241
+ one.focus();
242
+ fireEvent.keyDown(one, { key: "ArrowRight" });
243
+ await waitFor(() => expect(two).toHaveFocus());
244
+ expect(two).toHaveAttribute("aria-selected", "false");
245
+ fireEvent.keyDown(two, { key: "Enter" });
246
+ expect(two).toHaveAttribute("aria-selected", "true");
247
+ });
248
+
249
+ it("keeps an enabled entry point when the selected tab is disabled", () => {
250
+ renderWithProviders(<Tabs items={items} value="three" />);
251
+ const stops = screen.getAllByRole("tab").filter((tab) => tab.tabIndex === 0);
252
+ expect(stops).toHaveLength(1);
253
+ expect(stops[0]).toHaveAccessibleName("One");
254
+ });
255
+
256
+ it("renders a flat equal band with named tabs and no scrolling viewport", () => {
257
+ const { container } = renderWithProviders(<Tabs items={items} variant="band" />);
258
+ expect(container.querySelector('[data-tabs-distribution="equal"]')).not.toBeNull();
259
+ expect(container.querySelector('[data-tabs-scroll="true"]')).toBeNull();
260
+ expect(screen.getAllByRole("tab")).toHaveLength(3);
261
+ expect(container.querySelector("[data-tabs-indicator]")).toBeNull();
262
+ });
263
+
264
+ it("lets equal-band chrome shrink inside each allocated tab without horizontal insets", () => {
265
+ renderWithProviders(<Tabs items={items} variant="band" />);
266
+ const chrome = screen.getByRole("tab", { name: "One" }).firstElementChild!;
267
+ expect(chrome.className).toContain("_fs-1");
268
+ expect(chrome.className).toContain("_fb-0px");
269
+ expect(chrome.className).toContain("_pl-0px");
270
+ expect(chrome.className).toContain("_pr-0px");
271
+ });
272
+
223
273
  it("ariaLabel overrides the default tablist name", () => {
224
274
  renderWithProviders(<Tabs items={items} ariaLabel="Document sections" />);
225
275
  expect(screen.getByRole("tablist")).toHaveAttribute("aria-label", "Document sections");
@@ -26,7 +26,7 @@ const meta: Meta<typeof Tabs> = {
26
26
  },
27
27
  },
28
28
  argTypes: {
29
- variant: { control: "select", options: ["underline", "contained"] },
29
+ variant: { control: "select", options: ["underline", "contained", "band"] },
30
30
  size: { control: "select", options: ["$2", "$3", "$4", "$5"] },
31
31
  accent: { control: "boolean" },
32
32
  error: { control: "boolean" },
@@ -327,3 +327,29 @@ export const CompactAndSize: Story = {
327
327
  </YStack>
328
328
  ),
329
329
  };
330
+
331
+ export const EqualBand: Story = {
332
+ render: () => (
333
+ <YStack width={390} maxWidth="100%">
334
+ <Tabs
335
+ variant="band"
336
+ ariaLabel="Sections"
337
+ items={[
338
+ { value: "overview", label: "Overview" },
339
+ { value: "activity", label: "Activity" },
340
+ { value: "resources", label: "Resources" },
341
+ { value: "reports", label: "Reports" },
342
+ { value: "settings", label: "Settings" },
343
+ ]}
344
+ />
345
+ </YStack>
346
+ ),
347
+ };
348
+
349
+ export const IntrinsicHorizontalHost: Story = {
350
+ render: () => (
351
+ <ScrollView horizontal width={390}>
352
+ <Tabs items={baseItems} ariaLabel="Intrinsic tabs" />
353
+ </ScrollView>
354
+ ),
355
+ };