@ankhorage/zora 13.0.0 → 14.0.1
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 +14 -0
- package/README.md +182 -6
- package/dist/features/accordion/adapters/inbound/Accordion.js +4 -4
- package/dist/features/accordion/adapters/inbound/Accordion.js.map +1 -1
- package/dist/features/accordion/adapters/inbound/AccordionItem.js +1 -1
- package/dist/features/accordion/adapters/inbound/AccordionItem.js.map +1 -1
- package/dist/features/app-bar/adapters/inbound/AppBar.d.ts +5 -0
- package/dist/features/app-bar/adapters/inbound/AppBar.d.ts.map +1 -0
- package/dist/features/app-bar/adapters/inbound/AppBar.js +91 -0
- package/dist/features/app-bar/adapters/inbound/AppBar.js.map +1 -0
- package/dist/{components/app-bar/meta.d.ts → features/app-bar/appBarMeta.d.ts} +1 -1
- package/dist/features/app-bar/appBarMeta.d.ts.map +1 -0
- package/dist/{components/app-bar/meta.js → features/app-bar/appBarMeta.js} +1 -1
- package/dist/features/app-bar/appBarMeta.js.map +1 -0
- package/dist/features/app-bar/public.d.ts +3 -0
- package/dist/features/app-bar/public.d.ts.map +1 -0
- package/dist/features/app-bar/public.js +2 -0
- package/dist/features/app-bar/public.js.map +1 -0
- 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/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 +4 -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/settings-layout/SettingsLayout.d.ts +0 -2
- package/dist/layout/settings-layout/SettingsLayout.d.ts.map +1 -1
- package/dist/layout/settings-layout/SettingsLayout.js +1 -3
- package/dist/layout/settings-layout/SettingsLayout.js.map +1 -1
- package/dist/metadata/componentMeta.d.ts.map +1 -1
- package/dist/metadata/componentMeta.js +3 -2
- package/dist/metadata/componentMeta.js.map +1 -1
- package/dist/patterns/reader/ReaderSurface.js +1 -1
- package/dist/patterns/reader/ReaderSurface.js.map +1 -1
- 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.js +2 -2
- 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/{components/app-bar/types.d.ts → types/app-bar.d.ts} +2 -2
- package/dist/types/app-bar.d.ts.map +1 -0
- package/dist/types/app-bar.js +2 -0
- package/dist/types/app-bar.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/adapters/inbound/Accordion.tsx +4 -4
- package/src/features/accordion/adapters/inbound/AccordionItem.tsx +1 -1
- package/src/features/app-bar/adapters/inbound/AppBar.tsx +176 -0
- package/src/features/app-bar/public.ts +2 -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 +10 -4
- package/src/interactionPolicy.test.ts +7 -3
- package/src/layout/settings-layout/SettingsLayout.tsx +1 -3
- package/src/metadata/componentMeta.ts +3 -2
- package/src/patterns/reader/ReaderSurface.tsx +1 -1
- package/src/patterns/selection/SelectionProvider.tsx +4 -6
- package/src/registry.ts +2 -2
- 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 +26 -18
- package/src/{components/app-bar/types.ts → types/app-bar.ts} +1 -1
- package/src/types/provider.ts +22 -0
- package/src/{components/toast/types.ts → types/toast.ts} +5 -7
- package/dist/components/app-bar/AppBar.d.ts +0 -7
- package/dist/components/app-bar/AppBar.d.ts.map +0 -1
- package/dist/components/app-bar/AppBar.js +0 -67
- package/dist/components/app-bar/AppBar.js.map +0 -1
- package/dist/components/app-bar/index.d.ts +0 -3
- package/dist/components/app-bar/index.d.ts.map +0 -1
- package/dist/components/app-bar/index.js +0 -3
- package/dist/components/app-bar/index.js.map +0 -1
- package/dist/components/app-bar/meta.d.ts.map +0 -1
- package/dist/components/app-bar/meta.js.map +0 -1
- package/dist/components/app-bar/types.d.ts.map +0 -1
- package/dist/components/app-bar/types.js +0 -2
- package/dist/components/app-bar/types.js.map +0 -1
- 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/src/components/app-bar/AppBar.tsx +0 -140
- package/src/components/app-bar/index.ts +0 -2
- 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/{components/app-bar/meta.ts → features/app-bar/appBarMeta.ts} +0 -0
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { AppBar as SurfaceAppBar, type ButtonIconSpec } from '@ankhorage/surface';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
|
|
4
|
+
import { Inline } from '../../../../foundation';
|
|
5
|
+
import { useZoraTheme } from '../../../../theme/useZoraTheme';
|
|
6
|
+
import { withZoraThemeScope } from '../../../../theme/withZoraThemeScope';
|
|
7
|
+
import type { AppBarMode, AppBarOverflowAction, AppBarProps } from '../../../../types/app-bar';
|
|
8
|
+
import { IconButton } from '../../../button/public';
|
|
9
|
+
import { Box, Stack } from '../../../layout/public';
|
|
10
|
+
import { Heading, Text } from '../../../typography/public';
|
|
11
|
+
|
|
12
|
+
const DEFAULT_CANCEL_ICON = { name: 'close-outline' } satisfies ButtonIconSpec;
|
|
13
|
+
const DEFAULT_OVERFLOW_ICON = { name: 'ellipsis-vertical' } satisfies ButtonIconSpec;
|
|
14
|
+
|
|
15
|
+
/*** Resolves the effective AppBar mode. */
|
|
16
|
+
function resolveMode(mode: AppBarMode | undefined): AppBarMode {
|
|
17
|
+
return mode ?? { type: 'default' };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/*** Formats the active selection label with its optional count. */
|
|
21
|
+
function resolveSelectionLabel({ count, label }: { count?: number; label: string }): string {
|
|
22
|
+
if (count === undefined) return label;
|
|
23
|
+
return `${label} (${count})`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/*** Resolves the accessible label for the overflow action. */
|
|
27
|
+
function resolveOverflowLabel(overflow: AppBarOverflowAction): string {
|
|
28
|
+
return overflow.label ?? 'More options';
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/*** Resolves the accessible label for leaving selection mode. */
|
|
32
|
+
function resolveCancelLabel(mode: Extract<AppBarMode, { type: 'selection' }>): string {
|
|
33
|
+
return mode.cancelLabel ?? 'Cancel selection';
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/*** Renders a top app bar with title/subtitle and optional leading/trailing actions. */
|
|
37
|
+
export const AppBar = withZoraThemeScope(AppBarInner);
|
|
38
|
+
|
|
39
|
+
/*** Composes the opinionated ZORA AppBar over the Surface layout primitive. */
|
|
40
|
+
function AppBarInner({
|
|
41
|
+
themeId: _themeId,
|
|
42
|
+
mode: _mode,
|
|
43
|
+
title,
|
|
44
|
+
subtitle,
|
|
45
|
+
leading,
|
|
46
|
+
actions,
|
|
47
|
+
overflow,
|
|
48
|
+
appMode,
|
|
49
|
+
children,
|
|
50
|
+
safeAreaTop = true,
|
|
51
|
+
divider = true,
|
|
52
|
+
testID,
|
|
53
|
+
interactionPolicy,
|
|
54
|
+
}: AppBarProps) {
|
|
55
|
+
const { theme } = useZoraTheme();
|
|
56
|
+
const resolvedMode = resolveMode(appMode);
|
|
57
|
+
const isSelectionMode = resolvedMode.type === 'selection';
|
|
58
|
+
const resolvedLeading = leading ?? resolveSelectionLeading(resolvedMode, interactionPolicy);
|
|
59
|
+
const overflowButton = resolveOverflowButton(overflow, interactionPolicy);
|
|
60
|
+
const resolvedTrailing = resolveTrailing(actions, overflowButton);
|
|
61
|
+
const resolvedCenter = resolveCenter({
|
|
62
|
+
children,
|
|
63
|
+
isSelectionMode,
|
|
64
|
+
resolvedMode,
|
|
65
|
+
subtitle,
|
|
66
|
+
title,
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
return (
|
|
70
|
+
<SurfaceAppBar
|
|
71
|
+
bg={isSelectionMode ? theme.semantics.action.primary.softBg : undefined}
|
|
72
|
+
divider={divider}
|
|
73
|
+
leading={resolvedLeading}
|
|
74
|
+
safeAreaTop={safeAreaTop}
|
|
75
|
+
testID={testID}
|
|
76
|
+
trailing={resolvedTrailing}
|
|
77
|
+
>
|
|
78
|
+
{resolvedCenter ? <Box style={{ minWidth: 0 }}>{resolvedCenter}</Box> : null}
|
|
79
|
+
</SurfaceAppBar>
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/*** Resolves the selection action shown at the leading edge. */
|
|
84
|
+
function resolveSelectionLeading(
|
|
85
|
+
mode: AppBarMode,
|
|
86
|
+
interactionPolicy: AppBarProps['interactionPolicy'],
|
|
87
|
+
): React.ReactNode | undefined {
|
|
88
|
+
if (mode.type !== 'selection') return undefined;
|
|
89
|
+
|
|
90
|
+
return (
|
|
91
|
+
<IconButton
|
|
92
|
+
color="neutral"
|
|
93
|
+
icon={mode.cancelIcon ?? DEFAULT_CANCEL_ICON}
|
|
94
|
+
interactionPolicy={interactionPolicy}
|
|
95
|
+
label={resolveCancelLabel(mode)}
|
|
96
|
+
onPress={mode.onCancel}
|
|
97
|
+
size="m"
|
|
98
|
+
variant="ghost"
|
|
99
|
+
/>
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/*** Resolves the optional overflow action. */
|
|
104
|
+
function resolveOverflowButton(
|
|
105
|
+
overflow: AppBarOverflowAction | undefined,
|
|
106
|
+
interactionPolicy: AppBarProps['interactionPolicy'],
|
|
107
|
+
): React.ReactNode {
|
|
108
|
+
if (!overflow?.onPress) return null;
|
|
109
|
+
|
|
110
|
+
return (
|
|
111
|
+
<IconButton
|
|
112
|
+
color="neutral"
|
|
113
|
+
disabled={overflow.disabled}
|
|
114
|
+
icon={overflow.icon ?? DEFAULT_OVERFLOW_ICON}
|
|
115
|
+
interactionPolicy={interactionPolicy}
|
|
116
|
+
label={resolveOverflowLabel(overflow)}
|
|
117
|
+
onPress={overflow.onPress}
|
|
118
|
+
size="m"
|
|
119
|
+
variant="ghost"
|
|
120
|
+
/>
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/*** Composes trailing actions and overflow controls. */
|
|
125
|
+
function resolveTrailing(
|
|
126
|
+
actions: React.ReactNode,
|
|
127
|
+
overflowButton: React.ReactNode,
|
|
128
|
+
): React.ReactNode {
|
|
129
|
+
if (!actions && !overflowButton) return undefined;
|
|
130
|
+
return (
|
|
131
|
+
<Inline align="center" gap="s" wrap="nowrap">
|
|
132
|
+
{actions}
|
|
133
|
+
{overflowButton}
|
|
134
|
+
</Inline>
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/*** Resolves the center content for default and selection modes. */
|
|
139
|
+
function resolveCenter({
|
|
140
|
+
children,
|
|
141
|
+
isSelectionMode,
|
|
142
|
+
resolvedMode,
|
|
143
|
+
subtitle,
|
|
144
|
+
title,
|
|
145
|
+
}: {
|
|
146
|
+
children: React.ReactNode;
|
|
147
|
+
isSelectionMode: boolean;
|
|
148
|
+
resolvedMode: AppBarMode;
|
|
149
|
+
subtitle: React.ReactNode;
|
|
150
|
+
title: React.ReactNode;
|
|
151
|
+
}): React.ReactNode {
|
|
152
|
+
if (children !== undefined) return children;
|
|
153
|
+
if (isSelectionMode && resolvedMode.type === 'selection') {
|
|
154
|
+
return (
|
|
155
|
+
<Text emphasis="default" numberOfLines={1} variant="label" weight="semiBold">
|
|
156
|
+
{resolveSelectionLabel(resolvedMode)}
|
|
157
|
+
</Text>
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
if (title == null && subtitle == null) return null;
|
|
161
|
+
|
|
162
|
+
return (
|
|
163
|
+
<Stack gap="xs">
|
|
164
|
+
{title != null ? (
|
|
165
|
+
<Heading ellipsizeMode="tail" level={3} numberOfLines={1} size="h5">
|
|
166
|
+
{title}
|
|
167
|
+
</Heading>
|
|
168
|
+
) : null}
|
|
169
|
+
{subtitle != null ? (
|
|
170
|
+
<Text ellipsizeMode="tail" emphasis="muted" numberOfLines={1} variant="bodySmall">
|
|
171
|
+
{subtitle}
|
|
172
|
+
</Text>
|
|
173
|
+
) : null}
|
|
174
|
+
</Stack>
|
|
175
|
+
);
|
|
176
|
+
}
|
|
@@ -22,18 +22,21 @@ describe('ZORA BottomSheet public contract', () => {
|
|
|
22
22
|
expect(workletsPeer).toBeDefined();
|
|
23
23
|
});
|
|
24
24
|
|
|
25
|
-
test('installs
|
|
26
|
-
const [provider,
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
25
|
+
test('installs BottomSheet only in the enabled native runtime capability host', async () => {
|
|
26
|
+
const [provider, nativeCapabilities, webCapabilities, nativeDatePicker, nativeTimePicker] =
|
|
27
|
+
await Promise.all([
|
|
28
|
+
Bun.file('src/theme/ZoraProvider.tsx').text(),
|
|
29
|
+
Bun.file('src/theme/ZoraRuntimeCapabilities.native.tsx').text(),
|
|
30
|
+
Bun.file('src/theme/ZoraRuntimeCapabilities.web.tsx').text(),
|
|
31
|
+
Bun.file('src/features/date-picker/adapters/inbound/DatePicker.native.tsx').text(),
|
|
32
|
+
Bun.file('src/features/time-picker/adapters/inbound/TimePicker.native.tsx').text(),
|
|
33
|
+
]);
|
|
31
34
|
|
|
32
|
-
expect(provider).toContain('<
|
|
33
|
-
expect(provider).not.toContain('
|
|
34
|
-
expect(
|
|
35
|
-
expect(
|
|
36
|
-
expect(
|
|
37
|
-
expect(
|
|
35
|
+
expect(provider).toContain('<ZoraRuntimeCapabilities');
|
|
36
|
+
expect(provider).not.toContain('BottomSheetProvider');
|
|
37
|
+
expect(nativeCapabilities).toContain('<BottomSheetProvider>');
|
|
38
|
+
expect(webCapabilities).not.toContain('BottomSheetProvider');
|
|
39
|
+
expect(nativeDatePicker).toContain('useBottomSheet()');
|
|
40
|
+
expect(nativeTimePicker).toContain('useBottomSheet()');
|
|
38
41
|
});
|
|
39
42
|
});
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
import { withZoraThemeScope } from '../../../../theme/withZoraThemeScope';
|
|
4
|
+
import type { DatePickerProps } from '../../../../types/date-picker';
|
|
5
|
+
import { useBottomSheet } from '../../../bottom-sheet/public';
|
|
6
|
+
import { DatePickerContent } from '../../composition/DatePickerContent';
|
|
7
|
+
import { DatePickerField } from '../../composition/DatePickerField';
|
|
8
|
+
import { DatePickerTrigger } from '../../composition/DatePickerTrigger';
|
|
9
|
+
import { formatLocalDate } from '../../utils/localDate';
|
|
10
|
+
|
|
11
|
+
/*** Renders DatePicker with native BottomSheet presentation. */
|
|
12
|
+
export const DatePicker = withZoraThemeScope(DatePickerInner);
|
|
13
|
+
|
|
14
|
+
/*** Connects the shared DatePicker field and content to the native BottomSheet host. */
|
|
15
|
+
function DatePickerInner({ themeId: _themeId, mode: _mode, ...props }: DatePickerProps) {
|
|
16
|
+
const { dismiss, present } = useBottomSheet();
|
|
17
|
+
|
|
18
|
+
const openPicker = React.useCallback(() => {
|
|
19
|
+
if (props.interactionPolicy === 'passive') return;
|
|
20
|
+
|
|
21
|
+
present({
|
|
22
|
+
content: (
|
|
23
|
+
<DatePickerContent
|
|
24
|
+
description={props.description}
|
|
25
|
+
interactionPolicy={props.interactionPolicy}
|
|
26
|
+
label={props.label}
|
|
27
|
+
maxDate={props.maxDate}
|
|
28
|
+
minDate={props.minDate}
|
|
29
|
+
onDismiss={dismiss}
|
|
30
|
+
onSelect={(date) => {
|
|
31
|
+
props.onValueChange?.(formatLocalDate(date));
|
|
32
|
+
dismiss();
|
|
33
|
+
}}
|
|
34
|
+
testID={props.testID}
|
|
35
|
+
value={props.value}
|
|
36
|
+
/>
|
|
37
|
+
),
|
|
38
|
+
});
|
|
39
|
+
}, [dismiss, present, props]);
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<DatePickerField {...props}>
|
|
43
|
+
<DatePickerTrigger {...props} onPress={openPicker} />
|
|
44
|
+
</DatePickerField>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
@@ -1,356 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import React from 'react';
|
|
3
|
-
|
|
4
|
-
import { withZoraThemeScope } from '../../../../theme/withZoraThemeScope';
|
|
5
|
-
import type { DatePickerProps } from '../../../../types/date-picker';
|
|
6
|
-
import { useBottomSheet } from '../../../bottom-sheet/public';
|
|
7
|
-
import { Button } from '../../../button/public';
|
|
8
|
-
import { Box, Stack } from '../../../layout/public';
|
|
9
|
-
import { Text } from '../../../typography/public';
|
|
10
|
-
import { formatLocalDate, parseLocalDate } from '../../utils/localDate';
|
|
11
|
-
|
|
12
|
-
const WEEKDAY_LABELS = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'] as const;
|
|
13
|
-
const DAY_CELL_WIDTH = 42;
|
|
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
|
-
/*** Compare two values by local calendar day. */
|
|
32
|
-
function isSameLocalDay(left: string | null, right: Date): boolean {
|
|
33
|
-
return left === formatLocalDate(right);
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/*** Normalize a Date to the start of its local calendar day for range comparisons. */
|
|
37
|
-
function startOfLocalDay(value: Date): Date {
|
|
38
|
-
return new Date(value.getFullYear(), value.getMonth(), value.getDate());
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/*** Report whether one local calendar day precedes another. */
|
|
42
|
-
function isBeforeLocalDay(left: Date, right: Date): boolean {
|
|
43
|
-
return startOfLocalDay(left).getTime() < startOfLocalDay(right).getTime();
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/*** Report whether one local calendar day follows another. */
|
|
47
|
-
function isAfterLocalDay(left: Date, right: Date): boolean {
|
|
48
|
-
return startOfLocalDay(left).getTime() > startOfLocalDay(right).getTime();
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/*** Resolve whether a date falls outside the configured picker range. */
|
|
52
|
-
function isDateDisabled(
|
|
53
|
-
value: Date,
|
|
54
|
-
minDate: Date | undefined,
|
|
55
|
-
maxDate: Date | undefined,
|
|
56
|
-
): boolean {
|
|
57
|
-
if (minDate && isBeforeLocalDay(value, minDate)) return true;
|
|
58
|
-
if (maxDate && isAfterLocalDay(value, maxDate)) return true;
|
|
59
|
-
return false;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/*** Format the current calendar month for the picker heading. */
|
|
63
|
-
function formatMonthLabel(value: Date): string {
|
|
64
|
-
return value.toLocaleDateString(undefined, { month: 'long', year: 'numeric' });
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/*** Format a selected date with the platform locale defaults. */
|
|
68
|
-
function formatDefaultDate(value: string): string {
|
|
69
|
-
return parseLocalDate(value)?.toLocaleDateString() ?? value;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/*** Build the padded calendar grid for one month. */
|
|
73
|
-
function createMonthDays(month: Date): (Date | null)[] {
|
|
74
|
-
const firstDay = new Date(month.getFullYear(), month.getMonth(), 1);
|
|
75
|
-
const dayCount = new Date(month.getFullYear(), month.getMonth() + 1, 0).getDate();
|
|
76
|
-
const populatedLength = firstDay.getDay() + dayCount;
|
|
77
|
-
const paddedLength = Math.ceil(populatedLength / 7) * 7;
|
|
78
|
-
|
|
79
|
-
return Array.from({ length: paddedLength }, (_, index) =>
|
|
80
|
-
index < firstDay.getDay()
|
|
81
|
-
? null
|
|
82
|
-
: new Date(month.getFullYear(), month.getMonth(), index - firstDay.getDay() + 1),
|
|
83
|
-
);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
/*** Resolve the month initially shown by the calendar sheet. */
|
|
87
|
-
function resolveInitialMonth(value: string | null, minDate: string | undefined): Date {
|
|
88
|
-
const base = parseLocalDate(value) ?? parseLocalDate(minDate) ?? new Date();
|
|
89
|
-
return new Date(base.getFullYear(), base.getMonth(), 1);
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/*** Report whether the configured range intersects a candidate month. */
|
|
93
|
-
function canNavigateToMonth(
|
|
94
|
-
month: Date,
|
|
95
|
-
minDate: Date | undefined,
|
|
96
|
-
maxDate: Date | undefined,
|
|
97
|
-
): boolean {
|
|
98
|
-
const firstDay = new Date(month.getFullYear(), month.getMonth(), 1);
|
|
99
|
-
const lastDay = new Date(month.getFullYear(), month.getMonth() + 1, 0);
|
|
100
|
-
|
|
101
|
-
if (maxDate && isAfterLocalDay(firstDay, maxDate)) return false;
|
|
102
|
-
if (minDate && isBeforeLocalDay(lastDay, minDate)) return false;
|
|
103
|
-
return true;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
function DatePickerInner({
|
|
107
|
-
themeId: _themeId,
|
|
108
|
-
mode: _mode,
|
|
109
|
-
value,
|
|
110
|
-
onValueChange,
|
|
111
|
-
label,
|
|
112
|
-
description,
|
|
113
|
-
error,
|
|
114
|
-
placeholder = 'Choose date',
|
|
115
|
-
minDate,
|
|
116
|
-
maxDate,
|
|
117
|
-
disabled = false,
|
|
118
|
-
required = false,
|
|
119
|
-
formatDate,
|
|
120
|
-
testID,
|
|
121
|
-
interactionPolicy,
|
|
122
|
-
}: DatePickerProps) {
|
|
123
|
-
const { dismiss, present } = useBottomSheet();
|
|
124
|
-
const passive = interactionPolicy === 'passive';
|
|
125
|
-
const displayValue = value
|
|
126
|
-
? formatDate
|
|
127
|
-
? formatDate(value)
|
|
128
|
-
: formatDefaultDate(value)
|
|
129
|
-
: placeholder;
|
|
130
|
-
|
|
131
|
-
/*** Present the calendar content through the shared BottomSheet controller. */
|
|
132
|
-
function openPicker() {
|
|
133
|
-
if (passive) return;
|
|
134
|
-
present({
|
|
135
|
-
content: (
|
|
136
|
-
<DatePickerSheet
|
|
137
|
-
description={description}
|
|
138
|
-
interactionPolicy={interactionPolicy}
|
|
139
|
-
label={label}
|
|
140
|
-
maxDate={maxDate}
|
|
141
|
-
minDate={minDate}
|
|
142
|
-
onDismiss={dismiss}
|
|
143
|
-
onSelect={(date) => {
|
|
144
|
-
onValueChange?.(formatLocalDate(date));
|
|
145
|
-
dismiss();
|
|
146
|
-
}}
|
|
147
|
-
testID={testID}
|
|
148
|
-
value={value}
|
|
149
|
-
/>
|
|
150
|
-
),
|
|
151
|
-
});
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
return (
|
|
155
|
-
<Field
|
|
156
|
-
disabled={disabled}
|
|
157
|
-
errorText={error}
|
|
158
|
-
invalid={Boolean(error)}
|
|
159
|
-
label={label ? renderLabel(label, description) : undefined}
|
|
160
|
-
required={required}
|
|
161
|
-
testID={testID}
|
|
162
|
-
>
|
|
163
|
-
<Button
|
|
164
|
-
disabled={disabled}
|
|
165
|
-
interactionPolicy={interactionPolicy}
|
|
166
|
-
onPress={openPicker}
|
|
167
|
-
testID={testID ? `${testID}-trigger` : undefined}
|
|
168
|
-
trailingIcon={{ name: 'calendar-outline' }}
|
|
169
|
-
variant="outline"
|
|
170
|
-
>
|
|
171
|
-
{displayValue}
|
|
172
|
-
</Button>
|
|
173
|
-
</Field>
|
|
174
|
-
);
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
interface DatePickerSheetProps extends Pick<
|
|
178
|
-
DatePickerProps,
|
|
179
|
-
'description' | 'interactionPolicy' | 'label' | 'maxDate' | 'minDate' | 'testID' | 'value'
|
|
180
|
-
> {
|
|
181
|
-
onDismiss: () => void;
|
|
182
|
-
onSelect: (value: Date) => void;
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
/*** Render the calendar-specific content inside the shared BottomSheet host. */
|
|
186
|
-
function DatePickerSheet({
|
|
187
|
-
description,
|
|
188
|
-
interactionPolicy,
|
|
189
|
-
label,
|
|
190
|
-
maxDate,
|
|
191
|
-
minDate,
|
|
192
|
-
onDismiss,
|
|
193
|
-
onSelect,
|
|
194
|
-
testID,
|
|
195
|
-
value,
|
|
196
|
-
}: DatePickerSheetProps) {
|
|
197
|
-
const [displayMonth, setDisplayMonth] = React.useState(() => resolveInitialMonth(value, minDate));
|
|
198
|
-
const monthDays = React.useMemo(() => createMonthDays(displayMonth), [displayMonth]);
|
|
199
|
-
const resolvedMaxDate = React.useMemo(() => parseLocalDate(maxDate), [maxDate]);
|
|
200
|
-
const resolvedMinDate = React.useMemo(() => parseLocalDate(minDate), [minDate]);
|
|
201
|
-
|
|
202
|
-
return (
|
|
203
|
-
<Stack gap="m" p="m" testID={testID ? `${testID}-sheet` : undefined}>
|
|
204
|
-
<PickerSheetHeader description={description} title={label ?? 'Choose date'} />
|
|
205
|
-
<DatePickerMonthNavigation
|
|
206
|
-
displayMonth={displayMonth}
|
|
207
|
-
interactionPolicy={interactionPolicy}
|
|
208
|
-
maxDate={resolvedMaxDate}
|
|
209
|
-
minDate={resolvedMinDate}
|
|
210
|
-
onMonthChange={setDisplayMonth}
|
|
211
|
-
/>
|
|
212
|
-
<DatePickerWeekdays />
|
|
213
|
-
<DatePickerCalendar
|
|
214
|
-
interactionPolicy={interactionPolicy}
|
|
215
|
-
maxDate={resolvedMaxDate}
|
|
216
|
-
minDate={resolvedMinDate}
|
|
217
|
-
monthDays={monthDays}
|
|
218
|
-
onSelect={onSelect}
|
|
219
|
-
testID={testID}
|
|
220
|
-
value={value}
|
|
221
|
-
/>
|
|
222
|
-
<Button fullWidth interactionPolicy={interactionPolicy} onPress={onDismiss} variant="ghost">
|
|
223
|
-
Cancel
|
|
224
|
-
</Button>
|
|
225
|
-
</Stack>
|
|
226
|
-
);
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
/*** Render the shared title block used by the calendar sheet. */
|
|
230
|
-
function PickerSheetHeader({
|
|
231
|
-
description,
|
|
232
|
-
title,
|
|
233
|
-
}: {
|
|
234
|
-
description: React.ReactNode | undefined;
|
|
235
|
-
title: React.ReactNode;
|
|
236
|
-
}) {
|
|
237
|
-
return (
|
|
238
|
-
<Stack gap="xxs">
|
|
239
|
-
<Text align="center" variant="label" weight="semiBold">
|
|
240
|
-
{title}
|
|
241
|
-
</Text>
|
|
242
|
-
{description ? (
|
|
243
|
-
<Text align="center" emphasis="muted" variant="bodySmall">
|
|
244
|
-
{description}
|
|
245
|
-
</Text>
|
|
246
|
-
) : null}
|
|
247
|
-
</Stack>
|
|
248
|
-
);
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
/*** Render previous/next month controls for the calendar sheet. */
|
|
252
|
-
function DatePickerMonthNavigation({
|
|
253
|
-
displayMonth,
|
|
254
|
-
interactionPolicy,
|
|
255
|
-
maxDate,
|
|
256
|
-
minDate,
|
|
257
|
-
onMonthChange,
|
|
258
|
-
}: {
|
|
259
|
-
displayMonth: Date;
|
|
260
|
-
interactionPolicy: DatePickerProps['interactionPolicy'];
|
|
261
|
-
maxDate: Date | undefined;
|
|
262
|
-
minDate: Date | undefined;
|
|
263
|
-
onMonthChange: (month: Date) => void;
|
|
264
|
-
}) {
|
|
265
|
-
const previousMonth = new Date(displayMonth.getFullYear(), displayMonth.getMonth() - 1, 1);
|
|
266
|
-
const nextMonth = new Date(displayMonth.getFullYear(), displayMonth.getMonth() + 1, 1);
|
|
267
|
-
|
|
268
|
-
return (
|
|
269
|
-
<Stack align="center" direction="row" justify="space-between">
|
|
270
|
-
<Button
|
|
271
|
-
disabled={!canNavigateToMonth(previousMonth, minDate, maxDate)}
|
|
272
|
-
interactionPolicy={interactionPolicy}
|
|
273
|
-
onPress={() => onMonthChange(previousMonth)}
|
|
274
|
-
size="s"
|
|
275
|
-
variant="ghost"
|
|
276
|
-
>
|
|
277
|
-
Previous
|
|
278
|
-
</Button>
|
|
279
|
-
<Text align="center" variant="label" weight="semiBold">
|
|
280
|
-
{formatMonthLabel(displayMonth)}
|
|
281
|
-
</Text>
|
|
282
|
-
<Button
|
|
283
|
-
disabled={!canNavigateToMonth(nextMonth, minDate, maxDate)}
|
|
284
|
-
interactionPolicy={interactionPolicy}
|
|
285
|
-
onPress={() => onMonthChange(nextMonth)}
|
|
286
|
-
size="s"
|
|
287
|
-
variant="ghost"
|
|
288
|
-
>
|
|
289
|
-
Next
|
|
290
|
-
</Button>
|
|
291
|
-
</Stack>
|
|
292
|
-
);
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
/*** Render weekday headings for the calendar grid. */
|
|
296
|
-
function DatePickerWeekdays() {
|
|
297
|
-
return (
|
|
298
|
-
<Stack direction="row" justify="space-between">
|
|
299
|
-
{WEEKDAY_LABELS.map((weekday) => (
|
|
300
|
-
<Box key={weekday} width={DAY_CELL_WIDTH}>
|
|
301
|
-
<Text align="center" emphasis="muted" variant="caption" weight="semiBold">
|
|
302
|
-
{weekday}
|
|
303
|
-
</Text>
|
|
304
|
-
</Box>
|
|
305
|
-
))}
|
|
306
|
-
</Stack>
|
|
307
|
-
);
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
/*** Render selectable day cells for the current calendar month. */
|
|
311
|
-
function DatePickerCalendar({
|
|
312
|
-
interactionPolicy,
|
|
313
|
-
maxDate,
|
|
314
|
-
minDate,
|
|
315
|
-
monthDays,
|
|
316
|
-
onSelect,
|
|
317
|
-
testID,
|
|
318
|
-
value,
|
|
319
|
-
}: {
|
|
320
|
-
interactionPolicy: DatePickerProps['interactionPolicy'];
|
|
321
|
-
maxDate: Date | undefined;
|
|
322
|
-
minDate: Date | undefined;
|
|
323
|
-
monthDays: readonly (Date | null)[];
|
|
324
|
-
onSelect: (value: Date) => void;
|
|
325
|
-
testID: string | undefined;
|
|
326
|
-
value: DatePickerProps['value'];
|
|
327
|
-
}) {
|
|
328
|
-
return (
|
|
329
|
-
<Stack direction="row" gap="xs" wrap="wrap">
|
|
330
|
-
{monthDays.map((day, index) => {
|
|
331
|
-
if (!day) return <Box key={`empty-${index}`} width={DAY_CELL_WIDTH} />;
|
|
332
|
-
const selected = isSameLocalDay(value, day);
|
|
333
|
-
return (
|
|
334
|
-
<Box key={day.toISOString()} width={DAY_CELL_WIDTH}>
|
|
335
|
-
<Button
|
|
336
|
-
color={selected ? 'primary' : 'neutral'}
|
|
337
|
-
disabled={isDateDisabled(day, minDate, maxDate)}
|
|
338
|
-
interactionPolicy={interactionPolicy}
|
|
339
|
-
onPress={() => onSelect(day)}
|
|
340
|
-
size="s"
|
|
341
|
-
testID={testID ? `${testID}-day-${day.getDate()}` : undefined}
|
|
342
|
-
variant={selected ? 'solid' : 'ghost'}
|
|
343
|
-
>
|
|
344
|
-
{day.getDate()}
|
|
345
|
-
</Button>
|
|
346
|
-
</Box>
|
|
347
|
-
);
|
|
348
|
-
})}
|
|
349
|
-
</Stack>
|
|
350
|
-
);
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
/***
|
|
354
|
-
* Date input control with calendar selection and formatted display value.
|
|
355
|
-
*/
|
|
356
|
-
export const DatePicker = withZoraThemeScope(DatePickerInner);
|
|
1
|
+
export { DatePicker } from './DatePicker.web';
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { Popover } from '@ankhorage/surface';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
|
|
4
|
+
import { withZoraThemeScope } from '../../../../theme/withZoraThemeScope';
|
|
5
|
+
import type { DatePickerProps } from '../../../../types/date-picker';
|
|
6
|
+
import { Surface } from '../../../surface/public';
|
|
7
|
+
import { DatePickerContent } from '../../composition/DatePickerContent';
|
|
8
|
+
import { DatePickerField } from '../../composition/DatePickerField';
|
|
9
|
+
import { DatePickerTrigger } from '../../composition/DatePickerTrigger';
|
|
10
|
+
import { formatLocalDate } from '../../utils/localDate';
|
|
11
|
+
|
|
12
|
+
/*** Renders DatePicker with anchored web Popover presentation. */
|
|
13
|
+
export const DatePicker = withZoraThemeScope(DatePickerInner);
|
|
14
|
+
|
|
15
|
+
/*** Connects the shared DatePicker field and content to the web Popover host. */
|
|
16
|
+
function DatePickerInner({ themeId: _themeId, mode: _mode, ...props }: DatePickerProps) {
|
|
17
|
+
const [open, setOpen] = React.useState(false);
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<DatePickerField {...props}>
|
|
21
|
+
<Popover
|
|
22
|
+
anchor={({ toggle }) => <DatePickerTrigger {...props} onPress={toggle} />}
|
|
23
|
+
interactionPolicy={props.interactionPolicy}
|
|
24
|
+
onOpenChange={setOpen}
|
|
25
|
+
open={open}
|
|
26
|
+
placement="bottom-start"
|
|
27
|
+
testID={props.testID ? `${props.testID}-popover` : undefined}
|
|
28
|
+
>
|
|
29
|
+
<Surface variant="raised">
|
|
30
|
+
<DatePickerContent
|
|
31
|
+
description={props.description}
|
|
32
|
+
interactionPolicy={props.interactionPolicy}
|
|
33
|
+
label={props.label}
|
|
34
|
+
maxDate={props.maxDate}
|
|
35
|
+
minDate={props.minDate}
|
|
36
|
+
onDismiss={() => setOpen(false)}
|
|
37
|
+
onSelect={(date) => {
|
|
38
|
+
props.onValueChange?.(formatLocalDate(date));
|
|
39
|
+
setOpen(false);
|
|
40
|
+
}}
|
|
41
|
+
testID={props.testID}
|
|
42
|
+
value={props.value}
|
|
43
|
+
/>
|
|
44
|
+
</Surface>
|
|
45
|
+
</Popover>
|
|
46
|
+
</DatePickerField>
|
|
47
|
+
);
|
|
48
|
+
}
|