@multiplatform.one/forms 7.7.0 → 7.7.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/dist/cjs/Button/index.cjs +1 -1
- package/dist/cjs/Button/index.native.js +6 -1
- package/dist/cjs/Button/index.native.js.map +1 -1
- package/dist/cjs/DescriptionList/index.cjs +3 -1
- package/dist/cjs/DescriptionList/index.native.js +5 -1
- package/dist/cjs/DescriptionList/index.native.js.map +1 -1
- package/dist/cjs/fields/FileUpload/index.cjs +0 -1
- package/dist/cjs/fields/FileUpload/index.native.js +5 -1
- package/dist/cjs/fields/FileUpload/index.native.js.map +1 -1
- package/dist/cjs/fields/Select/index.cjs +14 -1
- package/dist/cjs/fields/Select/index.native.js +14 -1
- package/dist/cjs/fields/Select/index.native.js.map +1 -1
- package/dist/esm/Button/index.mjs +1 -1
- package/dist/esm/Button/index.mjs.map +1 -1
- package/dist/esm/Button/index.native.js +6 -1
- package/dist/esm/Button/index.native.js.map +1 -1
- package/dist/esm/DescriptionList/index.mjs +3 -1
- package/dist/esm/DescriptionList/index.mjs.map +1 -1
- package/dist/esm/DescriptionList/index.native.js +5 -1
- package/dist/esm/DescriptionList/index.native.js.map +1 -1
- package/dist/esm/fields/FileUpload/index.mjs +0 -1
- package/dist/esm/fields/FileUpload/index.mjs.map +1 -1
- package/dist/esm/fields/FileUpload/index.native.js +5 -1
- package/dist/esm/fields/FileUpload/index.native.js.map +1 -1
- package/dist/esm/fields/Select/index.mjs +4 -2
- package/dist/esm/fields/Select/index.mjs.map +1 -1
- package/dist/esm/fields/Select/index.native.js +4 -2
- package/dist/esm/fields/Select/index.native.js.map +1 -1
- package/dist/jsx/AdaptivePopup/index.native.js +1 -1
- package/dist/jsx/AdaptivePopup/index.native.js.map +1 -1
- package/dist/jsx/Button/index.mjs +1 -1
- package/dist/jsx/Button/index.mjs.map +1 -1
- package/dist/jsx/Button/index.native.js +7 -2
- package/dist/jsx/Button/index.native.js.map +1 -1
- package/dist/jsx/Chip/index.native.js.map +1 -1
- package/dist/jsx/ContextualSaveBar/UnsavedChangesDialog.native.js +1 -1
- package/dist/jsx/ContextualSaveBar/UnsavedChangesDialog.native.js.map +1 -1
- package/dist/jsx/ContextualSaveBar/clientKnownIdentity.native.js.map +1 -1
- package/dist/jsx/ContextualSaveBar/commitImmediateField.native.js.map +1 -1
- package/dist/jsx/ContextualSaveBar/index.native.js +3 -3
- package/dist/jsx/ContextualSaveBar/index.native.js.map +1 -1
- package/dist/jsx/ContextualSaveBar/useUnsavedChangesGuard.native.js.map +1 -1
- package/dist/jsx/ControlGroup/index.native.js +1 -1
- package/dist/jsx/ControlGroup/index.native.js.map +1 -1
- package/dist/jsx/DescriptionList/index.mjs +3 -1
- package/dist/jsx/DescriptionList/index.mjs.map +1 -1
- package/dist/jsx/DescriptionList/index.native.js +7 -3
- package/dist/jsx/DescriptionList/index.native.js.map +1 -1
- package/dist/jsx/ErrorSummary.native.js.map +1 -1
- package/dist/jsx/FieldDisplay/editMode.native.js.map +1 -1
- package/dist/jsx/FieldDisplay/formatters.native.js.map +1 -1
- package/dist/jsx/FieldDisplay/index.native.js +1 -1
- package/dist/jsx/FieldDisplay/index.native.js.map +1 -1
- package/dist/jsx/FieldGroup.native.js.map +1 -1
- package/dist/jsx/FloatingPanel/useFloatingPanel.native.js.map +1 -1
- package/dist/jsx/Form/index.native.js +1 -1
- package/dist/jsx/Form/index.native.js.map +1 -1
- package/dist/jsx/Form/validateOn.native.js.map +1 -1
- package/dist/jsx/FormGrid.native.js.map +1 -1
- package/dist/jsx/FormSection.native.js.map +1 -1
- package/dist/jsx/InputParts/index.native.js.map +1 -1
- package/dist/jsx/Label/index.native.js.map +1 -1
- package/dist/jsx/Skeleton/index.native.js.map +1 -1
- package/dist/jsx/Spinner/SpinnerGlyph.native.js.map +1 -1
- package/dist/jsx/Spinner/spinnerSize.native.js.map +1 -1
- package/dist/jsx/Table/index.native.js.map +1 -1
- package/dist/jsx/Wheel/haptic.native.js.map +1 -1
- package/dist/jsx/Wheel/index.native.js.map +1 -1
- package/dist/jsx/actionLabel.native.js.map +1 -1
- package/dist/jsx/fieldDefaults.native.js.map +1 -1
- package/dist/jsx/fieldLayout.native.js +1 -1
- package/dist/jsx/fieldLayout.native.js.map +1 -1
- package/dist/jsx/fields/Barcode/BarcodePreview.native.js.map +1 -1
- package/dist/jsx/fields/Barcode/BarcodeScanner.native.js +1 -1
- package/dist/jsx/fields/Barcode/code128.native.js.map +1 -1
- package/dist/jsx/fields/Barcode/index.native.js +2 -2
- package/dist/jsx/fields/Barcode/index.native.js.map +1 -1
- package/dist/jsx/fields/Barcode/qr.native.js.map +1 -1
- package/dist/jsx/fields/Barcode/scannerTypes.native.js.map +1 -1
- package/dist/jsx/fields/Checkbox/Checkbox.native.js +1 -1
- package/dist/jsx/fields/Checkbox/Checkbox.native.js.map +1 -1
- package/dist/jsx/fields/Checkbox/CheckboxBox.native.js.map +1 -1
- package/dist/jsx/fields/Checkbox/CheckboxCard.native.js.map +1 -1
- package/dist/jsx/fields/Checkbox/CheckboxFieldFrame.native.js.map +1 -1
- package/dist/jsx/fields/Checkbox/CheckboxGroup.native.js +1 -1
- package/dist/jsx/fields/Checkbox/CheckboxGroup.native.js.map +1 -1
- package/dist/jsx/fields/Checkbox/checkboxGroupContext.native.js.map +1 -1
- package/dist/jsx/fields/Checkbox/index.native.js.map +1 -1
- package/dist/jsx/fields/ColorPicker/index.native.js +3 -3
- package/dist/jsx/fields/ColorPicker/index.native.js.map +1 -1
- package/dist/jsx/fields/ColorPicker/parts.native.js +2 -2
- package/dist/jsx/fields/ColorPicker/parts.native.js.map +1 -1
- package/dist/jsx/fields/Combobox/authorOptions.native.js.map +1 -1
- package/dist/jsx/fields/Combobox/index.native.js +5 -5
- package/dist/jsx/fields/Combobox/index.native.js.map +1 -1
- package/dist/jsx/fields/DatePicker/Calendar.native.js.map +1 -1
- package/dist/jsx/fields/DatePicker/DateRangePicker.native.js +2 -2
- package/dist/jsx/fields/DatePicker/DateRangePicker.native.js.map +1 -1
- package/dist/jsx/fields/DatePicker/DatetimePicker.catalog.native.js +3 -3
- package/dist/jsx/fields/DatePicker/DatetimePicker.catalog.native.js.map +1 -1
- package/dist/jsx/fields/DatePicker/DatetimePicker.native.js +2 -2
- package/dist/jsx/fields/DatePicker/MonthPicker.native.js +2 -2
- package/dist/jsx/fields/DatePicker/MonthPicker.native.js.map +1 -1
- package/dist/jsx/fields/DatePicker/MultiDatePicker.native.js +2 -2
- package/dist/jsx/fields/DatePicker/MultiDatePicker.native.js.map +1 -1
- package/dist/jsx/fields/DatePicker/catalog.native.js +2 -2
- package/dist/jsx/fields/DatePicker/catalog.native.js.map +1 -1
- package/dist/jsx/fields/DatePicker/fieldValueSync.native.js.map +1 -1
- package/dist/jsx/fields/DatePicker/index.native.js +2 -2
- package/dist/jsx/fields/DatePicker/parts.native.js +2 -2
- package/dist/jsx/fields/DatePicker/parts.native.js.map +1 -1
- package/dist/jsx/fields/DatePicker/utils.native.js.map +1 -1
- package/dist/jsx/fields/Duration/durationMath.native.js.map +1 -1
- package/dist/jsx/fields/Duration/index.native.js +3 -3
- package/dist/jsx/fields/Duration/index.native.js.map +1 -1
- package/dist/jsx/fields/FileUpload/filePickerTypes.native.js.map +1 -1
- package/dist/jsx/fields/FileUpload/index.mjs +0 -1
- package/dist/jsx/fields/FileUpload/index.mjs.map +1 -1
- package/dist/jsx/fields/FileUpload/index.native.js +7 -3
- package/dist/jsx/fields/FileUpload/index.native.js.map +1 -1
- package/dist/jsx/fields/FileUpload/nativePickerLoader.native.js.map +1 -1
- package/dist/jsx/fields/Geolocation/expoMapsLoader.native.js.map +1 -1
- package/dist/jsx/fields/Geolocation/geoValue.native.js.map +1 -1
- package/dist/jsx/fields/Geolocation/index.native.js +5 -5
- package/dist/jsx/fields/Geolocation/index.native.js.map +1 -1
- package/dist/jsx/fields/Geolocation/mapMath.native.js.map +1 -1
- package/dist/jsx/fields/Geolocation/mapTypes.native.js.map +1 -1
- package/dist/jsx/fields/Icon/drawnSet.native.js.map +1 -1
- package/dist/jsx/fields/Icon/index.native.js +3 -3
- package/dist/jsx/fields/Icon/index.native.js.map +1 -1
- package/dist/jsx/fields/ImageField/index.native.js +2 -2
- package/dist/jsx/fields/ImageField/index.native.js.map +1 -1
- package/dist/jsx/fields/Input/index.native.js +2 -2
- package/dist/jsx/fields/Input/index.native.js.map +1 -1
- package/dist/jsx/fields/MentionInput/index.native.js +3 -3
- package/dist/jsx/fields/MentionInput/index.native.js.map +1 -1
- package/dist/jsx/fields/OTPInput/index.native.js +3 -3
- package/dist/jsx/fields/OTPInput/index.native.js.map +1 -1
- package/dist/jsx/fields/OTPInput/otpCellCss.native.js.map +1 -1
- package/dist/jsx/fields/PhoneInput/dialCodes.native.js.map +1 -1
- package/dist/jsx/fields/PhoneInput/index.native.js +3 -3
- package/dist/jsx/fields/PhoneInput/index.native.js.map +1 -1
- package/dist/jsx/fields/PhoneInput/phone.native.js.map +1 -1
- package/dist/jsx/fields/Progress/index.native.js +1 -1
- package/dist/jsx/fields/Progress/index.native.js.map +1 -1
- package/dist/jsx/fields/RadioGroup/index.native.js +1 -1
- package/dist/jsx/fields/RadioGroup/index.native.js.map +1 -1
- package/dist/jsx/fields/Rating/index.native.js +1 -1
- package/dist/jsx/fields/Rating/index.native.js.map +1 -1
- package/dist/jsx/fields/Rating/ratingMath.native.js.map +1 -1
- package/dist/jsx/fields/Rating/types.native.js.map +1 -1
- package/dist/jsx/fields/SearchInput/index.native.js +2 -2
- package/dist/jsx/fields/SearchInput/index.native.js.map +1 -1
- package/dist/jsx/fields/Select/index.mjs +4 -2
- package/dist/jsx/fields/Select/index.mjs.map +1 -1
- package/dist/jsx/fields/Select/index.native.js +18 -5
- package/dist/jsx/fields/Select/index.native.js.map +1 -1
- package/dist/jsx/fields/Signature/index.native.js +2 -2
- package/dist/jsx/fields/Signature/index.native.js.map +1 -1
- package/dist/jsx/fields/Slider/index.native.js +1 -1
- package/dist/jsx/fields/Slider/index.native.js.map +1 -1
- package/dist/jsx/fields/Slider/jumpMotion.native.js.map +1 -1
- package/dist/jsx/fields/Stepper/index.native.js +2 -2
- package/dist/jsx/fields/Stepper/index.native.js.map +1 -1
- package/dist/jsx/fields/Stepper/resolveStepperPlacement.native.js.map +1 -1
- package/dist/jsx/fields/Switch/index.native.js +2 -2
- package/dist/jsx/fields/Switch/index.native.js.map +1 -1
- package/dist/jsx/fields/TextArea/index.native.js +2 -2
- package/dist/jsx/fields/TextArea/index.native.js.map +1 -1
- package/dist/jsx/fields/TimePicker/catalog.native.js +3 -3
- package/dist/jsx/fields/TimePicker/catalog.native.js.map +1 -1
- package/dist/jsx/fields/TimePicker/index.native.js +2 -2
- package/dist/jsx/fields/TimePicker/parts.native.js +1 -1
- package/dist/jsx/fields/TimePicker/parts.native.js.map +1 -1
- package/dist/jsx/fields/TimeRangeScrubber/geometry.native.js.map +1 -1
- package/dist/jsx/fields/TimeRangeScrubber/index.native.js +2 -2
- package/dist/jsx/fields/TimeRangeScrubber/index.native.js.map +1 -1
- package/dist/jsx/fields/ToggleGroup/index.native.js +1 -1
- package/dist/jsx/fields/ToggleGroup/index.native.js.map +1 -1
- package/dist/jsx/fields/nativeDateTime/OsDateTimeSheet.native.js.map +1 -1
- package/dist/jsx/fields/nativeDateTime/osDateTimeIosDisplay.native.js.map +1 -1
- package/dist/jsx/fields/nativeSelect.native.js.map +1 -1
- package/dist/jsx/fields/selectedOrder.native.js.map +1 -1
- package/dist/jsx/formSpacing.native.js.map +1 -1
- package/dist/jsx/hooks/useFocusManagement.native.js.map +1 -1
- package/dist/jsx/index.native.js +43 -43
- package/dist/jsx/index.native.js.map +1 -1
- package/dist/jsx/requiredMarking.native.js.map +1 -1
- package/dist/jsx/shared/TimeColumn.native.js +1 -1
- package/dist/jsx/shared/TimeColumn.native.js.map +1 -1
- package/dist/jsx/shared/capabilityPreflight.native.js.map +1 -1
- package/dist/jsx/shared/colorRamps.native.js.map +1 -1
- package/dist/jsx/shared/devWarn.native.js.map +1 -1
- package/dist/jsx/shared/floatingList.native.js +1 -1
- package/dist/jsx/shared/floatingList.native.js.map +1 -1
- package/dist/jsx/shared/focusManagement.native.js.map +1 -1
- package/dist/jsx/shared/groupContext.native.js.map +1 -1
- package/dist/jsx/shared/i18n.native.js.map +1 -1
- package/dist/jsx/shared/t.native.js.map +1 -1
- package/dist/jsx/shared/tableCellContext.native.js.map +1 -1
- package/dist/jsx/shared/useDragScroll.native.js.map +1 -1
- package/dist/jsx/shared/useKeyboardInset.native.js.map +1 -1
- package/dist/jsx/shared/useSheetEscapeDismiss.native.js.map +1 -1
- package/dist/jsx/shared/useWheelIncrement.native.js.map +1 -1
- package/dist/jsx/shared/utils.native.js +1 -1
- package/dist/jsx/shared/utils.native.js.map +1 -1
- package/dist/jsx/shared/zIndex.native.js.map +1 -1
- package/dist/jsx/themedPrimitives.native.js.map +1 -1
- package/dist/jsx/types.native.js.map +1 -1
- package/dist/jsx/utils/LinkCell.native.js.map +1 -1
- package/package.json +7 -7
- package/src/Button/index.tsx +6 -1
- package/src/ContextualSaveBar/UnsavedChangesDialog.stories.tsx +118 -0
- package/src/ContextualSaveBar/unsavedChangesDialogStories.spec.tsx +79 -0
- package/src/DescriptionList/index.tsx +3 -1
- package/src/ErrorSummary.stories.tsx +197 -0
- package/src/InputParts/InputParts.stories.tsx +230 -0
- package/src/InputParts/inputPartsStories.spec.tsx +122 -0
- package/src/Skeleton/Skeleton.stories.tsx +148 -0
- package/src/Skeleton/skeletonStories.spec.tsx +107 -0
- package/src/Wheel/Wheel.stories.tsx +232 -0
- package/src/Wheel/wheelStories.spec.tsx +143 -0
- package/src/errorSummaryStories.spec.tsx +114 -0
- package/src/fields/FileUpload/index.tsx +5 -1
- package/src/fields/Input/Input.spec.tsx +3 -1
- package/src/fields/Select/index.tsx +12 -2
- package/types/Button/index.d.ts.map +1 -1
- package/types/DescriptionList/index.d.ts.map +1 -1
- package/types/fields/FileUpload/index.d.ts.map +1 -1
- package/types/fields/Select/index.d.ts.map +1 -1
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { action } from "@multiplatform.one/storybook";
|
|
2
|
+
import type { Meta, StoryObj } from "@storybook/react-native-web-vite";
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { Paragraph, YStack } from "tamagui";
|
|
5
|
+
import { Button } from "../Button";
|
|
6
|
+
import { UnsavedChangesDialog } from "./UnsavedChangesDialog";
|
|
7
|
+
|
|
8
|
+
const meta: Meta<typeof UnsavedChangesDialog> = {
|
|
9
|
+
title: "Forms/ContextualSaveBar/UnsavedChangesDialog",
|
|
10
|
+
component: UnsavedChangesDialog,
|
|
11
|
+
parameters: {
|
|
12
|
+
status: { type: "beta" },
|
|
13
|
+
docs: {
|
|
14
|
+
description: {
|
|
15
|
+
component:
|
|
16
|
+
"The route-leave confirm ContextualSaveBar raises (DG-OVL-02) when a navigation would throw away edits. It is an `alertdialog`, it cannot be dismissed by clicking the scrim, and the destructive choice is the one on the right — so nobody discards work by reflex. It lives in forms rather than components because ConfirmDialog would import the forms Button and cycle.",
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
argTypes: {
|
|
21
|
+
open: { control: "boolean" },
|
|
22
|
+
fieldCount: { control: { type: "number", min: 0, step: 1 } },
|
|
23
|
+
},
|
|
24
|
+
args: {
|
|
25
|
+
open: true,
|
|
26
|
+
fieldCount: 3,
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export default meta;
|
|
31
|
+
type Story = StoryObj<typeof UnsavedChangesDialog>;
|
|
32
|
+
|
|
33
|
+
export const Basic: Story = {
|
|
34
|
+
render: (args) => (
|
|
35
|
+
<UnsavedChangesDialog
|
|
36
|
+
{...args}
|
|
37
|
+
onKeepEditing={action("onKeepEditing")}
|
|
38
|
+
onDiscard={action("onDiscard")}
|
|
39
|
+
/>
|
|
40
|
+
),
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* The body pluralises: one field reads "1 field ... has edits", more than one
|
|
45
|
+
* reads "N fields ... have edits". Getting this wrong is the tell that a count
|
|
46
|
+
* was interpolated into a fixed string.
|
|
47
|
+
*/
|
|
48
|
+
export const SingleField: Story = {
|
|
49
|
+
render: () => (
|
|
50
|
+
<UnsavedChangesDialog
|
|
51
|
+
open
|
|
52
|
+
fieldCount={1}
|
|
53
|
+
onKeepEditing={action("onKeepEditing")}
|
|
54
|
+
onDiscard={action("onDiscard")}
|
|
55
|
+
/>
|
|
56
|
+
),
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export const ManyFields: Story = {
|
|
60
|
+
render: () => (
|
|
61
|
+
<UnsavedChangesDialog
|
|
62
|
+
open
|
|
63
|
+
fieldCount={14}
|
|
64
|
+
onKeepEditing={action("onKeepEditing")}
|
|
65
|
+
onDiscard={action("onDiscard")}
|
|
66
|
+
/>
|
|
67
|
+
),
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
/** Closed is a real state: it renders no scrim and nothing takes focus. */
|
|
71
|
+
export const Closed: Story = {
|
|
72
|
+
render: () => (
|
|
73
|
+
<YStack gap="$3" maxWidth={420}>
|
|
74
|
+
<Paragraph size="$3">Nothing should paint below this line.</Paragraph>
|
|
75
|
+
<UnsavedChangesDialog
|
|
76
|
+
open={false}
|
|
77
|
+
fieldCount={3}
|
|
78
|
+
onKeepEditing={action("onKeepEditing")}
|
|
79
|
+
onDiscard={action("onDiscard")}
|
|
80
|
+
/>
|
|
81
|
+
</YStack>
|
|
82
|
+
),
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* The live loop. "Keep editing" is the safe path and is also what Escape and
|
|
87
|
+
* `onOpenChange(false)` resolve to; "Discard changes" is the only way to lose
|
|
88
|
+
* the edits, and it is the destructive-styled button.
|
|
89
|
+
*/
|
|
90
|
+
export const Interactive: Story = {
|
|
91
|
+
render: () => {
|
|
92
|
+
const Example = () => {
|
|
93
|
+
const [open, setOpen] = useState(false);
|
|
94
|
+
const [outcome, setOutcome] = useState<string>("nothing yet");
|
|
95
|
+
return (
|
|
96
|
+
<YStack gap="$4" maxWidth={420} alignItems="flex-start">
|
|
97
|
+
<Button onPress={() => setOpen(true)}>Leave the page</Button>
|
|
98
|
+
<Paragraph size="$3">last outcome: {outcome}</Paragraph>
|
|
99
|
+
<UnsavedChangesDialog
|
|
100
|
+
open={open}
|
|
101
|
+
fieldCount={3}
|
|
102
|
+
onKeepEditing={() => {
|
|
103
|
+
setOpen(false);
|
|
104
|
+
setOutcome("kept editing");
|
|
105
|
+
action("onKeepEditing")();
|
|
106
|
+
}}
|
|
107
|
+
onDiscard={() => {
|
|
108
|
+
setOpen(false);
|
|
109
|
+
setOutcome("discarded");
|
|
110
|
+
action("onDiscard")();
|
|
111
|
+
}}
|
|
112
|
+
/>
|
|
113
|
+
</YStack>
|
|
114
|
+
);
|
|
115
|
+
};
|
|
116
|
+
return <Example />;
|
|
117
|
+
},
|
|
118
|
+
};
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mounts every Forms/ContextualSaveBar/UnsavedChangesDialog story and asserts
|
|
3
|
+
* what it exists to show — chiefly the copy pluralisation and that the safe
|
|
4
|
+
* choice, not the destructive one, is what a dismissal resolves to.
|
|
5
|
+
*
|
|
6
|
+
* Only one open dialog per test: Dialog.Portal mounts to the document body, so
|
|
7
|
+
* two open at once make every text query ambiguous.
|
|
8
|
+
*/
|
|
9
|
+
import { cleanup, fireEvent, screen } from "@testing-library/react";
|
|
10
|
+
import { renderWithProviders } from "@multiplatform.one/test-utils";
|
|
11
|
+
import type { ReactElement } from "react";
|
|
12
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
13
|
+
|
|
14
|
+
import * as storiesModule from "./UnsavedChangesDialog.stories";
|
|
15
|
+
|
|
16
|
+
afterEach(cleanup);
|
|
17
|
+
|
|
18
|
+
const meta = storiesModule.default;
|
|
19
|
+
const { Basic, SingleField, ManyFields, Closed, Interactive } = storiesModule;
|
|
20
|
+
|
|
21
|
+
type StoryLike = { render?: (args: Record<string, unknown>) => ReactElement };
|
|
22
|
+
|
|
23
|
+
function mount(story: StoryLike) {
|
|
24
|
+
if (!story.render) throw new Error("story has no render");
|
|
25
|
+
return renderWithProviders(story.render({ ...(meta.args as Record<string, unknown>) }));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
describe("Forms/ContextualSaveBar/UnsavedChangesDialog stories", () => {
|
|
29
|
+
it("nests under ContextualSaveBar so the gallery keeps it with the bar that raises it", () => {
|
|
30
|
+
expect(meta.title).toBe("Forms/ContextualSaveBar/UnsavedChangesDialog");
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("Basic opens an alertdialog with both choices", () => {
|
|
34
|
+
mount(Basic as StoryLike);
|
|
35
|
+
expect(screen.getByText("Discard unsaved changes?")).toBeTruthy();
|
|
36
|
+
expect(screen.getByText("Keep editing")).toBeTruthy();
|
|
37
|
+
expect(screen.getByText("Discard changes")).toBeTruthy();
|
|
38
|
+
expect(document.querySelector("[role='alertdialog']")).toBeTruthy();
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("SingleField uses the singular sentence", () => {
|
|
42
|
+
mount(SingleField as StoryLike);
|
|
43
|
+
expect(
|
|
44
|
+
screen.getByText("1 field on this page has edits that aren't saved. Leaving discards them."),
|
|
45
|
+
).toBeTruthy();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("ManyFields uses the plural sentence with the count interpolated", () => {
|
|
49
|
+
mount(ManyFields as StoryLike);
|
|
50
|
+
expect(
|
|
51
|
+
screen.getByText(
|
|
52
|
+
"14 fields on this page have edits that aren't saved. Leaving discards them.",
|
|
53
|
+
),
|
|
54
|
+
).toBeTruthy();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("Closed paints no dialog at all", () => {
|
|
58
|
+
const { getByText } = mount(Closed as StoryLike);
|
|
59
|
+
expect(getByText("Nothing should paint below this line.")).toBeTruthy();
|
|
60
|
+
expect(document.querySelector("[role='alertdialog']")).toBeNull();
|
|
61
|
+
expect(screen.queryByText("Discard unsaved changes?")).toBeNull();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("Interactive opens on the trigger and closes on Keep editing", () => {
|
|
65
|
+
const { getByText } = mount(Interactive as StoryLike);
|
|
66
|
+
expect(screen.queryByText("Discard unsaved changes?")).toBeNull();
|
|
67
|
+
fireEvent.click(getByText("Leave the page"));
|
|
68
|
+
expect(screen.getByText("Discard unsaved changes?")).toBeTruthy();
|
|
69
|
+
fireEvent.click(screen.getByText("Keep editing"));
|
|
70
|
+
expect(getByText("last outcome: kept editing")).toBeTruthy();
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("Interactive records a discard as a distinct outcome from keeping", () => {
|
|
74
|
+
const { getByText } = mount(Interactive as StoryLike);
|
|
75
|
+
fireEvent.click(getByText("Leave the page"));
|
|
76
|
+
fireEvent.click(screen.getByText("Discard changes"));
|
|
77
|
+
expect(getByText("last outcome: discarded")).toBeTruthy();
|
|
78
|
+
});
|
|
79
|
+
});
|
|
@@ -161,7 +161,9 @@ function TermText({ children }: { children: ReactNode }) {
|
|
|
161
161
|
{...knobProps.body}
|
|
162
162
|
color="$color11"
|
|
163
163
|
minWidth={0}
|
|
164
|
-
|
|
164
|
+
// `overflowWrap` is not a tamagui style prop; passed as one it reaches
|
|
165
|
+
// the DOM host as an unknown React prop. Web takes it through `style`.
|
|
166
|
+
{...(isWeb ? { style: { overflowWrap: "break-word" } } : undefined)}
|
|
165
167
|
>
|
|
166
168
|
{children}
|
|
167
169
|
</Text>
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { action } from "@multiplatform.one/storybook";
|
|
2
|
+
import type { Meta, StoryObj } from "@storybook/react-native-web-vite";
|
|
3
|
+
import { useForm } from "@tanstack/react-form";
|
|
4
|
+
import { useState } from "react";
|
|
5
|
+
import { Input as TamaguiInput, Paragraph, YStack } from "tamagui";
|
|
6
|
+
import { Button } from "./Button";
|
|
7
|
+
import { ErrorSummary, collectFormFieldErrors } from "./ErrorSummary";
|
|
8
|
+
import { FieldLayout } from "./fieldLayout";
|
|
9
|
+
import { Form } from "./Form";
|
|
10
|
+
import { FormSection } from "./FormSection";
|
|
11
|
+
|
|
12
|
+
const meta: Meta<typeof ErrorSummary> = {
|
|
13
|
+
title: "Forms/ErrorSummary",
|
|
14
|
+
component: ErrorSummary,
|
|
15
|
+
parameters: {
|
|
16
|
+
status: { type: "beta" },
|
|
17
|
+
docs: {
|
|
18
|
+
description: {
|
|
19
|
+
component:
|
|
20
|
+
'Form-level error summary (DG-VAL-02), the GOV.UK pattern. It lists every field error as a link that focuses the offending control, carries `role="alert"`, and takes keyboard focus itself the moment it appears — so a submit failure is announced once and the keyboard lands somewhere useful instead of at the top of the document.',
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
argTypes: {
|
|
25
|
+
title: { control: "text" },
|
|
26
|
+
disableAutoFocus: { control: "boolean" },
|
|
27
|
+
},
|
|
28
|
+
args: {
|
|
29
|
+
disableAutoFocus: true,
|
|
30
|
+
errors: [
|
|
31
|
+
{ name: "email", error: "Enter an email address in the format name@example.com" },
|
|
32
|
+
{ name: "password", error: "Enter a password" },
|
|
33
|
+
],
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export default meta;
|
|
38
|
+
type Story = StoryObj<typeof ErrorSummary>;
|
|
39
|
+
|
|
40
|
+
export const Basic: Story = {
|
|
41
|
+
render: (args) => (
|
|
42
|
+
<YStack maxWidth={520}>
|
|
43
|
+
<ErrorSummary {...args} onFocusField={action("onFocusField")} />
|
|
44
|
+
</YStack>
|
|
45
|
+
),
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Structured errors (DG-VAL-04): a validator may return `{ problem, action }`
|
|
50
|
+
* instead of a string, and the summary flattens the pair into one sentence —
|
|
51
|
+
* what is wrong, then what to do about it.
|
|
52
|
+
*/
|
|
53
|
+
export const StructuredErrors: Story = {
|
|
54
|
+
render: () => (
|
|
55
|
+
<YStack maxWidth={520}>
|
|
56
|
+
<ErrorSummary
|
|
57
|
+
disableAutoFocus
|
|
58
|
+
errors={[
|
|
59
|
+
{
|
|
60
|
+
name: "vatNumber",
|
|
61
|
+
error: {
|
|
62
|
+
problem: "That VAT number is not registered.",
|
|
63
|
+
action: "Check it against your registration certificate.",
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
name: "startDate",
|
|
68
|
+
error: { problem: "The start date is in the past." },
|
|
69
|
+
},
|
|
70
|
+
]}
|
|
71
|
+
onFocusField={action("onFocusField")}
|
|
72
|
+
/>
|
|
73
|
+
</YStack>
|
|
74
|
+
),
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
/** A custom `label` overrides the link text without touching the error payload. */
|
|
78
|
+
export const CustomLabels: Story = {
|
|
79
|
+
render: () => (
|
|
80
|
+
<YStack maxWidth={520}>
|
|
81
|
+
<ErrorSummary
|
|
82
|
+
disableAutoFocus
|
|
83
|
+
title="Check these before continuing"
|
|
84
|
+
errors={[
|
|
85
|
+
{ name: "email", label: "Email address", error: "Enter an email address" },
|
|
86
|
+
{ name: "phone", label: "Phone number", error: "Enter a phone number" },
|
|
87
|
+
]}
|
|
88
|
+
onFocusField={action("onFocusField")}
|
|
89
|
+
/>
|
|
90
|
+
</YStack>
|
|
91
|
+
),
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
/** One error, the common case after a single missed required field. */
|
|
95
|
+
export const SingleError: Story = {
|
|
96
|
+
render: () => (
|
|
97
|
+
<YStack maxWidth={520}>
|
|
98
|
+
<ErrorSummary
|
|
99
|
+
disableAutoFocus
|
|
100
|
+
errors={[{ name: "terms", error: "You must accept the terms to continue" }]}
|
|
101
|
+
onFocusField={action("onFocusField")}
|
|
102
|
+
/>
|
|
103
|
+
</YStack>
|
|
104
|
+
),
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
/** An empty list renders nothing at all — no empty banner, no reserved space. */
|
|
108
|
+
export const EmptyRendersNothing: Story = {
|
|
109
|
+
render: () => (
|
|
110
|
+
<YStack maxWidth={520} gap="$3">
|
|
111
|
+
<Paragraph size="$3">Nothing should paint between these two lines.</Paragraph>
|
|
112
|
+
<ErrorSummary errors={[]} />
|
|
113
|
+
<Paragraph size="$3">Second line.</Paragraph>
|
|
114
|
+
</YStack>
|
|
115
|
+
),
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* The live loop: submit empty, the summary appears, and clicking a link puts
|
|
120
|
+
* the caret in the field that caused it. `collectFormFieldErrors` turns
|
|
121
|
+
* TanStack `fieldMeta` into the item list.
|
|
122
|
+
*/
|
|
123
|
+
export const FocusesTheFieldOnPress: Story = {
|
|
124
|
+
render: () => {
|
|
125
|
+
const Example = () => {
|
|
126
|
+
const [errors, setErrors] = useState<ReturnType<typeof collectFormFieldErrors>>([]);
|
|
127
|
+
const form = useForm({
|
|
128
|
+
defaultValues: { email: "", password: "" },
|
|
129
|
+
onSubmit: async ({ value }) => {
|
|
130
|
+
action("onSubmit")(value);
|
|
131
|
+
},
|
|
132
|
+
});
|
|
133
|
+
const submit = () => {
|
|
134
|
+
const next = collectFormFieldErrors({
|
|
135
|
+
email: { errors: [form.state.values.email ? undefined : "Enter an email address"] },
|
|
136
|
+
password: { errors: [form.state.values.password ? undefined : "Enter a password"] },
|
|
137
|
+
});
|
|
138
|
+
setErrors(next);
|
|
139
|
+
if (next.length === 0) void form.handleSubmit();
|
|
140
|
+
};
|
|
141
|
+
return (
|
|
142
|
+
<Form form={form}>
|
|
143
|
+
<YStack maxWidth={520} gap="$4">
|
|
144
|
+
<ErrorSummary errors={errors} disableAutoFocus onFocusField={action("onFocusField")} />
|
|
145
|
+
<FormSection label="Sign in">
|
|
146
|
+
<FieldLayout id="email" label="Email">
|
|
147
|
+
<TamaguiInput
|
|
148
|
+
id="email"
|
|
149
|
+
placeholder="you@example.com"
|
|
150
|
+
onChangeText={(text) => form.setFieldValue("email", text)}
|
|
151
|
+
/>
|
|
152
|
+
</FieldLayout>
|
|
153
|
+
<FieldLayout id="password" label="Password">
|
|
154
|
+
<TamaguiInput
|
|
155
|
+
id="password"
|
|
156
|
+
secureTextEntry
|
|
157
|
+
onChangeText={(text) => form.setFieldValue("password", text)}
|
|
158
|
+
/>
|
|
159
|
+
</FieldLayout>
|
|
160
|
+
</FormSection>
|
|
161
|
+
<Button onPress={submit}>Submit</Button>
|
|
162
|
+
</YStack>
|
|
163
|
+
</Form>
|
|
164
|
+
);
|
|
165
|
+
};
|
|
166
|
+
return <Example />;
|
|
167
|
+
},
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* `disableAutoFocus` off is the shipping default: the summary steals focus so
|
|
172
|
+
* a screen reader announces it. Toggle the control to feel the difference.
|
|
173
|
+
*/
|
|
174
|
+
export const AutoFocusOnAppear: Story = {
|
|
175
|
+
render: () => {
|
|
176
|
+
const Example = () => {
|
|
177
|
+
const [shown, setShown] = useState(false);
|
|
178
|
+
return (
|
|
179
|
+
<YStack maxWidth={520} gap="$4">
|
|
180
|
+
<Button onPress={() => setShown((prev) => !prev)}>
|
|
181
|
+
{shown ? "Hide the summary" : "Show the summary"}
|
|
182
|
+
</Button>
|
|
183
|
+
{shown ? (
|
|
184
|
+
<ErrorSummary
|
|
185
|
+
errors={[
|
|
186
|
+
{ name: "email", error: "Enter an email address" },
|
|
187
|
+
{ name: "postcode", error: "Enter a postcode" },
|
|
188
|
+
]}
|
|
189
|
+
onFocusField={action("onFocusField")}
|
|
190
|
+
/>
|
|
191
|
+
) : null}
|
|
192
|
+
</YStack>
|
|
193
|
+
);
|
|
194
|
+
};
|
|
195
|
+
return <Example />;
|
|
196
|
+
},
|
|
197
|
+
};
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import { action } from "@multiplatform.one/storybook";
|
|
2
|
+
import type { Meta, StoryObj } from "@storybook/react-native-web-vite";
|
|
3
|
+
import {
|
|
4
|
+
EnvelopeIcon,
|
|
5
|
+
EyeIcon,
|
|
6
|
+
MagnifyingGlassIcon,
|
|
7
|
+
WarningCircleIcon,
|
|
8
|
+
} from "@phosphor-icons/react";
|
|
9
|
+
import { useState } from "react";
|
|
10
|
+
import { Paragraph, YStack } from "tamagui";
|
|
11
|
+
import { Input as InputParts } from "./index";
|
|
12
|
+
|
|
13
|
+
const meta: Meta<typeof InputParts> = {
|
|
14
|
+
title: "Forms/InputParts",
|
|
15
|
+
component: InputParts,
|
|
16
|
+
parameters: {
|
|
17
|
+
status: { type: "beta" },
|
|
18
|
+
docs: {
|
|
19
|
+
description: {
|
|
20
|
+
component:
|
|
21
|
+
"The unstyled anatomy the Input field is assembled from, for consumers who need a shape the field does not offer. `InputParts.Box` is the bordered frame that owns the focus ring; `Area` is the text-entry itself and never rings; `Icon`, `Button` and `Section` are the slots either side of it; `Label`, `Info` and `Meta` are the chrome above and below. Reach for the `Input` FIELD first — this is the escape hatch.",
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export default meta;
|
|
28
|
+
type Story = StoryObj<typeof InputParts>;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* The whole anatomy at once: Label above, Box holding a leading Icon and the
|
|
32
|
+
* Area, Meta reserving the caption line underneath.
|
|
33
|
+
*/
|
|
34
|
+
export const Anatomy: Story = {
|
|
35
|
+
render: () => (
|
|
36
|
+
<InputParts width={320}>
|
|
37
|
+
<InputParts.Label htmlFor="anatomy-email">Email</InputParts.Label>
|
|
38
|
+
<InputParts.Box>
|
|
39
|
+
<InputParts.Icon adornment="leading">
|
|
40
|
+
<EnvelopeIcon />
|
|
41
|
+
</InputParts.Icon>
|
|
42
|
+
<InputParts.Area
|
|
43
|
+
id="anatomy-email"
|
|
44
|
+
placeholder="you@example.com"
|
|
45
|
+
onChangeText={action("onChangeText")}
|
|
46
|
+
/>
|
|
47
|
+
</InputParts.Box>
|
|
48
|
+
<InputParts.Meta helperText="We only use this to send receipts." />
|
|
49
|
+
</InputParts>
|
|
50
|
+
),
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* RING-ON-BOX (LC-71): the Box carries the focus ring so adornments sit inside
|
|
55
|
+
* it; the Area's own outline is killed. Tab into the field and the whole frame
|
|
56
|
+
* rings once, not the text run.
|
|
57
|
+
*/
|
|
58
|
+
export const FocusRingOnTheBox: Story = {
|
|
59
|
+
render: () => (
|
|
60
|
+
<YStack gap="$4" width={320}>
|
|
61
|
+
<Paragraph size="$2">Tab in — the border box rings, the input does not.</Paragraph>
|
|
62
|
+
<InputParts>
|
|
63
|
+
<InputParts.Box>
|
|
64
|
+
<InputParts.Icon adornment="leading">
|
|
65
|
+
<MagnifyingGlassIcon />
|
|
66
|
+
</InputParts.Icon>
|
|
67
|
+
<InputParts.Area placeholder="Search" onChangeText={action("onChangeText")} />
|
|
68
|
+
</InputParts.Box>
|
|
69
|
+
</InputParts>
|
|
70
|
+
</YStack>
|
|
71
|
+
),
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
/** Leading and trailing slots. The trailing end-cap is square by size recipe. */
|
|
75
|
+
export const Adornments: Story = {
|
|
76
|
+
render: () => (
|
|
77
|
+
<YStack gap="$4" width={320}>
|
|
78
|
+
<InputParts>
|
|
79
|
+
<InputParts.Box>
|
|
80
|
+
<InputParts.Icon adornment="leading">
|
|
81
|
+
<MagnifyingGlassIcon />
|
|
82
|
+
</InputParts.Icon>
|
|
83
|
+
<InputParts.Area placeholder="Leading icon" />
|
|
84
|
+
</InputParts.Box>
|
|
85
|
+
</InputParts>
|
|
86
|
+
<InputParts>
|
|
87
|
+
<InputParts.Box>
|
|
88
|
+
<InputParts.Area placeholder="Trailing icon" />
|
|
89
|
+
<InputParts.Icon adornment="trailing">
|
|
90
|
+
<EnvelopeIcon />
|
|
91
|
+
</InputParts.Icon>
|
|
92
|
+
</InputParts.Box>
|
|
93
|
+
</InputParts>
|
|
94
|
+
<InputParts>
|
|
95
|
+
<InputParts.Box>
|
|
96
|
+
<InputParts.Icon adornment="leading">
|
|
97
|
+
<MagnifyingGlassIcon />
|
|
98
|
+
</InputParts.Icon>
|
|
99
|
+
<InputParts.Area placeholder="Both ends" />
|
|
100
|
+
<InputParts.Icon adornment="trailing">
|
|
101
|
+
<EnvelopeIcon />
|
|
102
|
+
</InputParts.Icon>
|
|
103
|
+
</InputParts.Box>
|
|
104
|
+
</InputParts>
|
|
105
|
+
</YStack>
|
|
106
|
+
),
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* `InputParts.Button` is the pressable end-cap — a reveal toggle, a clear, a
|
|
111
|
+
* copy. It squares off to the control height so the frame stays rectangular.
|
|
112
|
+
*/
|
|
113
|
+
export const TrailingButton: Story = {
|
|
114
|
+
render: () => {
|
|
115
|
+
const Example = () => {
|
|
116
|
+
const [secure, setSecure] = useState(true);
|
|
117
|
+
return (
|
|
118
|
+
<InputParts width={320}>
|
|
119
|
+
<InputParts.Label htmlFor="parts-password">Password</InputParts.Label>
|
|
120
|
+
<InputParts.Box paddingInlineEnd={0}>
|
|
121
|
+
<InputParts.Area
|
|
122
|
+
id="parts-password"
|
|
123
|
+
placeholder="••••••••"
|
|
124
|
+
secureTextEntry={secure}
|
|
125
|
+
onChangeText={action("onChangeText")}
|
|
126
|
+
/>
|
|
127
|
+
<InputParts.Button
|
|
128
|
+
glyphRing
|
|
129
|
+
type="button"
|
|
130
|
+
aria-label={secure ? "Show password" : "Hide password"}
|
|
131
|
+
onPress={() => {
|
|
132
|
+
setSecure((prev) => !prev);
|
|
133
|
+
action("togglePasswordVisibility")(!secure);
|
|
134
|
+
}}
|
|
135
|
+
>
|
|
136
|
+
<InputParts.Icon aria-hidden>
|
|
137
|
+
<EyeIcon />
|
|
138
|
+
</InputParts.Icon>
|
|
139
|
+
</InputParts.Button>
|
|
140
|
+
</InputParts.Box>
|
|
141
|
+
<InputParts.Meta helperText={secure ? "Hidden" : "Visible"} />
|
|
142
|
+
</InputParts>
|
|
143
|
+
);
|
|
144
|
+
};
|
|
145
|
+
return <Example />;
|
|
146
|
+
},
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* STABLE GROUND (Axiom 8 / DG-FORM-04): `Meta` owns ONE caption slot. The error
|
|
151
|
+
* replaces the helper in place, so surfacing it shifts nothing below.
|
|
152
|
+
*/
|
|
153
|
+
export const MetaReservesOneLine: Story = {
|
|
154
|
+
render: () => (
|
|
155
|
+
<YStack gap="$5" width={320}>
|
|
156
|
+
<InputParts>
|
|
157
|
+
<InputParts.Label>Helper</InputParts.Label>
|
|
158
|
+
<InputParts.Box>
|
|
159
|
+
<InputParts.Area placeholder="Valid" />
|
|
160
|
+
</InputParts.Box>
|
|
161
|
+
<InputParts.Meta helperText="Two to thirty characters." />
|
|
162
|
+
</InputParts>
|
|
163
|
+
<InputParts>
|
|
164
|
+
<InputParts.Label>Error replaces it</InputParts.Label>
|
|
165
|
+
<InputParts.Box>
|
|
166
|
+
<InputParts.Area placeholder="Invalid" aria-invalid />
|
|
167
|
+
<InputParts.Icon adornment="trailing" color="$red10">
|
|
168
|
+
<WarningCircleIcon aria-hidden />
|
|
169
|
+
</InputParts.Icon>
|
|
170
|
+
</InputParts.Box>
|
|
171
|
+
<InputParts.Meta
|
|
172
|
+
helperText="Two to thirty characters."
|
|
173
|
+
displayError="Enter at least two characters."
|
|
174
|
+
/>
|
|
175
|
+
</InputParts>
|
|
176
|
+
</YStack>
|
|
177
|
+
),
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
/** `Info` is the caption primitive `Meta` picks; use it directly for prose. */
|
|
181
|
+
export const Info: Story = {
|
|
182
|
+
render: () => (
|
|
183
|
+
<InputParts width={320}>
|
|
184
|
+
<InputParts.Label>Display name</InputParts.Label>
|
|
185
|
+
<InputParts.Box>
|
|
186
|
+
<InputParts.Area placeholder="Ada Lovelace" />
|
|
187
|
+
</InputParts.Box>
|
|
188
|
+
<InputParts.Info>Shown next to everything you publish.</InputParts.Info>
|
|
189
|
+
</InputParts>
|
|
190
|
+
),
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
/** `TextArea` is the multi-line Area: same focus wiring, no ring of its own. */
|
|
194
|
+
export const MultiLine: Story = {
|
|
195
|
+
render: () => (
|
|
196
|
+
<InputParts width={360}>
|
|
197
|
+
<InputParts.Label htmlFor="parts-notes">Notes</InputParts.Label>
|
|
198
|
+
<InputParts.Box alignItems="flex-start" height="auto" paddingVertical="$2">
|
|
199
|
+
<InputParts.TextArea
|
|
200
|
+
id="parts-notes"
|
|
201
|
+
rows={4}
|
|
202
|
+
placeholder="Anything the team should know"
|
|
203
|
+
onChangeText={action("onChangeText")}
|
|
204
|
+
/>
|
|
205
|
+
</InputParts.Box>
|
|
206
|
+
<InputParts.Meta helperText="Markdown is not parsed here." />
|
|
207
|
+
</InputParts>
|
|
208
|
+
),
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
/** Disabled chrome lives on the Box, not on the Area. */
|
|
212
|
+
export const DisabledBox: Story = {
|
|
213
|
+
render: () => (
|
|
214
|
+
<YStack gap="$4" width={320}>
|
|
215
|
+
<InputParts>
|
|
216
|
+
<InputParts.Box>
|
|
217
|
+
<InputParts.Area placeholder="Enabled" />
|
|
218
|
+
</InputParts.Box>
|
|
219
|
+
</InputParts>
|
|
220
|
+
<InputParts>
|
|
221
|
+
<InputParts.Box disabled>
|
|
222
|
+
<InputParts.Icon adornment="leading">
|
|
223
|
+
<EnvelopeIcon />
|
|
224
|
+
</InputParts.Icon>
|
|
225
|
+
<InputParts.Area placeholder="Disabled" disabled />
|
|
226
|
+
</InputParts.Box>
|
|
227
|
+
</InputParts>
|
|
228
|
+
</YStack>
|
|
229
|
+
),
|
|
230
|
+
};
|