@multiplatform.one/forms 7.88.8 → 7.88.10

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@multiplatform.one/forms",
3
- "version": "7.88.8",
3
+ "version": "7.88.10",
4
4
  "description": "Cross-platform form components with URL state management",
5
5
  "keywords": [
6
6
  "cross-platform",
@@ -57,10 +57,10 @@
57
57
  },
58
58
  "dependencies": {
59
59
  "@floating-ui/react": "^0.27.20",
60
- "@multiplatform.one/platform": "^7.88.8",
61
- "@multiplatform.one/router": "^7.88.8",
62
- "@multiplatform.one/table-primitives": "^7.88.8",
63
- "@multiplatform.one/theme": "^7.88.8",
60
+ "@multiplatform.one/platform": "^7.88.10",
61
+ "@multiplatform.one/router": "^7.88.10",
62
+ "@multiplatform.one/table-primitives": "^7.88.10",
63
+ "@multiplatform.one/theme": "^7.88.10",
64
64
  "@phosphor-icons/react": "^2.1.10",
65
65
  "@tamagui/get-font-sized": "2.7.6",
66
66
  "@tamagui/get-token": "2.7.6",
@@ -80,9 +80,9 @@
80
80
  },
81
81
  "devDependencies": {
82
82
  "@expo/ui": "~55.0.17",
83
- "@multiplatform.one/config": "7.88.8",
84
- "@multiplatform.one/storybook": "7.88.8",
85
- "@multiplatform.one/test-utils": "7.88.8",
83
+ "@multiplatform.one/config": "7.88.10",
84
+ "@multiplatform.one/storybook": "7.88.10",
85
+ "@multiplatform.one/test-utils": "7.88.10",
86
86
  "@tamagui/build": "2.7.6",
87
87
  "@tamagui/config": "2.7.6",
88
88
  "@tamagui/vite-plugin": "2.7.6",
@@ -129,9 +129,12 @@ describe("FZ-9617: native keeps every host-scope paint a literal", () => {
129
129
  <Button icon={IconProbe} aria-label="Add" />
130
130
  </>,
131
131
  );
132
- for (const el of Array.from(document.querySelectorAll<HTMLElement>("[style]"))) {
133
- expect(el.getAttribute("style") ?? "").not.toContain("--mp-host-");
134
- }
132
+ const themes = config.tamagui.themes as Record<string, Record<string, unknown>>;
133
+ const aliases = Object.keys(themes[scheme]).map((token) => `--mp-host-${token}`);
134
+ const declared = Array.from(document.body.querySelectorAll<HTMLElement>("*")).flatMap((el) =>
135
+ aliases.filter((alias) => el.style.getPropertyValue(alias) !== ""),
136
+ );
137
+ expect(declared).toEqual([]);
135
138
  });
136
139
 
