@multiplatform.one/forms 7.63.3 → 7.64.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 +3 -5
- package/dist/cjs/AdaptivePopup/index.native.js +3 -5
- package/dist/cjs/AdaptivePopup/index.native.js.map +1 -1
- package/dist/cjs/Button/index.cjs +1 -0
- package/dist/cjs/Button/index.native.js +1 -0
- package/dist/cjs/Button/index.native.js.map +1 -1
- package/dist/cjs/fields/ColorPicker/SystemColorField.cjs +106 -0
- package/dist/cjs/fields/ColorPicker/SystemColorField.native.js +110 -0
- package/dist/cjs/fields/ColorPicker/SystemColorField.native.js.map +1 -0
- package/dist/cjs/fields/ColorPicker/index.cjs +36 -4
- package/dist/cjs/fields/ColorPicker/index.native.js +36 -6
- package/dist/cjs/fields/ColorPicker/index.native.js.map +1 -1
- package/dist/cjs/fields/ColorPicker/systemColorWell.cjs +30 -0
- package/dist/cjs/fields/ColorPicker/systemColorWell.gnome.cjs +30 -0
- package/dist/cjs/fields/ColorPicker/systemColorWell.gnome.native.js +33 -0
- package/dist/cjs/fields/ColorPicker/systemColorWell.gnome.native.js.map +1 -0
- package/dist/cjs/fields/ColorPicker/systemColorWell.native.js +111 -0
- package/dist/cjs/fields/ColorPicker/systemColorWell.native.js.map +1 -0
- package/dist/cjs/fields/DatePicker/DatetimePicker.catalog.cjs +27 -0
- package/dist/cjs/fields/DatePicker/DatetimePicker.catalog.native.js +31 -0
- package/dist/cjs/fields/DatePicker/DatetimePicker.catalog.native.js.map +1 -1
- package/dist/cjs/fields/DatePicker/catalog.cjs +30 -1
- package/dist/cjs/fields/DatePicker/catalog.native.js +32 -1
- package/dist/cjs/fields/DatePicker/catalog.native.js.map +1 -1
- package/dist/cjs/fields/DatePicker/parts.cjs +17 -6
- package/dist/cjs/fields/DatePicker/parts.native.js +17 -6
- package/dist/cjs/fields/DatePicker/parts.native.js.map +1 -1
- package/dist/cjs/fields/TimePicker/catalog.cjs +24 -0
- package/dist/cjs/fields/TimePicker/catalog.native.js +28 -0
- package/dist/cjs/fields/TimePicker/catalog.native.js.map +1 -1
- package/dist/cjs/fields/TimePicker/parts.cjs +20 -8
- package/dist/cjs/fields/TimePicker/parts.native.js +20 -8
- package/dist/cjs/fields/TimePicker/parts.native.js.map +1 -1
- package/dist/cjs/shared/focusManagement.cjs +1 -5
- package/dist/cjs/shared/focusManagement.native.js +1 -5
- package/dist/cjs/shared/focusManagement.native.js.map +1 -1
- package/dist/cjs/shared/touchWebPicker.cjs +136 -0
- package/dist/cjs/shared/touchWebPicker.native.js +154 -0
- package/dist/cjs/shared/touchWebPicker.native.js.map +1 -0
- package/dist/esm/AdaptivePopup/index.mjs +4 -6
- package/dist/esm/AdaptivePopup/index.mjs.map +1 -1
- package/dist/esm/AdaptivePopup/index.native.js +4 -6
- package/dist/esm/AdaptivePopup/index.native.js.map +1 -1
- package/dist/esm/Button/index.mjs +2 -1
- package/dist/esm/Button/index.mjs.map +1 -1
- package/dist/esm/Button/index.native.js +2 -1
- package/dist/esm/Button/index.native.js.map +1 -1
- package/dist/esm/fields/ColorPicker/SystemColorField.mjs +81 -0
- package/dist/esm/fields/ColorPicker/SystemColorField.mjs.map +1 -0
- package/dist/esm/fields/ColorPicker/SystemColorField.native.js +82 -0
- package/dist/esm/fields/ColorPicker/SystemColorField.native.js.map +1 -0
- package/dist/esm/fields/ColorPicker/index.mjs +36 -4
- package/dist/esm/fields/ColorPicker/index.mjs.map +1 -1
- package/dist/esm/fields/ColorPicker/index.native.js +36 -6
- package/dist/esm/fields/ColorPicker/index.native.js.map +1 -1
- package/dist/esm/fields/ColorPicker/systemColorWell.gnome.mjs +5 -0
- package/dist/esm/fields/ColorPicker/systemColorWell.gnome.mjs.map +1 -0
- package/dist/esm/fields/ColorPicker/systemColorWell.gnome.native.js +5 -0
- package/dist/esm/fields/ColorPicker/systemColorWell.gnome.native.js.map +1 -0
- package/dist/esm/fields/ColorPicker/systemColorWell.mjs +5 -0
- package/dist/esm/fields/ColorPicker/systemColorWell.mjs.map +1 -0
- package/dist/esm/fields/ColorPicker/systemColorWell.native.js +77 -0
- package/dist/esm/fields/ColorPicker/systemColorWell.native.js.map +1 -0
- package/dist/esm/fields/DatePicker/DatetimePicker.catalog.mjs +27 -0
- package/dist/esm/fields/DatePicker/DatetimePicker.catalog.mjs.map +1 -1
- package/dist/esm/fields/DatePicker/DatetimePicker.catalog.native.js +31 -0
- package/dist/esm/fields/DatePicker/DatetimePicker.catalog.native.js.map +1 -1
- package/dist/esm/fields/DatePicker/catalog.mjs +30 -1
- package/dist/esm/fields/DatePicker/catalog.mjs.map +1 -1
- package/dist/esm/fields/DatePicker/catalog.native.js +32 -1
- package/dist/esm/fields/DatePicker/catalog.native.js.map +1 -1
- package/dist/esm/fields/DatePicker/parts.mjs +17 -6
- package/dist/esm/fields/DatePicker/parts.mjs.map +1 -1
- package/dist/esm/fields/DatePicker/parts.native.js +17 -6
- package/dist/esm/fields/DatePicker/parts.native.js.map +1 -1
- package/dist/esm/fields/TimePicker/catalog.mjs +24 -0
- package/dist/esm/fields/TimePicker/catalog.mjs.map +1 -1
- package/dist/esm/fields/TimePicker/catalog.native.js +28 -0
- package/dist/esm/fields/TimePicker/catalog.native.js.map +1 -1
- package/dist/esm/fields/TimePicker/parts.mjs +20 -8
- package/dist/esm/fields/TimePicker/parts.mjs.map +1 -1
- package/dist/esm/fields/TimePicker/parts.native.js +20 -8
- package/dist/esm/fields/TimePicker/parts.native.js.map +1 -1
- package/dist/esm/shared/focusManagement.mjs +2 -6
- package/dist/esm/shared/focusManagement.mjs.map +1 -1
- package/dist/esm/shared/focusManagement.native.js +2 -6
- package/dist/esm/shared/focusManagement.native.js.map +1 -1
- package/dist/esm/shared/touchWebPicker.mjs +105 -0
- package/dist/esm/shared/touchWebPicker.mjs.map +1 -0
- package/dist/esm/shared/touchWebPicker.native.js +120 -0
- package/dist/esm/shared/touchWebPicker.native.js.map +1 -0
- package/dist/jsx/AdaptivePopup/index.mjs +4 -6
- package/dist/jsx/AdaptivePopup/index.mjs.map +1 -1
- package/dist/jsx/AdaptivePopup/index.native.js +3 -5
- package/dist/jsx/AdaptivePopup/index.native.js.map +1 -1
- package/dist/jsx/Button/index.mjs +2 -1
- package/dist/jsx/Button/index.mjs.map +1 -1
- package/dist/jsx/Button/index.native.js +1 -0
- package/dist/jsx/Button/index.native.js.map +1 -1
- package/dist/jsx/fields/ColorPicker/SystemColorField.mjs +81 -0
- package/dist/jsx/fields/ColorPicker/SystemColorField.mjs.map +1 -0
- package/dist/jsx/fields/ColorPicker/SystemColorField.native.js +110 -0
- package/dist/jsx/fields/ColorPicker/SystemColorField.native.js.map +1 -0
- package/dist/jsx/fields/ColorPicker/index.mjs +36 -4
- package/dist/jsx/fields/ColorPicker/index.mjs.map +1 -1
- package/dist/jsx/fields/ColorPicker/index.native.js +36 -6
- package/dist/jsx/fields/ColorPicker/index.native.js.map +1 -1
- package/dist/jsx/fields/ColorPicker/systemColorWell.gnome.mjs +5 -0
- package/dist/jsx/fields/ColorPicker/systemColorWell.gnome.mjs.map +1 -0
- package/dist/jsx/fields/ColorPicker/systemColorWell.gnome.native.js +33 -0
- package/dist/jsx/fields/ColorPicker/systemColorWell.gnome.native.js.map +1 -0
- package/dist/jsx/fields/ColorPicker/systemColorWell.mjs +5 -0
- package/dist/jsx/fields/ColorPicker/systemColorWell.mjs.map +1 -0
- package/dist/jsx/fields/ColorPicker/systemColorWell.native.js +111 -0
- package/dist/jsx/fields/ColorPicker/systemColorWell.native.js.map +1 -0
- package/dist/jsx/fields/DatePicker/DatetimePicker.catalog.mjs +27 -0
- package/dist/jsx/fields/DatePicker/DatetimePicker.catalog.mjs.map +1 -1
- package/dist/jsx/fields/DatePicker/DatetimePicker.catalog.native.js +31 -0
- package/dist/jsx/fields/DatePicker/DatetimePicker.catalog.native.js.map +1 -1
- package/dist/jsx/fields/DatePicker/catalog.mjs +30 -1
- package/dist/jsx/fields/DatePicker/catalog.mjs.map +1 -1
- package/dist/jsx/fields/DatePicker/catalog.native.js +32 -1
- package/dist/jsx/fields/DatePicker/catalog.native.js.map +1 -1
- package/dist/jsx/fields/DatePicker/parts.mjs +17 -6
- package/dist/jsx/fields/DatePicker/parts.mjs.map +1 -1
- package/dist/jsx/fields/DatePicker/parts.native.js +17 -6
- package/dist/jsx/fields/DatePicker/parts.native.js.map +1 -1
- package/dist/jsx/fields/TimePicker/catalog.mjs +24 -0
- package/dist/jsx/fields/TimePicker/catalog.mjs.map +1 -1
- package/dist/jsx/fields/TimePicker/catalog.native.js +28 -0
- package/dist/jsx/fields/TimePicker/catalog.native.js.map +1 -1
- package/dist/jsx/fields/TimePicker/parts.mjs +20 -8
- package/dist/jsx/fields/TimePicker/parts.mjs.map +1 -1
- package/dist/jsx/fields/TimePicker/parts.native.js +20 -8
- package/dist/jsx/fields/TimePicker/parts.native.js.map +1 -1
- package/dist/jsx/shared/focusManagement.mjs +2 -6
- package/dist/jsx/shared/focusManagement.mjs.map +1 -1
- package/dist/jsx/shared/focusManagement.native.js +1 -5
- package/dist/jsx/shared/focusManagement.native.js.map +1 -1
- package/dist/jsx/shared/touchWebPicker.mjs +105 -0
- package/dist/jsx/shared/touchWebPicker.mjs.map +1 -0
- package/dist/jsx/shared/touchWebPicker.native.js +154 -0
- package/dist/jsx/shared/touchWebPicker.native.js.map +1 -0
- package/package.json +13 -8
- package/src/AdaptivePopup/index.tsx +4 -3
- package/src/Button/index.tsx +4 -0
- package/src/fields/ColorPicker/SystemColorField.tsx +101 -0
- package/src/fields/ColorPicker/index.tsx +48 -13
- package/src/fields/ColorPicker/systemColorWell.gnome.ts +7 -0
- package/src/fields/ColorPicker/systemColorWell.native.spec.tsx +266 -0
- package/src/fields/ColorPicker/systemColorWell.native.tsx +144 -0
- package/src/fields/ColorPicker/systemColorWell.ts +21 -0
- package/src/fields/DatePicker/DatePicker.stories.tsx +27 -1
- package/src/fields/DatePicker/DatetimePicker.catalog.tsx +45 -1
- package/src/fields/DatePicker/catalog.tsx +48 -8
- package/src/fields/DatePicker/parts.tsx +19 -4
- package/src/fields/DatePicker/touchWeb.spec.tsx +245 -0
- package/src/fields/TimePicker/catalog.tsx +33 -0
- package/src/fields/TimePicker/parts.tsx +19 -6
- package/src/overlayGround.spec.ts +50 -0
- package/src/shared/focusManagement.ts +7 -3
- package/src/shared/touchWebPicker.spec.ts +98 -0
- package/src/shared/touchWebPicker.tsx +158 -0
- package/types/AdaptivePopup/index.d.ts.map +1 -1
- package/types/Button/index.d.ts.map +1 -1
- package/types/fields/ColorPicker/SystemColorField.d.ts +22 -0
- package/types/fields/ColorPicker/SystemColorField.d.ts.map +1 -0
- package/types/fields/ColorPicker/index.d.ts.map +1 -1
- package/types/fields/ColorPicker/systemColorWell.d.ts +18 -0
- package/types/fields/ColorPicker/systemColorWell.d.ts.map +1 -0
- package/types/fields/ColorPicker/systemColorWell.gnome.d.ts +5 -0
- package/types/fields/ColorPicker/systemColorWell.gnome.d.ts.map +1 -0
- package/types/fields/ColorPicker/systemColorWell.native.d.ts +55 -0
- package/types/fields/ColorPicker/systemColorWell.native.d.ts.map +1 -0
- package/types/fields/DatePicker/DatetimePicker.catalog.d.ts +1 -1
- package/types/fields/DatePicker/DatetimePicker.catalog.d.ts.map +1 -1
- package/types/fields/DatePicker/catalog.d.ts.map +1 -1
- package/types/fields/DatePicker/parts.d.ts +6 -1
- package/types/fields/DatePicker/parts.d.ts.map +1 -1
- package/types/fields/RadioGroup/index.d.ts +2 -2
- package/types/fields/TimePicker/catalog.d.ts.map +1 -1
- package/types/fields/TimePicker/parts.d.ts +3 -1
- package/types/fields/TimePicker/parts.d.ts.map +1 -1
- package/types/shared/focusManagement.d.ts.map +1 -1
- package/types/shared/touchWebPicker.d.ts +40 -0
- package/types/shared/touchWebPicker.d.ts.map +1 -0
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LC-266: on iOS the Color field opens the system colour picker
|
|
3
|
+
* (UIColorPickerViewController) through @expo/ui's SwiftUI ColorPicker.
|
|
4
|
+
*
|
|
5
|
+
* @expo/ui is native code, so an OTA can carry this JS to a binary built
|
|
6
|
+
* without it. `requireOptionalNativeModule("ExpoUI")` answers null there and
|
|
7
|
+
* the field keeps the MPO picker. The probe runs before `@expo/ui` is
|
|
8
|
+
* required: its views call `requireNativeView` at module load, which throws
|
|
9
|
+
* on a binary that lacks them.
|
|
10
|
+
*/
|
|
11
|
+
import type { ComponentType, ReactNode } from "react";
|
|
12
|
+
import { Platform } from "react-native";
|
|
13
|
+
import type { SystemColorWellProps } from "./systemColorWell";
|
|
14
|
+
|
|
15
|
+
export type { SystemColorWellProps } from "./systemColorWell";
|
|
16
|
+
|
|
17
|
+
export const expoUiNativeModule = "ExpoUI";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* The SwiftUI well is a 28pt circle and only the well opens the picker, so it
|
|
21
|
+
* is drawn nearly transparent and scaled past the field's bounds: the whole
|
|
22
|
+
* field becomes the well's hit area while the MPO swatch and hex stay visible.
|
|
23
|
+
* Below 0.01 alpha UIKit stops hit-testing a view.
|
|
24
|
+
*/
|
|
25
|
+
export const wellPoints = 28;
|
|
26
|
+
export const wellOpacity = 0.02;
|
|
27
|
+
|
|
28
|
+
export function wellScale(width: number, height: number): number {
|
|
29
|
+
return Math.max(1, Math.ceil((Math.max(width, height) * 2) / wellPoints));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
type Modifier = unknown;
|
|
33
|
+
|
|
34
|
+
export interface SwiftUiColorModule {
|
|
35
|
+
Host: ComponentType<{
|
|
36
|
+
children?: ReactNode;
|
|
37
|
+
colorScheme?: "light" | "dark";
|
|
38
|
+
style?: SystemColorWellProps["style"];
|
|
39
|
+
}>;
|
|
40
|
+
ColorPicker: ComponentType<{
|
|
41
|
+
selection: string | null;
|
|
42
|
+
label?: string;
|
|
43
|
+
supportsOpacity?: boolean;
|
|
44
|
+
onSelectionChange?: (value: string) => void;
|
|
45
|
+
modifiers?: Modifier[];
|
|
46
|
+
}>;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface SwiftUiModifiers {
|
|
50
|
+
labelsHidden: () => Modifier;
|
|
51
|
+
scaleEffect: (scale: number) => Modifier;
|
|
52
|
+
opacity: (value: number) => Modifier;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface SystemColorWellSources {
|
|
56
|
+
core: () => { requireOptionalNativeModule?: (name: string) => unknown };
|
|
57
|
+
ui: () => Partial<SwiftUiColorModule>;
|
|
58
|
+
modifiers: () => Partial<SwiftUiModifiers>;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Each `require` sits in its own `try` so Metro bundles an app without
|
|
63
|
+
* @expo/ui (an optional peer) instead of failing the build (FZ-7357).
|
|
64
|
+
*/
|
|
65
|
+
const appSources: SystemColorWellSources = {
|
|
66
|
+
core: () => {
|
|
67
|
+
try {
|
|
68
|
+
return require("expo-modules-core");
|
|
69
|
+
} catch {
|
|
70
|
+
return {};
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
ui: () => {
|
|
74
|
+
try {
|
|
75
|
+
return require("@expo/ui/swift-ui");
|
|
76
|
+
} catch {
|
|
77
|
+
return {};
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
modifiers: () => {
|
|
81
|
+
try {
|
|
82
|
+
return require("@expo/ui/swift-ui/modifiers");
|
|
83
|
+
} catch {
|
|
84
|
+
return {};
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
export function makeSystemColorWell(
|
|
90
|
+
ui: SwiftUiColorModule,
|
|
91
|
+
modifiers: SwiftUiModifiers,
|
|
92
|
+
): ComponentType<SystemColorWellProps> {
|
|
93
|
+
return function SystemColorWell({
|
|
94
|
+
selection,
|
|
95
|
+
supportsOpacity,
|
|
96
|
+
label,
|
|
97
|
+
colorScheme,
|
|
98
|
+
width,
|
|
99
|
+
height,
|
|
100
|
+
onSelectionChange,
|
|
101
|
+
style,
|
|
102
|
+
}: SystemColorWellProps) {
|
|
103
|
+
return (
|
|
104
|
+
<ui.Host colorScheme={colorScheme} style={style}>
|
|
105
|
+
<ui.ColorPicker
|
|
106
|
+
label={label}
|
|
107
|
+
selection={selection || null}
|
|
108
|
+
supportsOpacity={supportsOpacity}
|
|
109
|
+
onSelectionChange={onSelectionChange}
|
|
110
|
+
modifiers={[
|
|
111
|
+
modifiers.labelsHidden(),
|
|
112
|
+
modifiers.scaleEffect(wellScale(width, height)),
|
|
113
|
+
modifiers.opacity(wellOpacity),
|
|
114
|
+
]}
|
|
115
|
+
/>
|
|
116
|
+
</ui.Host>
|
|
117
|
+
);
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** The system colour well, or null when this platform or binary cannot draw it. */
|
|
122
|
+
export function resolveSystemColorWell(
|
|
123
|
+
sources: SystemColorWellSources,
|
|
124
|
+
os: string,
|
|
125
|
+
): ComponentType<SystemColorWellProps> | null {
|
|
126
|
+
if (os !== "ios") return null;
|
|
127
|
+
try {
|
|
128
|
+
if (sources.core().requireOptionalNativeModule?.(expoUiNativeModule) == null) return null;
|
|
129
|
+
const ui = sources.ui();
|
|
130
|
+
const modifiers = sources.modifiers();
|
|
131
|
+
if (!ui.Host || !ui.ColorPicker) return null;
|
|
132
|
+
if (!modifiers.labelsHidden || !modifiers.scaleEffect || !modifiers.opacity) return null;
|
|
133
|
+
return makeSystemColorWell(ui as SwiftUiColorModule, modifiers as SwiftUiModifiers);
|
|
134
|
+
} catch {
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
let cached: ComponentType<SystemColorWellProps> | null | undefined;
|
|
140
|
+
|
|
141
|
+
export function useSystemColorWell(): ComponentType<SystemColorWellProps> | null {
|
|
142
|
+
if (cached === undefined) cached = resolveSystemColorWell(appSources, Platform.OS);
|
|
143
|
+
return cached;
|
|
144
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { ComponentType } from "react";
|
|
2
|
+
import type { StyleProp, ViewStyle } from "react-native";
|
|
3
|
+
|
|
4
|
+
export interface SystemColorWellProps {
|
|
5
|
+
/** `#RRGGBB`, or `#RRGGBBAA` with opacity; empty for no colour. */
|
|
6
|
+
selection: string;
|
|
7
|
+
supportsOpacity: boolean;
|
|
8
|
+
/** Spoken name; the visible label stays the field's. */
|
|
9
|
+
label: string;
|
|
10
|
+
colorScheme: "light" | "dark";
|
|
11
|
+
/** The area the well covers, in points. */
|
|
12
|
+
width: number;
|
|
13
|
+
height: number;
|
|
14
|
+
onSelectionChange: (hex: string) => void;
|
|
15
|
+
style?: StyleProp<ViewStyle>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Web: the MPO picker is the platform form (LC-266). */
|
|
19
|
+
export function useSystemColorWell(): ComponentType<SystemColorWellProps> | null {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
@@ -13,7 +13,8 @@ import { DatetimePicker } from "./DatetimePicker";
|
|
|
13
13
|
import { MonthPicker } from "./MonthPicker";
|
|
14
14
|
import { MultiDatePicker } from "./MultiDatePicker";
|
|
15
15
|
import { DatePicker } from "./index";
|
|
16
|
-
import { Text } from "@multiplatform.one/theme";
|
|
16
|
+
import { formatCalendarDate, Text } from "@multiplatform.one/theme";
|
|
17
|
+
import { TimePicker } from "../TimePicker";
|
|
17
18
|
|
|
18
19
|
const meta: Meta<typeof DatePicker> = {
|
|
19
20
|
title: "Forms/DatePicker",
|
|
@@ -128,6 +129,31 @@ export const StateContract: Story = {
|
|
|
128
129
|
},
|
|
129
130
|
};
|
|
130
131
|
|
|
132
|
+
/**
|
|
133
|
+
* LC-266 (FZ-8114): on a coarse-pointer browser each field opens the
|
|
134
|
+
* browser's own picker; a fine pointer keeps the MPO calendar and wheels. The
|
|
135
|
+
* lines below show the committed value, which is the same on both paths.
|
|
136
|
+
*/
|
|
137
|
+
export const TouchWeb: Story = {
|
|
138
|
+
render: () => {
|
|
139
|
+
const [date, setDate] = useState<Date | null>(new Date(2026, 2, 5));
|
|
140
|
+
const [time, setTime] = useState("14:30");
|
|
141
|
+
const [datetime, setDatetime] = useState<Date | null>(new Date(2026, 2, 5, 14, 30));
|
|
142
|
+
return (
|
|
143
|
+
<YStack gap="$4" maxWidth={400}>
|
|
144
|
+
<DatePicker label="Date" value={date} onChange={setDate} />
|
|
145
|
+
<TimePicker label="Time" value={time} onChange={setTime} />
|
|
146
|
+
<DatetimePicker label="Date and time" value={datetime} onChange={setDatetime} />
|
|
147
|
+
<YStack gap="$1">
|
|
148
|
+
<Text data-testid="touch-web-date">{`Date value: ${formatCalendarDate(date, "date") || "empty"}`}</Text>
|
|
149
|
+
<Text data-testid="touch-web-time">{`Time value: ${time || "empty"}`}</Text>
|
|
150
|
+
<Text data-testid="touch-web-datetime">{`Date and time value: ${formatCalendarDate(datetime, "datetime") || "empty"}`}</Text>
|
|
151
|
+
</YStack>
|
|
152
|
+
</YStack>
|
|
153
|
+
);
|
|
154
|
+
},
|
|
155
|
+
};
|
|
156
|
+
|
|
131
157
|
export const Basic: Story = {
|
|
132
158
|
render: (args) => <DatePicker {...args} />,
|
|
133
159
|
args: {
|
|
@@ -19,6 +19,12 @@ import type { FormFieldProps } from "../../fieldLayout";
|
|
|
19
19
|
import { Input as InputParts } from "../../InputParts";
|
|
20
20
|
import { Skeleton } from "../../Skeleton";
|
|
21
21
|
import type { FieldComponentProps, Validator } from "../../types";
|
|
22
|
+
import {
|
|
23
|
+
fromNativeDateValue,
|
|
24
|
+
toDatetimeLocalValue,
|
|
25
|
+
TouchWebPickerInput,
|
|
26
|
+
useCoarsePointer,
|
|
27
|
+
} from "../../shared/touchWebPicker";
|
|
22
28
|
|
|
23
29
|
import {
|
|
24
30
|
CalendarDays,
|
|
@@ -49,7 +55,9 @@ export type DatetimePickerProps<
|
|
|
49
55
|
TData extends DeepValue<TParentData, TName> = DeepValue<TParentData, TName>,
|
|
50
56
|
> = Omit<
|
|
51
57
|
FormFieldProps<TParentData, TName, TFieldValidator, TFormValidator, TData>,
|
|
52
|
-
|
|
58
|
+
// FormFieldProps carries YStack's DOM ChangeEventHandler; the field's own
|
|
59
|
+
// value callback replaces it, same as DatePicker.
|
|
60
|
+
"children" | "field" | "onChange"
|
|
53
61
|
> &
|
|
54
62
|
Partial<
|
|
55
63
|
Omit<
|
|
@@ -378,6 +386,7 @@ export function DatetimePicker<
|
|
|
378
386
|
|
|
379
387
|
const { resolvedForm, knobProps, id } = useFormField({ form, id: idProp });
|
|
380
388
|
const resolvedValidators = useResolvedValidators(required, validators, label, name);
|
|
389
|
+
const touchWeb = useCoarsePointer();
|
|
381
390
|
|
|
382
391
|
const [open, setOpen] = useState(false);
|
|
383
392
|
const [selectedDatetime, setSelectedDatetime] = useState<Date | null>(() =>
|
|
@@ -461,6 +470,40 @@ export function DatetimePicker<
|
|
|
461
470
|
/>
|
|
462
471
|
);
|
|
463
472
|
|
|
473
|
+
const renderTouchTrigger = (
|
|
474
|
+
hasError?: boolean,
|
|
475
|
+
blurHandler?: (...args: any[]) => void,
|
|
476
|
+
fieldChange?: (value: any) => void,
|
|
477
|
+
) => (
|
|
478
|
+
<DatePickerInputTrigger
|
|
479
|
+
value={displayValue}
|
|
480
|
+
placeholder={placeholder}
|
|
481
|
+
disabled={disabled}
|
|
482
|
+
onReset={() => handleClearDatetime(fieldChange)}
|
|
483
|
+
onButtonPress={() => {}}
|
|
484
|
+
touchInput={
|
|
485
|
+
<TouchWebPickerInput
|
|
486
|
+
kind="datetime-local"
|
|
487
|
+
id={id}
|
|
488
|
+
value={toDatetimeLocalValue(selectedDatetime, showSeconds)}
|
|
489
|
+
min={toDatetimeLocalValue(minDate, showSeconds)}
|
|
490
|
+
max={toDatetimeLocalValue(maxDate, showSeconds)}
|
|
491
|
+
showSeconds={showSeconds}
|
|
492
|
+
disabled={disabled}
|
|
493
|
+
required={required}
|
|
494
|
+
invalid={hasError}
|
|
495
|
+
aria-label={label ? undefined : t("Date and time")}
|
|
496
|
+
onValue={(next) => {
|
|
497
|
+
const date = fromNativeDateValue(next);
|
|
498
|
+
if (date) handleDatetimeSelect(date, fieldChange);
|
|
499
|
+
else handleClearDatetime(fieldChange);
|
|
500
|
+
}}
|
|
501
|
+
onBlur={blurHandler}
|
|
502
|
+
/>
|
|
503
|
+
}
|
|
504
|
+
/>
|
|
505
|
+
);
|
|
506
|
+
|
|
464
507
|
const renderPopover = (
|
|
465
508
|
hasError?: boolean,
|
|
466
509
|
blurHandler?: (...args: any[]) => void,
|
|
@@ -478,6 +521,7 @@ export function DatetimePicker<
|
|
|
478
521
|
/>
|
|
479
522
|
);
|
|
480
523
|
}
|
|
524
|
+
if (touchWeb) return renderTouchTrigger(hasError, blurHandler, fieldChange);
|
|
481
525
|
const handleOpenChange = (isOpen: boolean) => {
|
|
482
526
|
// Re-seed the visible month from the current value on every open.
|
|
483
527
|
if (isOpen && selectedDatetimeRef.current) setDisplayDate(selectedDatetimeRef.current);
|
|
@@ -16,6 +16,12 @@ import type { FormFieldProps } from "../../fieldLayout";
|
|
|
16
16
|
import { Input as InputParts } from "../../InputParts";
|
|
17
17
|
import { Skeleton } from "../../Skeleton";
|
|
18
18
|
import type { FieldComponentProps, Validator } from "../../types";
|
|
19
|
+
import {
|
|
20
|
+
fromNativeDateValue,
|
|
21
|
+
toDateInputValue,
|
|
22
|
+
TouchWebPickerInput,
|
|
23
|
+
useCoarsePointer,
|
|
24
|
+
} from "../../shared/touchWebPicker";
|
|
19
25
|
|
|
20
26
|
import {
|
|
21
27
|
CalendarDays,
|
|
@@ -211,6 +217,7 @@ export function DatePicker<
|
|
|
211
217
|
const { resolvedForm, knobProps, id } = useFormField({ form, id: idProp });
|
|
212
218
|
const resolvedValidators = useResolvedValidators(required, validators, label, name);
|
|
213
219
|
const hasLabel = Boolean(label);
|
|
220
|
+
const touchWeb = useCoarsePointer();
|
|
214
221
|
|
|
215
222
|
const [open, setOpen] = useState(false);
|
|
216
223
|
const [selectedDate, setSelectedDate] = useState<Date | null>(() =>
|
|
@@ -293,6 +300,46 @@ export function DatePicker<
|
|
|
293
300
|
);
|
|
294
301
|
}
|
|
295
302
|
|
|
303
|
+
const describedBy =
|
|
304
|
+
[
|
|
305
|
+
helperText && id ? `${id}-description` : undefined,
|
|
306
|
+
hasError && id ? `${id}-error` : undefined,
|
|
307
|
+
]
|
|
308
|
+
.filter(Boolean)
|
|
309
|
+
.join(" ") || undefined;
|
|
310
|
+
|
|
311
|
+
if (touchWeb) {
|
|
312
|
+
return (
|
|
313
|
+
<DatePickerInputTrigger
|
|
314
|
+
value={formatDateDisplay(selectedDate)}
|
|
315
|
+
placeholder={placeholder}
|
|
316
|
+
disabled={disabled}
|
|
317
|
+
onReset={() => handleClearDate(fieldChange)}
|
|
318
|
+
onButtonPress={() => {}}
|
|
319
|
+
touchInput={
|
|
320
|
+
<TouchWebPickerInput
|
|
321
|
+
kind="date"
|
|
322
|
+
id={id}
|
|
323
|
+
value={toDateInputValue(selectedDate)}
|
|
324
|
+
min={toDateInputValue(minDate)}
|
|
325
|
+
max={toDateInputValue(maxDate)}
|
|
326
|
+
disabled={disabled}
|
|
327
|
+
required={required}
|
|
328
|
+
invalid={hasError}
|
|
329
|
+
aria-label={hasLabel ? undefined : t("Date")}
|
|
330
|
+
aria-describedby={describedBy}
|
|
331
|
+
onValue={(next) => {
|
|
332
|
+
const date = fromNativeDateValue(next);
|
|
333
|
+
if (date) handleDateSelect(date, fieldChange);
|
|
334
|
+
else handleClearDate(fieldChange);
|
|
335
|
+
}}
|
|
336
|
+
onBlur={blurHandler}
|
|
337
|
+
/>
|
|
338
|
+
}
|
|
339
|
+
/>
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
|
|
296
343
|
const handleOpenChange = (isOpen: boolean) => {
|
|
297
344
|
// Re-seed the visible month from the current value on every open —
|
|
298
345
|
// the value may have changed (externally or by a pick) since mount.
|
|
@@ -336,14 +383,7 @@ export function DatePicker<
|
|
|
336
383
|
// Labelled: FieldLayout's Label wires `aria-labelledby` onto this
|
|
337
384
|
// id itself — naming it here too doubles the spoken name.
|
|
338
385
|
aria-label={hasLabel ? undefined : t("Date")}
|
|
339
|
-
aria-describedby={
|
|
340
|
-
[
|
|
341
|
-
helperText && id ? `${id}-description` : undefined,
|
|
342
|
-
hasError && id ? `${id}-error` : undefined,
|
|
343
|
-
]
|
|
344
|
-
.filter(Boolean)
|
|
345
|
-
.join(" ") || undefined
|
|
346
|
-
}
|
|
386
|
+
aria-describedby={describedBy}
|
|
347
387
|
aria-required={required || undefined}
|
|
348
388
|
aria-invalid={hasError || undefined}
|
|
349
389
|
/>
|
|
@@ -861,6 +861,11 @@ interface DatePickerInputTriggerProps {
|
|
|
861
861
|
"aria-describedby"?: string;
|
|
862
862
|
"aria-required"?: boolean;
|
|
863
863
|
"aria-invalid"?: boolean;
|
|
864
|
+
/**
|
|
865
|
+
* Touch web (LC-266): the browser's own picker input, laid over the box.
|
|
866
|
+
* The input then owns the tap, the focus and the field's a11y name.
|
|
867
|
+
*/
|
|
868
|
+
touchInput?: ReactNode;
|
|
864
869
|
}
|
|
865
870
|
|
|
866
871
|
export function DatePickerClearButton({
|
|
@@ -900,6 +905,7 @@ export function DatePickerInputTrigger({
|
|
|
900
905
|
"aria-describedby": ariaDescribedBy,
|
|
901
906
|
"aria-required": ariaRequired,
|
|
902
907
|
"aria-invalid": ariaInvalid,
|
|
908
|
+
touchInput,
|
|
903
909
|
}: DatePickerInputTriggerProps) {
|
|
904
910
|
const handleTriggerKey = (
|
|
905
911
|
e: PanelActionEvent & { key?: string; code?: string; preventDefault?: () => void },
|
|
@@ -942,10 +948,10 @@ export function DatePickerInputTrigger({
|
|
|
942
948
|
// on the closed text-field trigger.
|
|
943
949
|
elevation={undefined}
|
|
944
950
|
data-testid="date-picker-trigger"
|
|
945
|
-
id={id}
|
|
946
|
-
onPress={isWeb && !disabled ? onButtonPress : undefined}
|
|
951
|
+
id={touchInput ? undefined : id}
|
|
952
|
+
onPress={isWeb && !disabled && !touchInput ? onButtonPress : undefined}
|
|
947
953
|
{...(disabled ? { opacity: 0.5, pointerEvents: "none" } : undefined)}
|
|
948
|
-
{...(isWeb
|
|
954
|
+
{...(isWeb && !touchInput
|
|
949
955
|
? {
|
|
950
956
|
role: "combobox" as const,
|
|
951
957
|
"aria-haspopup": "dialog",
|
|
@@ -960,6 +966,7 @@ export function DatePickerInputTrigger({
|
|
|
960
966
|
}
|
|
961
967
|
: undefined)}
|
|
962
968
|
>
|
|
969
|
+
{touchInput}
|
|
963
970
|
<InputParts.Section data-testid="date-picker-glyph">
|
|
964
971
|
<InputParts.Icon adornment="leading" color={formCommonColors.muted}>
|
|
965
972
|
<Glyph />
|
|
@@ -970,6 +977,7 @@ export function DatePickerInputTrigger({
|
|
|
970
977
|
readOnly
|
|
971
978
|
pointerEvents="none"
|
|
972
979
|
tabIndex={-1}
|
|
980
|
+
aria-hidden={touchInput ? true : undefined}
|
|
973
981
|
value={value}
|
|
974
982
|
placeholder={placeholder}
|
|
975
983
|
disabled={disabled}
|
|
@@ -979,7 +987,14 @@ export function DatePickerInputTrigger({
|
|
|
979
987
|
</InputParts.Section>
|
|
980
988
|
{value && isWeb ? (
|
|
981
989
|
<InputParts.Section>
|
|
982
|
-
|
|
990
|
+
{touchInput ? (
|
|
991
|
+
// Above the touch input, so Clear stays reachable on the box.
|
|
992
|
+
<View zIndex={2}>
|
|
993
|
+
<DatePickerClearButton onReset={onReset} disabled={disabled} />
|
|
994
|
+
</View>
|
|
995
|
+
) : (
|
|
996
|
+
<DatePickerClearButton onReset={onReset} disabled={disabled} />
|
|
997
|
+
)}
|
|
983
998
|
</InputParts.Section>
|
|
984
999
|
) : null}
|
|
985
1000
|
</InputParts.Box>
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FZ-8114 (LC-266): Date, Time and Datetime on a coarse-pointer web viewport
|
|
3
|
+
* hand the pick to the browser's own control, and a fine pointer keeps the
|
|
4
|
+
* MPO calendar. Both paths must commit the same value.
|
|
5
|
+
*/
|
|
6
|
+
import { formatCalendarDate } from "@multiplatform.one/theme";
|
|
7
|
+
import { useForm } from "@tanstack/react-form";
|
|
8
|
+
import { act, fireEvent, waitFor } from "@testing-library/react";
|
|
9
|
+
import { renderWithProviders } from "@multiplatform.one/test-utils";
|
|
10
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
11
|
+
import { Button } from "../../Button";
|
|
12
|
+
import { Form } from "../../Form";
|
|
13
|
+
import { TimePicker } from "../TimePicker";
|
|
14
|
+
import { DatePicker, DatetimePicker } from "./index";
|
|
15
|
+
|
|
16
|
+
let coarse = false;
|
|
17
|
+
const originalMatchMedia = window.matchMedia;
|
|
18
|
+
|
|
19
|
+
beforeEach(() => {
|
|
20
|
+
Object.defineProperty(window, "matchMedia", {
|
|
21
|
+
configurable: true,
|
|
22
|
+
writable: true,
|
|
23
|
+
value: (media: string) => ({
|
|
24
|
+
media,
|
|
25
|
+
matches: media === "(pointer: coarse)" ? coarse : false,
|
|
26
|
+
onchange: null,
|
|
27
|
+
addEventListener: () => {},
|
|
28
|
+
removeEventListener: () => {},
|
|
29
|
+
addListener: () => {},
|
|
30
|
+
removeListener: () => {},
|
|
31
|
+
dispatchEvent: () => false,
|
|
32
|
+
}),
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
afterEach(() => {
|
|
37
|
+
coarse = false;
|
|
38
|
+
Object.defineProperty(window, "matchMedia", {
|
|
39
|
+
configurable: true,
|
|
40
|
+
writable: true,
|
|
41
|
+
value: originalMatchMedia,
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
function touchInput(root: ParentNode, type: string): HTMLInputElement {
|
|
46
|
+
const input = root.querySelector(`input[type="${type}"]`) as HTMLInputElement | null;
|
|
47
|
+
expect(input).not.toBeNull();
|
|
48
|
+
return input as HTMLInputElement;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function dayButton(root: ParentNode, day: string): HTMLElement {
|
|
52
|
+
const match = Array.from(root.querySelectorAll('button, [role="button"]')).find(
|
|
53
|
+
(el) => el.textContent?.trim() === day && el.getAttribute("aria-disabled") !== "true",
|
|
54
|
+
);
|
|
55
|
+
expect(match).toBeTruthy();
|
|
56
|
+
return match as HTMLElement;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
describe("Date on touch web (FZ-8114)", () => {
|
|
60
|
+
it("a coarse pointer renders the browser's date input, labelled by the field", () => {
|
|
61
|
+
coarse = true;
|
|
62
|
+
const result = renderWithProviders(<DatePicker label="Due" name="due" />);
|
|
63
|
+
const input = touchInput(result.container, "date");
|
|
64
|
+
const label = result.container.querySelector("label");
|
|
65
|
+
expect(label?.getAttribute("for")).toBe(input.id);
|
|
66
|
+
expect(result.container.querySelector('[role="combobox"]')).toBeNull();
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("a fine pointer keeps the MPO combobox and no date input", () => {
|
|
70
|
+
const result = renderWithProviders(<DatePicker label="Due" name="due" />);
|
|
71
|
+
expect(result.container.querySelector('input[type="date"]')).toBeNull();
|
|
72
|
+
expect(result.container.querySelector('[role="combobox"]')).not.toBeNull();
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("the picked string shows in the MPO box and emits the calendar's Date", async () => {
|
|
76
|
+
coarse = true;
|
|
77
|
+
const onChange = vi.fn();
|
|
78
|
+
const result = renderWithProviders(<DatePicker label="Due" name="due" onChange={onChange} />);
|
|
79
|
+
await act(async () => {
|
|
80
|
+
fireEvent.change(touchInput(result.container, "date"), { target: { value: "2026-03-05" } });
|
|
81
|
+
});
|
|
82
|
+
expect(onChange).toHaveBeenCalledTimes(1);
|
|
83
|
+
const picked = onChange.mock.calls[0][0] as Date;
|
|
84
|
+
expect(picked.getTime()).toBe(new Date(2026, 2, 5).getTime());
|
|
85
|
+
expect(result.container.querySelector('input[value="Mar 5, 2026"]')).not.toBeNull();
|
|
86
|
+
expect(touchInput(result.container, "date").value).toBe("2026-03-05");
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("both paths commit the same value for the same day", async () => {
|
|
90
|
+
const fine = vi.fn();
|
|
91
|
+
const desktop = renderWithProviders(
|
|
92
|
+
<DatePicker label="Due" name="due" value="2026-03-01" onChange={fine} />,
|
|
93
|
+
);
|
|
94
|
+
await act(async () => {
|
|
95
|
+
fireEvent.click(desktop.container.querySelector('[role="combobox"]') as HTMLElement);
|
|
96
|
+
});
|
|
97
|
+
await waitFor(() => dayButton(document.body, "5"));
|
|
98
|
+
await act(async () => {
|
|
99
|
+
fireEvent.click(dayButton(document.body, "5"));
|
|
100
|
+
});
|
|
101
|
+
desktop.unmount();
|
|
102
|
+
|
|
103
|
+
coarse = true;
|
|
104
|
+
const touch = vi.fn();
|
|
105
|
+
const phone = renderWithProviders(
|
|
106
|
+
<DatePicker label="Due" name="due" value="2026-03-01" onChange={touch} />,
|
|
107
|
+
);
|
|
108
|
+
expect(touchInput(phone.container, "date").value).toBe("2026-03-01");
|
|
109
|
+
await act(async () => {
|
|
110
|
+
fireEvent.change(touchInput(phone.container, "date"), { target: { value: "2026-03-05" } });
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
expect(fine).toHaveBeenCalledTimes(1);
|
|
114
|
+
expect(touch).toHaveBeenCalledTimes(1);
|
|
115
|
+
expect((touch.mock.calls[0][0] as Date).getTime()).toBe(
|
|
116
|
+
(fine.mock.calls[0][0] as Date).getTime(),
|
|
117
|
+
);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it("clearing in the browser's control clears the field", async () => {
|
|
121
|
+
coarse = true;
|
|
122
|
+
const onChange = vi.fn();
|
|
123
|
+
const result = renderWithProviders(
|
|
124
|
+
<DatePicker label="Due" name="due" value="2026-03-05" onChange={onChange} />,
|
|
125
|
+
);
|
|
126
|
+
await act(async () => {
|
|
127
|
+
fireEvent.change(touchInput(result.container, "date"), { target: { value: "" } });
|
|
128
|
+
});
|
|
129
|
+
expect(onChange).toHaveBeenLastCalledWith(null);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it("min and max ride the input as local calendar days", () => {
|
|
133
|
+
coarse = true;
|
|
134
|
+
const result = renderWithProviders(
|
|
135
|
+
<DatePicker
|
|
136
|
+
label="Due"
|
|
137
|
+
name="due"
|
|
138
|
+
minDate={new Date(2026, 0, 2)}
|
|
139
|
+
maxDate={new Date(2026, 11, 30)}
|
|
140
|
+
/>,
|
|
141
|
+
);
|
|
142
|
+
const input = touchInput(result.container, "date");
|
|
143
|
+
expect(input.min).toBe("2026-01-02");
|
|
144
|
+
expect(input.max).toBe("2026-12-30");
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it("commits into form state", async () => {
|
|
148
|
+
coarse = true;
|
|
149
|
+
const onSubmit = vi.fn();
|
|
150
|
+
const Fixture = () => {
|
|
151
|
+
const form = useForm({
|
|
152
|
+
defaultValues: { due: null as Date | null },
|
|
153
|
+
onSubmit: async ({ value }) => onSubmit(value),
|
|
154
|
+
});
|
|
155
|
+
return (
|
|
156
|
+
<Form form={form}>
|
|
157
|
+
<DatePicker label="Due" name="due" />
|
|
158
|
+
<Button action="submit">Submit</Button>
|
|
159
|
+
</Form>
|
|
160
|
+
);
|
|
161
|
+
};
|
|
162
|
+
const result = renderWithProviders(<Fixture />);
|
|
163
|
+
await act(async () => {
|
|
164
|
+
fireEvent.change(touchInput(result.container, "date"), { target: { value: "2026-08-15" } });
|
|
165
|
+
});
|
|
166
|
+
await act(async () => {
|
|
167
|
+
fireEvent.click(result.getSubmitButton() as HTMLElement);
|
|
168
|
+
});
|
|
169
|
+
await waitFor(() => expect(onSubmit).toHaveBeenCalledTimes(1));
|
|
170
|
+
expect(formatCalendarDate(onSubmit.mock.calls[0][0].due, "date")).toBe("2026-08-15");
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
describe("Datetime on touch web (FZ-8114)", () => {
|
|
175
|
+
it("a coarse pointer renders datetime-local and emits the local wall clock", async () => {
|
|
176
|
+
coarse = true;
|
|
177
|
+
const onChange = vi.fn();
|
|
178
|
+
const result = renderWithProviders(
|
|
179
|
+
<DatetimePicker label="Starts" name="starts" onChange={onChange} />,
|
|
180
|
+
);
|
|
181
|
+
await act(async () => {
|
|
182
|
+
fireEvent.change(touchInput(result.container, "datetime-local"), {
|
|
183
|
+
target: { value: "2026-03-05T14:30" },
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
const picked = onChange.mock.calls[0][0] as Date;
|
|
187
|
+
expect(formatCalendarDate(picked, "datetime")).toBe("2026-03-05 14:30:00");
|
|
188
|
+
expect(touchInput(result.container, "datetime-local").value).toBe("2026-03-05T14:30");
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it("seconds widen the input's step and survive the round trip", () => {
|
|
192
|
+
coarse = true;
|
|
193
|
+
const result = renderWithProviders(
|
|
194
|
+
<DatetimePicker
|
|
195
|
+
label="Starts"
|
|
196
|
+
name="starts"
|
|
197
|
+
showSeconds
|
|
198
|
+
value={new Date(2026, 2, 5, 14, 30, 15)}
|
|
199
|
+
/>,
|
|
200
|
+
);
|
|
201
|
+
const input = touchInput(result.container, "datetime-local");
|
|
202
|
+
expect(input.step).toBe("1");
|
|
203
|
+
expect(input.value).toBe("2026-03-05T14:30:15");
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it("a fine pointer keeps the MPO panel trigger", () => {
|
|
207
|
+
const result = renderWithProviders(<DatetimePicker label="Starts" name="starts" />);
|
|
208
|
+
expect(result.container.querySelector('input[type="datetime-local"]')).toBeNull();
|
|
209
|
+
expect(result.container.querySelector('[role="combobox"]')).not.toBeNull();
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
describe("Time on touch web (FZ-8114)", () => {
|
|
214
|
+
it("a coarse pointer renders the time input and emits TimePicker's string", async () => {
|
|
215
|
+
coarse = true;
|
|
216
|
+
const onChange = vi.fn();
|
|
217
|
+
const result = renderWithProviders(
|
|
218
|
+
<TimePicker label="At" name="at" value="09:15" onChange={onChange} />,
|
|
219
|
+
);
|
|
220
|
+
const input = touchInput(result.container, "time");
|
|
221
|
+
expect(input.value).toBe("09:15");
|
|
222
|
+
await act(async () => {
|
|
223
|
+
fireEvent.change(input, { target: { value: "14:30" } });
|
|
224
|
+
});
|
|
225
|
+
expect(onChange).toHaveBeenLastCalledWith("14:30");
|
|
226
|
+
expect(result.container.querySelector('input[value="2:30 PM"]')).not.toBeNull();
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it("showSeconds emits HH:mm:ss even when the browser drops :00", async () => {
|
|
230
|
+
coarse = true;
|
|
231
|
+
const onChange = vi.fn();
|
|
232
|
+
const result = renderWithProviders(
|
|
233
|
+
<TimePicker label="At" name="at" showSeconds onChange={onChange} />,
|
|
234
|
+
);
|
|
235
|
+
await act(async () => {
|
|
236
|
+
fireEvent.change(touchInput(result.container, "time"), { target: { value: "14:30" } });
|
|
237
|
+
});
|
|
238
|
+
expect(onChange).toHaveBeenLastCalledWith("14:30:00");
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it("a fine pointer keeps the MPO wheels trigger", () => {
|
|
242
|
+
const result = renderWithProviders(<TimePicker label="At" name="at" />);
|
|
243
|
+
expect(result.container.querySelector('input[type="time"]')).toBeNull();
|
|
244
|
+
});
|
|
245
|
+
});
|