@colixsystems/widget-sdk 0.234.0 → 0.236.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/README.md +9 -1
- package/dist/contract.cjs +6 -2
- package/dist/contract.js +6 -2
- package/dist/datetimepicker-calendar.js +225 -0
- package/dist/datetimepicker-cells.js +71 -0
- package/dist/datetimepicker-format.js +107 -45
- package/dist/datetimepicker-names.js +99 -0
- package/dist/datetimepicker-sheet-header.js +159 -0
- package/dist/datetimepicker-sheet-parts.js +146 -0
- package/dist/datetimepicker-sheet-state.js +117 -0
- package/dist/datetimepicker-sheet.js +113 -0
- package/dist/datetimepicker-time-columns.js +107 -0
- package/dist/datetimepicker-view.js +221 -0
- package/dist/datetimepicker.js +12 -163
- package/dist/datetimepicker.native.js +36 -124
- package/dist/hooks.js +8 -0
- package/dist/host.d.ts +10 -2
- package/dist/host.js +1 -0
- package/dist/theme-background.cjs +18 -0
- package/dist/theme-background.js +17 -0
- package/package.json +1 -1
- package/src/contract.js +6 -2
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
// REQ-WSDK-PLATFORM §6 — the ONE `<DateTimePicker>` field both hosts render.
|
|
2
|
+
// The field (icon, value in the viewer's language, clear button) is shared;
|
|
3
|
+
// what opens when it is pressed is the host's `Surface`: a themed calendar
|
|
4
|
+
// sheet on the web, the operating system's own picker on a device.
|
|
5
|
+
|
|
6
|
+
import React from "react";
|
|
7
|
+
import { useHostLocale, useHostTheme } from "./hooks.js";
|
|
8
|
+
import { DEFAULT_THEME_TOKENS } from "./_theme-tokens.js";
|
|
9
|
+
import { Icon } from "./icon.js";
|
|
10
|
+
import { FIELD_OUTLINE_OFF, fieldStateStyle, resolveFormControlTokens } from "./form-control-tokens.js";
|
|
11
|
+
import {
|
|
12
|
+
formatDisplayLabel,
|
|
13
|
+
normaliseMode,
|
|
14
|
+
partsToIso,
|
|
15
|
+
valueToParts,
|
|
16
|
+
} from "./datetimepicker-format.js";
|
|
17
|
+
import { resolveBounds, resolvePickerLocale } from "./datetimepicker-calendar.js";
|
|
18
|
+
|
|
19
|
+
const h = React.createElement;
|
|
20
|
+
const CLEAR_ICON_SIZE = 18;
|
|
21
|
+
|
|
22
|
+
function isSet(value) {
|
|
23
|
+
return value != null && value !== "";
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Bind the shared field to one platform's primitives and picking surface.
|
|
28
|
+
*
|
|
29
|
+
* @param {{View: any, Text: any, Pressable: any, Platform: any, Surface: any}} rn
|
|
30
|
+
*/
|
|
31
|
+
export function makeDateTimePicker(rn) {
|
|
32
|
+
const { View, Text, Pressable, Platform, Surface } = rn;
|
|
33
|
+
|
|
34
|
+
function focusNode(node) {
|
|
35
|
+
if (Platform.OS === "web" && node && typeof node.focus === "function") node.focus();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function ClearButton({ t, label, onPress, testID }) {
|
|
39
|
+
return h(
|
|
40
|
+
Pressable,
|
|
41
|
+
{
|
|
42
|
+
onPress,
|
|
43
|
+
accessibilityRole: "button",
|
|
44
|
+
accessibilityLabel: label,
|
|
45
|
+
hitSlop: 8,
|
|
46
|
+
testID: testID ? `${testID}-clear` : undefined,
|
|
47
|
+
style: ({ pressed }) => [
|
|
48
|
+
{
|
|
49
|
+
alignItems: "center",
|
|
50
|
+
justifyContent: "center",
|
|
51
|
+
alignSelf: "stretch",
|
|
52
|
+
paddingHorizontal: t.paddingHorizontal,
|
|
53
|
+
},
|
|
54
|
+
FIELD_OUTLINE_OFF,
|
|
55
|
+
pressed ? { opacity: 0.6 } : null,
|
|
56
|
+
],
|
|
57
|
+
},
|
|
58
|
+
h(Icon, { name: "x", size: CLEAR_ICON_SIZE, color: t.placeholderColor }),
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function Trigger({ t, triggerRef, open, canEdit, hasValue, shown, mode, spoken, onOpen, onFocusChange, testID }) {
|
|
63
|
+
const ink = hasValue ? t.textColor : t.placeholderColor;
|
|
64
|
+
return h(
|
|
65
|
+
Pressable,
|
|
66
|
+
{
|
|
67
|
+
ref: triggerRef,
|
|
68
|
+
onPress: () => {
|
|
69
|
+
if (canEdit) onOpen();
|
|
70
|
+
},
|
|
71
|
+
onKeyDown: (event) => {
|
|
72
|
+
if (canEdit && event.key === "ArrowDown") {
|
|
73
|
+
event.preventDefault();
|
|
74
|
+
onOpen();
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
onFocus: () => onFocusChange(true),
|
|
78
|
+
onBlur: () => onFocusChange(false),
|
|
79
|
+
disabled: !canEdit,
|
|
80
|
+
accessibilityRole: "button",
|
|
81
|
+
"aria-haspopup": "dialog",
|
|
82
|
+
"aria-expanded": open,
|
|
83
|
+
"aria-disabled": !canEdit,
|
|
84
|
+
accessibilityLabel: spoken,
|
|
85
|
+
testID,
|
|
86
|
+
style: [
|
|
87
|
+
{
|
|
88
|
+
flex: 1,
|
|
89
|
+
flexDirection: "row",
|
|
90
|
+
alignItems: "center",
|
|
91
|
+
gap: t.rowGap,
|
|
92
|
+
minHeight: t.height,
|
|
93
|
+
paddingLeft: t.paddingHorizontal,
|
|
94
|
+
paddingRight: hasValue && canEdit ? 0 : t.paddingHorizontal,
|
|
95
|
+
paddingVertical: t.paddingVertical,
|
|
96
|
+
},
|
|
97
|
+
FIELD_OUTLINE_OFF,
|
|
98
|
+
],
|
|
99
|
+
},
|
|
100
|
+
h(Icon, { name: mode === "time" ? "clock" : "calendar", size: CLEAR_ICON_SIZE, color: ink }),
|
|
101
|
+
h(
|
|
102
|
+
Text,
|
|
103
|
+
{ numberOfLines: 1, style: { flexShrink: 1, color: ink, fontFamily: t.fontFamily, fontSize: t.fontSize } },
|
|
104
|
+
shown,
|
|
105
|
+
),
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function DateTimePicker({
|
|
110
|
+
value,
|
|
111
|
+
onChange,
|
|
112
|
+
mode,
|
|
113
|
+
minimumDate,
|
|
114
|
+
maximumDate,
|
|
115
|
+
disabled = false,
|
|
116
|
+
accessibilityLabel,
|
|
117
|
+
placeholder,
|
|
118
|
+
clearLabel = "Clear",
|
|
119
|
+
doneLabel = "Done",
|
|
120
|
+
closeAccessibilityLabel,
|
|
121
|
+
testID,
|
|
122
|
+
}) {
|
|
123
|
+
const theme = useHostTheme() || DEFAULT_THEME_TOKENS;
|
|
124
|
+
const t = resolveFormControlTokens(theme);
|
|
125
|
+
const appLocale = useHostLocale();
|
|
126
|
+
const locale = React.useMemo(() => resolvePickerLocale(appLocale), [appLocale]);
|
|
127
|
+
const effectiveMode = normaliseMode(mode);
|
|
128
|
+
const [open, setOpen] = React.useState(false);
|
|
129
|
+
const [focused, setFocused] = React.useState(false);
|
|
130
|
+
const triggerRef = React.useRef(null);
|
|
131
|
+
const parts = React.useMemo(() => valueToParts(value, effectiveMode), [value, effectiveMode]);
|
|
132
|
+
const bounds = React.useMemo(
|
|
133
|
+
() => resolveBounds(minimumDate, maximumDate, effectiveMode),
|
|
134
|
+
[minimumDate, maximumDate, effectiveMode],
|
|
135
|
+
);
|
|
136
|
+
const hasValue = isSet(value);
|
|
137
|
+
const shown =
|
|
138
|
+
!hasValue && isSet(placeholder) ? placeholder : formatDisplayLabel(value, effectiveMode, locale);
|
|
139
|
+
const canEdit = !disabled && typeof onChange === "function";
|
|
140
|
+
|
|
141
|
+
const emit = (next) => {
|
|
142
|
+
if (typeof onChange === "function") onChange(partsToIso(next, effectiveMode));
|
|
143
|
+
};
|
|
144
|
+
// Back to the field once the sheet is gone, or its focus trap pulls it in.
|
|
145
|
+
const wasOpen = React.useRef(false);
|
|
146
|
+
React.useEffect(() => {
|
|
147
|
+
if (wasOpen.current && !open) focusNode(triggerRef.current);
|
|
148
|
+
wasOpen.current = open;
|
|
149
|
+
}, [open]);
|
|
150
|
+
|
|
151
|
+
const trigger = h(Trigger, {
|
|
152
|
+
t,
|
|
153
|
+
triggerRef,
|
|
154
|
+
open,
|
|
155
|
+
canEdit,
|
|
156
|
+
hasValue,
|
|
157
|
+
shown,
|
|
158
|
+
mode: effectiveMode,
|
|
159
|
+
spoken: [accessibilityLabel, shown].filter(Boolean).join(", "),
|
|
160
|
+
onOpen: () => setOpen(true),
|
|
161
|
+
onFocusChange: setFocused,
|
|
162
|
+
testID,
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
const field = h(
|
|
166
|
+
View,
|
|
167
|
+
{
|
|
168
|
+
style: [
|
|
169
|
+
{
|
|
170
|
+
flexDirection: "row",
|
|
171
|
+
alignItems: "center",
|
|
172
|
+
borderWidth: 1,
|
|
173
|
+
borderColor: t.borderColor,
|
|
174
|
+
borderRadius: t.radius,
|
|
175
|
+
backgroundColor: t.background,
|
|
176
|
+
},
|
|
177
|
+
fieldStateStyle(t, { focused, invalid: false }),
|
|
178
|
+
disabled ? { opacity: t.disabledOpacity } : null,
|
|
179
|
+
],
|
|
180
|
+
},
|
|
181
|
+
trigger,
|
|
182
|
+
hasValue && canEdit
|
|
183
|
+
? h(ClearButton, {
|
|
184
|
+
t,
|
|
185
|
+
label: clearLabel,
|
|
186
|
+
testID,
|
|
187
|
+
onPress: () => {
|
|
188
|
+
emit(null);
|
|
189
|
+
// The button unmounts once cleared; focus stays on the field.
|
|
190
|
+
focusNode(triggerRef.current);
|
|
191
|
+
},
|
|
192
|
+
})
|
|
193
|
+
: null,
|
|
194
|
+
);
|
|
195
|
+
|
|
196
|
+
// A sibling of the field: iOS draws its picker inline, below it.
|
|
197
|
+
return h(
|
|
198
|
+
React.Fragment,
|
|
199
|
+
null,
|
|
200
|
+
field,
|
|
201
|
+
h(Surface, {
|
|
202
|
+
open,
|
|
203
|
+
onClose: () => setOpen(false),
|
|
204
|
+
mode: effectiveMode,
|
|
205
|
+
parts,
|
|
206
|
+
bounds,
|
|
207
|
+
locale,
|
|
208
|
+
onPick: emit,
|
|
209
|
+
title: accessibilityLabel || shown,
|
|
210
|
+
heading: accessibilityLabel,
|
|
211
|
+
doneLabel,
|
|
212
|
+
closeAccessibilityLabel,
|
|
213
|
+
testID,
|
|
214
|
+
t,
|
|
215
|
+
}),
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
DateTimePicker.displayName = "DateTimePicker";
|
|
220
|
+
return DateTimePicker;
|
|
221
|
+
}
|
package/dist/datetimepicker.js
CHANGED
|
@@ -1,166 +1,15 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
// no react-native-web mapping, so importing it in the browser yields no
|
|
5
|
-
// rendered control. The web build therefore uses the browser's native
|
|
6
|
-
// `<input type="date" | "time" | "datetime-local">` directly and keeps the
|
|
7
|
-
// public contract identical to the native build: the value prop and the
|
|
8
|
-
// onChange callback both speak ISO 8601 strings (the datastore wire format),
|
|
9
|
-
// so a widget's JSX is byte-for-byte the same on both platforms.
|
|
10
|
-
//
|
|
11
|
-
// Props (mirrors datetimepicker.native.js):
|
|
12
|
-
// value: string | null — ISO 8601 (`2026-05-28` for date mode,
|
|
13
|
-
// `14:30` for time mode,
|
|
14
|
-
// `2026-05-28T14:30:00.000Z` for datetime mode).
|
|
15
|
-
// `null` / "" leaves the input blank.
|
|
16
|
-
// onChange: (iso: string) => void
|
|
17
|
-
// mode: "date" | "time" | "datetime" — default "date"
|
|
18
|
-
// minimumDate / maximumDate: string | null — ISO bounds
|
|
19
|
-
// disabled: boolean
|
|
20
|
-
// accessibilityLabel: string | undefined — written to the DOM input as
|
|
21
|
-
// `aria-label`. The shared form-field renderer
|
|
22
|
-
// in `frontend/src/components/widgets/_shared/
|
|
23
|
-
// formFields.jsx` passes the column's label
|
|
24
|
-
// through this prop so Playwright / screen
|
|
25
|
-
// readers can locate the input by its label.
|
|
1
|
+
// `<DateTimePicker>` (web). Binds ./datetimepicker-view.js to react-native-web
|
|
2
|
+
// and the themed calendar sheet; see ./primitives.js for why not the bare
|
|
3
|
+
// specifier.
|
|
26
4
|
|
|
27
|
-
import
|
|
28
|
-
import {
|
|
29
|
-
import {
|
|
30
|
-
|
|
31
|
-
isoToDateTimeLocalInput,
|
|
32
|
-
dateTimeLocalInputToIso,
|
|
33
|
-
} from "./datetimepicker-format.js";
|
|
34
|
-
import { DEFAULT_THEME_TOKENS } from "./_theme-tokens.js";
|
|
5
|
+
import { View, Text, Pressable, ScrollView, Platform } from "react-native-web";
|
|
6
|
+
import { makeDateTimePicker } from "./datetimepicker-view.js";
|
|
7
|
+
import { makeDateTimeSheet } from "./datetimepicker-sheet.js";
|
|
8
|
+
import { Overlay } from "./overlay.js";
|
|
35
9
|
|
|
36
|
-
const
|
|
37
|
-
date: "date",
|
|
38
|
-
time: "time",
|
|
39
|
-
datetime: "datetime-local",
|
|
40
|
-
};
|
|
10
|
+
const primitives = { View, Text, Pressable, ScrollView, Platform };
|
|
41
11
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
// in LOCAL time — we round-trip through Date to convert a stored UTC ISO
|
|
47
|
-
// (e.g. `2026-05-28T13:30:00.000Z`) to the user's local clock.
|
|
48
|
-
function _isoToInputValue(value, mode) {
|
|
49
|
-
if (value == null || value === "") return "";
|
|
50
|
-
if (mode === "date") {
|
|
51
|
-
// Strip any time portion if a datetime ISO accidentally lands in a
|
|
52
|
-
// date-mode picker — keep the YYYY-MM-DD head.
|
|
53
|
-
return String(value).slice(0, 10);
|
|
54
|
-
}
|
|
55
|
-
if (mode === "time") {
|
|
56
|
-
return String(value).slice(0, 5);
|
|
57
|
-
}
|
|
58
|
-
return isoToDateTimeLocalInput(value);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
// Translate the input's typed string back to the ISO format the wire wants.
|
|
62
|
-
// `date` and `time` are already in the right shape; `datetime-local` is
|
|
63
|
-
// parsed as local time and serialized as the full UTC ISO so the value
|
|
64
|
-
// round-trips into a DATE column unchanged.
|
|
65
|
-
function _inputValueToIso(raw, mode) {
|
|
66
|
-
if (raw == null || raw === "") return "";
|
|
67
|
-
if (mode === "date" || mode === "time") return raw;
|
|
68
|
-
return dateTimeLocalInputToIso(raw);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
export function DateTimePicker({
|
|
72
|
-
value,
|
|
73
|
-
onChange,
|
|
74
|
-
mode,
|
|
75
|
-
minimumDate,
|
|
76
|
-
maximumDate,
|
|
77
|
-
disabled,
|
|
78
|
-
accessibilityLabel,
|
|
79
|
-
}) {
|
|
80
|
-
const theme = useHostTheme() || DEFAULT_THEME_TOKENS;
|
|
81
|
-
const effectiveMode = mode === "time" || mode === "datetime" ? mode : "date";
|
|
82
|
-
const inputType = MODE_TO_INPUT_TYPE[effectiveMode];
|
|
83
|
-
const inputValue = _isoToInputValue(value, effectiveMode);
|
|
84
|
-
|
|
85
|
-
const handleChange = (event) => {
|
|
86
|
-
if (typeof onChange !== "function") return;
|
|
87
|
-
const iso = _inputValueToIso(event.target.value, effectiveMode);
|
|
88
|
-
onChange(iso);
|
|
89
|
-
};
|
|
90
|
-
|
|
91
|
-
// The min/max props accept ISO strings too — pass them through in the
|
|
92
|
-
// input-friendly representation. Undefined when not provided so the
|
|
93
|
-
// browser doesn't show an empty constraint chip.
|
|
94
|
-
const min =
|
|
95
|
-
minimumDate != null && minimumDate !== ""
|
|
96
|
-
? _isoToInputValue(minimumDate, effectiveMode)
|
|
97
|
-
: undefined;
|
|
98
|
-
const max =
|
|
99
|
-
maximumDate != null && maximumDate !== ""
|
|
100
|
-
? _isoToInputValue(maximumDate, effectiveMode)
|
|
101
|
-
: undefined;
|
|
102
|
-
|
|
103
|
-
// sc-1878 — clicking the formatted date text (or focusing it and pressing
|
|
104
|
-
// Enter / Space) opens the native calendar, not just the small built-in
|
|
105
|
-
// calendar icon. `showPicker()` requires a user gesture; click/keydown both
|
|
106
|
-
// qualify. Guarded: not every browser/build exposes it, and calling it while
|
|
107
|
-
// the picker is already open throws — neither should break the field.
|
|
108
|
-
const openPicker = (target) => {
|
|
109
|
-
if (disabled || !target || typeof target.showPicker !== "function") return;
|
|
110
|
-
try {
|
|
111
|
-
target.showPicker();
|
|
112
|
-
} catch {
|
|
113
|
-
// Already open, or blocked outside a user gesture — leave the input as-is.
|
|
114
|
-
}
|
|
115
|
-
};
|
|
116
|
-
|
|
117
|
-
const handleClick = (event) => openPicker(event.currentTarget);
|
|
118
|
-
|
|
119
|
-
const handleKeyDown = (event) => {
|
|
120
|
-
if (event.key === "Enter" || event.key === " " || event.key === "Spacebar") {
|
|
121
|
-
event.preventDefault();
|
|
122
|
-
openPicker(event.currentTarget);
|
|
123
|
-
}
|
|
124
|
-
};
|
|
125
|
-
|
|
126
|
-
// Inline styles match the SDK's other web-only primitives — the host's
|
|
127
|
-
// form widgets wrap this in their own labelled field, so the input just
|
|
128
|
-
// needs to look like a normal text input. `cursor: pointer` signals the
|
|
129
|
-
// whole field is the click affordance (sc-1878). Colours come from the
|
|
130
|
-
// theme so the field is legible on any surface (sc-3370); `colorScheme`
|
|
131
|
-
// steers the browser's built-in date UI (edit segments + calendar icon).
|
|
132
|
-
// The scheme keys off the theme's TEXT colour (onSurface, which the host
|
|
133
|
-
// already picked to contrast the surface) rather than re-deriving dark-vs-
|
|
134
|
-
// light from the surface with a second luminance formula that could disagree
|
|
135
|
-
// with the host: light text ⇒ dark surface ⇒ dark scheme.
|
|
136
|
-
const style = {
|
|
137
|
-
boxSizing: "border-box",
|
|
138
|
-
width: "100%",
|
|
139
|
-
minHeight: 44,
|
|
140
|
-
padding: "8px 12px",
|
|
141
|
-
fontSize: 16,
|
|
142
|
-
fontFamily: "inherit",
|
|
143
|
-
color: theme.colors.onSurface,
|
|
144
|
-
backgroundColor: "transparent",
|
|
145
|
-
colorScheme: isDarkColor(theme.colors.onSurface) ? "light" : "dark",
|
|
146
|
-
borderWidth: 1,
|
|
147
|
-
borderStyle: "solid",
|
|
148
|
-
borderColor: theme.colors.border,
|
|
149
|
-
borderRadius: 6,
|
|
150
|
-
outline: "none",
|
|
151
|
-
cursor: disabled ? "default" : "pointer",
|
|
152
|
-
};
|
|
153
|
-
|
|
154
|
-
return React.createElement("input", {
|
|
155
|
-
type: inputType,
|
|
156
|
-
value: inputValue,
|
|
157
|
-
onChange: handleChange,
|
|
158
|
-
onClick: handleClick,
|
|
159
|
-
onKeyDown: handleKeyDown,
|
|
160
|
-
min,
|
|
161
|
-
max,
|
|
162
|
-
disabled: !!disabled,
|
|
163
|
-
style,
|
|
164
|
-
"aria-label": accessibilityLabel || undefined,
|
|
165
|
-
});
|
|
166
|
-
}
|
|
12
|
+
export const DateTimePicker = makeDateTimePicker({
|
|
13
|
+
...primitives,
|
|
14
|
+
Surface: makeDateTimeSheet({ ...primitives, Overlay }),
|
|
15
|
+
});
|
|
@@ -1,131 +1,43 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
// the only way to reopen it — so the primitive owns a small open/closed state
|
|
6
|
-
// and renders a tappable, formatted-date trigger (sc-1878). Tapping the
|
|
7
|
-
// formatted date (the trigger is an accessible button, reachable by keyboard /
|
|
8
|
-
// screen-reader) opens the picker; choosing a value closes it and emits the
|
|
9
|
-
// ISO string. This mirrors the web build, where clicking the `<input>` text
|
|
10
|
-
// calls `showPicker()` — both hosts: tap the date, the picker opens.
|
|
11
|
-
//
|
|
12
|
-
// Props (mirrors datetimepicker.js):
|
|
13
|
-
// value: string | null — ISO 8601 (`2026-05-28` for date mode,
|
|
14
|
-
// `14:30` for time mode,
|
|
15
|
-
// `2026-05-28T14:30:00.000Z` for datetime mode).
|
|
16
|
-
// `null` / "" shows the placeholder, opens at "now".
|
|
17
|
-
// onChange: (iso: string) => void
|
|
18
|
-
// mode: "date" | "time" | "datetime" — default "date"
|
|
19
|
-
// minimumDate / maximumDate: string | null — ISO bounds
|
|
20
|
-
// disabled: boolean
|
|
21
|
-
// accessibilityLabel: string | undefined — names the trigger button so the
|
|
22
|
-
// shared form-field renderer's column label reaches
|
|
23
|
-
// screen readers / test tooling, matching the web
|
|
24
|
-
// input's `aria-label`.
|
|
1
|
+
// `<DateTimePicker>` (native). Binds ./datetimepicker-view.js to React Native
|
|
2
|
+
// with the operating system's own picker as its surface, via
|
|
3
|
+
// `@react-native-community/datetimepicker`. That library is imperative on
|
|
4
|
+
// Android — rendering it opens the dialog — so it mounts only while open.
|
|
25
5
|
|
|
26
|
-
import React
|
|
6
|
+
import React from "react";
|
|
27
7
|
// eslint-disable-next-line no-restricted-syntax
|
|
28
8
|
import RNDateTimePicker from "@react-native-community/datetimepicker";
|
|
29
|
-
import {
|
|
30
|
-
import {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
const styles = StyleSheet.create({
|
|
39
|
-
trigger: {
|
|
40
|
-
minHeight: 44,
|
|
41
|
-
paddingVertical: 8,
|
|
42
|
-
paddingHorizontal: 12,
|
|
43
|
-
borderWidth: 1,
|
|
44
|
-
borderRadius: 6,
|
|
45
|
-
justifyContent: "center",
|
|
46
|
-
},
|
|
47
|
-
triggerDisabled: { opacity: 0.5 },
|
|
48
|
-
label: { fontSize: 16 },
|
|
49
|
-
placeholder: { fontSize: 16, opacity: 0.5 },
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
export function DateTimePicker({
|
|
53
|
-
value,
|
|
54
|
-
onChange,
|
|
55
|
-
mode,
|
|
56
|
-
minimumDate,
|
|
57
|
-
maximumDate,
|
|
58
|
-
disabled,
|
|
59
|
-
accessibilityLabel,
|
|
60
|
-
}) {
|
|
61
|
-
const theme = useHostTheme() || DEFAULT_THEME_TOKENS;
|
|
62
|
-
const effectiveMode = mode === "time" || mode === "datetime" ? mode : "date";
|
|
63
|
-
const [open, setOpen] = useState(false);
|
|
64
|
-
const dateValue = useMemo(() => parseToDate(value), [value]);
|
|
65
|
-
const min = useMemo(
|
|
66
|
-
() => (minimumDate ? parseToDate(minimumDate) : undefined),
|
|
67
|
-
[minimumDate],
|
|
68
|
-
);
|
|
69
|
-
const max = useMemo(
|
|
70
|
-
() => (maximumDate ? parseToDate(maximumDate) : undefined),
|
|
71
|
-
[maximumDate],
|
|
72
|
-
);
|
|
73
|
-
|
|
74
|
-
const isEmpty = value == null || value === "";
|
|
75
|
-
|
|
76
|
-
const handleChange = (event, picked) => {
|
|
77
|
-
// Android fires "dismissed" when the user cancels; close without emitting.
|
|
78
|
-
setOpen(false);
|
|
79
|
-
if (event?.type === "dismissed") return;
|
|
80
|
-
if (typeof onChange !== "function") return;
|
|
81
|
-
if (!(picked instanceof Date)) return;
|
|
82
|
-
const iso = formatToIso(picked, effectiveMode);
|
|
83
|
-
if (iso != null) onChange(iso);
|
|
84
|
-
};
|
|
9
|
+
import { View, Text, Pressable, Platform } from "react-native";
|
|
10
|
+
import { makeDateTimePicker } from "./datetimepicker-view.js";
|
|
11
|
+
import { nowParts, partsToDate, valueToParts } from "./datetimepicker-format.js";
|
|
12
|
+
import { clampToBounds } from "./datetimepicker-calendar.js";
|
|
13
|
+
|
|
14
|
+
function boundDate(parts, mode) {
|
|
15
|
+
if (!parts || mode === "time") return undefined;
|
|
16
|
+
return partsToDate(parts);
|
|
17
|
+
}
|
|
85
18
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
19
|
+
function OsPicker({ open, onClose, mode, parts, bounds, onPick }) {
|
|
20
|
+
if (!open) return null;
|
|
21
|
+
const start = parts ? { ...nowParts(), ...dropEmpty(parts) } : clampToBounds(nowParts(), bounds);
|
|
22
|
+
return React.createElement(RNDateTimePicker, {
|
|
23
|
+
value: partsToDate(start),
|
|
24
|
+
// iOS honours "datetime"; Android shows its date dialog for it.
|
|
25
|
+
mode,
|
|
26
|
+
minimumDate: boundDate(bounds.min, mode),
|
|
27
|
+
maximumDate: boundDate(bounds.max, mode),
|
|
28
|
+
onChange: (event, picked) => {
|
|
29
|
+
onClose();
|
|
30
|
+
// Android reports a cancelled dialog as "dismissed".
|
|
31
|
+
if (event && event.type === "dismissed") return;
|
|
32
|
+
if (!(picked instanceof Date)) return;
|
|
33
|
+
// Android's time dialog cannot be bounded, so a pick is clamped here.
|
|
34
|
+
onPick(clampToBounds(valueToParts(picked.toISOString(), "datetime"), bounds));
|
|
100
35
|
},
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
{
|
|
104
|
-
style: [
|
|
105
|
-
isEmpty ? styles.placeholder : styles.label,
|
|
106
|
-
{ color: theme.colors.onSurface },
|
|
107
|
-
],
|
|
108
|
-
},
|
|
109
|
-
formatDisplayLabel(value, effectiveMode),
|
|
110
|
-
),
|
|
111
|
-
);
|
|
112
|
-
|
|
113
|
-
if (!open) return trigger;
|
|
36
|
+
});
|
|
37
|
+
}
|
|
114
38
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
// here). The web build uses datetimepicker.js, so this file is never web.
|
|
118
|
-
return React.createElement(
|
|
119
|
-
React.Fragment,
|
|
120
|
-
null,
|
|
121
|
-
trigger,
|
|
122
|
-
React.createElement(RNDateTimePicker, {
|
|
123
|
-
value: dateValue,
|
|
124
|
-
mode: effectiveMode,
|
|
125
|
-
minimumDate: min,
|
|
126
|
-
maximumDate: max,
|
|
127
|
-
disabled: !!disabled,
|
|
128
|
-
onChange: handleChange,
|
|
129
|
-
}),
|
|
130
|
-
);
|
|
39
|
+
function dropEmpty(parts) {
|
|
40
|
+
return Object.fromEntries(Object.entries(parts).filter(([, v]) => v != null));
|
|
131
41
|
}
|
|
42
|
+
|
|
43
|
+
export const DateTimePicker = makeDateTimePicker({ View, Text, Pressable, Platform, Surface: OsPicker });
|
package/dist/hooks.js
CHANGED
|
@@ -238,6 +238,14 @@ export function useHostNavigation() {
|
|
|
238
238
|
return ctx ? ctx.navigation : undefined;
|
|
239
239
|
}
|
|
240
240
|
|
|
241
|
+
// @internal — the app's language, read WITHOUT asserting a widget context so
|
|
242
|
+
// `<DateTimePicker>` can name months in it wherever it renders.
|
|
243
|
+
export function useHostLocale() {
|
|
244
|
+
const ctx = useContext(HostWidgetContext);
|
|
245
|
+
const locale = ctx && ctx.i18n ? ctx.i18n.locale : undefined;
|
|
246
|
+
return typeof locale === "string" && locale ? locale : undefined;
|
|
247
|
+
}
|
|
248
|
+
|
|
241
249
|
// A host translation, or null on a miss: both hosts answer a miss with the
|
|
242
250
|
// key itself or a `{{t:…}}` placeholder.
|
|
243
251
|
function hostTranslation(hostT, key) {
|
package/dist/host.d.ts
CHANGED
|
@@ -578,6 +578,12 @@ export function resolveLoginLogoUrl(
|
|
|
578
578
|
appLogoUrl: string | null | undefined,
|
|
579
579
|
): string | null;
|
|
580
580
|
|
|
581
|
+
/** sc-11754 — whether the sign-in card shows its heading and subheading. */
|
|
582
|
+
export function resolveLoginTextVisibility(login: unknown): {
|
|
583
|
+
heading: boolean;
|
|
584
|
+
subheading: boolean;
|
|
585
|
+
};
|
|
586
|
+
|
|
581
587
|
/** sc-7800 — true when a stored value is a `theme:<path>` colour binding. */
|
|
582
588
|
export function isThemeTokenRef(value: unknown): boolean;
|
|
583
589
|
|
|
@@ -626,10 +632,12 @@ export function isoToDateTimeLocalInput(value: unknown): string;
|
|
|
626
632
|
export function dateTimeLocalInputToIso(raw: unknown): string;
|
|
627
633
|
|
|
628
634
|
/**
|
|
629
|
-
*
|
|
630
|
-
*
|
|
635
|
+
* The `<DateTimePicker>` field's label: the value in `locale` (default: the
|
|
636
|
+
* runtime's), a `"datetime"` instant on the viewer's clock (sc-8448, sc-11777).
|
|
637
|
+
* Empty gives the mode's placeholder; an unreadable value is returned as is.
|
|
631
638
|
*/
|
|
632
639
|
export function formatDateTimeLabel(
|
|
633
640
|
value: unknown,
|
|
634
641
|
mode: "date" | "time" | "datetime",
|
|
642
|
+
locale?: string,
|
|
635
643
|
): string;
|
package/dist/host.js
CHANGED
|
@@ -153,9 +153,27 @@ function resolveLoginLogoUrl(login, appLogoUrl) {
|
|
|
153
153
|
return own || normaliseImageUrl(appLogoUrl);
|
|
154
154
|
}
|
|
155
155
|
|
|
156
|
+
/**
|
|
157
|
+
* sc-11754 — which of the card's two text lines the author left showing.
|
|
158
|
+
* Only a literal `true` hides, so every app saved before this keeps both.
|
|
159
|
+
* Not in `normaliseLoginDesign`: hidden text is not a backdrop, so it must
|
|
160
|
+
* not lift the card or drop the shell background.
|
|
161
|
+
*
|
|
162
|
+
* @param {object|null|undefined} login - `themeConfig.login`.
|
|
163
|
+
* @returns {{ heading: boolean, subheading: boolean }}
|
|
164
|
+
*/
|
|
165
|
+
function resolveLoginTextVisibility(login) {
|
|
166
|
+
const own = login && typeof login === "object" ? login : {};
|
|
167
|
+
return {
|
|
168
|
+
heading: own.hideHeading !== true,
|
|
169
|
+
subheading: own.hideSubheading !== true,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
156
173
|
module.exports = {
|
|
157
174
|
LOGIN_TEXT_MAX_LENGTH,
|
|
158
175
|
normaliseThemeGradient,
|
|
159
176
|
normaliseLoginDesign,
|
|
160
177
|
resolveLoginLogoUrl,
|
|
178
|
+
resolveLoginTextVisibility,
|
|
161
179
|
};
|
package/dist/theme-background.js
CHANGED
|
@@ -144,3 +144,20 @@ export function resolveLoginLogoUrl(login, appLogoUrl) {
|
|
|
144
144
|
: null;
|
|
145
145
|
return own || normaliseImageUrl(appLogoUrl);
|
|
146
146
|
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* sc-11754 — which of the card's two text lines the author left showing.
|
|
150
|
+
* Only a literal `true` hides, so every app saved before this keeps both.
|
|
151
|
+
* Not in `normaliseLoginDesign`: hidden text is not a backdrop, so it must
|
|
152
|
+
* not lift the card or drop the shell background.
|
|
153
|
+
*
|
|
154
|
+
* @param {object|null|undefined} login - `themeConfig.login`.
|
|
155
|
+
* @returns {{ heading: boolean, subheading: boolean }}
|
|
156
|
+
*/
|
|
157
|
+
export function resolveLoginTextVisibility(login) {
|
|
158
|
+
const own = login && typeof login === "object" ? login : {};
|
|
159
|
+
return {
|
|
160
|
+
heading: own.hideHeading !== true,
|
|
161
|
+
subheading: own.hideSubheading !== true,
|
|
162
|
+
};
|
|
163
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@colixsystems/widget-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.236.0",
|
|
4
4
|
"description": "Common widget interface for AppStudio. Implements WidgetManifest, WidgetContext, property schema, and helper hooks.",
|
|
5
5
|
"homepage": "https://github.com/Colix-AB/AppStudio",
|
|
6
6
|
"type": "module",
|