137
140
  it.each(SCHEMES)("%s: the ring is the page's literal $outlineColor", (scheme) => {
@@ -0,0 +1,116 @@
1
+ import { renderWithProviders } from "@multiplatform.one/test-utils";
2
+ import { fireEvent } from "@testing-library/react";
3
+ import { describe, expect, it, vi } from "vitest";
4
+ import { InlineChoices, type InlineChoice } from "./InlineChoices";
5
+
6
+ const knobProps = {
7
+ textAccentColor: "$color12",
8
+ menuRow: { minHeight: 44 },
9
+ panelPadding: { padding: "$3" },
10
+ gap: { gap: "$2" },
11
+ body: { fontFamily: "$body", fontWeight: "400" },
12
+ borderRadius: { borderRadius: "$3" },
13
+ };
14
+
15
+ const options: InlineChoice[] = [
16
+ { value: "light", label: "Light", icon: <svg data-icon="sun" /> },
17
+ { value: "dark", label: "Dark" },
18
+ { value: "system", label: "System" },
19
+ ];
20
+
21
+ function setup(props: Partial<Parameters<typeof InlineChoices>[0]> = {}) {
22
+ const onValueChange = vi.fn();
23
+ const view = renderWithProviders(
24
+ <InlineChoices
25
+ options={options}
26
+ value="dark"
27
+ onValueChange={onValueChange}
28
+ knobProps={knobProps}
29
+ markColor="$accent9"
30
+ aria-label="Appearance"
31
+ {...props}
32
+ />,
33
+ );
34
+ const radios = () => view.getAllByRole("radio");
35
+ return { ...view, onValueChange, radios };
36
+ }
37
+
38
+ describe("InlineChoices (LC-262, APG radio group)", () => {
39
+ it("is one labelled radio group with the chosen row checked and the only tab stop", () => {
40
+ const { getByRole, radios } = setup();
41
+ const group = getByRole("radiogroup");
42
+ expect(group.getAttribute("aria-label")).toBe("Appearance");
43
+ expect(radios().map((radio) => radio.textContent)).toEqual(["Light", "Dark", "System"]);
44
+ expect(radios().map((radio) => radio.getAttribute("aria-checked"))).toEqual([
45
+ "false",
46
+ "true",
47
+ "false",
48
+ ]);
49
+ expect(radios().map((radio) => radio.tabIndex)).toEqual([-1, 0, -1]);
50
+ });
51
+
52
+ it("puts the tab stop on the first row when nothing is chosen, and names by labelledBy", () => {
53
+ const { getByRole, radios } = setup({ value: undefined, labelledBy: "field-label" });
54
+ expect(radios().map((radio) => radio.tabIndex)).toEqual([0, -1, -1]);
55
+ const group = getByRole("radiogroup");
56
+ expect(group.getAttribute("aria-labelledby")).toBe("field-label");
57
+ expect(group.getAttribute("aria-label")).toBeNull();
58
+ });
59
+
60
+ it("chooses a row on press, but not the row already chosen", () => {
61
+ const { radios, onValueChange } = setup();
62
+ fireEvent.click(radios()[1]);
63
+ expect(onValueChange).not.toHaveBeenCalled();
64
+ fireEvent.click(radios()[2]);
65
+ expect(onValueChange).toHaveBeenCalledWith("system");
66
+ });
67
+
68
+ it("moves and chooses with the arrows, wrapping at both ends", () => {
69
+ const { radios, onValueChange } = setup();
70
+ fireEvent.keyDown(radios()[1], { key: "ArrowDown" });
71
+ expect(onValueChange).toHaveBeenLastCalledWith("system");
72
+ expect(document.activeElement).toBe(radios()[2]);
73
+ fireEvent.keyDown(radios()[2], { key: "ArrowRight" });
74
+ expect(onValueChange).toHaveBeenLastCalledWith("light");
75
+ expect(document.activeElement).toBe(radios()[0]);
76
+ fireEvent.keyDown(radios()[0], { key: "ArrowUp" });
77
+ expect(onValueChange).toHaveBeenLastCalledWith("system");
78
+ fireEvent.keyDown(radios()[2], { key: "ArrowLeft" });
79
+ expect(document.activeElement).toBe(radios()[1]);
80
+ expect(onValueChange).toHaveBeenCalledTimes(3);
81
+ });
82
+
83
+ it("chooses with Space and Enter and lets other keys through", () => {
84
+ const { radios, onValueChange } = setup();
85
+ fireEvent.keyDown(radios()[0], { key: " " });
86
+ expect(onValueChange).toHaveBeenLastCalledWith("light");
87
+ fireEvent.keyDown(radios()[2], { key: "Enter" });
88
+ expect(onValueChange).toHaveBeenLastCalledWith("system");
89
+ fireEvent.keyDown(radios()[0], { key: "Tab" });
90
+ expect(onValueChange).toHaveBeenCalledTimes(2);
91
+ });
92
+
93
+ it("chooses nothing while disabled and takes every row out of the tab order", () => {
94
+ const { getByRole, radios, onValueChange } = setup({ disabled: true });
95
+ expect(getByRole("radiogroup").getAttribute("aria-disabled")).toBe("true");
96
+ expect(radios().map((radio) => radio.tabIndex)).toEqual([-1, -1, -1]);
97
+ fireEvent.click(radios()[0]);
98
+ fireEvent.keyDown(radios()[1], { key: "ArrowDown" });
99
+ expect(onValueChange).not.toHaveBeenCalled();
100
+ });
101
+
102
+ it("reports blur from a row", () => {
103
+ const onBlur = vi.fn();
104
+ const { radios } = setup({ onBlur });
105
+ fireEvent.blur(radios()[1]);
106
+ expect(onBlur).toHaveBeenCalled();
107
+ });
108
+
109
+ it("gives every row a glyph column only when some option has an icon", () => {
110
+ const withIcon = setup();
111
+ expect(withIcon.container.querySelectorAll("[data-icon]")).toHaveLength(1);
112
+ withIcon.unmount();
113
+ const plain = setup({ options: options.map(({ icon: _icon, ...rest }) => rest) });
114
+ expect(plain.container.querySelectorAll("[data-icon]")).toHaveLength(0);
115
+ });
116
+ });
@@ -0,0 +1,101 @@
1
+ import { renderWithProviders } from "@multiplatform.one/test-utils";
2
+ import { act, fireEvent } from "@testing-library/react";
3
+ import { describe, expect, it, vi } from "vitest";
4
+ import { createSheetNavigationStore } from "../../shared/sheetNavigation";
5
+ import type { InlineChoice } from "./InlineChoices";
6
+ import { SheetPickerRow } from "./SheetPickerRow";
7
+
8
+ const knobProps = {
9
+ textAccentColor: "$color12",
10
+ menuRow: { minHeight: 44 },
11
+ panelPadding: { padding: "$3" },
12
+ gap: { gap: "$2" },
13
+ body: { fontFamily: "$body", fontWeight: "400" },
14
+ borderRadius: { borderRadius: "$3" },
15
+ };
16
+
17
+ const options: InlineChoice[] = [
18
+ { value: "us", label: "United States" },
19
+ { value: "in", label: "India" },
20
+ ];
21
+
22
+ function setup(props: Partial<Parameters<typeof SheetPickerRow>[0]> = {}) {
23
+ const store = createSheetNavigationStore();
24
+ const onValueChange = vi.fn();
25
+ const element = (extra: Partial<Parameters<typeof SheetPickerRow>[0]> = {}) => (
26
+ <SheetPickerRow
27
+ store={store}
28
+ label="Country"
29
+ options={options}
30
+ value="in"
31
+ placeholder="Choose"
32
+ onValueChange={onValueChange}
33
+ knobProps={knobProps}
34
+ markColor="$accent9"
35
+ {...props}
36
+ {...extra}
37
+ />
38
+ );
39
+ const view = renderWithProviders(element());
40
+ const row = () => view.getByTestId("sheet-picker-row");
41
+ return { ...view, store, onValueChange, row, element };
42
+ }
43
+
44
+ describe("SheetPickerRow (LC-262)", () => {
45
+ it("is a labelled button showing the current value, or the placeholder with none", () => {
46
+ const { row, getByTestId, rerender, element } = setup();
47
+ expect(row().getAttribute("role")).toBe("button");
48
+ expect(row().getAttribute("aria-label")).toBe("Country");
49
+ expect(row().tabIndex).toBe(0);
50
+ expect(getByTestId("sheet-picker-value").textContent).toBe("India");
51
+ rerender(element({ value: undefined }));
52
+ expect(getByTestId("sheet-picker-value").textContent).toBe("Choose");
53
+ });
54
+
55
+ it("pushes its checkmark list onto the sheet's stack on press, Enter and Space", () => {
56
+ const { row, store } = setup();
57
+ fireEvent.click(row());
58
+ expect(store.getPages()).toHaveLength(1);
59
+ expect(store.getPages()[0].title).toBe("Country");
60
+ act(() => store.pop());
61
+ fireEvent.keyDown(row(), { key: "Enter" });
62
+ expect(store.getPages()).toHaveLength(1);
63
+ act(() => store.pop());
64
+ fireEvent.keyDown(row(), { key: " " });
65
+ expect(store.getPages()).toHaveLength(1);
66
+ act(() => store.pop());
67
+ fireEvent.keyDown(row(), { key: "ArrowDown" });
68
+ expect(store.getPages()).toHaveLength(0);
69
+ });
70
+
71
+ it("chooses from the pushed page and keeps that page current as the value changes", () => {
72
+ const { row, store, onValueChange, rerender, element } = setup();
73
+ fireEvent.click(row());
74
+ const page = renderWithProviders(<>{store.getPages()[0].content}</>);
75
+ fireEvent.click(page.getAllByRole("radio")[0]);
76
+ expect(onValueChange).toHaveBeenCalledWith("us");
77
+ page.unmount();
78
+ rerender(element({ value: "us" }));
79
+ const updated = renderWithProviders(<>{store.getPages()[0].content}</>);
80
+ expect(
81
+ updated.getAllByRole("radio").map((radio) => radio.getAttribute("aria-checked")),
82
+ ).toEqual(["true", "false"]);
83
+ });
84
+
85
+ it("does not open while disabled and leaves the tab order", () => {
86
+ const { row, store } = setup({ disabled: true });
87
+ expect(row().tabIndex).toBe(-1);
88
+ expect(row().getAttribute("aria-disabled")).toBe("true");
89
+ fireEvent.click(row());
90
+ fireEvent.keyDown(row(), { key: "Enter" });
91
+ expect(store.getPages()).toHaveLength(0);
92
+ });
93
+
94
+ it("takes its page off the stack when it unmounts", () => {
95
+ const { row, store, unmount } = setup();
96
+ fireEvent.click(row());
97
+ expect(store.getPages()).toHaveLength(1);
98
+ unmount();
99
+ expect(store.getPages()).toHaveLength(0);
100
+ });
101
+ });
@@ -0,0 +1,105 @@
1
+ import { act, cleanup, render } from "@testing-library/react";
2
+ import { afterEach, describe, expect, it, vi } from "vitest";
3
+ import {
4
+ createSheetNavigationStore,
5
+ SheetNavigationProvider,
6
+ useSheetNavigation,
7
+ useSheetPageOpen,
8
+ useSheetPages,
9
+ type SheetNavigationStore,
10
+ type SheetPage,
11
+ } from "./sheetNavigation";
12
+
13
+ const page = (id: string, title = id): SheetPage => ({ id, title, content: title });
14
+ const ids = (store: SheetNavigationStore) => store.getPages().map((p) => p.id);
15
+
16
+ afterEach(cleanup);
17
+
18
+ describe("sheet navigation store (LC-262)", () => {
19
+ it("pushes pages oldest first, and a repeat push moves that page to the top", () => {
20
+ const store = createSheetNavigationStore();
21
+ store.push(page("country"));
22
+ store.push(page("region"));
23
+ store.push(page("country", "Country again"));
24
+ expect(ids(store)).toEqual(["region", "country"]);
25
+ expect(store.getPages()[1].title).toBe("Country again");
26
+ expect(store.has("region")).toBe(true);
27
+ expect(store.has("city")).toBe(false);
28
+ });
29
+
30
+ it("updates a page in place and ignores an update for one not on the stack", () => {
31
+ const store = createSheetNavigationStore();
32
+ store.push(page("a"));
33
+ store.push(page("b"));
34
+ const listener = vi.fn();
35
+ store.subscribe(listener);
36
+ store.update(page("a", "A2"));
37
+ expect(store.getPages().map((p) => p.title)).toEqual(["A2", "b"]);
38
+ store.update(page("missing"));
39
+ expect(listener).toHaveBeenCalledTimes(1);
40
+ });
41
+
42
+ it("removes a page by id, pops the top, and notifies only on a real change", () => {
43
+ const store = createSheetNavigationStore();
44
+ const listener = vi.fn();
45
+ const stop = store.subscribe(listener);
46
+ store.pop();
47
+ store.remove("nothing");
48
+ expect(listener).not.toHaveBeenCalled();
49
+ store.push(page("a"));
50
+ store.push(page("b"));
51
+ store.push(page("c"));
52
+ store.remove("b");
53
+ expect(ids(store)).toEqual(["a", "c"]);
54
+ store.pop();
55
+ expect(ids(store)).toEqual(["a"]);
56
+ expect(listener).toHaveBeenCalledTimes(5);
57
+ stop();
58
+ store.pop();
59
+ expect(listener).toHaveBeenCalledTimes(5);
60
+ expect(ids(store)).toEqual([]);
61
+ });
62
+ });
63
+
64
+ describe("sheet navigation hooks", () => {
65
+ function Probe({
66
+ id,
67
+ out,
68
+ }: {
69
+ id: string;
70
+ out: { store?: unknown; pages?: unknown; open?: boolean };
71
+ }) {
72
+ const store = useSheetNavigation();
73
+ out.store = store;
74
+ out.pages = useSheetPages(store);
75
+ out.open = useSheetPageOpen(store, id);
76
+ return null;
77
+ }
78
+
79
+ it("reads nothing outside a sheet", () => {
80
+ const out: { store?: unknown; pages?: unknown; open?: boolean } = {};
81
+ render(<Probe id="a" out={out} />);
82
+ expect(out.store).toBeUndefined();
83
+ expect(out.pages).toEqual([]);
84
+ expect(out.open).toBe(false);
85
+ });
86
+
87
+ it("follows the sheet's stack as pages come and go", () => {
88
+ const store = createSheetNavigationStore();
89
+ const out: { store?: unknown; pages?: SheetPage[]; open?: boolean } = {};
90
+ render(
91
+ <SheetNavigationProvider value={store}>
92
+ <Probe id="a" out={out as never} />
93
+ </SheetNavigationProvider>,
94
+ );
95
+ expect(out.store).toBe(store);
96
+ expect(out.open).toBe(false);
97
+ act(() => store.push(page("a")));
98
+ expect(out.open).toBe(true);
99
+ expect(out.pages?.map((p) => p.id)).toEqual(["a"]);
100
+ act(() => store.push(page("b")));
101
+ expect(out.pages?.map((p) => p.id)).toEqual(["a", "b"]);
102
+ act(() => store.remove("a"));
103
+ expect(out.open).toBe(false);
104
+ });
105
+ });