@ankhorage/zora 12.1.0 → 14.0.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/CHANGELOG.md +16 -0
- package/README.md +11 -5
- package/dist/features/accordion/accordionItemMeta.d.ts +53 -0
- package/dist/features/accordion/accordionItemMeta.d.ts.map +1 -0
- package/dist/features/accordion/accordionItemMeta.js +45 -0
- package/dist/features/accordion/accordionItemMeta.js.map +1 -0
- package/dist/features/accordion/accordionMeta.d.ts +36 -0
- package/dist/features/accordion/accordionMeta.d.ts.map +1 -0
- package/dist/features/accordion/accordionMeta.js +33 -0
- package/dist/features/accordion/accordionMeta.js.map +1 -0
- package/dist/features/accordion/adapters/inbound/Accordion.d.ts +5 -0
- package/dist/features/accordion/adapters/inbound/Accordion.d.ts.map +1 -0
- package/dist/features/accordion/adapters/inbound/Accordion.js +41 -0
- package/dist/features/accordion/adapters/inbound/Accordion.js.map +1 -0
- package/dist/features/accordion/adapters/inbound/AccordionItem.d.ts +5 -0
- package/dist/features/accordion/adapters/inbound/AccordionItem.d.ts.map +1 -0
- package/dist/features/accordion/adapters/inbound/AccordionItem.js +52 -0
- package/dist/features/accordion/adapters/inbound/AccordionItem.js.map +1 -0
- package/dist/features/accordion/composition/AccordionPresentationContext.d.ts +4 -0
- package/dist/features/accordion/composition/AccordionPresentationContext.d.ts.map +1 -0
- package/dist/features/accordion/composition/AccordionPresentationContext.js +3 -0
- package/dist/features/accordion/composition/AccordionPresentationContext.js.map +1 -0
- package/dist/features/accordion/public.d.ts +4 -0
- package/dist/features/accordion/public.d.ts.map +1 -0
- package/dist/features/accordion/public.js +3 -0
- package/dist/features/accordion/public.js.map +1 -0
- package/dist/features/bottom-sheet/bottomSheetMeta.d.ts +1 -1
- package/dist/features/card/cardMeta.d.ts +1 -1
- package/dist/features/card/mediaCardMeta.d.ts +1 -1
- package/dist/features/card/postCardMeta.d.ts +1 -1
- package/dist/features/date-picker/adapters/inbound/DatePicker.d.ts +1 -6
- package/dist/features/date-picker/adapters/inbound/DatePicker.d.ts.map +1 -1
- package/dist/features/date-picker/adapters/inbound/DatePicker.js +1 -175
- package/dist/features/date-picker/adapters/inbound/DatePicker.js.map +1 -1
- package/dist/features/date-picker/adapters/inbound/DatePicker.native.d.ts +5 -0
- package/dist/features/date-picker/adapters/inbound/DatePicker.native.d.ts.map +1 -0
- package/dist/features/date-picker/adapters/inbound/DatePicker.native.js +27 -0
- package/dist/features/date-picker/adapters/inbound/DatePicker.native.js.map +1 -0
- package/dist/features/date-picker/adapters/inbound/DatePicker.web.d.ts +5 -0
- package/dist/features/date-picker/adapters/inbound/DatePicker.web.d.ts.map +1 -0
- package/dist/features/date-picker/adapters/inbound/DatePicker.web.js +25 -0
- package/dist/features/date-picker/adapters/inbound/DatePicker.web.js.map +1 -0
- package/dist/features/date-picker/composition/DatePickerContent.d.ts +10 -0
- package/dist/features/date-picker/composition/DatePickerContent.d.ts.map +1 -0
- package/dist/features/date-picker/composition/DatePickerContent.js +129 -0
- package/dist/features/date-picker/composition/DatePickerContent.js.map +1 -0
- package/dist/features/date-picker/composition/DatePickerField.d.ts +9 -0
- package/dist/features/date-picker/composition/DatePickerField.d.ts.map +1 -0
- package/dist/features/date-picker/composition/DatePickerField.js +21 -0
- package/dist/features/date-picker/composition/DatePickerField.js.map +1 -0
- package/dist/features/date-picker/composition/DatePickerTrigger.d.ts +9 -0
- package/dist/features/date-picker/composition/DatePickerTrigger.d.ts.map +1 -0
- package/dist/features/date-picker/composition/DatePickerTrigger.js +18 -0
- package/dist/features/date-picker/composition/DatePickerTrigger.js.map +1 -0
- package/dist/features/gradient/gradientMeta.d.ts +1 -1
- package/dist/features/keyboard-avoiding-view/keyboardAvoidingViewMeta.d.ts +1 -1
- package/dist/features/layout/boxMeta.d.ts +1 -1
- package/dist/features/layout/containerMeta.d.ts +1 -1
- package/dist/features/layout/gridMeta.d.ts +1 -1
- package/dist/features/layout/stackMeta.d.ts +1 -1
- package/dist/features/list/flatListMeta.d.ts +1 -1
- package/dist/features/list/sectionListMeta.d.ts +1 -1
- package/dist/features/surface/surfaceMeta.d.ts +1 -1
- package/dist/features/time-picker/adapters/inbound/TimePicker.d.ts +1 -6
- package/dist/features/time-picker/adapters/inbound/TimePicker.d.ts.map +1 -1
- package/dist/features/time-picker/adapters/inbound/TimePicker.js +1 -116
- package/dist/features/time-picker/adapters/inbound/TimePicker.js.map +1 -1
- package/dist/features/time-picker/adapters/inbound/TimePicker.native.d.ts +5 -0
- package/dist/features/time-picker/adapters/inbound/TimePicker.native.d.ts.map +1 -0
- package/dist/features/time-picker/adapters/inbound/TimePicker.native.js +26 -0
- package/dist/features/time-picker/adapters/inbound/TimePicker.native.js.map +1 -0
- package/dist/features/time-picker/adapters/inbound/TimePicker.web.d.ts +5 -0
- package/dist/features/time-picker/adapters/inbound/TimePicker.web.d.ts.map +1 -0
- package/dist/features/time-picker/adapters/inbound/TimePicker.web.js +24 -0
- package/dist/features/time-picker/adapters/inbound/TimePicker.web.js.map +1 -0
- package/dist/features/time-picker/composition/TimePickerContent.d.ts +10 -0
- package/dist/features/time-picker/composition/TimePickerContent.d.ts.map +1 -0
- package/dist/features/time-picker/composition/TimePickerContent.js +78 -0
- package/dist/features/time-picker/composition/TimePickerContent.js.map +1 -0
- package/dist/features/time-picker/composition/TimePickerField.d.ts +9 -0
- package/dist/features/time-picker/composition/TimePickerField.d.ts.map +1 -0
- package/dist/features/time-picker/composition/TimePickerField.js +21 -0
- package/dist/features/time-picker/composition/TimePickerField.js.map +1 -0
- package/dist/features/time-picker/composition/TimePickerTrigger.d.ts +8 -0
- package/dist/features/time-picker/composition/TimePickerTrigger.d.ts.map +1 -0
- package/dist/features/time-picker/composition/TimePickerTrigger.js +9 -0
- package/dist/features/time-picker/composition/TimePickerTrigger.js.map +1 -0
- package/dist/features/toast/adapters/inbound/Toast.d.ts +5 -0
- package/dist/features/toast/adapters/inbound/Toast.d.ts.map +1 -0
- package/dist/features/toast/adapters/inbound/Toast.js +10 -0
- package/dist/features/toast/adapters/inbound/Toast.js.map +1 -0
- package/dist/features/toast/adapters/inbound/ToastProvider.d.ts +5 -0
- package/dist/features/toast/adapters/inbound/ToastProvider.d.ts.map +1 -0
- package/dist/features/toast/adapters/inbound/ToastProvider.js +7 -0
- package/dist/features/toast/adapters/inbound/ToastProvider.js.map +1 -0
- package/dist/features/toast/public.d.ts +5 -0
- package/dist/features/toast/public.d.ts.map +1 -0
- package/dist/features/toast/public.js +4 -0
- package/dist/features/toast/public.js.map +1 -0
- package/dist/features/toast/toastMeta.d.ts +9 -0
- package/dist/features/toast/toastMeta.d.ts.map +1 -0
- package/dist/features/toast/toastMeta.js +9 -0
- package/dist/features/toast/toastMeta.js.map +1 -0
- package/dist/features/toast/toastProviderMeta.d.ts +9 -0
- package/dist/features/toast/toastProviderMeta.d.ts.map +1 -0
- package/dist/features/toast/toastProviderMeta.js +9 -0
- package/dist/features/toast/toastProviderMeta.js.map +1 -0
- package/dist/index.d.ts +3 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/layout/screen/meta.d.ts +1 -1
- package/dist/layout/screen-section/meta.d.ts +1 -1
- package/dist/metadata/allowedChildren.d.ts +3 -3
- package/dist/metadata/allowedChildren.d.ts.map +1 -1
- package/dist/metadata/allowedChildren.js +1 -0
- package/dist/metadata/allowedChildren.js.map +1 -1
- package/dist/metadata/bindableComponentMeta.d.ts +2 -2
- package/dist/metadata/componentMeta.d.ts.map +1 -1
- package/dist/metadata/componentMeta.js +6 -3
- package/dist/metadata/componentMeta.js.map +1 -1
- package/dist/patterns/message-bubble/meta.d.ts +1 -1
- package/dist/patterns/notice/meta.d.ts +1 -1
- package/dist/patterns/panel/meta.d.ts +1 -1
- package/dist/patterns/reader/meta.d.ts +2 -2
- package/dist/patterns/scanner/meta.d.ts +2 -2
- package/dist/patterns/selection/SelectionProvider.d.ts +0 -4
- package/dist/patterns/selection/SelectionProvider.d.ts.map +1 -1
- package/dist/patterns/selection/SelectionProvider.js +4 -6
- package/dist/patterns/selection/SelectionProvider.js.map +1 -1
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +4 -3
- package/dist/registry.js.map +1 -1
- package/dist/theme/ZoraProvider.d.ts +4 -10
- package/dist/theme/ZoraProvider.d.ts.map +1 -1
- package/dist/theme/ZoraProvider.js +8 -6
- package/dist/theme/ZoraProvider.js.map +1 -1
- package/dist/theme/ZoraRuntimeCapabilities.d.ts +2 -0
- package/dist/theme/ZoraRuntimeCapabilities.d.ts.map +1 -0
- package/dist/theme/ZoraRuntimeCapabilities.js +2 -0
- package/dist/theme/ZoraRuntimeCapabilities.js.map +1 -0
- package/dist/theme/ZoraRuntimeCapabilities.native.d.ts +4 -0
- package/dist/theme/ZoraRuntimeCapabilities.native.d.ts.map +1 -0
- package/dist/theme/ZoraRuntimeCapabilities.native.js +8 -0
- package/dist/theme/ZoraRuntimeCapabilities.native.js.map +1 -0
- package/dist/theme/ZoraRuntimeCapabilities.web.d.ts +4 -0
- package/dist/theme/ZoraRuntimeCapabilities.web.d.ts.map +1 -0
- package/dist/theme/ZoraRuntimeCapabilities.web.js +8 -0
- package/dist/theme/ZoraRuntimeCapabilities.web.js.map +1 -0
- package/dist/theme/ZoraThemeRuntimeContext.d.ts +1 -0
- package/dist/theme/ZoraThemeRuntimeContext.d.ts.map +1 -1
- package/dist/theme/ZoraThemeRuntimeContext.js +3 -2
- package/dist/theme/ZoraThemeRuntimeContext.js.map +1 -1
- package/dist/theme/ZoraThemeScope.d.ts +1 -0
- package/dist/theme/ZoraThemeScope.d.ts.map +1 -1
- package/dist/theme/ZoraThemeScope.js +12 -10
- package/dist/theme/ZoraThemeScope.js.map +1 -1
- package/dist/theme/index.d.ts +1 -1
- package/dist/theme/index.d.ts.map +1 -1
- package/dist/theme/index.js.map +1 -1
- package/dist/types/accordion.d.ts +23 -0
- package/dist/types/accordion.d.ts.map +1 -0
- package/dist/types/accordion.js +2 -0
- package/dist/types/accordion.js.map +1 -0
- package/dist/types/provider.d.ts +19 -0
- package/dist/types/provider.d.ts.map +1 -0
- package/dist/types/provider.js +2 -0
- package/dist/types/provider.js.map +1 -0
- package/dist/types/toast.d.ts +9 -0
- package/dist/types/toast.d.ts.map +1 -0
- package/dist/types/toast.js +2 -0
- package/dist/types/toast.js.map +1 -0
- package/package.json +2 -2
- package/src/features/accordion/accordionItemMeta.ts +46 -0
- package/src/features/accordion/accordionMeta.ts +34 -0
- package/src/features/accordion/adapters/inbound/Accordion.tsx +94 -0
- package/src/features/accordion/adapters/inbound/AccordionItem.tsx +86 -0
- package/src/features/accordion/composition/AccordionPresentationContext.ts +6 -0
- package/src/features/accordion/public.ts +9 -0
- package/src/features/bottom-sheet/public.test.ts +15 -12
- package/src/features/date-picker/adapters/inbound/DatePicker.native.tsx +46 -0
- package/src/features/date-picker/adapters/inbound/DatePicker.tsx +1 -356
- package/src/features/date-picker/adapters/inbound/DatePicker.web.tsx +48 -0
- package/src/features/date-picker/composition/DatePickerContent.tsx +256 -0
- package/src/features/date-picker/composition/DatePickerField.tsx +53 -0
- package/src/features/date-picker/composition/DatePickerTrigger.tsx +47 -0
- package/src/features/date-picker/public.test.ts +14 -4
- package/src/features/time-picker/adapters/inbound/TimePicker.native.tsx +47 -0
- package/src/features/time-picker/adapters/inbound/TimePicker.tsx +1 -258
- package/src/features/time-picker/adapters/inbound/TimePicker.web.tsx +49 -0
- package/src/features/time-picker/composition/TimePickerContent.tsx +164 -0
- package/src/features/time-picker/composition/TimePickerField.tsx +53 -0
- package/src/features/time-picker/composition/TimePickerTrigger.tsx +35 -0
- package/src/features/time-picker/public.test.ts +14 -3
- package/src/features/toast/adapters/inbound/Toast.tsx +13 -0
- package/src/features/toast/adapters/inbound/ToastProvider.tsx +9 -0
- package/src/features/toast/public.test.ts +19 -0
- package/src/features/toast/public.ts +10 -0
- package/src/features/toast/toastMeta.ts +10 -0
- package/src/features/toast/toastProviderMeta.ts +10 -0
- package/src/index.ts +17 -4
- package/src/metadata/allowedChildren.ts +1 -0
- package/src/metadata/componentMeta.test.ts +9 -0
- package/src/metadata/componentMeta.ts +6 -3
- package/src/patterns/selection/SelectionProvider.tsx +4 -6
- package/src/registry.ts +4 -3
- package/src/showcaseCoverage.test.ts +2 -1
- package/src/theme/ZoraProvider.tsx +11 -13
- package/src/theme/ZoraRuntimeCapabilities.native.tsx +19 -0
- package/src/theme/ZoraRuntimeCapabilities.tsx +1 -0
- package/src/theme/ZoraRuntimeCapabilities.web.tsx +10 -0
- package/src/theme/ZoraThemeRuntimeContext.tsx +3 -2
- package/src/theme/ZoraThemeScope.tsx +14 -12
- package/src/theme/index.ts +1 -1
- package/src/theme/themeConfigScope.test.ts +4 -1
- package/src/theme/themeScopeStructure.test.ts +27 -18
- package/src/types/accordion.ts +38 -0
- package/src/types/provider.ts +22 -0
- package/src/{components/toast/types.ts → types/toast.ts} +5 -7
- package/dist/components/toast/Toast.d.ts +0 -9
- package/dist/components/toast/Toast.d.ts.map +0 -1
- package/dist/components/toast/Toast.js +0 -13
- package/dist/components/toast/Toast.js.map +0 -1
- package/dist/components/toast/ToastProvider.d.ts +0 -9
- package/dist/components/toast/ToastProvider.d.ts.map +0 -1
- package/dist/components/toast/ToastProvider.js +0 -14
- package/dist/components/toast/ToastProvider.js.map +0 -1
- package/dist/components/toast/index.d.ts +0 -5
- package/dist/components/toast/index.d.ts.map +0 -1
- package/dist/components/toast/index.js +0 -4
- package/dist/components/toast/index.js.map +0 -1
- package/dist/components/toast/meta.d.ts +0 -17
- package/dist/components/toast/meta.d.ts.map +0 -1
- package/dist/components/toast/meta.js +0 -17
- package/dist/components/toast/meta.js.map +0 -1
- package/dist/components/toast/types.d.ts +0 -12
- package/dist/components/toast/types.d.ts.map +0 -1
- package/dist/components/toast/types.js +0 -2
- package/dist/components/toast/types.js.map +0 -1
- package/dist/patterns/disclosure-section/DisclosureSection.d.ts +0 -9
- package/dist/patterns/disclosure-section/DisclosureSection.d.ts.map +0 -1
- package/dist/patterns/disclosure-section/DisclosureSection.js +0 -86
- package/dist/patterns/disclosure-section/DisclosureSection.js.map +0 -1
- package/dist/patterns/disclosure-section/index.d.ts +0 -3
- package/dist/patterns/disclosure-section/index.d.ts.map +0 -1
- package/dist/patterns/disclosure-section/index.js +0 -3
- package/dist/patterns/disclosure-section/index.js.map +0 -1
- package/dist/patterns/disclosure-section/meta.d.ts +0 -9
- package/dist/patterns/disclosure-section/meta.d.ts.map +0 -1
- package/dist/patterns/disclosure-section/meta.js +0 -9
- package/dist/patterns/disclosure-section/meta.js.map +0 -1
- package/dist/patterns/disclosure-section/types.d.ts +0 -15
- package/dist/patterns/disclosure-section/types.d.ts.map +0 -1
- package/dist/patterns/disclosure-section/types.js +0 -2
- package/dist/patterns/disclosure-section/types.js.map +0 -1
- package/src/components/toast/Toast.tsx +0 -16
- package/src/components/toast/ToastProvider.tsx +0 -17
- package/src/components/toast/index.ts +0 -4
- package/src/components/toast/meta.ts +0 -19
- package/src/patterns/disclosure-section/DisclosureSection.tsx +0 -130
- package/src/patterns/disclosure-section/index.ts +0 -2
- package/src/patterns/disclosure-section/meta.ts +0 -10
- package/src/patterns/disclosure-section/types.ts +0 -16
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { Field } from '@ankhorage/surface';
|
|
2
|
+
import type React from 'react';
|
|
3
|
+
|
|
4
|
+
import type { DatePickerProps } from '../../../types/date-picker';
|
|
5
|
+
import { Stack } from '../../layout/public';
|
|
6
|
+
import { Text } from '../../typography/public';
|
|
7
|
+
|
|
8
|
+
/*** Renders the shared DatePicker field frame around a platform-specific trigger. */
|
|
9
|
+
export function DatePickerField({
|
|
10
|
+
children,
|
|
11
|
+
description,
|
|
12
|
+
disabled,
|
|
13
|
+
error,
|
|
14
|
+
label,
|
|
15
|
+
required,
|
|
16
|
+
testID,
|
|
17
|
+
}: DatePickerFieldProps) {
|
|
18
|
+
return (
|
|
19
|
+
<Field
|
|
20
|
+
disabled={disabled}
|
|
21
|
+
errorText={error}
|
|
22
|
+
invalid={Boolean(error)}
|
|
23
|
+
label={label ? renderLabel(label, description) : undefined}
|
|
24
|
+
required={required}
|
|
25
|
+
testID={testID}
|
|
26
|
+
>
|
|
27
|
+
{children}
|
|
28
|
+
</Field>
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface DatePickerFieldProps extends Pick<
|
|
33
|
+
DatePickerProps,
|
|
34
|
+
'description' | 'disabled' | 'error' | 'label' | 'required' | 'testID'
|
|
35
|
+
> {
|
|
36
|
+
children: React.ReactNode;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/*** Renders the picker label and optional supporting description. */
|
|
40
|
+
function renderLabel(label: React.ReactNode, description: React.ReactNode | undefined) {
|
|
41
|
+
return (
|
|
42
|
+
<Stack gap="xs">
|
|
43
|
+
<Text variant="label" weight="semiBold">
|
|
44
|
+
{label}
|
|
45
|
+
</Text>
|
|
46
|
+
{description ? (
|
|
47
|
+
<Text emphasis="muted" variant="bodySmall">
|
|
48
|
+
{description}
|
|
49
|
+
</Text>
|
|
50
|
+
) : null}
|
|
51
|
+
</Stack>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type React from 'react';
|
|
2
|
+
|
|
3
|
+
import type { DatePickerProps } from '../../../types/date-picker';
|
|
4
|
+
import { Button } from '../../button/public';
|
|
5
|
+
import { parseLocalDate } from '../utils/localDate';
|
|
6
|
+
|
|
7
|
+
/*** Renders the shared DatePicker trigger button for native and web hosts. */
|
|
8
|
+
export function DatePickerTrigger({
|
|
9
|
+
disabled,
|
|
10
|
+
formatDate,
|
|
11
|
+
interactionPolicy,
|
|
12
|
+
onPress,
|
|
13
|
+
placeholder = 'Choose date',
|
|
14
|
+
testID,
|
|
15
|
+
value,
|
|
16
|
+
}: DatePickerTriggerProps) {
|
|
17
|
+
const displayValue = value
|
|
18
|
+
? formatDate
|
|
19
|
+
? formatDate(value)
|
|
20
|
+
: formatDefaultDate(value)
|
|
21
|
+
: placeholder;
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<Button
|
|
25
|
+
disabled={disabled}
|
|
26
|
+
interactionPolicy={interactionPolicy}
|
|
27
|
+
onPress={onPress}
|
|
28
|
+
testID={testID ? `${testID}-trigger` : undefined}
|
|
29
|
+
trailingIcon={{ name: 'calendar-outline' }}
|
|
30
|
+
variant="outline"
|
|
31
|
+
>
|
|
32
|
+
{displayValue}
|
|
33
|
+
</Button>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
interface DatePickerTriggerProps extends Pick<
|
|
38
|
+
DatePickerProps,
|
|
39
|
+
'disabled' | 'formatDate' | 'interactionPolicy' | 'placeholder' | 'testID' | 'value'
|
|
40
|
+
> {
|
|
41
|
+
onPress: () => void;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/*** Formats a date using the current platform locale when no formatter is supplied. */
|
|
45
|
+
function formatDefaultDate(value: string): React.ReactNode {
|
|
46
|
+
return parseLocalDate(value)?.toLocaleDateString() ?? value;
|
|
47
|
+
}
|
|
@@ -2,14 +2,24 @@ import { expect, test } from 'bun:test';
|
|
|
2
2
|
|
|
3
3
|
import { ZORA_COMPONENT_META } from '../../metadata/componentMeta';
|
|
4
4
|
|
|
5
|
-
test('DatePicker
|
|
6
|
-
const
|
|
5
|
+
test('DatePicker keeps one manifest contract across native and web presentation hosts', async () => {
|
|
6
|
+
const nativeSource = await Bun.file(
|
|
7
|
+
'src/features/date-picker/adapters/inbound/DatePicker.native.tsx',
|
|
8
|
+
).text();
|
|
9
|
+
const webSource = await Bun.file(
|
|
10
|
+
'src/features/date-picker/adapters/inbound/DatePicker.web.tsx',
|
|
11
|
+
).text();
|
|
12
|
+
const contentSource = await Bun.file(
|
|
13
|
+
'src/features/date-picker/composition/DatePickerContent.tsx',
|
|
14
|
+
).text();
|
|
7
15
|
const meta = ZORA_COMPONENT_META.DatePicker;
|
|
8
16
|
|
|
9
17
|
expect(meta.directManifestNode).toBe(true);
|
|
10
18
|
expect(meta.props.value?.type).toBe('string');
|
|
11
19
|
expect(meta.events?.valueChange?.eventType).toBe('datePicker.valueChange');
|
|
12
20
|
expect(meta.bindings?.props?.value?.value.type).toBe('string');
|
|
13
|
-
expect(
|
|
14
|
-
expect(
|
|
21
|
+
expect(nativeSource).toContain('useBottomSheet()');
|
|
22
|
+
expect(webSource).toContain('<Popover');
|
|
23
|
+
expect(webSource).not.toContain('useBottomSheet');
|
|
24
|
+
expect(contentSource).toContain('DatePickerCalendar');
|
|
15
25
|
});
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
import { withZoraThemeScope } from '../../../../theme/withZoraThemeScope';
|
|
4
|
+
import type { TimePickerProps } from '../../../../types/time-picker';
|
|
5
|
+
import { useBottomSheet } from '../../../bottom-sheet/public';
|
|
6
|
+
import { TimePickerContent } from '../../composition/TimePickerContent';
|
|
7
|
+
import { TimePickerField } from '../../composition/TimePickerField';
|
|
8
|
+
import { TimePickerTrigger } from '../../composition/TimePickerTrigger';
|
|
9
|
+
|
|
10
|
+
/*** Renders TimePicker with native BottomSheet presentation. */
|
|
11
|
+
export const TimePicker = withZoraThemeScope(TimePickerInner);
|
|
12
|
+
|
|
13
|
+
/*** Connects the shared TimePicker field and content to the native BottomSheet host. */
|
|
14
|
+
function TimePickerInner({ themeId: _themeId, mode: _mode, ...props }: TimePickerProps) {
|
|
15
|
+
const { dismiss, present } = useBottomSheet();
|
|
16
|
+
|
|
17
|
+
const openPicker = React.useCallback(() => {
|
|
18
|
+
if (props.interactionPolicy === 'passive') return;
|
|
19
|
+
|
|
20
|
+
present({
|
|
21
|
+
content: (
|
|
22
|
+
<TimePickerContent
|
|
23
|
+
description={props.description}
|
|
24
|
+
formatTime={props.formatTime}
|
|
25
|
+
interactionPolicy={props.interactionPolicy}
|
|
26
|
+
label={props.label}
|
|
27
|
+
maxTime={props.maxTime}
|
|
28
|
+
minTime={props.minTime}
|
|
29
|
+
onDismiss={dismiss}
|
|
30
|
+
onSelect={(time) => {
|
|
31
|
+
props.onValueChange?.(time);
|
|
32
|
+
dismiss();
|
|
33
|
+
}}
|
|
34
|
+
stepMinutes={props.stepMinutes}
|
|
35
|
+
testID={props.testID}
|
|
36
|
+
value={props.value}
|
|
37
|
+
/>
|
|
38
|
+
),
|
|
39
|
+
});
|
|
40
|
+
}, [dismiss, present, props]);
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<TimePickerField {...props}>
|
|
44
|
+
<TimePickerTrigger {...props} onPress={openPicker} />
|
|
45
|
+
</TimePickerField>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
@@ -1,258 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import React from 'react';
|
|
3
|
-
import { ScrollView, StyleSheet } from 'react-native';
|
|
4
|
-
|
|
5
|
-
import { withZoraThemeScope } from '../../../../theme/withZoraThemeScope';
|
|
6
|
-
import type { TimePickerProps } from '../../../../types/time-picker';
|
|
7
|
-
import { useBottomSheet } from '../../../bottom-sheet/public';
|
|
8
|
-
import { Button } from '../../../button/public';
|
|
9
|
-
import { Stack } from '../../../layout/public';
|
|
10
|
-
import { Text } from '../../../typography/public';
|
|
11
|
-
|
|
12
|
-
const MINUTES_PER_DAY = 24 * 60;
|
|
13
|
-
const DEFAULT_STEP_MINUTES = 30;
|
|
14
|
-
|
|
15
|
-
/*** Render the field label and optional supporting description. */
|
|
16
|
-
function renderLabel(label: React.ReactNode, description: React.ReactNode | undefined) {
|
|
17
|
-
return (
|
|
18
|
-
<Stack gap="xs">
|
|
19
|
-
<Text variant="label" weight="semiBold">
|
|
20
|
-
{label}
|
|
21
|
-
</Text>
|
|
22
|
-
{description ? (
|
|
23
|
-
<Text emphasis="muted" variant="bodySmall">
|
|
24
|
-
{description}
|
|
25
|
-
</Text>
|
|
26
|
-
) : null}
|
|
27
|
-
</Stack>
|
|
28
|
-
);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/*** Parse one HH:mm value to minutes since midnight. */
|
|
32
|
-
function parseTimeToMinutes(value: string | undefined): number | undefined {
|
|
33
|
-
if (!value) return undefined;
|
|
34
|
-
const match = /^(\d{2}):(\d{2})$/.exec(value);
|
|
35
|
-
if (!match) return undefined;
|
|
36
|
-
|
|
37
|
-
const hours = Number(match[1]);
|
|
38
|
-
const minutes = Number(match[2]);
|
|
39
|
-
if (hours < 0 || hours > 23 || minutes < 0 || minutes > 59) return undefined;
|
|
40
|
-
return hours * 60 + minutes;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/*** Format minutes since midnight as HH:mm. */
|
|
44
|
-
function formatMinutes(value: number): string {
|
|
45
|
-
const hours = Math.floor(value / 60);
|
|
46
|
-
const minutes = value % 60;
|
|
47
|
-
return `${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}`;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/*** Normalize the configured time step to a positive whole minute. */
|
|
51
|
-
function resolveStepMinutes(stepMinutes: number | undefined): number {
|
|
52
|
-
if (stepMinutes === undefined || !Number.isFinite(stepMinutes) || stepMinutes <= 0) {
|
|
53
|
-
return DEFAULT_STEP_MINUTES;
|
|
54
|
-
}
|
|
55
|
-
return Math.max(1, Math.floor(stepMinutes));
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/*** Generate selectable HH:mm values within the configured range. */
|
|
59
|
-
function generateTimeOptions({
|
|
60
|
-
minTime,
|
|
61
|
-
maxTime,
|
|
62
|
-
stepMinutes,
|
|
63
|
-
}: Pick<TimePickerProps, 'maxTime' | 'minTime' | 'stepMinutes'>): string[] {
|
|
64
|
-
const step = resolveStepMinutes(stepMinutes);
|
|
65
|
-
const min = parseTimeToMinutes(minTime) ?? 0;
|
|
66
|
-
const max = parseTimeToMinutes(maxTime) ?? MINUTES_PER_DAY - 1;
|
|
67
|
-
return Array.from({ length: Math.ceil(MINUTES_PER_DAY / step) }, (_, index) => index * step)
|
|
68
|
-
.filter((minute) => minute >= min && minute <= max)
|
|
69
|
-
.map(formatMinutes);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
function TimePickerInner({
|
|
73
|
-
themeId: _themeId,
|
|
74
|
-
mode: _mode,
|
|
75
|
-
value,
|
|
76
|
-
onValueChange,
|
|
77
|
-
label,
|
|
78
|
-
description,
|
|
79
|
-
error,
|
|
80
|
-
placeholder = 'Choose time',
|
|
81
|
-
minTime,
|
|
82
|
-
maxTime,
|
|
83
|
-
stepMinutes,
|
|
84
|
-
disabled = false,
|
|
85
|
-
required = false,
|
|
86
|
-
formatTime,
|
|
87
|
-
testID,
|
|
88
|
-
interactionPolicy,
|
|
89
|
-
}: TimePickerProps) {
|
|
90
|
-
const { dismiss, present } = useBottomSheet();
|
|
91
|
-
const passive = interactionPolicy === 'passive';
|
|
92
|
-
const displayValue = value ? (formatTime ? formatTime(value) : value) : placeholder;
|
|
93
|
-
|
|
94
|
-
/*** Present the time options through the shared BottomSheet controller. */
|
|
95
|
-
function openPicker() {
|
|
96
|
-
if (passive) return;
|
|
97
|
-
present({
|
|
98
|
-
content: (
|
|
99
|
-
<TimePickerSheet
|
|
100
|
-
description={description}
|
|
101
|
-
formatTime={formatTime}
|
|
102
|
-
interactionPolicy={interactionPolicy}
|
|
103
|
-
label={label}
|
|
104
|
-
maxTime={maxTime}
|
|
105
|
-
minTime={minTime}
|
|
106
|
-
onDismiss={dismiss}
|
|
107
|
-
onSelect={(time) => {
|
|
108
|
-
onValueChange?.(time);
|
|
109
|
-
dismiss();
|
|
110
|
-
}}
|
|
111
|
-
stepMinutes={stepMinutes}
|
|
112
|
-
testID={testID}
|
|
113
|
-
value={value}
|
|
114
|
-
/>
|
|
115
|
-
),
|
|
116
|
-
});
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
return (
|
|
120
|
-
<Field
|
|
121
|
-
disabled={disabled}
|
|
122
|
-
errorText={error}
|
|
123
|
-
invalid={Boolean(error)}
|
|
124
|
-
label={label ? renderLabel(label, description) : undefined}
|
|
125
|
-
required={required}
|
|
126
|
-
testID={testID}
|
|
127
|
-
>
|
|
128
|
-
<Button
|
|
129
|
-
disabled={disabled}
|
|
130
|
-
interactionPolicy={interactionPolicy}
|
|
131
|
-
onPress={openPicker}
|
|
132
|
-
testID={testID ? `${testID}-trigger` : undefined}
|
|
133
|
-
trailingIcon={{ name: 'chevron-down-outline' }}
|
|
134
|
-
variant="outline"
|
|
135
|
-
>
|
|
136
|
-
{displayValue}
|
|
137
|
-
</Button>
|
|
138
|
-
</Field>
|
|
139
|
-
);
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
interface TimePickerSheetProps extends Pick<
|
|
143
|
-
TimePickerProps,
|
|
144
|
-
| 'description'
|
|
145
|
-
| 'formatTime'
|
|
146
|
-
| 'interactionPolicy'
|
|
147
|
-
| 'label'
|
|
148
|
-
| 'maxTime'
|
|
149
|
-
| 'minTime'
|
|
150
|
-
| 'stepMinutes'
|
|
151
|
-
| 'testID'
|
|
152
|
-
| 'value'
|
|
153
|
-
> {
|
|
154
|
-
onDismiss: () => void;
|
|
155
|
-
onSelect: (value: string) => void;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
/*** Render time selection content inside the shared BottomSheet host. */
|
|
159
|
-
function TimePickerSheet({
|
|
160
|
-
description,
|
|
161
|
-
formatTime,
|
|
162
|
-
interactionPolicy,
|
|
163
|
-
label,
|
|
164
|
-
maxTime,
|
|
165
|
-
minTime,
|
|
166
|
-
onDismiss,
|
|
167
|
-
onSelect,
|
|
168
|
-
stepMinutes,
|
|
169
|
-
testID,
|
|
170
|
-
value,
|
|
171
|
-
}: TimePickerSheetProps) {
|
|
172
|
-
const options = React.useMemo(
|
|
173
|
-
() => generateTimeOptions({ maxTime, minTime, stepMinutes }),
|
|
174
|
-
[maxTime, minTime, stepMinutes],
|
|
175
|
-
);
|
|
176
|
-
|
|
177
|
-
return (
|
|
178
|
-
<Stack gap="m" p="m" testID={testID ? `${testID}-sheet` : undefined}>
|
|
179
|
-
<PickerSheetHeader description={description} title={label ?? 'Choose time'} />
|
|
180
|
-
<ScrollView style={styles.options}>
|
|
181
|
-
<Stack gap="xxs">
|
|
182
|
-
{options.map((option) => (
|
|
183
|
-
<TimePickerOption
|
|
184
|
-
formatTime={formatTime}
|
|
185
|
-
interactionPolicy={interactionPolicy}
|
|
186
|
-
key={option}
|
|
187
|
-
onSelect={onSelect}
|
|
188
|
-
option={option}
|
|
189
|
-
selected={option === value}
|
|
190
|
-
testID={testID}
|
|
191
|
-
/>
|
|
192
|
-
))}
|
|
193
|
-
</Stack>
|
|
194
|
-
</ScrollView>
|
|
195
|
-
<Button fullWidth interactionPolicy={interactionPolicy} onPress={onDismiss} variant="ghost">
|
|
196
|
-
Cancel
|
|
197
|
-
</Button>
|
|
198
|
-
</Stack>
|
|
199
|
-
);
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
/*** Render the shared title block used by the time sheet. */
|
|
203
|
-
function PickerSheetHeader({
|
|
204
|
-
description,
|
|
205
|
-
title,
|
|
206
|
-
}: {
|
|
207
|
-
description: React.ReactNode | undefined;
|
|
208
|
-
title: React.ReactNode;
|
|
209
|
-
}) {
|
|
210
|
-
return (
|
|
211
|
-
<Stack gap="xxs">
|
|
212
|
-
<Text align="center" variant="label" weight="semiBold">
|
|
213
|
-
{title}
|
|
214
|
-
</Text>
|
|
215
|
-
{description ? (
|
|
216
|
-
<Text align="center" emphasis="muted" variant="bodySmall">
|
|
217
|
-
{description}
|
|
218
|
-
</Text>
|
|
219
|
-
) : null}
|
|
220
|
-
</Stack>
|
|
221
|
-
);
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
/*** Render one accessible selectable time option. */
|
|
225
|
-
function TimePickerOption({
|
|
226
|
-
formatTime,
|
|
227
|
-
interactionPolicy,
|
|
228
|
-
onSelect,
|
|
229
|
-
option,
|
|
230
|
-
selected,
|
|
231
|
-
testID,
|
|
232
|
-
}: Pick<TimePickerSheetProps, 'formatTime' | 'interactionPolicy' | 'testID'> & {
|
|
233
|
-
onSelect: (value: string) => void;
|
|
234
|
-
option: string;
|
|
235
|
-
selected: boolean;
|
|
236
|
-
}) {
|
|
237
|
-
return (
|
|
238
|
-
<Button
|
|
239
|
-
accessibilityState={{ selected }}
|
|
240
|
-
fullWidth
|
|
241
|
-
interactionPolicy={interactionPolicy}
|
|
242
|
-
onPress={() => onSelect(option)}
|
|
243
|
-
testID={testID ? `${testID}-option-${option}` : undefined}
|
|
244
|
-
variant={selected ? 'soft' : 'ghost'}
|
|
245
|
-
>
|
|
246
|
-
{formatTime ? formatTime(option) : option}
|
|
247
|
-
</Button>
|
|
248
|
-
);
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
const styles = StyleSheet.create({
|
|
252
|
-
options: { maxHeight: 360 },
|
|
253
|
-
});
|
|
254
|
-
|
|
255
|
-
/***
|
|
256
|
-
* Time input control with bottom-sheet selection and formatted display value.
|
|
257
|
-
*/
|
|
258
|
-
export const TimePicker = withZoraThemeScope(TimePickerInner);
|
|
1
|
+
export { TimePicker } from './TimePicker.web';
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { Popover } from '@ankhorage/surface';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
|
|
4
|
+
import { withZoraThemeScope } from '../../../../theme/withZoraThemeScope';
|
|
5
|
+
import type { TimePickerProps } from '../../../../types/time-picker';
|
|
6
|
+
import { Surface } from '../../../surface/public';
|
|
7
|
+
import { TimePickerContent } from '../../composition/TimePickerContent';
|
|
8
|
+
import { TimePickerField } from '../../composition/TimePickerField';
|
|
9
|
+
import { TimePickerTrigger } from '../../composition/TimePickerTrigger';
|
|
10
|
+
|
|
11
|
+
/*** Renders TimePicker with anchored web Popover presentation. */
|
|
12
|
+
export const TimePicker = withZoraThemeScope(TimePickerInner);
|
|
13
|
+
|
|
14
|
+
/*** Connects the shared TimePicker field and content to the web Popover host. */
|
|
15
|
+
function TimePickerInner({ themeId: _themeId, mode: _mode, ...props }: TimePickerProps) {
|
|
16
|
+
const [open, setOpen] = React.useState(false);
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<TimePickerField {...props}>
|
|
20
|
+
<Popover
|
|
21
|
+
anchor={({ toggle }) => <TimePickerTrigger {...props} onPress={toggle} />}
|
|
22
|
+
interactionPolicy={props.interactionPolicy}
|
|
23
|
+
onOpenChange={setOpen}
|
|
24
|
+
open={open}
|
|
25
|
+
placement="bottom-start"
|
|
26
|
+
testID={props.testID ? `${props.testID}-popover` : undefined}
|
|
27
|
+
>
|
|
28
|
+
<Surface variant="raised">
|
|
29
|
+
<TimePickerContent
|
|
30
|
+
description={props.description}
|
|
31
|
+
formatTime={props.formatTime}
|
|
32
|
+
interactionPolicy={props.interactionPolicy}
|
|
33
|
+
label={props.label}
|
|
34
|
+
maxTime={props.maxTime}
|
|
35
|
+
minTime={props.minTime}
|
|
36
|
+
onDismiss={() => setOpen(false)}
|
|
37
|
+
onSelect={(time) => {
|
|
38
|
+
props.onValueChange?.(time);
|
|
39
|
+
setOpen(false);
|
|
40
|
+
}}
|
|
41
|
+
stepMinutes={props.stepMinutes}
|
|
42
|
+
testID={props.testID}
|
|
43
|
+
value={props.value}
|
|
44
|
+
/>
|
|
45
|
+
</Surface>
|
|
46
|
+
</Popover>
|
|
47
|
+
</TimePickerField>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { ScrollView, StyleSheet } from 'react-native';
|
|
3
|
+
|
|
4
|
+
import type { TimePickerProps } from '../../../types/time-picker';
|
|
5
|
+
import { Button } from '../../button/public';
|
|
6
|
+
import { Stack } from '../../layout/public';
|
|
7
|
+
import { Text } from '../../typography/public';
|
|
8
|
+
|
|
9
|
+
const MINUTES_PER_DAY = 24 * 60;
|
|
10
|
+
const DEFAULT_STEP_MINUTES = 30;
|
|
11
|
+
|
|
12
|
+
/*** Renders shared time options for native BottomSheet and web Popover hosts. */
|
|
13
|
+
export function TimePickerContent({
|
|
14
|
+
description,
|
|
15
|
+
formatTime,
|
|
16
|
+
interactionPolicy,
|
|
17
|
+
label,
|
|
18
|
+
maxTime,
|
|
19
|
+
minTime,
|
|
20
|
+
onDismiss,
|
|
21
|
+
onSelect,
|
|
22
|
+
stepMinutes,
|
|
23
|
+
testID,
|
|
24
|
+
value,
|
|
25
|
+
}: TimePickerContentProps) {
|
|
26
|
+
const options = React.useMemo(
|
|
27
|
+
() => generateTimeOptions({ maxTime, minTime, stepMinutes }),
|
|
28
|
+
[maxTime, minTime, stepMinutes],
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<Stack gap="m" p="m" testID={testID ? `${testID}-content` : undefined}>
|
|
33
|
+
<PickerHeader description={description} title={label ?? 'Choose time'} />
|
|
34
|
+
<ScrollView style={styles.options}>
|
|
35
|
+
<Stack gap="xxs">
|
|
36
|
+
{options.map((option) => (
|
|
37
|
+
<TimePickerOption
|
|
38
|
+
formatTime={formatTime}
|
|
39
|
+
interactionPolicy={interactionPolicy}
|
|
40
|
+
key={option}
|
|
41
|
+
onSelect={onSelect}
|
|
42
|
+
option={option}
|
|
43
|
+
selected={option === value}
|
|
44
|
+
testID={testID}
|
|
45
|
+
/>
|
|
46
|
+
))}
|
|
47
|
+
</Stack>
|
|
48
|
+
</ScrollView>
|
|
49
|
+
<Button fullWidth interactionPolicy={interactionPolicy} onPress={onDismiss} variant="ghost">
|
|
50
|
+
Cancel
|
|
51
|
+
</Button>
|
|
52
|
+
</Stack>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
interface TimePickerContentProps extends Pick<
|
|
57
|
+
TimePickerProps,
|
|
58
|
+
| 'description'
|
|
59
|
+
| 'formatTime'
|
|
60
|
+
| 'interactionPolicy'
|
|
61
|
+
| 'label'
|
|
62
|
+
| 'maxTime'
|
|
63
|
+
| 'minTime'
|
|
64
|
+
| 'stepMinutes'
|
|
65
|
+
| 'testID'
|
|
66
|
+
| 'value'
|
|
67
|
+
> {
|
|
68
|
+
onDismiss: () => void;
|
|
69
|
+
onSelect: (value: string) => void;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/*** Renders the shared title block used by picker overlays. */
|
|
73
|
+
function PickerHeader({
|
|
74
|
+
description,
|
|
75
|
+
title,
|
|
76
|
+
}: {
|
|
77
|
+
description: React.ReactNode | undefined;
|
|
78
|
+
title: React.ReactNode;
|
|
79
|
+
}) {
|
|
80
|
+
return (
|
|
81
|
+
<Stack gap="xxs">
|
|
82
|
+
<Text align="center" variant="label" weight="semiBold">
|
|
83
|
+
{title}
|
|
84
|
+
</Text>
|
|
85
|
+
{description ? (
|
|
86
|
+
<Text align="center" emphasis="muted" variant="bodySmall">
|
|
87
|
+
{description}
|
|
88
|
+
</Text>
|
|
89
|
+
) : null}
|
|
90
|
+
</Stack>
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/*** Renders one accessible selectable time option. */
|
|
95
|
+
function TimePickerOption({
|
|
96
|
+
formatTime,
|
|
97
|
+
interactionPolicy,
|
|
98
|
+
onSelect,
|
|
99
|
+
option,
|
|
100
|
+
selected,
|
|
101
|
+
testID,
|
|
102
|
+
}: Pick<TimePickerContentProps, 'formatTime' | 'interactionPolicy' | 'testID'> & {
|
|
103
|
+
onSelect: (value: string) => void;
|
|
104
|
+
option: string;
|
|
105
|
+
selected: boolean;
|
|
106
|
+
}) {
|
|
107
|
+
return (
|
|
108
|
+
<Button
|
|
109
|
+
accessibilityState={{ selected }}
|
|
110
|
+
fullWidth
|
|
111
|
+
interactionPolicy={interactionPolicy}
|
|
112
|
+
onPress={() => onSelect(option)}
|
|
113
|
+
testID={testID ? `${testID}-option-${option}` : undefined}
|
|
114
|
+
variant={selected ? 'soft' : 'ghost'}
|
|
115
|
+
>
|
|
116
|
+
{formatTime ? formatTime(option) : option}
|
|
117
|
+
</Button>
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/*** Parses one HH:mm value to minutes since midnight. */
|
|
122
|
+
function parseTimeToMinutes(value: string | undefined): number | undefined {
|
|
123
|
+
if (!value) return undefined;
|
|
124
|
+
const match = /^(\d{2}):(\d{2})$/.exec(value);
|
|
125
|
+
if (!match) return undefined;
|
|
126
|
+
|
|
127
|
+
const hours = Number(match[1]);
|
|
128
|
+
const minutes = Number(match[2]);
|
|
129
|
+
if (hours < 0 || hours > 23 || minutes < 0 || minutes > 59) return undefined;
|
|
130
|
+
return hours * 60 + minutes;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/*** Formats minutes since midnight as HH:mm. */
|
|
134
|
+
function formatMinutes(value: number): string {
|
|
135
|
+
const hours = Math.floor(value / 60);
|
|
136
|
+
const minutes = value % 60;
|
|
137
|
+
return `${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}`;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/*** Normalizes the configured time step to a positive whole minute. */
|
|
141
|
+
function resolveStepMinutes(stepMinutes: number | undefined): number {
|
|
142
|
+
if (stepMinutes === undefined || !Number.isFinite(stepMinutes) || stepMinutes <= 0) {
|
|
143
|
+
return DEFAULT_STEP_MINUTES;
|
|
144
|
+
}
|
|
145
|
+
return Math.max(1, Math.floor(stepMinutes));
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/*** Generates selectable HH:mm values within the configured range. */
|
|
149
|
+
function generateTimeOptions({
|
|
150
|
+
minTime,
|
|
151
|
+
maxTime,
|
|
152
|
+
stepMinutes,
|
|
153
|
+
}: Pick<TimePickerProps, 'maxTime' | 'minTime' | 'stepMinutes'>): string[] {
|
|
154
|
+
const step = resolveStepMinutes(stepMinutes);
|
|
155
|
+
const min = parseTimeToMinutes(minTime) ?? 0;
|
|
156
|
+
const max = parseTimeToMinutes(maxTime) ?? MINUTES_PER_DAY - 1;
|
|
157
|
+
return Array.from({ length: Math.ceil(MINUTES_PER_DAY / step) }, (_, index) => index * step)
|
|
158
|
+
.filter((minute) => minute >= min && minute <= max)
|
|
159
|
+
.map(formatMinutes);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const styles = StyleSheet.create({
|
|
163
|
+
options: { maxHeight: 360 },
|
|
164
|
+
});
|