@multiplatform.one/forms 7.83.0 → 7.83.2
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/Chip/index.cjs +2 -1
- package/dist/cjs/Chip/index.native.js +2 -1
- package/dist/cjs/Chip/index.native.js.map +1 -1
- package/dist/cjs/fields/nativeDateTime/OsDateTimeSheet.cjs +7 -1
- package/dist/cjs/fields/nativeDateTime/OsDateTimeSheet.native.js +7 -1
- package/dist/cjs/fields/nativeDateTime/OsDateTimeSheet.native.js.map +1 -1
- package/dist/esm/Chip/index.mjs +3 -3
- package/dist/esm/Chip/index.mjs.map +1 -1
- package/dist/esm/Chip/index.native.js +3 -3
- package/dist/esm/Chip/index.native.js.map +1 -1
- package/dist/esm/fields/nativeDateTime/OsDateTimeSheet.mjs +8 -2
- package/dist/esm/fields/nativeDateTime/OsDateTimeSheet.mjs.map +1 -1
- package/dist/esm/fields/nativeDateTime/OsDateTimeSheet.native.js +8 -2
- package/dist/esm/fields/nativeDateTime/OsDateTimeSheet.native.js.map +1 -1
- package/dist/jsx/Chip/index.mjs +3 -3
- package/dist/jsx/Chip/index.mjs.map +1 -1
- package/dist/jsx/Chip/index.native.js +2 -1
- package/dist/jsx/Chip/index.native.js.map +1 -1
- package/dist/jsx/fields/nativeDateTime/OsDateTimeSheet.mjs +8 -2
- package/dist/jsx/fields/nativeDateTime/OsDateTimeSheet.mjs.map +1 -1
- package/dist/jsx/fields/nativeDateTime/OsDateTimeSheet.native.js +7 -1
- package/dist/jsx/fields/nativeDateTime/OsDateTimeSheet.native.js.map +1 -1
- package/package.json +8 -8
- package/src/Button/Button.statePrecedence.spec.tsx +151 -0
- package/src/Chip/Chip.spec.tsx +42 -1
- package/src/Chip/index.tsx +8 -5
- package/src/FloatingPanel/FloatingPanel.scrollThenSheet.spec.tsx +85 -0
- package/src/fields/nativeDateTime/OsDateTimeSheet.countdownValue.native.spec.tsx +94 -0
- package/src/fields/nativeDateTime/OsDateTimeSheet.tsx +11 -2
- package/types/Chip/index.d.ts +5 -0
- package/types/Chip/index.d.ts.map +1 -1
- package/types/fields/nativeDateTime/OsDateTimeSheet.d.ts.map +1 -1
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LC-187 STATE-PRECEDENCE, the state matrix on Button (FZ-8031).
|
|
3
|
+
*
|
|
4
|
+
* When states coincide the paint resolves in one order: disabled beats hover
|
|
5
|
+
* and press; loading is disabled plus the spinner in the leading slot (the
|
|
6
|
+
* label stays, FZ-7812); press beats hover; selected suppresses hover; error
|
|
7
|
+
* and focus compose with everything.
|
|
8
|
+
*
|
|
9
|
+
* happy-dom cascades no Tamagui CSS, so this reads the atomic classes the
|
|
10
|
+
* frame asks for: `_bg-<v>` at rest, `_bg-<n>hover-<v>` and `_bg-<n>active-<v>`
|
|
11
|
+
* for hover and press, and the focus-visible ring classes.
|
|
12
|
+
*/
|
|
13
|
+
import { renderWithProviders } from "@multiplatform.one/test-utils";
|
|
14
|
+
import { Preset } from "@multiplatform.one/theme";
|
|
15
|
+
import { cleanup, fireEvent, screen } from "@testing-library/react";
|
|
16
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
17
|
+
import { __resetDevWarnSeen } from "../shared/devWarn";
|
|
18
|
+
import { Button } from "./index";
|
|
19
|
+
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
__resetDevWarnSeen();
|
|
22
|
+
vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
afterEach(() => {
|
|
26
|
+
cleanup();
|
|
27
|
+
vi.restoreAllMocks();
|
|
28
|
+
__resetDevWarnSeen();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const PSEUDO = /^_bg-\d*(hover|active|focus|press|focus-visible|focusVisible)-/;
|
|
32
|
+
|
|
33
|
+
/** The background value a class list asks for in one state, or "" for none. */
|
|
34
|
+
function bg(el: Element, state: "rest" | "hover" | "press"): string {
|
|
35
|
+
const classes = Array.from(el.classList).filter((c) => c.startsWith("_bg-"));
|
|
36
|
+
if (state === "rest") {
|
|
37
|
+
return classes
|
|
38
|
+
.filter((c) => !PSEUDO.test(c))
|
|
39
|
+
.map((c) => c.slice("_bg-".length))
|
|
40
|
+
.join(" ");
|
|
41
|
+
}
|
|
42
|
+
const pseudo = state === "hover" ? "hover" : "active";
|
|
43
|
+
const re = new RegExp(`^_bg-\\d*${pseudo}-(.+)$`);
|
|
44
|
+
return classes
|
|
45
|
+
.map((c) => c.match(re)?.[1])
|
|
46
|
+
.filter(Boolean)
|
|
47
|
+
.join(" ");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** The paint a state actually shows: its own class, or the rest paint when it has none. */
|
|
51
|
+
const shown = (el: Element, state: "hover" | "press") => bg(el, state) || bg(el, "rest");
|
|
52
|
+
|
|
53
|
+
const ringed = (el: Element) => /focus-?visible/i.test(el.className);
|
|
54
|
+
const frame = (name: string) => screen.getByRole("button", { name: new RegExp(name) });
|
|
55
|
+
const dump = (el: Element) => `class="${el.className}"`;
|
|
56
|
+
|
|
57
|
+
describe("LC-187: Button resolves coinciding states in one order", () => {
|
|
58
|
+
it("an enabled button lights on hover, steps further on press, and rings on keyboard focus", () => {
|
|
59
|
+
renderWithProviders(<Button>Save</Button>);
|
|
60
|
+
const el = frame("Save");
|
|
61
|
+
expect(bg(el, "hover"), dump(el)).not.toBe("");
|
|
62
|
+
expect(shown(el, "hover"), dump(el)).not.toBe(bg(el, "rest"));
|
|
63
|
+
expect(shown(el, "press"), dump(el)).not.toBe(shown(el, "hover"));
|
|
64
|
+
expect(ringed(el), dump(el)).toBe(true);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("disabled beats hover and press: an explained disable shows its rest paint in every state", () => {
|
|
68
|
+
renderWithProviders(
|
|
69
|
+
<Button disabled disabledReason="Needs owner approval">
|
|
70
|
+
Publish
|
|
71
|
+
</Button>,
|
|
72
|
+
);
|
|
73
|
+
const el = frame("Publish");
|
|
74
|
+
expect(el.getAttribute("aria-disabled")).toBe("true");
|
|
75
|
+
expect(shown(el, "hover"), dump(el)).toBe(bg(el, "rest"));
|
|
76
|
+
expect(shown(el, "press"), dump(el)).toBe(bg(el, "rest"));
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("disabled keeps focus-visible: the explained disable still rings", () => {
|
|
80
|
+
renderWithProviders(
|
|
81
|
+
<Button disabled disabledReason="Needs owner approval">
|
|
82
|
+
Publish
|
|
83
|
+
</Button>,
|
|
84
|
+
);
|
|
85
|
+
expect(ringed(frame("Publish")), dump(frame("Publish"))).toBe(true);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("a plain disable takes no pointer at all, so nothing hovers or presses", () => {
|
|
89
|
+
const onPress = vi.fn();
|
|
90
|
+
renderWithProviders(
|
|
91
|
+
<Button disabled onPress={onPress}>
|
|
92
|
+
Publish
|
|
93
|
+
</Button>,
|
|
94
|
+
);
|
|
95
|
+
const el = frame("Publish");
|
|
96
|
+
expect(el.className, dump(el)).toMatch(/(^|\s)_pe-none(\s|$)/);
|
|
97
|
+
fireEvent.click(el);
|
|
98
|
+
expect(onPress).not.toHaveBeenCalled();
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("loading is disabled plus the spinner: no press, no pointer, the label and the rest paint stay", () => {
|
|
102
|
+
const onPress = vi.fn();
|
|
103
|
+
renderWithProviders(
|
|
104
|
+
<>
|
|
105
|
+
<Button onPress={onPress}>Idle</Button>
|
|
106
|
+
<Button loading onPress={onPress}>
|
|
107
|
+
Busy
|
|
108
|
+
</Button>
|
|
109
|
+
</>,
|
|
110
|
+
);
|
|
111
|
+
const idle = frame("Idle");
|
|
112
|
+
const busy = frame("Busy");
|
|
113
|
+
expect(busy.getAttribute("aria-busy")).toBe("true");
|
|
114
|
+
expect(busy.querySelector("[data-button-loading]")).toBeTruthy();
|
|
115
|
+
expect(busy.textContent).toContain("Busy");
|
|
116
|
+
expect(busy.className, dump(busy)).toMatch(/(^|\s)_pe-none(\s|$)/);
|
|
117
|
+
expect(bg(busy, "rest")).toBe(bg(idle, "rest"));
|
|
118
|
+
fireEvent.click(busy);
|
|
119
|
+
expect(onPress).not.toHaveBeenCalled();
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("selected suppresses hover: a selected fill shows the same paint at rest and under the pointer", () => {
|
|
123
|
+
renderWithProviders(<Button selected>Bold</Button>);
|
|
124
|
+
const el = frame("Bold");
|
|
125
|
+
expect(shown(el, "hover"), dump(el)).toBe(bg(el, "rest"));
|
|
126
|
+
expect(ringed(el), dump(el)).toBe(true);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
// FZ-8932 (FZ-8031 finding): an outlined selected Button rests on $background and
|
|
130
|
+
// lifts to $backgroundHover, a hover on an already-selected control. Once
|
|
131
|
+
// that is fixed this body passes, `it.fails` reports it, and it becomes `it`.
|
|
132
|
+
// A neutral Button's look is its LC-153 tier, so the outlined frame comes
|
|
133
|
+
// from the preset; the use site's `outlined` alone renders it tinted.
|
|
134
|
+
it.fails("selected suppresses hover on an outlined button too", () => {
|
|
135
|
+
renderWithProviders(
|
|
136
|
+
<Preset overrides={{ fillStyle: "outlined" }}>
|
|
137
|
+
<Button selected>Italic</Button>
|
|
138
|
+
</Preset>,
|
|
139
|
+
);
|
|
140
|
+
const el = frame("Italic");
|
|
141
|
+
expect(shown(el, "hover"), dump(el)).toBe(bg(el, "rest"));
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it("error composes: an error button still lights on hover, steps on press, and rings", () => {
|
|
145
|
+
renderWithProviders(<Button error>Delete</Button>);
|
|
146
|
+
const el = frame("Delete");
|
|
147
|
+
expect(shown(el, "hover"), dump(el)).not.toBe(bg(el, "rest"));
|
|
148
|
+
expect(shown(el, "press"), dump(el)).not.toBe(shown(el, "hover"));
|
|
149
|
+
expect(ringed(el), dump(el)).toBe(true);
|
|
150
|
+
});
|
|
151
|
+
});
|
package/src/Chip/Chip.spec.tsx
CHANGED
|
@@ -3,12 +3,20 @@ import { fireEvent } from "@testing-library/react";
|
|
|
3
3
|
import { describe, expect, it, vi } from "vitest";
|
|
4
4
|
import { renderWithProviders } from "@multiplatform.one/test-utils";
|
|
5
5
|
import {
|
|
6
|
+
DEFAULT_SCALE_ID,
|
|
6
7
|
Preset,
|
|
8
|
+
applyThemeScale,
|
|
7
9
|
generateScale,
|
|
8
10
|
defaultScaleInputs,
|
|
9
11
|
createDefaultThemeConfig,
|
|
12
|
+
densePreset,
|
|
10
13
|
} from "@multiplatform.one/theme";
|
|
11
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
CHIP_DISMISS_MIN_TARGET_FINE,
|
|
16
|
+
Chip,
|
|
17
|
+
chipDismissFloorOutset,
|
|
18
|
+
chipFrameGeometry,
|
|
19
|
+
} from "./index";
|
|
12
20
|
|
|
13
21
|
describe("Chip", () => {
|
|
14
22
|
it("renders its label", () => {
|
|
@@ -248,6 +256,39 @@ describe("Chip", () => {
|
|
|
248
256
|
});
|
|
249
257
|
});
|
|
250
258
|
|
|
259
|
+
describe("Chip vertical inset on every preset's scale (lockstep with components Chip, FZ-8018)", () => {
|
|
260
|
+
const sizes = ["$2", "$3", "$4", "$5"] as const;
|
|
261
|
+
|
|
262
|
+
it("draws the default scale exactly as a quarter of padX", () => {
|
|
263
|
+
for (const size of sizes) {
|
|
264
|
+
const { paddingHorizontal, paddingVertical } = chipFrameGeometry(size);
|
|
265
|
+
expect(paddingVertical).toBe(Math.round(paddingHorizontal * 0.25));
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
it("paints a space token on the installed scale wherever the default does", () => {
|
|
270
|
+
const themeConfig = createDefaultThemeConfig({ presets: { dense: densePreset } });
|
|
271
|
+
const dense = themeConfig.scales?.dense;
|
|
272
|
+
const defaults = themeConfig.scales?.[DEFAULT_SCALE_ID];
|
|
273
|
+
if (!dense || !defaults) throw new Error("scales missing");
|
|
274
|
+
const onDefault = Object.fromEntries(
|
|
275
|
+
sizes.map((size) => [size, chipFrameGeometry(size).paddingVertical]),
|
|
276
|
+
);
|
|
277
|
+
const defaultSpace = new Set(Object.values(defaults.tokens.space));
|
|
278
|
+
const denseSpace = new Set(Object.values(dense.tokens.space));
|
|
279
|
+
expect(defaultSpace.has(onDefault.$3)).toBe(true);
|
|
280
|
+
try {
|
|
281
|
+
applyThemeScale(dense);
|
|
282
|
+
for (const size of sizes) {
|
|
283
|
+
if (!defaultSpace.has(onDefault[size])) continue;
|
|
284
|
+
expect(denseSpace.has(chipFrameGeometry(size).paddingVertical)).toBe(true);
|
|
285
|
+
}
|
|
286
|
+
} finally {
|
|
287
|
+
applyThemeScale(defaults);
|
|
288
|
+
}
|
|
289
|
+
});
|
|
290
|
+
});
|
|
291
|
+
|
|
251
292
|
describe("Chip label line height", () => {
|
|
252
293
|
for (const [size, key] of [
|
|
253
294
|
[undefined, "3"],
|
package/src/Chip/index.tsx
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
pressTargetHitSlop,
|
|
10
10
|
radiusStopFromToken,
|
|
11
11
|
resolveRadiusClass,
|
|
12
|
+
scaleSpaceFractionPx,
|
|
12
13
|
sizeRecipeForToken,
|
|
13
14
|
sizeKeyBelow,
|
|
14
15
|
stackRadiusProps,
|
|
@@ -138,18 +139,20 @@ function resolveVariant(variant: ChipVariantProp | undefined): ChipVariant {
|
|
|
138
139
|
* Chip geometry rides the GENERATED `chip` family (LC-75 / LC-80):
|
|
139
140
|
* padX / font / icon / inner gap come from the recipe table — no handwritten
|
|
140
141
|
* per-size px map. The pill has no recipe height: it hugs its label, and the
|
|
141
|
-
* vertical inset
|
|
142
|
-
*
|
|
143
|
-
*
|
|
142
|
+
* vertical inset is a quarter of the recipe padX (preserves the audited $3/$4
|
|
143
|
+
* pill height), painted as the installed scale's `$space` token where the
|
|
144
|
+
* default scale's quarter lands on one (`scaleSpaceFractionPx`). Module-eval
|
|
145
|
+
* objects so styled() can flatten (pilot pattern: InputParts spreads
|
|
146
|
+
* `recipeFamilies.control.boxVariants`).
|
|
144
147
|
*/
|
|
145
148
|
const CHIP_RECIPE_FAMILY = "chip" as const;
|
|
146
149
|
const CHIP_SIZE_TOKENS = ["$2", "$3", "$4", "$5"] as const;
|
|
147
150
|
|
|
148
151
|
function chipPadY(paddingHorizontal: number): number {
|
|
149
|
-
return
|
|
152
|
+
return scaleSpaceFractionPx(paddingHorizontal, 0.25);
|
|
150
153
|
}
|
|
151
154
|
|
|
152
|
-
function chipFrameGeometry(token: string) {
|
|
155
|
+
export function chipFrameGeometry(token: string) {
|
|
153
156
|
const recipe = sizeRecipeForToken(token, { family: CHIP_RECIPE_FAMILY });
|
|
154
157
|
return {
|
|
155
158
|
paddingHorizontal: recipe.paddingHorizontal,
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LC-219 SCROLL-THEN-SHEET on the floating panel's sheet face (FZ-8031), the
|
|
3
|
+
* twin of `SheetModal.scrollThenSheet.spec.tsx`: a drag on the panel's
|
|
4
|
+
* scrolling content scrolls it and leaves the sheet still, and only the
|
|
5
|
+
* grabber zone moves the sheet. The hand-off at the top of the scroll is the
|
|
6
|
+
* open half, the same as on SheetModal.
|
|
7
|
+
*/
|
|
8
|
+
import { renderWithProviders } from "@multiplatform.one/test-utils";
|
|
9
|
+
import { cleanup } from "@testing-library/react";
|
|
10
|
+
import { PanResponder } from "react-native";
|
|
11
|
+
import { afterEach, expect, it, vi } from "vitest";
|
|
12
|
+
import { Text } from "tamagui";
|
|
13
|
+
import { FloatingPanel } from "./index.native";
|
|
14
|
+
|
|
15
|
+
const views = vi.hoisted(() => [] as Array<{ kind: string; props: Record<string, unknown> }>);
|
|
16
|
+
|
|
17
|
+
vi.mock("react-native-web", async (importOriginal) => {
|
|
18
|
+
const actual = await importOriginal<typeof import("react-native")>();
|
|
19
|
+
const React = await import("react");
|
|
20
|
+
const record = (kind: string, Base: unknown) =>
|
|
21
|
+
React.forwardRef((props: Record<string, unknown>, ref) => {
|
|
22
|
+
views.push({ kind, props });
|
|
23
|
+
return React.createElement(Base as never, { ...props, ref });
|
|
24
|
+
});
|
|
25
|
+
return {
|
|
26
|
+
...actual,
|
|
27
|
+
ActionSheetIOS: undefined,
|
|
28
|
+
Modal: ({ children }: { children: import("react").ReactNode }) => children,
|
|
29
|
+
useWindowDimensions: () => ({ width: 390, height: 844, scale: 3, fontScale: 1 }),
|
|
30
|
+
View: record("View", actual.View),
|
|
31
|
+
ScrollView: record("ScrollView", actual.ScrollView),
|
|
32
|
+
Animated: { ...actual.Animated, View: record("View", actual.Animated.View) },
|
|
33
|
+
};
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
afterEach(() => {
|
|
37
|
+
cleanup();
|
|
38
|
+
views.length = 0;
|
|
39
|
+
vi.restoreAllMocks();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
function mountWithSheetPan() {
|
|
43
|
+
const move = vi.fn();
|
|
44
|
+
const claim = () => true;
|
|
45
|
+
vi.spyOn(PanResponder, "create").mockImplementation(
|
|
46
|
+
(config) =>
|
|
47
|
+
({
|
|
48
|
+
panHandlers: { onResponderMove: move, onMoveShouldSetResponder: claim },
|
|
49
|
+
config,
|
|
50
|
+
}) as never,
|
|
51
|
+
);
|
|
52
|
+
renderWithProviders(
|
|
53
|
+
<FloatingPanel open onOpenChange={() => {}} trigger={<Text>Author</Text>} scrollable>
|
|
54
|
+
{Array.from({ length: 40 }, (_, i) => (
|
|
55
|
+
<Text key={i}>{`row ${i + 1}`}</Text>
|
|
56
|
+
))}
|
|
57
|
+
</FloatingPanel>,
|
|
58
|
+
);
|
|
59
|
+
const latest = new Map<unknown, { kind: string; props: Record<string, unknown> }>();
|
|
60
|
+
for (const view of views) latest.set(view.props.testID ?? view, view);
|
|
61
|
+
return { move, claim, mounted: [...latest.values()] };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
it("draws the sheet face at phone width, with a scrolling body", () => {
|
|
65
|
+
const { mounted } = mountWithSheetPan();
|
|
66
|
+
expect(mounted.some((v) => v.props.testID === "floating-panel-sheet")).toBe(true);
|
|
67
|
+
expect(mounted.some((v) => v.kind === "ScrollView")).toBe(true);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("puts the sheet's pan on the grabber zone only", () => {
|
|
71
|
+
const { move, mounted } = mountWithSheetPan();
|
|
72
|
+
const holders = mounted.filter((v) => v.props.onResponderMove === move);
|
|
73
|
+
expect(holders.map((v) => v.props.testID)).toEqual(["floating-panel-grabber"]);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("leaves the sheet still for a pan on the scrolling content", () => {
|
|
77
|
+
const { move, claim, mounted } = mountWithSheetPan();
|
|
78
|
+
for (const scroll of mounted.filter((v) => v.kind === "ScrollView")) {
|
|
79
|
+
expect(scroll.props.onResponderMove).not.toBe(move);
|
|
80
|
+
expect(scroll.props.onMoveShouldSetResponder).not.toBe(claim);
|
|
81
|
+
}
|
|
82
|
+
expect(move).not.toHaveBeenCalled();
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it.todo("at scroll offset 0 a downward pan on the content moves the sheet in the same gesture");
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { renderWithProviders } from "@multiplatform.one/test-utils";
|
|
2
|
+
import { useEffect, useLayoutEffect } from "react";
|
|
3
|
+
import { describe, expect, it, vi } from "vitest";
|
|
4
|
+
import { OsDateTimeSheet } from "./OsDateTimeSheet";
|
|
5
|
+
|
|
6
|
+
const wheel = vi.hoisted(() => ({
|
|
7
|
+
mode: undefined as string | undefined,
|
|
8
|
+
date: undefined as number | undefined,
|
|
9
|
+
minutes: undefined as number | undefined,
|
|
10
|
+
}));
|
|
11
|
+
|
|
12
|
+
vi.mock("react-native-web", async (importOriginal) => ({
|
|
13
|
+
...(await importOriginal<typeof import("react-native")>()),
|
|
14
|
+
useWindowDimensions: () => ({ width: 390, height: 844, scale: 3, fontScale: 1 }),
|
|
15
|
+
}));
|
|
16
|
+
|
|
17
|
+
vi.mock("@multiplatform.one/platform", async (importOriginal) => {
|
|
18
|
+
const actual = (await importOriginal()) as Record<string, unknown>;
|
|
19
|
+
const real = actual.platform as Record<string, unknown>;
|
|
20
|
+
return {
|
|
21
|
+
...actual,
|
|
22
|
+
platform: new Proxy(
|
|
23
|
+
{},
|
|
24
|
+
{
|
|
25
|
+
get(_target, prop) {
|
|
26
|
+
if (prop === "isIos") return true;
|
|
27
|
+
if (prop === "isAndroid") return false;
|
|
28
|
+
return real[prop as string];
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
),
|
|
32
|
+
};
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
// RNDateTimePickerComponentView.updatePropsForPicker on each commit: a changed
|
|
36
|
+
// `date` is set before a changed `mode`, and UIDatePicker only turns a date
|
|
37
|
+
// into a countdown while it is already in countdown mode (its floor is 1 min).
|
|
38
|
+
vi.mock("./dateTimePickerLoader", () => {
|
|
39
|
+
function UIDatePicker(props: { value: Date; mode: string }) {
|
|
40
|
+
useLayoutEffect(() => {
|
|
41
|
+
const instant = props.value.getTime();
|
|
42
|
+
if (instant !== wheel.date) {
|
|
43
|
+
wheel.date = instant;
|
|
44
|
+
if (wheel.mode === "countdown")
|
|
45
|
+
wheel.minutes = props.value.getHours() * 60 + props.value.getMinutes();
|
|
46
|
+
}
|
|
47
|
+
if (props.mode !== wheel.mode) {
|
|
48
|
+
wheel.mode = props.mode;
|
|
49
|
+
if (props.mode === "countdown") wheel.minutes = 1;
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
useEffect(
|
|
53
|
+
() => () => {
|
|
54
|
+
wheel.mode = undefined;
|
|
55
|
+
wheel.date = undefined;
|
|
56
|
+
wheel.minutes = undefined;
|
|
57
|
+
},
|
|
58
|
+
[],
|
|
59
|
+
);
|
|
60
|
+
return <div data-testid="os-picker" />;
|
|
61
|
+
}
|
|
62
|
+
return { loadDateTimePicker: () => UIDatePicker };
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
const at = (h: number, m: number) => {
|
|
66
|
+
const d = new Date(2026, 10, 2);
|
|
67
|
+
d.setHours(h, m, 0, 0);
|
|
68
|
+
return d;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
describe("OsDateTimeSheet countdown opens at the field's value (FZ-8111)", () => {
|
|
72
|
+
it("rests the iOS wheel at 1 h 30 m, not the 1 min floor", () => {
|
|
73
|
+
renderWithProviders(
|
|
74
|
+
<OsDateTimeSheet
|
|
75
|
+
open
|
|
76
|
+
mode="countdown"
|
|
77
|
+
value={at(1, 30)}
|
|
78
|
+
onCancel={vi.fn()}
|
|
79
|
+
onConfirm={vi.fn()}
|
|
80
|
+
/>,
|
|
81
|
+
);
|
|
82
|
+
expect(wheel.mode).toBe("countdown");
|
|
83
|
+
expect(wheel.minutes).toBe(90);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("reaches the value again when the sheet reopens", () => {
|
|
87
|
+
const props = { mode: "countdown" as const, onCancel: vi.fn(), onConfirm: vi.fn() };
|
|
88
|
+
const result = renderWithProviders(<OsDateTimeSheet {...props} open value={at(2, 15)} />);
|
|
89
|
+
expect(wheel.minutes).toBe(135);
|
|
90
|
+
result.rerender(<OsDateTimeSheet {...props} open={false} value={at(0, 45)} />);
|
|
91
|
+
result.rerender(<OsDateTimeSheet {...props} open value={at(0, 45)} />);
|
|
92
|
+
expect(wheel.minutes).toBe(45);
|
|
93
|
+
});
|
|
94
|
+
});
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* does a missing `@react-native-community/datetimepicker` (an optional peer,
|
|
11
11
|
* loaded through `loadDateTimePicker`).
|
|
12
12
|
*/
|
|
13
|
-
import { useCallback, useContext, useState } from "react";
|
|
13
|
+
import { useCallback, useContext, useEffect, useState } from "react";
|
|
14
14
|
import {
|
|
15
15
|
Modal,
|
|
16
16
|
Pressable,
|
|
@@ -88,13 +88,22 @@ export function OsDateTimeSheet({
|
|
|
88
88
|
}, [value]);
|
|
89
89
|
|
|
90
90
|
const [wasOpen, setWasOpen] = useState(false);
|
|
91
|
+
const [countdownPrimed, setCountdownPrimed] = useState(false);
|
|
91
92
|
if (open && !wasOpen) {
|
|
92
93
|
setWasOpen(true);
|
|
93
94
|
setTemp(value);
|
|
94
95
|
setShowTimePicker(false);
|
|
96
|
+
setCountdownPrimed(false);
|
|
95
97
|
} else if (!open && wasOpen) {
|
|
96
98
|
setWasOpen(false);
|
|
97
99
|
}
|
|
100
|
+
// The native picker applies `date` before `mode` on mount, so the countdown
|
|
101
|
+
// wheel drops the first value and rests at 0 h 1 m. A second, different
|
|
102
|
+
// instant after mount lands while the wheel is already in countdown mode.
|
|
103
|
+
const primeCountdown = open && isIOS && pickerMode === "countdown" && !countdownPrimed;
|
|
104
|
+
useEffect(() => {
|
|
105
|
+
if (primeCountdown) setCountdownPrimed(true);
|
|
106
|
+
}, [primeCountdown]);
|
|
98
107
|
|
|
99
108
|
function handleAndroidChange(event: DateTimePickerEvent, selected?: Date) {
|
|
100
109
|
if (event.type === "dismissed") {
|
|
@@ -241,7 +250,7 @@ export function OsDateTimeSheet({
|
|
|
241
250
|
</XStack>
|
|
242
251
|
<YStack {...knobProps.panelPadding} flexShrink={1} minHeight={0}>
|
|
243
252
|
<DateTimePicker
|
|
244
|
-
value={temp}
|
|
253
|
+
value={primeCountdown ? new Date(temp.getTime() + 1) : temp}
|
|
245
254
|
mode={pickerMode}
|
|
246
255
|
display={iosDisplay}
|
|
247
256
|
themeVariant={themeVariant}
|
package/types/Chip/index.d.ts
CHANGED
|
@@ -22,6 +22,11 @@ export interface ChipProps {
|
|
|
22
22
|
groupPosition?: GroupPosition;
|
|
23
23
|
groupOrientation?: GroupOrientation;
|
|
24
24
|
}
|
|
25
|
+
export declare function chipFrameGeometry(token: string): {
|
|
26
|
+
paddingHorizontal: number;
|
|
27
|
+
paddingVertical: number;
|
|
28
|
+
gap: number;
|
|
29
|
+
};
|
|
25
30
|
/**
|
|
26
31
|
* Exported so chip-scale companions (e.g. the Tags "Add Tag" trigger) share
|
|
27
32
|
* the exact same size variants — padding/font stay in lockstep with Chip for
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/Chip/index.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAiB,SAAS,EAAE,MAAM,OAAO,CAAC;AAGtD,OAAO,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/Chip/index.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAiB,SAAS,EAAE,MAAM,OAAO,CAAC;AAGtD,OAAO,EAaL,KAAK,gBAAgB,EACrB,KAAK,aAAa,EACnB,MAAM,0BAA0B,CAAC;AAClC,OAAO,KAAK,EAA+B,QAAQ,EAAE,MAAM,SAAS,CAAC;AAMrE,MAAM,MAAM,WAAW,GAAG,OAAO,GAAG,QAAQ,GAAG,SAAS,CAAC;AACzD,MAAM,MAAM,SAAS,GAAG,MAAM,GAAG,KAAK,GAAG,OAAO,GAAG,MAAM,GAAG,QAAQ,GAAG,QAAQ,GAAG,QAAQ,CAAC;AAC3F,MAAM,MAAM,QAAQ,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC;AAEjD,gFAAgF;AAChF,MAAM,MAAM,eAAe,GAAG,WAAW,GAAG,QAAQ,GAAG,UAAU,GAAG,OAAO,CAAC;AAE5E,MAAM,WAAW,SAAS;IACxB,QAAQ,EAAE,SAAS,CAAC;IACpB,IAAI,CAAC,EAAE,QAAQ,CAAC;IAChB,OAAO,CAAC,EAAE,eAAe,CAAC;IAC1B,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,IAAI,CAAC;IACvB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,yEAAyE;IACzE,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB,+EAA+E;IAC/E,IAAI,CAAC,EAAE,SAAS,CAAC;IACjB,oEAAoE;IACpE,aAAa,CAAC,EAAE,aAAa,CAAC;IAC9B,gBAAgB,CAAC,EAAE,gBAAgB,CAAC;CACrC;AA0GD,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM;;;;EAO9C;AAyBD;;;;GAIG;AACH,eAAO,MAAM,SAAS;;;6CA+BpB,CAAC;AAEH,eAAO,MAAM,QAAQ;;;6CAgBnB,CAAC;AAiBH,sFAAsF;AACtF,wBAAgB,YAAY,CAAC,IAAI,EAAE,QAAQ,GAAG,MAAM,CAEnD;AAED;;;;;GAKG;AACH,eAAO,MAAM,4BAA4B,KAAK,CAAC;AAE/C;;;;GAIG;AACH,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE3D;AA2GD,wBAAgB,IAAI,CAAC,EACnB,QAAQ,EACR,IAAI,EACJ,OAAO,EAAE,WAAsB,EAC/B,KAAc,EACd,SAAS,EACT,QAAQ,EACR,QAAgB,EAChB,OAAO,EACP,IAAI,EACJ,aAAa,EACb,gBAA+B,GAChC,EAAE,SAAS,2CA6NX;AAED,MAAM,MAAM,cAAc,GAAG,QAAQ,CAAC,OAAO,SAAS,CAAC,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"OsDateTimeSheet.d.ts","sourceRoot":"","sources":["../../../src/fields/nativeDateTime/OsDateTimeSheet.tsx"],"names":[],"mappings":"AAmCA,MAAM,MAAM,cAAc,GAAG,MAAM,GAAG,MAAM,GAAG,UAAU,GAAG,WAAW,CAAC;AAExE,MAAM,MAAM,oBAAoB,GAAG;IACjC,IAAI,EAAE,OAAO,CAAC;IACd,KAAK,EAAE,IAAI,CAAC;IACZ,IAAI,EAAE,cAAc,CAAC;IACrB,OAAO,CAAC,EAAE,IAAI,CAAC;IACf,OAAO,CAAC,EAAE,IAAI,CAAC;IACf,QAAQ,EAAE,MAAM,IAAI,CAAC;IACrB,SAAS,EAAE,CAAC,KAAK,EAAE,IAAI,KAAK,IAAI,CAAC;CAClC,CAAC;AASF,wBAAgB,eAAe,CAAC,EAC9B,IAAI,EACJ,KAAK,EACL,IAAI,EACJ,OAAO,EACP,OAAO,EACP,QAAQ,EACR,SAAS,GACV,EAAE,oBAAoB,
|
|
1
|
+
{"version":3,"file":"OsDateTimeSheet.d.ts","sourceRoot":"","sources":["../../../src/fields/nativeDateTime/OsDateTimeSheet.tsx"],"names":[],"mappings":"AAmCA,MAAM,MAAM,cAAc,GAAG,MAAM,GAAG,MAAM,GAAG,UAAU,GAAG,WAAW,CAAC;AAExE,MAAM,MAAM,oBAAoB,GAAG;IACjC,IAAI,EAAE,OAAO,CAAC;IACd,KAAK,EAAE,IAAI,CAAC;IACZ,IAAI,EAAE,cAAc,CAAC;IACrB,OAAO,CAAC,EAAE,IAAI,CAAC;IACf,OAAO,CAAC,EAAE,IAAI,CAAC;IACf,QAAQ,EAAE,MAAM,IAAI,CAAC;IACrB,SAAS,EAAE,CAAC,KAAK,EAAE,IAAI,KAAK,IAAI,CAAC;CAClC,CAAC;AASF,wBAAgB,eAAe,CAAC,EAC9B,IAAI,EACJ,KAAK,EACL,IAAI,EACJ,OAAO,EACP,OAAO,EACP,QAAQ,EACR,SAAS,GACV,EAAE,oBAAoB,kDA8MtB"}
|