@multiplatform.one/forms 7.63.4 → 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/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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@multiplatform.one/forms",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.64.0",
|
|
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.
|
|
61
|
-
"@multiplatform.one/router": "^7.
|
|
62
|
-
"@multiplatform.one/table-primitives": "^7.
|
|
63
|
-
"@multiplatform.one/theme": "^7.
|
|
60
|
+
"@multiplatform.one/platform": "^7.64.0",
|
|
61
|
+
"@multiplatform.one/router": "^7.64.0",
|
|
62
|
+
"@multiplatform.one/table-primitives": "^7.64.0",
|
|
63
|
+
"@multiplatform.one/theme": "^7.64.0",
|
|
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",
|
|
@@ -79,9 +79,10 @@
|
|
|
79
79
|
"tamagui": "2.7.6"
|
|
80
80
|
},
|
|
81
81
|
"devDependencies": {
|
|
82
|
-
"@
|
|
83
|
-
"@multiplatform.one/
|
|
84
|
-
"@multiplatform.one/
|
|
82
|
+
"@expo/ui": "~55.0.17",
|
|
83
|
+
"@multiplatform.one/config": "7.64.0",
|
|
84
|
+
"@multiplatform.one/storybook": "7.64.0",
|
|
85
|
+
"@multiplatform.one/test-utils": "7.64.0",
|
|
85
86
|
"@tamagui/build": "2.7.6",
|
|
86
87
|
"@tamagui/config": "2.7.6",
|
|
87
88
|
"@tamagui/vite-plugin": "2.7.6",
|
|
@@ -103,6 +104,7 @@
|
|
|
103
104
|
"vitest": "^4.1.5"
|
|
104
105
|
},
|
|
105
106
|
"peerDependencies": {
|
|
107
|
+
"@expo/ui": "*",
|
|
106
108
|
"@react-native-community/datetimepicker": ">=8.0.1",
|
|
107
109
|
"expo-document-picker": "*",
|
|
108
110
|
"expo-haptics": "55.0.17",
|
|
@@ -117,6 +119,9 @@
|
|
|
117
119
|
"react-native-safe-area-context": "*"
|
|
118
120
|
},
|
|
119
121
|
"peerDependenciesMeta": {
|
|
122
|
+
"@expo/ui": {
|
|
123
|
+
"optional": true
|
|
124
|
+
},
|
|
120
125
|
"@react-native-community/datetimepicker": {
|
|
121
126
|
"optional": true
|
|
122
127
|
},
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
OVERLAY_BREAKPOINT,
|
|
13
13
|
Surface,
|
|
14
14
|
ensureFocusVisibleRing,
|
|
15
|
+
focusOnOpen,
|
|
15
16
|
useResolvedKnobs,
|
|
16
17
|
} from "@multiplatform.one/theme";
|
|
17
18
|
import {
|
|
@@ -192,9 +193,9 @@ function focusFirstField(event: Event) {
|
|
|
192
193
|
const field = root?.querySelector<HTMLElement>(
|
|
193
194
|
"input, textarea, select, [contenteditable='true']",
|
|
194
195
|
);
|
|
195
|
-
if (!field) return;
|
|
196
|
+
if (!root || !field) return;
|
|
196
197
|
event.preventDefault();
|
|
197
|
-
field
|
|
198
|
+
focusOnOpen(field, root);
|
|
198
199
|
}
|
|
199
200
|
|
|
200
201
|
export function AdaptivePopup({
|
|
@@ -401,7 +402,7 @@ export function AdaptivePopup({
|
|
|
401
402
|
borderTopLeftRadius: free,
|
|
402
403
|
borderBottomLeftRadius: free,
|
|
403
404
|
};
|
|
404
|
-
const slideFromEnd = rtl ? -
|
|
405
|
+
const slideFromEnd = rtl ? -16 : 16;
|
|
405
406
|
return (
|
|
406
407
|
<Dialog modal open={open} onOpenChange={onOpenChange}>
|
|
407
408
|
{trigger && (
|
package/src/Button/index.tsx
CHANGED
|
@@ -30,6 +30,7 @@ import {
|
|
|
30
30
|
solidButtonIntents,
|
|
31
31
|
solidPaletteThemes,
|
|
32
32
|
useAccentOnSurface,
|
|
33
|
+
ensureKeyboardModalityTracking,
|
|
33
34
|
useBarSurface,
|
|
34
35
|
useElevatedSurface,
|
|
35
36
|
useIdentityInkOnSurface,
|
|
@@ -487,6 +488,9 @@ export function Button({
|
|
|
487
488
|
const grouped = useControlGrouped();
|
|
488
489
|
// LC-153 CONTROL-TIERS: a bar paints its neutral buttons plain; content
|
|
489
490
|
// paints them tinted. The scheme picks the wash step.
|
|
491
|
+
// A Button is most overlays' trigger: the modality must be known before its
|
|
492
|
+
// opening key press, or the open focus cannot tell keyboard from pointer.
|
|
493
|
+
if (isWeb) ensureKeyboardModalityTracking();
|
|
490
494
|
const barSurface = useBarSurface();
|
|
491
495
|
const elevatedGround = useElevatedSurface();
|
|
492
496
|
const darkScheme = isDarkThemeName(useThemeName());
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { isDarkThemeName, useResolvedKnobs } from "@multiplatform.one/theme";
|
|
2
|
+
import { type ComponentType, type ReactNode, useState } from "react";
|
|
3
|
+
import { ScrollView, View, YStack, useThemeName } from "tamagui";
|
|
4
|
+
|
|
5
|
+
import { useIsInTableCell } from "../../shared/tableCellContext";
|
|
6
|
+
import { t } from "../../shared/t";
|
|
7
|
+
import { PresetSwatches, hexAlphaByte, parseHex, rgbToHex } from "./parts";
|
|
8
|
+
import type { SystemColorWellProps } from "./systemColorWell";
|
|
9
|
+
|
|
10
|
+
export interface SystemColorFieldProps {
|
|
11
|
+
Well: ComponentType<SystemColorWellProps>;
|
|
12
|
+
/** The MPO trigger row: swatch, hex and clear button. */
|
|
13
|
+
trigger: ReactNode;
|
|
14
|
+
selectedColor: string;
|
|
15
|
+
alpha: number;
|
|
16
|
+
showAlpha: boolean;
|
|
17
|
+
presetColors: string[];
|
|
18
|
+
disabled?: boolean;
|
|
19
|
+
a11yLabel?: string;
|
|
20
|
+
onPick: (hex: string, alpha?: number) => void;
|
|
21
|
+
onSelectPreset: (hex: string) => void;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* LC-266 on iOS: the field opens the system colour picker; the theme swatches
|
|
26
|
+
* stay one tap away in a row under it, because the system picker cannot show
|
|
27
|
+
* them. With a value, the trailing clear button stays outside the well.
|
|
28
|
+
*/
|
|
29
|
+
export function SystemColorField({
|
|
30
|
+
Well,
|
|
31
|
+
trigger,
|
|
32
|
+
selectedColor,
|
|
33
|
+
alpha,
|
|
34
|
+
showAlpha,
|
|
35
|
+
presetColors,
|
|
36
|
+
disabled,
|
|
37
|
+
a11yLabel,
|
|
38
|
+
onPick,
|
|
39
|
+
onSelectPreset,
|
|
40
|
+
}: SystemColorFieldProps) {
|
|
41
|
+
const { knobProps } = useResolvedKnobs();
|
|
42
|
+
const inTableCell = useIsInTableCell();
|
|
43
|
+
const scheme = isDarkThemeName(useThemeName()) ? "dark" : "light";
|
|
44
|
+
const [size, setSize] = useState({ width: 0, height: 0 });
|
|
45
|
+
const clearInset = selectedColor ? size.height : 0;
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<YStack {...knobProps.gap}>
|
|
49
|
+
<View
|
|
50
|
+
position="relative"
|
|
51
|
+
testID="color-picker-system-field"
|
|
52
|
+
onLayout={(e) => {
|
|
53
|
+
const { width, height } = e.nativeEvent.layout;
|
|
54
|
+
if (width !== size.width || height !== size.height) setSize({ width, height });
|
|
55
|
+
}}
|
|
56
|
+
>
|
|
57
|
+
{trigger}
|
|
58
|
+
{!disabled ? (
|
|
59
|
+
<Well
|
|
60
|
+
selection={
|
|
61
|
+
selectedColor && showAlpha ? `${selectedColor}${hexAlphaByte(alpha)}` : selectedColor
|
|
62
|
+
}
|
|
63
|
+
supportsOpacity={showAlpha}
|
|
64
|
+
label={a11yLabel || t("Color")}
|
|
65
|
+
colorScheme={scheme}
|
|
66
|
+
width={Math.max(0, size.width - clearInset)}
|
|
67
|
+
height={size.height}
|
|
68
|
+
onSelectionChange={(picked) => {
|
|
69
|
+
const parsed = parseHex(picked);
|
|
70
|
+
if (!parsed) return;
|
|
71
|
+
onPick(rgbToHex(parsed.r, parsed.g, parsed.b), showAlpha ? parsed.alpha : undefined);
|
|
72
|
+
}}
|
|
73
|
+
style={{
|
|
74
|
+
position: "absolute",
|
|
75
|
+
top: 0,
|
|
76
|
+
bottom: 0,
|
|
77
|
+
left: 0,
|
|
78
|
+
right: clearInset,
|
|
79
|
+
overflow: "hidden",
|
|
80
|
+
}}
|
|
81
|
+
/>
|
|
82
|
+
) : null}
|
|
83
|
+
</View>
|
|
84
|
+
{!inTableCell && presetColors.length > 0 ? (
|
|
85
|
+
<ScrollView
|
|
86
|
+
horizontal
|
|
87
|
+
showsHorizontalScrollIndicator={false}
|
|
88
|
+
testID="color-picker-system-presets"
|
|
89
|
+
>
|
|
90
|
+
<PresetSwatches
|
|
91
|
+
presetColors={presetColors}
|
|
92
|
+
selectedColor={selectedColor}
|
|
93
|
+
onSelect={(color) => {
|
|
94
|
+
if (color !== null && !disabled) onSelectPreset(color);
|
|
95
|
+
}}
|
|
96
|
+
/>
|
|
97
|
+
</ScrollView>
|
|
98
|
+
) : null}
|
|
99
|
+
</YStack>
|
|
100
|
+
);
|
|
101
|
+
}
|
|
@@ -40,6 +40,8 @@ import {
|
|
|
40
40
|
parseHex,
|
|
41
41
|
rgbToHex,
|
|
42
42
|
} from "./parts";
|
|
43
|
+
import { SystemColorField } from "./SystemColorField";
|
|
44
|
+
import { useSystemColorWell } from "./systemColorWell";
|
|
43
45
|
|
|
44
46
|
// ---------------------------------------------------------------------------
|
|
45
47
|
// Types
|
|
@@ -475,6 +477,7 @@ function ColorPickerControl({
|
|
|
475
477
|
onBlurControl,
|
|
476
478
|
}: ColorPickerControlProps) {
|
|
477
479
|
const { knobProps } = useResolvedKnobs();
|
|
480
|
+
const SystemWell = useSystemColorWell();
|
|
478
481
|
const [open, setOpen] = useState(false);
|
|
479
482
|
const parsedInitial = parseColorValue(value, format);
|
|
480
483
|
const initialColor = parsedInitial.hex;
|
|
@@ -620,6 +623,50 @@ function ColorPickerControl({
|
|
|
620
623
|
);
|
|
621
624
|
}
|
|
622
625
|
|
|
626
|
+
const triggerAria = {
|
|
627
|
+
// Labelled: the sibling Label wires `aria-labelledby` onto this id
|
|
628
|
+
// itself — naming it here too doubles the spoken name.
|
|
629
|
+
"aria-label": hasLabel ? undefined : t("Color"),
|
|
630
|
+
"aria-describedby":
|
|
631
|
+
[
|
|
632
|
+
helperText && id ? `${id}-description` : undefined,
|
|
633
|
+
hasError && id ? `${id}-error` : undefined,
|
|
634
|
+
]
|
|
635
|
+
.filter(Boolean)
|
|
636
|
+
.join(" ") || undefined,
|
|
637
|
+
"aria-required": required || undefined,
|
|
638
|
+
"aria-invalid": hasError || undefined,
|
|
639
|
+
};
|
|
640
|
+
|
|
641
|
+
if (SystemWell) {
|
|
642
|
+
return (
|
|
643
|
+
<SystemColorField
|
|
644
|
+
Well={SystemWell}
|
|
645
|
+
selectedColor={selectedColor}
|
|
646
|
+
alpha={alpha}
|
|
647
|
+
showAlpha={showAlpha}
|
|
648
|
+
presetColors={presetColors}
|
|
649
|
+
disabled={disabled}
|
|
650
|
+
a11yLabel={a11yLabel}
|
|
651
|
+
onPick={handleHexInput}
|
|
652
|
+
onSelectPreset={handleSelectPreset}
|
|
653
|
+
trigger={
|
|
654
|
+
<ColorPickerInputTrigger
|
|
655
|
+
value={value || selectedColor}
|
|
656
|
+
placeholder={placeholder}
|
|
657
|
+
disabled={disabled}
|
|
658
|
+
onReset={handleClear}
|
|
659
|
+
onButtonPress={() => {}}
|
|
660
|
+
error={hasError}
|
|
661
|
+
id={id}
|
|
662
|
+
{...triggerAria}
|
|
663
|
+
inputProps={inputProps}
|
|
664
|
+
/>
|
|
665
|
+
}
|
|
666
|
+
/>
|
|
667
|
+
);
|
|
668
|
+
}
|
|
669
|
+
|
|
623
670
|
return (
|
|
624
671
|
<ColorPickerPopoverShell
|
|
625
672
|
open={open}
|
|
@@ -652,19 +699,7 @@ function ColorPickerControl({
|
|
|
652
699
|
}}
|
|
653
700
|
error={hasError}
|
|
654
701
|
id={id}
|
|
655
|
-
|
|
656
|
-
// itself — naming it here too doubles the spoken name.
|
|
657
|
-
aria-label={hasLabel ? undefined : t("Color")}
|
|
658
|
-
aria-describedby={
|
|
659
|
-
[
|
|
660
|
-
helperText && id ? `${id}-description` : undefined,
|
|
661
|
-
hasError && id ? `${id}-error` : undefined,
|
|
662
|
-
]
|
|
663
|
-
.filter(Boolean)
|
|
664
|
-
.join(" ") || undefined
|
|
665
|
-
}
|
|
666
|
-
aria-required={required || undefined}
|
|
667
|
-
aria-invalid={hasError || undefined}
|
|
702
|
+
{...triggerAria}
|
|
668
703
|
inputProps={inputProps}
|
|
669
704
|
/>
|
|
670
705
|
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { ComponentType } from "react";
|
|
2
|
+
import type { SystemColorWellProps } from "./systemColorWell";
|
|
3
|
+
|
|
4
|
+
/** GNOME: the MPO picker is the platform form (LC-266). */
|
|
5
|
+
export function useSystemColorWell(): ComponentType<SystemColorWellProps> | null {
|
|
6
|
+
return null;
|
|
7
|
+
}
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LC-266 (FZ-8121): on iOS the Color field opens the system colour picker
|
|
3
|
+
* when the binary carries @expo/ui, and keeps the MPO picker when it does not,
|
|
4
|
+
* so an OTA on an older binary never crashes.
|
|
5
|
+
*/
|
|
6
|
+
import { act, cleanup, fireEvent, render, within } from "@testing-library/react";
|
|
7
|
+
import { renderWithProviders } from "@multiplatform.one/test-utils";
|
|
8
|
+
import { createElement } from "react";
|
|
9
|
+
import { afterEach, beforeAll, describe, expect, it, vi } from "vitest";
|
|
10
|
+
import type { SystemColorWellProps } from "./systemColorWell";
|
|
11
|
+
import {
|
|
12
|
+
type SwiftUiColorModule,
|
|
13
|
+
type SwiftUiModifiers,
|
|
14
|
+
type SystemColorWellSources,
|
|
15
|
+
expoUiNativeModule,
|
|
16
|
+
makeSystemColorWell,
|
|
17
|
+
resolveSystemColorWell,
|
|
18
|
+
wellOpacity,
|
|
19
|
+
wellScale,
|
|
20
|
+
} from "./systemColorWell.native";
|
|
21
|
+
|
|
22
|
+
vi.mock("@tamagui/lucide-icons-2", () => ({
|
|
23
|
+
Palette: () => null,
|
|
24
|
+
X: () => null,
|
|
25
|
+
Check: () => null,
|
|
26
|
+
Pipette: () => null,
|
|
27
|
+
}));
|
|
28
|
+
|
|
29
|
+
const wellState = vi.hoisted(() => ({
|
|
30
|
+
present: false,
|
|
31
|
+
last: undefined as SystemColorWellProps | undefined,
|
|
32
|
+
}));
|
|
33
|
+
|
|
34
|
+
vi.mock("./systemColorWell", () => ({
|
|
35
|
+
useSystemColorWell: () =>
|
|
36
|
+
wellState.present
|
|
37
|
+
? (props: SystemColorWellProps) => {
|
|
38
|
+
wellState.last = props;
|
|
39
|
+
return createElement("div", {
|
|
40
|
+
"data-testid": "system-color-well",
|
|
41
|
+
"data-selection": props.selection,
|
|
42
|
+
"data-scheme": props.colorScheme,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
: null,
|
|
46
|
+
}));
|
|
47
|
+
|
|
48
|
+
afterEach(() => {
|
|
49
|
+
cleanup();
|
|
50
|
+
wellState.present = false;
|
|
51
|
+
wellState.last = undefined;
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const FakeHost: SwiftUiColorModule["Host"] = ({ children, colorScheme }) =>
|
|
55
|
+
createElement("div", { "data-testid": "host", "data-scheme": colorScheme }, children);
|
|
56
|
+
|
|
57
|
+
const FakePicker: SwiftUiColorModule["ColorPicker"] = (props) =>
|
|
58
|
+
createElement("button", {
|
|
59
|
+
"data-testid": "swiftui-color-picker",
|
|
60
|
+
"aria-label": props.label,
|
|
61
|
+
"data-selection": props.selection ?? "",
|
|
62
|
+
"data-modifiers": JSON.stringify(props.modifiers),
|
|
63
|
+
onClick: () => props.onSelectionChange?.("#3366FF"),
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
const fakeModifiers: SwiftUiModifiers = {
|
|
67
|
+
labelsHidden: () => ({ $type: "labelsHidden" }),
|
|
68
|
+
scaleEffect: (scale) => ({ $type: "scaleEffect", x: scale, y: scale }),
|
|
69
|
+
opacity: (value) => ({ $type: "opacity", value }),
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
function sources(
|
|
73
|
+
nativeModule: unknown,
|
|
74
|
+
ui: SystemColorWellSources["ui"] = () => ({ Host: FakeHost, ColorPicker: FakePicker }),
|
|
75
|
+
): SystemColorWellSources {
|
|
76
|
+
return {
|
|
77
|
+
core: () => ({ requireOptionalNativeModule: () => nativeModule }),
|
|
78
|
+
ui,
|
|
79
|
+
modifiers: () => fakeModifiers,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
describe("system colour well detection", () => {
|
|
84
|
+
it("draws @expo/ui's ColorPicker on iOS when the binary registers ExpoUI", () => {
|
|
85
|
+
const probe = vi.fn((name: string) => (name === expoUiNativeModule ? {} : null));
|
|
86
|
+
const well = resolveSystemColorWell(
|
|
87
|
+
{
|
|
88
|
+
core: () => ({ requireOptionalNativeModule: probe }),
|
|
89
|
+
ui: () => ({ Host: FakeHost, ColorPicker: FakePicker }),
|
|
90
|
+
modifiers: () => fakeModifiers,
|
|
91
|
+
},
|
|
92
|
+
"ios",
|
|
93
|
+
);
|
|
94
|
+
expect(well).toBeTypeOf("function");
|
|
95
|
+
expect(probe).toHaveBeenCalledWith("ExpoUI");
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("never requires @expo/ui on a binary without ExpoUI (the OTA case)", () => {
|
|
99
|
+
const ui = vi.fn(() => ({ Host: FakeHost, ColorPicker: FakePicker }));
|
|
100
|
+
expect(resolveSystemColorWell(sources(null, ui), "ios")).toBeNull();
|
|
101
|
+
expect(ui).not.toHaveBeenCalled();
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("keeps the MPO picker on Android and web", () => {
|
|
105
|
+
expect(resolveSystemColorWell(sources({}), "android")).toBeNull();
|
|
106
|
+
expect(resolveSystemColorWell(sources({}), "web")).toBeNull();
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("falls back when the probe or the require throws, or an export is missing", () => {
|
|
110
|
+
const throwing = () => {
|
|
111
|
+
throw new Error("no native view");
|
|
112
|
+
};
|
|
113
|
+
expect(
|
|
114
|
+
resolveSystemColorWell(
|
|
115
|
+
{ core: throwing, ui: () => ({}), modifiers: () => fakeModifiers },
|
|
116
|
+
"ios",
|
|
117
|
+
),
|
|
118
|
+
).toBeNull();
|
|
119
|
+
expect(resolveSystemColorWell(sources({}, throwing), "ios")).toBeNull();
|
|
120
|
+
expect(
|
|
121
|
+
resolveSystemColorWell(
|
|
122
|
+
sources({}, () => ({ Host: FakeHost })),
|
|
123
|
+
"ios",
|
|
124
|
+
),
|
|
125
|
+
).toBeNull();
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("covers the field with a near-transparent well scaled past its bounds", () => {
|
|
129
|
+
const Well = makeSystemColorWell({ Host: FakeHost, ColorPicker: FakePicker }, fakeModifiers);
|
|
130
|
+
const onSelectionChange = vi.fn();
|
|
131
|
+
const result = render(
|
|
132
|
+
createElement(Well, {
|
|
133
|
+
selection: "#EF4444",
|
|
134
|
+
supportsOpacity: false,
|
|
135
|
+
label: "Accent",
|
|
136
|
+
colorScheme: "dark",
|
|
137
|
+
width: 358,
|
|
138
|
+
height: 44,
|
|
139
|
+
onSelectionChange,
|
|
140
|
+
}),
|
|
141
|
+
);
|
|
142
|
+
expect(result.getByTestId("host").getAttribute("data-scheme")).toBe("dark");
|
|
143
|
+
const picker = result.getByTestId("swiftui-color-picker");
|
|
144
|
+
expect(picker.getAttribute("aria-label")).toBe("Accent");
|
|
145
|
+
expect(picker.getAttribute("data-selection")).toBe("#EF4444");
|
|
146
|
+
const modifiers = JSON.parse(picker.getAttribute("data-modifiers") ?? "[]");
|
|
147
|
+
expect(modifiers).toEqual([
|
|
148
|
+
{ $type: "labelsHidden" },
|
|
149
|
+
{ $type: "scaleEffect", x: wellScale(358, 44), y: wellScale(358, 44) },
|
|
150
|
+
{ $type: "opacity", value: wellOpacity },
|
|
151
|
+
]);
|
|
152
|
+
expect(wellScale(358, 44) * 28).toBeGreaterThanOrEqual(358 * 2);
|
|
153
|
+
expect(wellScale(0, 0)).toBe(1);
|
|
154
|
+
expect(wellOpacity).toBeGreaterThan(0.01);
|
|
155
|
+
fireEvent.click(picker);
|
|
156
|
+
expect(onSelectionChange).toHaveBeenCalledWith("#3366FF");
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
async function loadColorPicker() {
|
|
161
|
+
return (await import("./index")).ColorPicker;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
beforeAll(async () => {
|
|
165
|
+
const ColorPicker = await loadColorPicker();
|
|
166
|
+
wellState.present = true;
|
|
167
|
+
renderWithProviders(<ColorPicker label="Accent" value="#EF4444" />);
|
|
168
|
+
cleanup();
|
|
169
|
+
wellState.present = false;
|
|
170
|
+
wellState.last = undefined;
|
|
171
|
+
}, 120_000);
|
|
172
|
+
|
|
173
|
+
describe("ColorPicker with the system colour well (iOS)", () => {
|
|
174
|
+
it("renders the system well over the field and the theme swatches under it", async () => {
|
|
175
|
+
wellState.present = true;
|
|
176
|
+
const ColorPicker = await loadColorPicker();
|
|
177
|
+
const result = renderWithProviders(
|
|
178
|
+
<ColorPicker label="Accent" value="#EF4444" presetColors={["#EF4444", "#22C55E"]} />,
|
|
179
|
+
);
|
|
180
|
+
const field = result.getByTestId("color-picker-system-field");
|
|
181
|
+
expect(field.querySelector('[data-testid="system-color-well"]')).toBeTruthy();
|
|
182
|
+
expect(field.querySelector('[data-testid="color-picker-trigger-swatch"]')).toBeTruthy();
|
|
183
|
+
expect(result.getByTestId("color-picker-system-presets")).toBeTruthy();
|
|
184
|
+
expect(document.body.querySelector('[data-testid="color-picker-panel"]')).toBeNull();
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it("writes the hex the system picker returns", async () => {
|
|
188
|
+
wellState.present = true;
|
|
189
|
+
const ColorPicker = await loadColorPicker();
|
|
190
|
+
const onChange = vi.fn();
|
|
191
|
+
const result = renderWithProviders(
|
|
192
|
+
<ColorPicker label="Accent" value="#EF4444" onChange={onChange} />,
|
|
193
|
+
);
|
|
194
|
+
expect(result.container.querySelectorAll('[data-testid="system-color-well"]')).toHaveLength(1);
|
|
195
|
+
const well = wellState.last;
|
|
196
|
+
expect(well?.selection).toBe("#EF4444");
|
|
197
|
+
expect(well?.label).toBe("Accent");
|
|
198
|
+
expect(well?.supportsOpacity).toBe(false);
|
|
199
|
+
await act(async () => well?.onSelectionChange("#3366ff"));
|
|
200
|
+
expect(onChange).toHaveBeenLastCalledWith("#3366FF");
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it("carries opacity through when showAlpha is on", async () => {
|
|
204
|
+
wellState.present = true;
|
|
205
|
+
const ColorPicker = await loadColorPicker();
|
|
206
|
+
const onChange = vi.fn();
|
|
207
|
+
renderWithProviders(
|
|
208
|
+
<ColorPicker label="Accent" value="#EF444480" showAlpha onChange={onChange} />,
|
|
209
|
+
);
|
|
210
|
+
expect(wellState.last?.supportsOpacity).toBe(true);
|
|
211
|
+
expect(wellState.last?.selection).toBe("#EF444480");
|
|
212
|
+
await act(async () => wellState.last?.onSelectionChange("#3366FF40"));
|
|
213
|
+
expect(onChange).toHaveBeenLastCalledWith("#3366FF40");
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it("picks a theme swatch from the row under the field", async () => {
|
|
217
|
+
wellState.present = true;
|
|
218
|
+
const ColorPicker = await loadColorPicker();
|
|
219
|
+
const onChange = vi.fn();
|
|
220
|
+
const result = renderWithProviders(
|
|
221
|
+
<ColorPicker
|
|
222
|
+
label="Accent"
|
|
223
|
+
value="#EF4444"
|
|
224
|
+
presetColors={["#EF4444", "#22C55E"]}
|
|
225
|
+
onChange={onChange}
|
|
226
|
+
/>,
|
|
227
|
+
);
|
|
228
|
+
await act(async () => {
|
|
229
|
+
fireEvent.click(within(result.container).getByRole("radio", { name: "Color #22C55E" }));
|
|
230
|
+
});
|
|
231
|
+
expect(onChange).toHaveBeenLastCalledWith("#22C55E");
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it("keeps the clear button outside the well", async () => {
|
|
235
|
+
wellState.present = true;
|
|
236
|
+
const ColorPicker = await loadColorPicker();
|
|
237
|
+
const onChange = vi.fn();
|
|
238
|
+
const result = renderWithProviders(
|
|
239
|
+
<ColorPicker label="Accent" value="#EF4444" onChange={onChange} />,
|
|
240
|
+
);
|
|
241
|
+
await act(async () => {
|
|
242
|
+
fireEvent.click(within(result.container).getByRole("button", { name: "Clear color" }));
|
|
243
|
+
});
|
|
244
|
+
expect(onChange).toHaveBeenLastCalledWith("");
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it("draws no well while disabled", async () => {
|
|
248
|
+
wellState.present = true;
|
|
249
|
+
const ColorPicker = await loadColorPicker();
|
|
250
|
+
renderWithProviders(<ColorPicker label="Accent" value="#EF4444" disabled />);
|
|
251
|
+
expect(wellState.last).toBeUndefined();
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
describe("ColorPicker without @expo/ui (older binary)", () => {
|
|
256
|
+
it("opens the MPO picker and does not crash", async () => {
|
|
257
|
+
const ColorPicker = await loadColorPicker();
|
|
258
|
+
const result = renderWithProviders(<ColorPicker label="Accent" value="#EF4444" />);
|
|
259
|
+
expect(result.queryByTestId("color-picker-system-field")).toBeNull();
|
|
260
|
+
const input = result.container.querySelector("input");
|
|
261
|
+
await act(async () => {
|
|
262
|
+
if (input) fireEvent.click(input);
|
|
263
|
+
});
|
|
264
|
+
expect(document.body.querySelector('[data-testid="color-picker-panel"]')).toBeTruthy();
|
|
265
|
+
});
|
|
266
|
+
});
|