@studio-piot/native-ui 0.2.4 → 0.16.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 +22 -18
- package/dist/components/Button/Button.d.ts +20 -4
- package/dist/components/Button/Button.d.ts.map +1 -1
- package/dist/components/Button/Button.js +34 -21
- package/dist/components/Button/Button.js.map +1 -1
- package/dist/components/Button/buttonAppearance.d.ts +20 -0
- package/dist/components/Button/buttonAppearance.d.ts.map +1 -0
- package/dist/components/Button/buttonAppearance.js +28 -0
- package/dist/components/Button/buttonAppearance.js.map +1 -0
- package/dist/components/Button/buttonThemeRoles.d.ts +6 -0
- package/dist/components/Button/buttonThemeRoles.d.ts.map +1 -0
- package/dist/components/Button/buttonThemeRoles.js +8 -0
- package/dist/components/Button/buttonThemeRoles.js.map +1 -0
- package/dist/components/Button/index.d.ts +3 -1
- package/dist/components/Button/index.d.ts.map +1 -1
- package/dist/components/Button/index.js +4 -1
- package/dist/components/Button/index.js.map +1 -1
- package/dist/components/Choice/Choice.d.ts +8 -0
- package/dist/components/Choice/Choice.d.ts.map +1 -0
- package/dist/components/Choice/Choice.js +60 -0
- package/dist/components/Choice/Choice.js.map +1 -0
- package/dist/components/Choice/choiceLayout.d.ts +7 -0
- package/dist/components/Choice/choiceLayout.d.ts.map +1 -0
- package/dist/components/Choice/choiceLayout.js +13 -0
- package/dist/components/Choice/choiceLayout.js.map +1 -0
- package/dist/components/Choice/choiceThemeRoles.d.ts +8 -0
- package/dist/components/Choice/choiceThemeRoles.d.ts.map +1 -0
- package/dist/components/Choice/choiceThemeRoles.js +10 -0
- package/dist/components/Choice/choiceThemeRoles.js.map +1 -0
- package/dist/components/Choice/index.d.ts +5 -0
- package/dist/components/Choice/index.d.ts.map +1 -0
- package/dist/components/Choice/index.js +10 -0
- package/dist/components/Choice/index.js.map +1 -0
- package/dist/components/Choice/types.d.ts +26 -0
- package/dist/components/Choice/types.d.ts.map +1 -0
- package/dist/components/Choice/types.js +3 -0
- package/dist/components/Choice/types.js.map +1 -0
- package/dist/components/ErrorBanner/ErrorBanner.d.ts +1 -1
- package/dist/components/ErrorBanner/ErrorBanner.d.ts.map +1 -1
- package/dist/components/ErrorBanner/ErrorBanner.js +19 -19
- package/dist/components/ErrorBanner/ErrorBanner.js.map +1 -1
- package/dist/components/ErrorBanner/errorBannerContext.d.ts +23 -3
- package/dist/components/ErrorBanner/errorBannerContext.d.ts.map +1 -1
- package/dist/components/ErrorBanner/errorBannerContext.js +9 -5
- package/dist/components/ErrorBanner/errorBannerContext.js.map +1 -1
- package/dist/components/ErrorBanner/errorBannerLayout.d.ts +18 -0
- package/dist/components/ErrorBanner/errorBannerLayout.d.ts.map +1 -0
- package/dist/components/ErrorBanner/errorBannerLayout.js +54 -0
- package/dist/components/ErrorBanner/errorBannerLayout.js.map +1 -0
- package/dist/components/ErrorBanner/icons.d.ts +7 -5
- package/dist/components/ErrorBanner/icons.d.ts.map +1 -1
- package/dist/components/ErrorBanner/icons.js +8 -4
- package/dist/components/ErrorBanner/icons.js.map +1 -1
- package/dist/components/ErrorBanner/index.d.ts +1 -1
- package/dist/components/ErrorBanner/index.d.ts.map +1 -1
- package/dist/components/ErrorBanner/index.js.map +1 -1
- package/dist/components/Form/Form.d.ts +9 -1
- package/dist/components/Form/Form.d.ts.map +1 -1
- package/dist/components/Form/Form.js +56 -78
- package/dist/components/Form/Form.js.map +1 -1
- package/dist/components/Form/FormCloseButton.d.ts +3 -1
- package/dist/components/Form/FormCloseButton.d.ts.map +1 -1
- package/dist/components/Form/FormCloseButton.js +5 -4
- package/dist/components/Form/FormCloseButton.js.map +1 -1
- package/dist/components/Form/FormErrorBanner.d.ts +15 -0
- package/dist/components/Form/FormErrorBanner.d.ts.map +1 -0
- package/dist/components/Form/FormErrorBanner.js +30 -0
- package/dist/components/Form/FormErrorBanner.js.map +1 -0
- package/dist/components/Form/FormSubmitFooter.d.ts +17 -0
- package/dist/components/Form/FormSubmitFooter.d.ts.map +1 -0
- package/dist/components/Form/FormSubmitFooter.js +32 -0
- package/dist/components/Form/FormSubmitFooter.js.map +1 -0
- package/dist/components/Form/formThemeRoles.d.ts +7 -0
- package/dist/components/Form/formThemeRoles.d.ts.map +1 -0
- package/dist/components/Form/formThemeRoles.js +9 -0
- package/dist/components/Form/formThemeRoles.js.map +1 -0
- package/dist/components/Form/index.d.ts +1 -0
- package/dist/components/Form/index.d.ts.map +1 -1
- package/dist/components/Form/index.js +3 -1
- package/dist/components/Form/index.js.map +1 -1
- package/dist/components/Form/types.d.ts +14 -10
- package/dist/components/Form/types.d.ts.map +1 -1
- package/dist/components/FormSheet/FormSheet.d.ts +10 -0
- package/dist/components/FormSheet/FormSheet.d.ts.map +1 -0
- package/dist/components/FormSheet/FormSheet.js +153 -0
- package/dist/components/FormSheet/FormSheet.js.map +1 -0
- package/dist/components/FormSheet/formSheetLayout.d.ts +32 -0
- package/dist/components/FormSheet/formSheetLayout.d.ts.map +1 -0
- package/dist/components/FormSheet/formSheetLayout.js +43 -0
- package/dist/components/FormSheet/formSheetLayout.js.map +1 -0
- package/dist/components/FormSheet/formSheetThemeRoles.d.ts +8 -0
- package/dist/components/FormSheet/formSheetThemeRoles.d.ts.map +1 -0
- package/dist/components/FormSheet/formSheetThemeRoles.js +10 -0
- package/dist/components/FormSheet/formSheetThemeRoles.js.map +1 -0
- package/dist/components/FormSheet/index.d.ts +5 -0
- package/dist/components/FormSheet/index.d.ts.map +1 -0
- package/dist/components/FormSheet/index.js +10 -0
- package/dist/components/FormSheet/index.js.map +1 -0
- package/dist/components/FormSheet/types.d.ts +64 -0
- package/dist/components/FormSheet/types.d.ts.map +1 -0
- package/dist/components/FormSheet/types.js +3 -0
- package/dist/components/FormSheet/types.js.map +1 -0
- package/dist/components/HeaderButton/HeaderCapsule.d.ts +4 -0
- package/dist/components/HeaderButton/HeaderCapsule.d.ts.map +1 -0
- package/dist/components/HeaderButton/HeaderCapsule.js +32 -0
- package/dist/components/HeaderButton/HeaderCapsule.js.map +1 -0
- package/dist/components/HeaderButton/HeaderGroup.d.ts +4 -0
- package/dist/components/HeaderButton/HeaderGroup.d.ts.map +1 -0
- package/dist/components/HeaderButton/HeaderGroup.js +23 -0
- package/dist/components/HeaderButton/HeaderGroup.js.map +1 -0
- package/dist/components/HeaderButton/HeaderIcon.d.ts +4 -0
- package/dist/components/HeaderButton/HeaderIcon.d.ts.map +1 -0
- package/dist/components/HeaderButton/HeaderIcon.js +28 -0
- package/dist/components/HeaderButton/HeaderIcon.js.map +1 -0
- package/dist/components/HeaderButton/headerButtonLayout.d.ts +21 -0
- package/dist/components/HeaderButton/headerButtonLayout.d.ts.map +1 -0
- package/dist/components/HeaderButton/headerButtonLayout.js +34 -0
- package/dist/components/HeaderButton/headerButtonLayout.js.map +1 -0
- package/dist/components/HeaderButton/headerButtonThemeRoles.d.ts +5 -0
- package/dist/components/HeaderButton/headerButtonThemeRoles.d.ts.map +1 -0
- package/dist/components/HeaderButton/headerButtonThemeRoles.js +7 -0
- package/dist/components/HeaderButton/headerButtonThemeRoles.js.map +1 -0
- package/dist/components/HeaderButton/index.d.ts +7 -0
- package/dist/components/HeaderButton/index.d.ts.map +1 -0
- package/dist/components/HeaderButton/index.js +14 -0
- package/dist/components/HeaderButton/index.js.map +1 -0
- package/dist/components/HeaderButton/types.d.ts +40 -0
- package/dist/components/HeaderButton/types.d.ts.map +1 -0
- package/dist/components/HeaderButton/types.js +3 -0
- package/dist/components/HeaderButton/types.js.map +1 -0
- package/dist/components/HeaderWash/HeaderWash.d.ts +10 -0
- package/dist/components/HeaderWash/HeaderWash.d.ts.map +1 -0
- package/dist/components/HeaderWash/HeaderWash.js +19 -0
- package/dist/components/HeaderWash/HeaderWash.js.map +1 -0
- package/dist/components/HeaderWash/headerWashGradient.d.ts +8 -0
- package/dist/components/HeaderWash/headerWashGradient.d.ts.map +1 -0
- package/dist/components/HeaderWash/headerWashGradient.js +21 -0
- package/dist/components/HeaderWash/headerWashGradient.js.map +1 -0
- package/dist/components/HeaderWash/index.d.ts +2 -0
- package/dist/components/HeaderWash/index.d.ts.map +1 -0
- package/dist/components/HeaderWash/index.js +6 -0
- package/dist/components/HeaderWash/index.js.map +1 -0
- package/dist/components/Page/Page.d.ts +13 -0
- package/dist/components/Page/Page.d.ts.map +1 -0
- package/dist/components/Page/Page.js +49 -0
- package/dist/components/Page/Page.js.map +1 -0
- package/dist/components/Page/PageBleed.d.ts +9 -0
- package/dist/components/Page/PageBleed.d.ts.map +1 -0
- package/dist/components/Page/PageBleed.js +12 -0
- package/dist/components/Page/PageBleed.js.map +1 -0
- package/dist/components/Page/index.d.ts +5 -0
- package/dist/components/Page/index.d.ts.map +1 -0
- package/dist/components/Page/index.js +10 -0
- package/dist/components/Page/index.js.map +1 -0
- package/dist/components/Page/pageContext.d.ts +4 -0
- package/dist/components/Page/pageContext.d.ts.map +1 -0
- package/dist/components/Page/pageContext.js +11 -0
- package/dist/components/Page/pageContext.js.map +1 -0
- package/dist/components/Page/pageInsets.d.ts +26 -0
- package/dist/components/Page/pageInsets.d.ts.map +1 -0
- package/dist/components/Page/pageInsets.js +37 -0
- package/dist/components/Page/pageInsets.js.map +1 -0
- package/dist/components/Page/pageLayout.d.ts +5 -0
- package/dist/components/Page/pageLayout.d.ts.map +1 -0
- package/dist/components/Page/pageLayout.js +8 -0
- package/dist/components/Page/pageLayout.js.map +1 -0
- package/dist/components/Page/pageThemeRoles.d.ts +5 -0
- package/dist/components/Page/pageThemeRoles.d.ts.map +1 -0
- package/dist/components/Page/pageThemeRoles.js +7 -0
- package/dist/components/Page/pageThemeRoles.js.map +1 -0
- package/dist/components/Page/types.d.ts +22 -0
- package/dist/components/Page/types.d.ts.map +1 -0
- package/dist/components/Page/types.js +3 -0
- package/dist/components/Page/types.js.map +1 -0
- package/dist/components/SegmentedControl/types.d.ts +0 -1
- package/dist/components/SegmentedControl/types.d.ts.map +1 -1
- package/dist/components/Select/Select.d.ts +1 -1
- package/dist/components/Select/Select.d.ts.map +1 -1
- package/dist/components/Select/Select.js +8 -9
- package/dist/components/Select/Select.js.map +1 -1
- package/dist/components/Select/selectThemeRoles.d.ts +2 -0
- package/dist/components/Select/selectThemeRoles.d.ts.map +1 -1
- package/dist/components/Select/selectThemeRoles.js +2 -0
- package/dist/components/Select/selectThemeRoles.js.map +1 -1
- package/dist/components/Select/types.d.ts +4 -0
- package/dist/components/Select/types.d.ts.map +1 -1
- package/dist/components/TabAccessoryButton/TabAccessoryButton.d.ts +8 -0
- package/dist/components/TabAccessoryButton/TabAccessoryButton.d.ts.map +1 -0
- package/dist/components/TabAccessoryButton/TabAccessoryButton.js +77 -0
- package/dist/components/TabAccessoryButton/TabAccessoryButton.js.map +1 -0
- package/dist/components/TabAccessoryButton/index.d.ts +5 -0
- package/dist/components/TabAccessoryButton/index.d.ts.map +1 -0
- package/dist/components/TabAccessoryButton/index.js +11 -0
- package/dist/components/TabAccessoryButton/index.js.map +1 -0
- package/dist/components/TabAccessoryButton/tabAccessoryLayout.d.ts +27 -0
- package/dist/components/TabAccessoryButton/tabAccessoryLayout.d.ts.map +1 -0
- package/dist/components/TabAccessoryButton/tabAccessoryLayout.js +38 -0
- package/dist/components/TabAccessoryButton/tabAccessoryLayout.js.map +1 -0
- package/dist/components/TabAccessoryButton/tabAccessoryThemeRoles.d.ts +14 -0
- package/dist/components/TabAccessoryButton/tabAccessoryThemeRoles.d.ts.map +1 -0
- package/dist/components/TabAccessoryButton/tabAccessoryThemeRoles.js +19 -0
- package/dist/components/TabAccessoryButton/tabAccessoryThemeRoles.js.map +1 -0
- package/dist/components/TabAccessoryButton/types.d.ts +27 -0
- package/dist/components/TabAccessoryButton/types.d.ts.map +1 -0
- package/dist/components/TabAccessoryButton/types.js +3 -0
- package/dist/components/TabAccessoryButton/types.js.map +1 -0
- package/dist/components/index.d.ts +9 -3
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/index.js +30 -1
- package/dist/components/index.js.map +1 -1
- package/dist/index.d.ts +11 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +34 -2
- package/dist/index.js.map +1 -1
- package/dist/layout/HeaderInsetProbe.d.ts +15 -0
- package/dist/layout/HeaderInsetProbe.d.ts.map +1 -0
- package/dist/layout/HeaderInsetProbe.js +28 -0
- package/dist/layout/HeaderInsetProbe.js.map +1 -0
- package/dist/layout/headerInset.d.ts +26 -0
- package/dist/layout/headerInset.d.ts.map +1 -0
- package/dist/layout/headerInset.js +29 -0
- package/dist/layout/headerInset.js.map +1 -0
- package/dist/layout/index.d.ts +1 -0
- package/dist/layout/index.d.ts.map +1 -1
- package/dist/layout/index.js +3 -1
- package/dist/layout/index.js.map +1 -1
- package/dist/layout/sheet.d.ts +3 -0
- package/dist/layout/sheet.d.ts.map +1 -0
- package/dist/layout/sheet.js +6 -0
- package/dist/layout/sheet.js.map +1 -0
- package/dist/theme/ThemeProvider.d.ts +4 -1
- package/dist/theme/ThemeProvider.d.ts.map +1 -1
- package/dist/theme/ThemeProvider.js +5 -2
- package/dist/theme/ThemeProvider.js.map +1 -1
- package/dist/theme/index.d.ts +2 -0
- package/dist/theme/index.d.ts.map +1 -1
- package/dist/theme/index.js +6 -1
- package/dist/theme/index.js.map +1 -1
- package/dist/theme/resolveThemeSpacing.d.ts +12 -0
- package/dist/theme/resolveThemeSpacing.d.ts.map +1 -0
- package/dist/theme/resolveThemeSpacing.js +14 -0
- package/dist/theme/resolveThemeSpacing.js.map +1 -0
- package/dist/theme/types.d.ts +2 -0
- package/dist/theme/types.d.ts.map +1 -1
- package/dist/theme/useThemeSpacing.d.ts +4 -0
- package/dist/theme/useThemeSpacing.d.ts.map +1 -0
- package/dist/theme/useThemeSpacing.js +11 -0
- package/dist/theme/useThemeSpacing.js.map +1 -0
- package/docs/components/button.md +92 -38
- package/docs/components/choice.md +88 -0
- package/docs/components/error-banner.md +46 -3
- package/docs/components/form-sheet.md +140 -0
- package/docs/components/form.md +80 -45
- package/docs/components/header-button.md +310 -0
- package/docs/components/header-wash.md +40 -0
- package/docs/components/page.md +183 -0
- package/docs/components/select.md +2 -2
- package/docs/components/tab-accessory-button.md +146 -0
- package/docs/specifications/Liquid Glass header buttons in Expo Router.md +327 -0
- package/package.json +1 -1
- package/src/components/Button/Button.tsx +82 -38
- package/src/components/Button/buttonAppearance.test.ts +44 -0
- package/src/components/Button/buttonAppearance.ts +38 -0
- package/src/components/Button/buttonThemeRoles.test.ts +10 -0
- package/src/components/Button/buttonThemeRoles.ts +6 -0
- package/src/components/Button/index.ts +12 -1
- package/src/components/Choice/Choice.tsx +109 -0
- package/src/components/Choice/choiceLayout.test.ts +14 -0
- package/src/components/Choice/choiceLayout.ts +9 -0
- package/src/components/Choice/choiceThemeRoles.test.ts +14 -0
- package/src/components/Choice/choiceThemeRoles.ts +8 -0
- package/src/components/Choice/index.ts +4 -0
- package/src/components/Choice/types.ts +26 -0
- package/src/components/ErrorBanner/ErrorBanner.tsx +32 -22
- package/src/components/ErrorBanner/errorBannerContext.tsx +31 -6
- package/src/components/ErrorBanner/errorBannerLayout.test.ts +49 -0
- package/src/components/ErrorBanner/errorBannerLayout.ts +59 -0
- package/src/components/ErrorBanner/icons.tsx +25 -6
- package/src/components/ErrorBanner/index.ts +1 -0
- package/src/components/Form/Form.tsx +77 -173
- package/src/components/Form/FormCloseButton.tsx +12 -4
- package/src/components/Form/FormErrorBanner.tsx +57 -0
- package/src/components/Form/FormSubmitFooter.tsx +70 -0
- package/src/components/Form/formThemeRoles.test.ts +13 -0
- package/src/components/Form/formThemeRoles.ts +7 -0
- package/src/components/Form/index.ts +1 -0
- package/src/components/Form/types.ts +14 -10
- package/src/components/FormSheet/FormSheet.tsx +279 -0
- package/src/components/FormSheet/formSheetLayout.test.ts +66 -0
- package/src/components/FormSheet/formSheetLayout.ts +55 -0
- package/src/components/FormSheet/formSheetThemeRoles.test.ts +14 -0
- package/src/components/FormSheet/formSheetThemeRoles.ts +8 -0
- package/src/components/FormSheet/index.ts +7 -0
- package/src/components/FormSheet/types.ts +66 -0
- package/src/components/HeaderButton/HeaderCapsule.tsx +63 -0
- package/src/components/HeaderButton/HeaderGroup.tsx +36 -0
- package/src/components/HeaderButton/HeaderIcon.tsx +56 -0
- package/src/components/HeaderButton/headerButtonLayout.test.ts +47 -0
- package/src/components/HeaderButton/headerButtonLayout.ts +36 -0
- package/src/components/HeaderButton/headerButtonThemeRoles.test.ts +9 -0
- package/src/components/HeaderButton/headerButtonThemeRoles.ts +5 -0
- package/src/components/HeaderButton/index.ts +14 -0
- package/src/components/HeaderButton/types.ts +40 -0
- package/src/components/HeaderWash/HeaderWash.tsx +20 -0
- package/src/components/HeaderWash/headerWashGradient.test.ts +22 -0
- package/src/components/{Form/withAlpha.ts → HeaderWash/headerWashGradient.ts} +9 -0
- package/src/components/HeaderWash/index.ts +1 -0
- package/src/components/Page/Page.tsx +80 -0
- package/src/components/Page/PageBleed.tsx +14 -0
- package/src/components/Page/index.ts +4 -0
- package/src/components/Page/pageContext.ts +8 -0
- package/src/components/Page/pageInsets.test.ts +73 -0
- package/src/components/Page/pageInsets.ts +52 -0
- package/src/components/Page/pageThemeRoles.test.ts +9 -0
- package/src/components/Page/pageThemeRoles.ts +5 -0
- package/src/components/Page/types.ts +26 -0
- package/src/components/SegmentedControl/types.ts +0 -1
- package/src/components/Select/Select.tsx +11 -4
- package/src/components/Select/selectThemeRoles.test.ts +5 -0
- package/src/components/Select/selectThemeRoles.ts +2 -0
- package/src/components/Select/types.ts +4 -0
- package/src/components/TabAccessoryButton/TabAccessoryButton.tsx +150 -0
- package/src/components/TabAccessoryButton/index.ts +12 -0
- package/src/components/TabAccessoryButton/tabAccessoryLayout.test.ts +42 -0
- package/src/components/TabAccessoryButton/tabAccessoryLayout.ts +43 -0
- package/src/components/TabAccessoryButton/tabAccessoryThemeRoles.test.ts +28 -0
- package/src/components/TabAccessoryButton/tabAccessoryThemeRoles.ts +22 -0
- package/src/components/TabAccessoryButton/types.ts +27 -0
- package/src/components/index.ts +64 -2
- package/src/index.ts +61 -2
- package/src/layout/headerInset.test.ts +60 -0
- package/src/layout/headerInset.ts +41 -0
- package/src/layout/index.ts +1 -0
- package/src/layout/sheet.ts +2 -0
- package/src/theme/ThemeProvider.tsx +9 -1
- package/src/theme/index.ts +6 -0
- package/src/theme/resolveThemeSpacing.test.ts +29 -0
- package/src/theme/resolveThemeSpacing.ts +21 -0
- package/src/theme/types.ts +3 -0
- package/src/theme/useThemeSpacing.ts +8 -0
- package/src/components/Form/withAlpha.test.ts +0 -13
|
@@ -8,10 +8,15 @@ import {
|
|
|
8
8
|
type ReactNode,
|
|
9
9
|
type RefObject,
|
|
10
10
|
} from "react";
|
|
11
|
-
import { View, type ReactNativeElement
|
|
11
|
+
import { View, type ReactNativeElement } from "react-native";
|
|
12
12
|
import { fieldScrollY } from "./errorBannerCopy";
|
|
13
13
|
import type { FieldError } from "./types";
|
|
14
14
|
|
|
15
|
+
/** Anything that scrolls: a ScrollView, a KeyboardAwareScrollView, a BottomSheetScrollView. */
|
|
16
|
+
export type ErrorBannerScrollTarget = {
|
|
17
|
+
scrollTo(options: { y: number; animated?: boolean }): void;
|
|
18
|
+
};
|
|
19
|
+
|
|
15
20
|
export type ErrorBannerAnchor = {
|
|
16
21
|
view: View;
|
|
17
22
|
focus?: () => void;
|
|
@@ -76,12 +81,28 @@ export function ErrorBannerProvider({
|
|
|
76
81
|
errors,
|
|
77
82
|
onDismiss,
|
|
78
83
|
bannerHeightRef,
|
|
84
|
+
measureRef,
|
|
85
|
+
onBeforeJump,
|
|
86
|
+
scrollYForField,
|
|
79
87
|
}: {
|
|
80
88
|
children: ReactNode;
|
|
81
|
-
scrollRef: RefObject<
|
|
89
|
+
scrollRef: RefObject<ErrorBannerScrollTarget | null>;
|
|
82
90
|
errors: readonly FieldError[];
|
|
83
91
|
onDismiss: () => void;
|
|
92
|
+
/** How far the banner covers the top of the scroll area. */
|
|
84
93
|
bannerHeightRef: RefObject<number>;
|
|
94
|
+
/**
|
|
95
|
+
* View that field positions are measured against: the scroll content's
|
|
96
|
+
* top. Defaults to `scrollRef`, which works for a plain ScrollView.
|
|
97
|
+
*/
|
|
98
|
+
measureRef?: RefObject<View | null>;
|
|
99
|
+
/** Runs before scrolling to a field, e.g. to expand a sheet. */
|
|
100
|
+
onBeforeJump?: () => void;
|
|
101
|
+
/**
|
|
102
|
+
* Scroll offset for a field at content y `fieldY`. Defaults to clearing
|
|
103
|
+
* `bannerHeightRef` from the top of the scroll area.
|
|
104
|
+
*/
|
|
105
|
+
scrollYForField?: (fieldY: number) => number;
|
|
85
106
|
}) {
|
|
86
107
|
const anchors = useRef(new Map<string, ErrorBannerAnchor>());
|
|
87
108
|
|
|
@@ -96,21 +117,25 @@ export function ErrorBannerProvider({
|
|
|
96
117
|
const jump = useCallback((field: string) => {
|
|
97
118
|
const anchor = anchors.current.get(field);
|
|
98
119
|
const scroll = scrollRef.current;
|
|
99
|
-
|
|
120
|
+
const relativeTo = measureRef?.current ?? scroll;
|
|
121
|
+
if (!anchor || !scroll || !relativeTo) {
|
|
100
122
|
return;
|
|
101
123
|
}
|
|
124
|
+
onBeforeJump?.();
|
|
102
125
|
anchor.view.measureLayout(
|
|
103
|
-
|
|
126
|
+
relativeTo as unknown as ReactNativeElement,
|
|
104
127
|
(_x, y) => {
|
|
105
128
|
scroll.scrollTo({
|
|
106
|
-
y:
|
|
129
|
+
y: scrollYForField
|
|
130
|
+
? scrollYForField(y)
|
|
131
|
+
: fieldScrollY(y, bannerHeightRef.current ?? 0),
|
|
107
132
|
animated: true,
|
|
108
133
|
});
|
|
109
134
|
anchor.focus?.();
|
|
110
135
|
},
|
|
111
136
|
() => undefined,
|
|
112
137
|
);
|
|
113
|
-
}, [bannerHeightRef, scrollRef]);
|
|
138
|
+
}, [bannerHeightRef, measureRef, onBeforeJump, scrollRef, scrollYForField]);
|
|
114
139
|
|
|
115
140
|
const value = useMemo(
|
|
116
141
|
() => ({ errors, register, dismiss: onDismiss, jump }),
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { describe, it } from "node:test";
|
|
3
|
+
import {
|
|
4
|
+
ERROR_BANNER_DISC_SIZE,
|
|
5
|
+
ERROR_BANNER_FONT_SIZE,
|
|
6
|
+
ERROR_BANNER_HEADER_GAP,
|
|
7
|
+
ERROR_BANNER_ROW_INSET,
|
|
8
|
+
ERROR_BANNER_ROW_MIN_HEIGHT,
|
|
9
|
+
errorBannerBarStyle,
|
|
10
|
+
} from "./errorBannerLayout.ts";
|
|
11
|
+
|
|
12
|
+
describe("errorBannerBarStyle", () => {
|
|
13
|
+
it("uses light status bar text on a dark banner", () => {
|
|
14
|
+
assert.equal(errorBannerBarStyle("#1F2937"), "light-content");
|
|
15
|
+
assert.equal(errorBannerBarStyle("#000"), "light-content");
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("uses dark status bar text on a light banner", () => {
|
|
19
|
+
assert.equal(errorBannerBarStyle("#F9FAFB"), "dark-content");
|
|
20
|
+
assert.equal(errorBannerBarStyle("#fff"), "dark-content");
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("reads rgb() and 8-digit hex colours", () => {
|
|
24
|
+
assert.equal(errorBannerBarStyle("rgb(255, 255, 255)"), "dark-content");
|
|
25
|
+
assert.equal(errorBannerBarStyle("rgba(31, 41, 55, 0.9)"), "light-content");
|
|
26
|
+
assert.equal(errorBannerBarStyle("#FFFFFFCC"), "dark-content");
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("falls back to the system default when the colour cannot be read", () => {
|
|
30
|
+
assert.equal(errorBannerBarStyle("red"), "default");
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
describe("error banner sizes", () => {
|
|
35
|
+
it("matches the 17pt body text used by the other components", () => {
|
|
36
|
+
assert.equal(ERROR_BANNER_FONT_SIZE, 17);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("gives each row the 44pt minimum tap target", () => {
|
|
40
|
+
assert.ok(ERROR_BANNER_ROW_MIN_HEIGHT >= 44);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("lines rows up with the title, after the disc and gap", () => {
|
|
44
|
+
assert.equal(
|
|
45
|
+
ERROR_BANNER_ROW_INSET,
|
|
46
|
+
ERROR_BANNER_DISC_SIZE + ERROR_BANNER_HEADER_GAP,
|
|
47
|
+
);
|
|
48
|
+
});
|
|
49
|
+
});
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/** Title and rows, matching the 17pt body text of Button, Field and Select. */
|
|
2
|
+
export const ERROR_BANNER_FONT_SIZE = 17;
|
|
3
|
+
export const ERROR_BANNER_LINE_HEIGHT = 22;
|
|
4
|
+
export const ERROR_BANNER_DISC_SIZE = 32;
|
|
5
|
+
export const ERROR_BANNER_MEGAPHONE_SIZE = 18;
|
|
6
|
+
export const ERROR_BANNER_ARROW_SIZE = 20;
|
|
7
|
+
export const ERROR_BANNER_DISMISS_SIZE = 18;
|
|
8
|
+
export const ERROR_BANNER_HEADER_GAP = 12;
|
|
9
|
+
export const ERROR_BANNER_ROW_MIN_HEIGHT = 44;
|
|
10
|
+
/** Rows start under the title, past the disc and gap. */
|
|
11
|
+
export const ERROR_BANNER_ROW_INSET =
|
|
12
|
+
ERROR_BANNER_DISC_SIZE + ERROR_BANNER_HEADER_GAP;
|
|
13
|
+
|
|
14
|
+
export type ErrorBannerBarStyle = "light-content" | "dark-content" | "default";
|
|
15
|
+
|
|
16
|
+
function parseRgb(color: string): [number, number, number] | null {
|
|
17
|
+
const value = color.trim();
|
|
18
|
+
const hex = /^#([0-9a-f]{3,8})$/i.exec(value)?.[1];
|
|
19
|
+
if (hex) {
|
|
20
|
+
const full =
|
|
21
|
+
hex.length === 3 || hex.length === 4
|
|
22
|
+
? hex
|
|
23
|
+
.slice(0, 3)
|
|
24
|
+
.split("")
|
|
25
|
+
.map((c) => c + c)
|
|
26
|
+
.join("")
|
|
27
|
+
: hex.slice(0, 6);
|
|
28
|
+
if (full.length !== 6) {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
return [0, 2, 4].map((i) => parseInt(full.slice(i, i + 2), 16)) as [
|
|
32
|
+
number,
|
|
33
|
+
number,
|
|
34
|
+
number,
|
|
35
|
+
];
|
|
36
|
+
}
|
|
37
|
+
const rgb = /^rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/i.exec(value);
|
|
38
|
+
if (rgb) {
|
|
39
|
+
return [Number(rgb[1]), Number(rgb[2]), Number(rgb[3])];
|
|
40
|
+
}
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Status bar text that stays readable over the banner fill: light text on a
|
|
46
|
+
* dark banner, dark text on a light one. Pure so it can be unit-tested.
|
|
47
|
+
*/
|
|
48
|
+
export function errorBannerBarStyle(backgroundColor: string): ErrorBannerBarStyle {
|
|
49
|
+
const rgb = parseRgb(backgroundColor);
|
|
50
|
+
if (!rgb) {
|
|
51
|
+
return "default";
|
|
52
|
+
}
|
|
53
|
+
const [r, g, b] = rgb.map((c) => {
|
|
54
|
+
const s = c / 255;
|
|
55
|
+
return s <= 0.03928 ? s / 12.92 : ((s + 0.055) / 1.055) ** 2.4;
|
|
56
|
+
});
|
|
57
|
+
const luminance = 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
|
58
|
+
return luminance > 0.179 ? "dark-content" : "light-content";
|
|
59
|
+
}
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import Svg, { Line, Path, Polyline } from "react-native-svg";
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
type IconProps = { color: string; size: number };
|
|
4
|
+
|
|
5
|
+
export function Megaphone({ color, size }: IconProps) {
|
|
4
6
|
return (
|
|
5
7
|
<Svg
|
|
6
|
-
width={
|
|
7
|
-
height={
|
|
8
|
+
width={size}
|
|
9
|
+
height={size}
|
|
8
10
|
viewBox="0 0 24 24"
|
|
9
11
|
fill="none"
|
|
10
12
|
stroke={color}
|
|
@@ -19,11 +21,11 @@ export function Megaphone({ color }: { color: string }) {
|
|
|
19
21
|
);
|
|
20
22
|
}
|
|
21
23
|
|
|
22
|
-
export function ArrowRight({ color }:
|
|
24
|
+
export function ArrowRight({ color, size }: IconProps) {
|
|
23
25
|
return (
|
|
24
26
|
<Svg
|
|
25
|
-
width={
|
|
26
|
-
height={
|
|
27
|
+
width={size}
|
|
28
|
+
height={size}
|
|
27
29
|
viewBox="0 0 24 24"
|
|
28
30
|
fill="none"
|
|
29
31
|
stroke={color}
|
|
@@ -36,3 +38,20 @@ export function ArrowRight({ color }: { color: string }) {
|
|
|
36
38
|
</Svg>
|
|
37
39
|
);
|
|
38
40
|
}
|
|
41
|
+
|
|
42
|
+
export function Cross({ color, size }: IconProps) {
|
|
43
|
+
return (
|
|
44
|
+
<Svg
|
|
45
|
+
width={size}
|
|
46
|
+
height={size}
|
|
47
|
+
viewBox="0 0 24 24"
|
|
48
|
+
fill="none"
|
|
49
|
+
stroke={color}
|
|
50
|
+
strokeWidth={2.25}
|
|
51
|
+
strokeLinecap="round"
|
|
52
|
+
>
|
|
53
|
+
<Line x1={6} y1={6} x2={18} y2={18} />
|
|
54
|
+
<Line x1={18} y1={6} x2={6} y2={18} />
|
|
55
|
+
</Svg>
|
|
56
|
+
);
|
|
57
|
+
}
|
|
@@ -1,55 +1,20 @@
|
|
|
1
|
-
import { useRef, useState
|
|
2
|
-
import {
|
|
3
|
-
StyleSheet,
|
|
4
|
-
Text,
|
|
5
|
-
View,
|
|
6
|
-
type LayoutChangeEvent,
|
|
7
|
-
} from "react-native";
|
|
1
|
+
import { useCallback, useRef, useState } from "react";
|
|
2
|
+
import { Platform, StyleSheet, View, type LayoutChangeEvent } from "react-native";
|
|
8
3
|
import {
|
|
9
4
|
KeyboardAwareScrollView,
|
|
10
|
-
KeyboardStickyView,
|
|
11
5
|
type KeyboardAwareScrollViewRef,
|
|
12
6
|
} from "react-native-keyboard-controller";
|
|
13
|
-
import {
|
|
14
|
-
ErrorBanner,
|
|
15
|
-
ErrorBannerProvider,
|
|
16
|
-
useErrorBanner,
|
|
17
|
-
useErrorBannerContext,
|
|
18
|
-
type FieldError,
|
|
19
|
-
} from "../ErrorBanner";
|
|
20
7
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
23
|
-
import {
|
|
8
|
+
import { estimateHeaderHeight, formFieldScrollY } from "../../layout/headerInset";
|
|
9
|
+
import { useThemeSpacing } from "../../theme/useThemeSpacing";
|
|
10
|
+
import { ErrorBannerProvider, useErrorBanner } from "../ErrorBanner";
|
|
11
|
+
import { ERROR_BANNER_SCROLL_GAP } from "../ErrorBanner/errorBannerCopy";
|
|
12
|
+
import { FormErrorBanner } from "./FormErrorBanner";
|
|
13
|
+
import { FormSubmitFooter } from "./FormSubmitFooter";
|
|
14
|
+
import { FORM_THEME_ROLES } from "./formThemeRoles";
|
|
24
15
|
import type { FormProps } from "./types";
|
|
25
16
|
import { useResolvedThemeColor } from "./useResolvedThemeColor";
|
|
26
|
-
import { withAlpha } from "./withAlpha";
|
|
27
17
|
|
|
28
|
-
/** How far the header wash continues below the buttons. */
|
|
29
|
-
const FADE_TAIL = 32;
|
|
30
|
-
|
|
31
|
-
function FormErrorBanner({
|
|
32
|
-
open,
|
|
33
|
-
errors,
|
|
34
|
-
onDismiss,
|
|
35
|
-
onHeightChange,
|
|
36
|
-
}: {
|
|
37
|
-
open: boolean;
|
|
38
|
-
errors: readonly FieldError[];
|
|
39
|
-
onDismiss: () => void;
|
|
40
|
-
onHeightChange: (height: number) => void;
|
|
41
|
-
}) {
|
|
42
|
-
const ctx = useErrorBannerContext();
|
|
43
|
-
return (
|
|
44
|
-
<ErrorBanner
|
|
45
|
-
errors={errors}
|
|
46
|
-
onDismiss={onDismiss}
|
|
47
|
-
onHeightChange={onHeightChange}
|
|
48
|
-
onJump={(field) => ctx?.jump(field)}
|
|
49
|
-
open={open}
|
|
50
|
-
/>
|
|
51
|
-
);
|
|
52
|
-
}
|
|
53
18
|
|
|
54
19
|
/** Space kept between the focused field and the top of the footer. */
|
|
55
20
|
const FIELD_GAP = 12;
|
|
@@ -60,55 +25,79 @@ const FIELD_GAP = 12;
|
|
|
60
25
|
*/
|
|
61
26
|
const FIELD_BELOW_CARET = 36;
|
|
62
27
|
|
|
28
|
+
/**
|
|
29
|
+
* A full-screen form under the native stack header. Set the title and the
|
|
30
|
+
* close button in the screen's stack options. The scroll view is the
|
|
31
|
+
* screen's first child, so iOS insets it for the header (including a large
|
|
32
|
+
* title) and content scrolls behind the bar. Render `Form` as the screen's
|
|
33
|
+
* root element. For a wash behind a transparent header, use `HeaderWash` as
|
|
34
|
+
* the stack's `headerBackground`.
|
|
35
|
+
*/
|
|
63
36
|
export function Form({
|
|
64
|
-
title,
|
|
65
37
|
children,
|
|
66
|
-
onClose,
|
|
67
38
|
onSubmit,
|
|
68
39
|
errors = [],
|
|
69
40
|
submitLabel = "Save",
|
|
70
41
|
submitting = false,
|
|
71
42
|
submitDisabled = false,
|
|
72
|
-
closeButton,
|
|
73
43
|
submitRadius = "lg",
|
|
74
|
-
|
|
44
|
+
headerHeight: headerHeightProp,
|
|
45
|
+
paddingHorizontal: paddingHorizontalProp,
|
|
75
46
|
backgroundColor: backgroundColorProp,
|
|
76
|
-
titleColor: titleColorProp,
|
|
77
47
|
footerBackgroundColor: footerBackgroundColorProp,
|
|
78
|
-
buttonBackgroundColor
|
|
79
|
-
buttonLabelColor
|
|
48
|
+
buttonBackgroundColor,
|
|
49
|
+
buttonLabelColor,
|
|
80
50
|
}: FormProps) {
|
|
51
|
+
const spacing = useThemeSpacing();
|
|
81
52
|
const insets = useSafeAreaInsets();
|
|
82
|
-
const
|
|
83
|
-
const
|
|
53
|
+
const paddingHorizontal = paddingHorizontalProp ?? spacing.screen;
|
|
54
|
+
const background = useResolvedThemeColor(
|
|
55
|
+
backgroundColorProp,
|
|
56
|
+
FORM_THEME_ROLES.backgroundColor,
|
|
57
|
+
);
|
|
84
58
|
const footerBackground = useResolvedThemeColor(
|
|
85
59
|
footerBackgroundColorProp,
|
|
86
|
-
|
|
87
|
-
);
|
|
88
|
-
const buttonBackground = useResolvedThemeColor(
|
|
89
|
-
buttonBackgroundColorProp,
|
|
90
|
-
"buttonBackground",
|
|
91
|
-
);
|
|
92
|
-
const buttonLabel = useResolvedThemeColor(
|
|
93
|
-
buttonLabelColorProp,
|
|
94
|
-
"buttonLabel",
|
|
60
|
+
FORM_THEME_ROLES.footerBackgroundColor,
|
|
95
61
|
);
|
|
96
62
|
const scrollRef = useRef<KeyboardAwareScrollViewRef>(null);
|
|
97
63
|
const bannerHeightRef = useRef(0);
|
|
64
|
+
const scrollTopRef = useRef(0);
|
|
65
|
+
const headerHeight =
|
|
66
|
+
headerHeightProp ?? estimateHeaderHeight(Platform.OS, insets.top);
|
|
67
|
+
const headerHeightRef = useRef(headerHeight);
|
|
68
|
+
headerHeightRef.current = headerHeight;
|
|
98
69
|
const banner = useErrorBanner(errors);
|
|
99
70
|
const [footerHeight, setFooterHeight] = useState(0);
|
|
100
|
-
const [cancelWidth, setCancelWidth] = useState(0);
|
|
101
|
-
const [barHeight, setBarHeight] = useState(0);
|
|
102
|
-
const resolvedBar = barHeight || insets.top + 52;
|
|
103
71
|
|
|
104
72
|
const onFooterLayout = (event: LayoutChangeEvent) => {
|
|
105
73
|
const next = Math.round(event.nativeEvent.layout.height);
|
|
106
74
|
setFooterHeight((current) => (current === next ? current : next));
|
|
107
75
|
};
|
|
108
76
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
)
|
|
77
|
+
// Window y of the scroll view: 0 under a transparent header, the header's
|
|
78
|
+
// bottom under an opaque one.
|
|
79
|
+
const onScrollLayout = (event: LayoutChangeEvent) => {
|
|
80
|
+
(event.currentTarget as unknown as View).measureInWindow((_x, y) => {
|
|
81
|
+
scrollTopRef.current = Math.round(y);
|
|
82
|
+
});
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
// A banner row scrolls its field clear of the header and the banner.
|
|
86
|
+
const scrollYForField = useCallback((fieldY: number) => {
|
|
87
|
+
const scrollTop = scrollTopRef.current;
|
|
88
|
+
const header = headerHeightRef.current;
|
|
89
|
+
// iOS draws the banner over the window top; Android inside the screen.
|
|
90
|
+
const bannerTop = Platform.OS === "ios" ? 0 : scrollTop;
|
|
91
|
+
return formFieldScrollY({
|
|
92
|
+
fieldY,
|
|
93
|
+
scrollTop,
|
|
94
|
+
visibleTop: Math.max(scrollTop, header),
|
|
95
|
+
bannerBottom: bannerTop + bannerHeightRef.current,
|
|
96
|
+
// Under a transparent header iOS rests the content at -(header).
|
|
97
|
+
minScrollY: Math.min(0, scrollTop - header),
|
|
98
|
+
gap: ERROR_BANNER_SCROLL_GAP,
|
|
99
|
+
});
|
|
100
|
+
}, []);
|
|
112
101
|
|
|
113
102
|
return (
|
|
114
103
|
<ErrorBannerProvider
|
|
@@ -116,96 +105,42 @@ export function Form({
|
|
|
116
105
|
errors={errors}
|
|
117
106
|
onDismiss={banner.dismiss}
|
|
118
107
|
scrollRef={scrollRef}
|
|
108
|
+
scrollYForField={scrollYForField}
|
|
119
109
|
>
|
|
120
|
-
<View style={[styles.screen, { backgroundColor: background }]}>
|
|
121
110
|
<KeyboardAwareScrollView
|
|
122
111
|
ref={scrollRef}
|
|
123
112
|
automaticallyAdjustKeyboardInsets={false}
|
|
124
113
|
bottomOffset={footerHeight + FIELD_GAP + FIELD_BELOW_CARET}
|
|
125
114
|
contentContainerStyle={[
|
|
126
115
|
styles.content,
|
|
127
|
-
{
|
|
128
|
-
paddingTop: resolvedBar + FADE_TAIL,
|
|
129
|
-
paddingHorizontal,
|
|
130
|
-
},
|
|
116
|
+
{ paddingTop: spacing.pageTop, paddingHorizontal },
|
|
131
117
|
]}
|
|
132
|
-
contentInsetAdjustmentBehavior="
|
|
118
|
+
contentInsetAdjustmentBehavior="automatic"
|
|
133
119
|
keyboardDismissMode="interactive"
|
|
134
120
|
keyboardShouldPersistTaps="handled"
|
|
121
|
+
onLayout={onScrollLayout}
|
|
135
122
|
showsVerticalScrollIndicator={false}
|
|
136
|
-
style={styles.scroll}
|
|
123
|
+
style={[styles.scroll, { backgroundColor: background }]}
|
|
137
124
|
>
|
|
138
125
|
{children}
|
|
139
126
|
</KeyboardAwareScrollView>
|
|
140
127
|
|
|
141
|
-
<
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
pointerEvents="box-none"
|
|
157
|
-
style={[
|
|
158
|
-
styles.header,
|
|
159
|
-
{ paddingTop: insets.top, paddingHorizontal },
|
|
160
|
-
]}
|
|
161
|
-
>
|
|
162
|
-
<View
|
|
163
|
-
onLayout={(event) => {
|
|
164
|
-
const next = Math.round(event.nativeEvent.layout.width);
|
|
165
|
-
setCancelWidth((current) => (current === next ? current : next));
|
|
166
|
-
}}
|
|
167
|
-
>
|
|
168
|
-
{resolvedClose}
|
|
169
|
-
</View>
|
|
170
|
-
<Text
|
|
171
|
-
accessibilityRole="header"
|
|
172
|
-
numberOfLines={1}
|
|
173
|
-
pointerEvents="none"
|
|
174
|
-
style={[styles.title, { flex: 1, color: titleColor }]}
|
|
175
|
-
>
|
|
176
|
-
{title}
|
|
177
|
-
</Text>
|
|
178
|
-
<View style={{ width: cancelWidth }} />
|
|
179
|
-
</View>
|
|
180
|
-
</View>
|
|
128
|
+
<FormSubmitFooter
|
|
129
|
+
backgroundColor={footerBackground}
|
|
130
|
+
buttonBackgroundColor={buttonBackgroundColor}
|
|
131
|
+
buttonLabelColor={buttonLabelColor}
|
|
132
|
+
disabled={submitDisabled}
|
|
133
|
+
label={submitLabel}
|
|
134
|
+
onLayout={onFooterLayout}
|
|
135
|
+
onPress={() => {
|
|
136
|
+
banner.present();
|
|
137
|
+
onSubmit();
|
|
138
|
+
}}
|
|
139
|
+
paddingHorizontal={paddingHorizontal}
|
|
140
|
+
radius={submitRadius}
|
|
141
|
+
submitting={submitting}
|
|
142
|
+
/>
|
|
181
143
|
|
|
182
|
-
{/*
|
|
183
|
-
The spacer is the home-indicator clearance. `opened` equals that
|
|
184
|
-
spacer, so once the keyboard is up the spacer sits inside the
|
|
185
|
-
keyboard and the button rests on whatever height the keyboard is.
|
|
186
|
-
*/}
|
|
187
|
-
<KeyboardStickyView offset={{ closed: 0, opened: insets.bottom }}>
|
|
188
|
-
<View style={[styles.footerShell, { backgroundColor: footerBackground }]}>
|
|
189
|
-
<View
|
|
190
|
-
onLayout={onFooterLayout}
|
|
191
|
-
style={[styles.footer, { paddingHorizontal }]}
|
|
192
|
-
>
|
|
193
|
-
<Button
|
|
194
|
-
backgroundColor={buttonBackground}
|
|
195
|
-
disabled={submitDisabled}
|
|
196
|
-
label={submitLabel}
|
|
197
|
-
labelColor={buttonLabel}
|
|
198
|
-
loading={submitting}
|
|
199
|
-
onPress={() => {
|
|
200
|
-
banner.present();
|
|
201
|
-
onSubmit();
|
|
202
|
-
}}
|
|
203
|
-
radius={submitRadius}
|
|
204
|
-
/>
|
|
205
|
-
</View>
|
|
206
|
-
<View style={{ height: insets.bottom }} />
|
|
207
|
-
</View>
|
|
208
|
-
</KeyboardStickyView>
|
|
209
144
|
<FormErrorBanner
|
|
210
145
|
errors={errors}
|
|
211
146
|
onDismiss={banner.dismiss}
|
|
@@ -214,46 +149,15 @@ export function Form({
|
|
|
214
149
|
}}
|
|
215
150
|
open={banner.open}
|
|
216
151
|
/>
|
|
217
|
-
</View>
|
|
218
152
|
</ErrorBannerProvider>
|
|
219
153
|
);
|
|
220
154
|
}
|
|
221
155
|
|
|
222
156
|
const styles = StyleSheet.create({
|
|
223
|
-
screen: {
|
|
224
|
-
flex: 1,
|
|
225
|
-
},
|
|
226
157
|
scroll: {
|
|
227
158
|
flex: 1,
|
|
228
159
|
},
|
|
229
160
|
content: {
|
|
230
|
-
paddingTop: 20,
|
|
231
161
|
paddingBottom: 24,
|
|
232
162
|
},
|
|
233
|
-
fade: {
|
|
234
|
-
position: "absolute",
|
|
235
|
-
top: 0,
|
|
236
|
-
left: 0,
|
|
237
|
-
right: 0,
|
|
238
|
-
},
|
|
239
|
-
header: {
|
|
240
|
-
flexDirection: "row",
|
|
241
|
-
alignItems: "center",
|
|
242
|
-
},
|
|
243
|
-
title: {
|
|
244
|
-
fontSize: 17,
|
|
245
|
-
fontWeight: "600",
|
|
246
|
-
textAlign: "center",
|
|
247
|
-
},
|
|
248
|
-
footerShell: {
|
|
249
|
-
// Cast upward so the sticky footer separates from the scroll body.
|
|
250
|
-
shadowColor: "#000",
|
|
251
|
-
shadowOffset: { width: 0, height: -4 },
|
|
252
|
-
shadowOpacity: 0.08,
|
|
253
|
-
shadowRadius: 12,
|
|
254
|
-
elevation: 8,
|
|
255
|
-
},
|
|
256
|
-
footer: {
|
|
257
|
-
paddingVertical: 12,
|
|
258
|
-
},
|
|
259
163
|
});
|
|
@@ -1,12 +1,20 @@
|
|
|
1
1
|
import { Pressable, StyleSheet, Text } from "react-native";
|
|
2
|
-
import {
|
|
2
|
+
import { useResolvedFieldColor } from "../Field/useResolvedFieldColor";
|
|
3
|
+
import { FORM_THEME_ROLES } from "./formThemeRoles";
|
|
3
4
|
|
|
4
|
-
export function FormCloseButton({
|
|
5
|
-
|
|
5
|
+
export function FormCloseButton({
|
|
6
|
+
onPress,
|
|
7
|
+
color: colorProp,
|
|
8
|
+
}: {
|
|
9
|
+
onPress: () => void;
|
|
10
|
+
/** Defaults to theme role `formClose`. */
|
|
11
|
+
color?: string;
|
|
12
|
+
}) {
|
|
13
|
+
const color = useResolvedFieldColor(colorProp, FORM_THEME_ROLES.closeColor);
|
|
6
14
|
|
|
7
15
|
return (
|
|
8
16
|
<Pressable accessibilityRole="button" hitSlop={8} onPress={onPress}>
|
|
9
|
-
<Text style={[styles.label, { color
|
|
17
|
+
<Text style={[styles.label, { color }]}>Cancel</Text>
|
|
10
18
|
</Pressable>
|
|
11
19
|
);
|
|
12
20
|
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { Platform, StyleSheet, View } from "react-native";
|
|
2
|
+
import { FullWindowOverlay } from "react-native-screens";
|
|
3
|
+
import {
|
|
4
|
+
ErrorBanner,
|
|
5
|
+
useErrorBannerContext,
|
|
6
|
+
type FieldError,
|
|
7
|
+
} from "../ErrorBanner";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The screen banner wired to the nearest `ErrorBannerProvider`. Shared by
|
|
11
|
+
* `Form` and `FormSheet`. On iOS it renders in a native overlay window so it
|
|
12
|
+
* covers the native header. Android native headers do not overlap the
|
|
13
|
+
* screen, so there it sits `androidTop` below the screen top, under the header.
|
|
14
|
+
*/
|
|
15
|
+
export function FormErrorBanner({
|
|
16
|
+
open,
|
|
17
|
+
errors,
|
|
18
|
+
onDismiss,
|
|
19
|
+
onHeightChange,
|
|
20
|
+
androidTop = 0,
|
|
21
|
+
}: {
|
|
22
|
+
open: boolean;
|
|
23
|
+
errors: readonly FieldError[];
|
|
24
|
+
onDismiss: () => void;
|
|
25
|
+
onHeightChange: (height: number) => void;
|
|
26
|
+
androidTop?: number;
|
|
27
|
+
}) {
|
|
28
|
+
const ctx = useErrorBannerContext();
|
|
29
|
+
const banner = (
|
|
30
|
+
<ErrorBanner
|
|
31
|
+
errors={errors}
|
|
32
|
+
onDismiss={onDismiss}
|
|
33
|
+
onHeightChange={onHeightChange}
|
|
34
|
+
onJump={(field) => ctx?.jump(field)}
|
|
35
|
+
open={open}
|
|
36
|
+
safeAreaTop={Platform.OS === "ios" ? undefined : 0}
|
|
37
|
+
/>
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
if (Platform.OS === "ios") {
|
|
41
|
+
return <FullWindowOverlay>{banner}</FullWindowOverlay>;
|
|
42
|
+
}
|
|
43
|
+
return (
|
|
44
|
+
<View pointerEvents="box-none" style={[styles.below, { top: androidTop }]}>
|
|
45
|
+
{banner}
|
|
46
|
+
</View>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const styles = StyleSheet.create({
|
|
51
|
+
below: {
|
|
52
|
+
position: "absolute",
|
|
53
|
+
left: 0,
|
|
54
|
+
right: 0,
|
|
55
|
+
bottom: 0,
|
|
56
|
+
},
|
|
57
|
+
});
|