Skip to content

Commit e9a119a

Browse files
authored
SearchField: Add validation props and refine states (#2363)
## Summary: To make the SearchField component more consistent with other form field components like TextField and TextArea, we add validation related props to the component. See [LabeledField Implementation Spec](https://khanacademy.atlassian.net/wiki/spaces/WB/pages/2261352551/LabeledField+Implementation+Spec) for more details! The changes include: - Add error, instantValidation, validate, onValidate props to SearchField - Minor tweaks - Hide clear icon button if the field is disabled - Refine magnifying glass icon styling to make it [match Figma component](https://www.figma.com/design/VbVu3h2BpBhH80niq101MHHE/%F0%9F%92%A0-Main-Components?node-id=13840-8605&t=83MBW2IFJ6nx42vp-4) more (smaller, bold icon, spacing, update disabled state) - Storybook: - Add error and validation stories for SearchField - Add variant stories for SearchField states Issue: WB-1761 ## Test plan: - Review Storybook docs for Error and Validation stories - `?path=/docs/packages-searchfield--docs#error` - `?path=/docs/packages-searchfield--docs#validation` - Review prop documentation for SearchField (`?path=/docs/packages-searchfield--docs`) - Review new Search Field All Variants stories (`?path=/docs/packages-searchfield-all-variants--docs`) - Review Chromatic diff for styling updates (left some comments in the UI Tests!) Author: beaesguerra Reviewers: beaesguerra, jandrade, marcysutton Required Reviewers: Approved By: jandrade, marcysutton Checks: ✅ Chromatic - Get results on regular PRs (ubuntu-latest, 20.x), ✅ Test / Test (ubuntu-latest, 20.x, 2/2), ✅ Test / Test (ubuntu-latest, 20.x, 1/2), ✅ Lint / Lint (ubuntu-latest, 20.x), ✅ Check build sizes (ubuntu-latest, 20.x), ✅ Publish npm snapshot (ubuntu-latest, 20.x), ✅ Chromatic - Build on regular PRs / chromatic (ubuntu-latest, 20.x), ✅ Prime node_modules cache for primary configuration (ubuntu-latest, 20.x), ⏭️ Chromatic - Skip on Release PR (changesets), ✅ Check for .changeset entries for all changed files (ubuntu-latest, 20.x), ✅ gerald, ⏭️ dependabot Pull Request URL: #2363
1 parent 61c1849 commit e9a119a

File tree

5 files changed

+551
-28
lines changed

5 files changed

+551
-28
lines changed

.changeset/breezy-bears-teach.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
"@khanacademy/wonder-blocks-search-field": minor
3+
---
4+
5+
# SearchField
6+
7+
- Adds `error`, `instantValidation`, `validate`, and `onValidate` props to be consistent with form components.
8+
- Refine magnifying glass icon styling to make it match Figma more (smaller, bold icon, spacing, update disabled state)
9+
- Hide the clear button if the SearchField is disabled
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
import * as React from "react";
2+
import {StyleSheet} from "aphrodite";
3+
import type {Meta, StoryObj} from "@storybook/react";
4+
5+
import {View} from "@khanacademy/wonder-blocks-core";
6+
import {color, spacing} from "@khanacademy/wonder-blocks-tokens";
7+
import {LabelLarge, LabelMedium} from "@khanacademy/wonder-blocks-typography";
8+
import SearchField from "@khanacademy/wonder-blocks-search-field";
9+
10+
/**
11+
* The following stories are used to generate the pseudo states for the
12+
* SearchField component. This is only used for visual testing in Chromatic.
13+
*/
14+
export default {
15+
title: "Packages / SearchField / All Variants",
16+
parameters: {
17+
docs: {
18+
autodocs: false,
19+
},
20+
},
21+
} as Meta;
22+
23+
type StoryComponentType = StoryObj<typeof SearchField>;
24+
25+
const longText =
26+
"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.";
27+
const longTextWithNoWordBreak =
28+
"Loremipsumdolorsitametconsecteturadipiscingelitseddoeiusmodtemporincididuntutlaboreetdoloremagnaaliqua";
29+
30+
const states = [
31+
{
32+
label: "Default",
33+
props: {},
34+
},
35+
{
36+
label: "Disabled",
37+
props: {disabled: true},
38+
},
39+
{
40+
label: "Error",
41+
props: {error: true},
42+
},
43+
];
44+
const States = (props: {
45+
light: boolean;
46+
label: string;
47+
value?: string;
48+
placeholder?: string;
49+
}) => {
50+
return (
51+
<View
52+
style={[props.light && styles.darkDefault, styles.statesContainer]}
53+
>
54+
<LabelLarge style={props.light && {color: color.white}}>
55+
{props.label}
56+
</LabelLarge>
57+
<View style={[styles.scenarios]}>
58+
{states.map((scenario) => {
59+
return (
60+
<View style={styles.scenario} key={scenario.label}>
61+
<LabelMedium
62+
style={props.light && {color: color.white}}
63+
>
64+
{scenario.label}
65+
</LabelMedium>
66+
<SearchField
67+
value=""
68+
onChange={() => {}}
69+
{...props}
70+
{...scenario.props}
71+
/>
72+
</View>
73+
);
74+
})}
75+
</View>
76+
</View>
77+
);
78+
};
79+
80+
const AllVariants = () => (
81+
<View>
82+
{[false, true].map((light) => {
83+
return (
84+
<React.Fragment key={`light-${light}`}>
85+
<States light={light} label="Default" />
86+
<States light={light} label="With Value" value="Text" />
87+
<States
88+
light={light}
89+
label="With Value (long)"
90+
value={longText}
91+
/>
92+
<States
93+
light={light}
94+
label="With Value (long, no word breaks)"
95+
value={longTextWithNoWordBreak}
96+
/>
97+
<States
98+
light={light}
99+
label="With Placeholder"
100+
placeholder="Placeholder text"
101+
/>
102+
<States
103+
light={light}
104+
label="With Placeholder (long)"
105+
placeholder={longText}
106+
/>
107+
<States
108+
light={light}
109+
label="With Placeholder (long, no word breaks)"
110+
placeholder={longTextWithNoWordBreak}
111+
/>
112+
</React.Fragment>
113+
);
114+
})}
115+
</View>
116+
);
117+
118+
export const Default: StoryComponentType = {
119+
render: AllVariants,
120+
};
121+
122+
/**
123+
* There are currently only hover styles on the clear button.
124+
*/
125+
export const Hover: StoryComponentType = {
126+
render: AllVariants,
127+
parameters: {pseudo: {hover: true}},
128+
};
129+
130+
export const Focus: StoryComponentType = {
131+
render: AllVariants,
132+
parameters: {pseudo: {focusVisible: true}},
133+
};
134+
135+
export const HoverFocus: StoryComponentType = {
136+
name: "Hover + Focus",
137+
render: AllVariants,
138+
parameters: {pseudo: {hover: true, focusVisible: true}},
139+
};
140+
141+
/**
142+
* There are currently no active styles.
143+
*/
144+
export const Active: StoryComponentType = {
145+
render: AllVariants,
146+
parameters: {pseudo: {active: true}},
147+
};
148+
149+
const styles = StyleSheet.create({
150+
darkDefault: {
151+
backgroundColor: color.darkBlue,
152+
},
153+
statesContainer: {
154+
padding: spacing.medium_16,
155+
},
156+
scenarios: {
157+
flexDirection: "row",
158+
alignItems: "center",
159+
gap: spacing.xxxLarge_64,
160+
flexWrap: "wrap",
161+
},
162+
scenario: {
163+
gap: spacing.small_12,
164+
overflow: "hidden",
165+
},
166+
});

__docs__/wonder-blocks-search-field/search-field.stories.tsx

Lines changed: 93 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,10 @@ import {StyleSheet} from "aphrodite";
33
import {action} from "@storybook/addon-actions";
44
import type {Meta, StoryObj} from "@storybook/react";
55

6-
import {View} from "@khanacademy/wonder-blocks-core";
6+
import {PropsFor, View} from "@khanacademy/wonder-blocks-core";
77
import Button from "@khanacademy/wonder-blocks-button";
88
import {color, spacing} from "@khanacademy/wonder-blocks-tokens";
9-
import {LabelLarge} from "@khanacademy/wonder-blocks-typography";
9+
import {LabelLarge, LabelSmall} from "@khanacademy/wonder-blocks-typography";
1010

1111
import SearchField from "@khanacademy/wonder-blocks-search-field";
1212

@@ -52,8 +52,11 @@ export default {
5252

5353
type StoryComponentType = StoryObj<typeof SearchField>;
5454

55-
const Template = (args: any) => {
56-
const [value, setValue] = React.useState("");
55+
const Template = (args: PropsFor<typeof SearchField>) => {
56+
const [value, setValue] = React.useState(args?.value || "");
57+
const [errorMessage, setErrorMessage] = React.useState<
58+
string | null | undefined
59+
>("");
5760

5861
const handleChange = (newValue: string) => {
5962
setValue(newValue);
@@ -66,15 +69,23 @@ const Template = (args: any) => {
6669
};
6770

6871
return (
69-
<SearchField
70-
{...args}
71-
value={value}
72-
onChange={handleChange}
73-
onKeyDown={(e) => {
74-
action("onKeyDown")(e);
75-
handleKeyDown(e);
76-
}}
77-
/>
72+
<View style={{gap: spacing.xSmall_8}}>
73+
<SearchField
74+
{...args}
75+
value={value}
76+
onChange={handleChange}
77+
onKeyDown={(e) => {
78+
action("onKeyDown")(e);
79+
handleKeyDown(e);
80+
}}
81+
onValidate={setErrorMessage}
82+
/>
83+
{(errorMessage || args.error) && (
84+
<LabelSmall style={styles.errorMessage}>
85+
{errorMessage || "Error from error prop"}
86+
</LabelSmall>
87+
)}
88+
</View>
7889
);
7990
};
8091

@@ -217,9 +228,78 @@ export const WithAutofocus: StoryComponentType = {
217228
},
218229
};
219230

231+
/**
232+
* The SearchField can be put in an error state using the `error` prop.
233+
*/
234+
export const Error: StoryComponentType = {
235+
args: {
236+
error: true,
237+
},
238+
render: Template,
239+
parameters: {
240+
chromatic: {
241+
// Disabling because this is covered by the All Variants stories
242+
disableSnapshot: true,
243+
},
244+
},
245+
};
246+
247+
/**
248+
* The SearchField supports `validate`, `onValidate`, and `instantValidation`
249+
* props.
250+
*
251+
* See docs for the TextField component for more details around validation
252+
* since SearchField uses TextField internally.
253+
*/
254+
export const Validation: StoryComponentType = {
255+
args: {
256+
validate(value) {
257+
if (value.length < 5) {
258+
return "Too short. Value should be at least 5 characters";
259+
}
260+
},
261+
},
262+
render: (args) => {
263+
return (
264+
<View style={{gap: spacing.small_12}}>
265+
<LabelSmall htmlFor="instant-validation-true">
266+
Validation on mount if there is a value
267+
</LabelSmall>
268+
<Template {...args} id="instant-validation-true" value="T" />
269+
<LabelSmall htmlFor="instant-validation-true">
270+
Error shown immediately (instantValidation: true)
271+
</LabelSmall>
272+
<Template
273+
{...args}
274+
id="instant-validation-true"
275+
instantValidation={true}
276+
/>
277+
<LabelSmall htmlFor="instant-validation-false">
278+
Error shown onBlur (instantValidation: false)
279+
</LabelSmall>
280+
<Template
281+
{...args}
282+
id="instant-validation-false"
283+
instantValidation={false}
284+
/>
285+
</View>
286+
);
287+
},
288+
parameters: {
289+
chromatic: {
290+
// Disabling because this doesn't test anything visual.
291+
disableSnapshot: true,
292+
},
293+
},
294+
};
295+
220296
const styles = StyleSheet.create({
221297
darkBackground: {
222298
background: color.darkBlue,
223299
padding: spacing.medium_16,
224300
},
301+
errorMessage: {
302+
color: color.red,
303+
paddingLeft: spacing.xxxSmall_4,
304+
},
225305
});

0 commit comments

Comments
 (0)