-
Notifications
You must be signed in to change notification settings - Fork 10
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
SearchField: Add validation props and refine states #2363
base: main
Are you sure you want to change the base?
Changes from 15 commits
db91d02
e6a0a31
5be918d
9a012f2
9981f8e
96337dc
133be86
173aaf5
519c6a7
f7ca531
38d94fc
ce89fc7
5f9f586
7898c96
e0569b1
9151f56
d2e3190
b286e3a
d2a4cd5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
--- | ||
"@khanacademy/wonder-blocks-search-field": minor | ||
--- | ||
|
||
# SearchField | ||
|
||
- Adds `error`, `instantValidation`, `validate`, and `onValidate` props to be consistent with form components. | ||
- Refine magnifying glass icon styling to make it match Figma more (smaller, bold icon, spacing, update disabled state) | ||
- Hide the clear button if the SearchField is disabled |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,167 @@ | ||
import * as React from "react"; | ||
import {StyleSheet} from "aphrodite"; | ||
import type {Meta, StoryObj} from "@storybook/react"; | ||
|
||
import {View} from "@khanacademy/wonder-blocks-core"; | ||
import {color, spacing} from "@khanacademy/wonder-blocks-tokens"; | ||
import {LabelLarge, LabelMedium} from "@khanacademy/wonder-blocks-typography"; | ||
import SearchField from "@khanacademy/wonder-blocks-search-field"; | ||
|
||
/** | ||
* The following stories are used to generate the pseudo states for the | ||
* SearchField component. This is only used for visual testing in Chromatic. | ||
*/ | ||
export default { | ||
title: "Packages / SearchField / All Variants", | ||
parameters: { | ||
docs: { | ||
autodocs: false, | ||
}, | ||
}, | ||
} as Meta; | ||
|
||
type StoryComponentType = StoryObj<typeof SearchField>; | ||
|
||
const longText = | ||
"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."; | ||
const longTextWithNoWordBreak = | ||
"Loremipsumdolorsitametconsecteturadipiscingelitseddoeiusmodtemporincididuntutlaboreetdoloremagnaaliqua"; | ||
|
||
const states = [ | ||
{ | ||
label: "Default", | ||
props: {}, | ||
}, | ||
{ | ||
label: "Disabled", | ||
props: {disabled: true}, | ||
}, | ||
{ | ||
label: "Error", | ||
props: {error: true}, | ||
}, | ||
]; | ||
const States = (props: { | ||
light: boolean; | ||
label: string; | ||
value?: string; | ||
placeholder?: string; | ||
}) => { | ||
return ( | ||
<View | ||
style={[props.light && styles.darkDefault, styles.statesContainer]} | ||
> | ||
<LabelLarge style={props.light && {color: color.white}}> | ||
{props.label} | ||
</LabelLarge> | ||
<View style={[styles.scenarios]}> | ||
{states.map((scenario) => { | ||
return ( | ||
<View style={styles.scenario} key={scenario.label}> | ||
<LabelMedium | ||
style={props.light && {color: color.white}} | ||
> | ||
{scenario.label} | ||
</LabelMedium> | ||
<SearchField | ||
value="" | ||
onChange={() => {}} | ||
{...props} | ||
{...scenario.props} | ||
/> | ||
</View> | ||
); | ||
})} | ||
</View> | ||
</View> | ||
); | ||
}; | ||
|
||
const AllVariants = () => ( | ||
<View> | ||
{[false, true].map((light) => { | ||
return ( | ||
<React.Fragment key={`light-${light}`}> | ||
<States light={light} label="Default" /> | ||
<States light={light} label="With Value" value="Text" /> | ||
<States | ||
light={light} | ||
label="With Value (long)" | ||
value={longText} | ||
/> | ||
<States | ||
light={light} | ||
label="With Value (long, no word breaks)" | ||
value={longTextWithNoWordBreak} | ||
/> | ||
<States | ||
light={light} | ||
label="With Placeholder" | ||
placeholder="Placeholder text" | ||
/> | ||
<States | ||
light={light} | ||
label="With Placeholder (long)" | ||
placeholder={longText} | ||
/> | ||
<States | ||
light={light} | ||
label="With Placeholder (long, no word breaks)" | ||
placeholder={longTextWithNoWordBreak} | ||
/> | ||
</React.Fragment> | ||
); | ||
})} | ||
</View> | ||
); | ||
|
||
export const Default: StoryComponentType = { | ||
render: AllVariants, | ||
}; | ||
|
||
/** | ||
* There are currently only hover styles on the clear button. | ||
*/ | ||
export const Hover: StoryComponentType = { | ||
render: AllVariants, | ||
parameters: {pseudo: {hover: true}}, | ||
}; | ||
|
||
export const Focus: StoryComponentType = { | ||
render: AllVariants, | ||
parameters: {pseudo: {focusVisible: true}}, | ||
}; | ||
|
||
export const HoverFocus: StoryComponentType = { | ||
name: "Hover + Focus", | ||
render: AllVariants, | ||
parameters: {pseudo: {hover: true, focusVisible: true}}, | ||
}; | ||
|
||
/** | ||
* There are currently no active styles. | ||
*/ | ||
export const Active: StoryComponentType = { | ||
render: AllVariants, | ||
parameters: {pseudo: {active: true}}, | ||
}; | ||
|
||
const styles = StyleSheet.create({ | ||
darkDefault: { | ||
backgroundColor: color.darkBlue, | ||
}, | ||
statesContainer: { | ||
padding: spacing.medium_16, | ||
}, | ||
scenarios: { | ||
display: "flex", | ||
flexDirection: "row", | ||
alignItems: "center", | ||
gap: spacing.xxxLarge_64, | ||
flexWrap: "wrap", | ||
}, | ||
scenario: { | ||
gap: spacing.small_12, | ||
overflow: "hidden", | ||
}, | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,16 +3,17 @@ import {StyleSheet} from "aphrodite"; | |
import {action} from "@storybook/addon-actions"; | ||
import type {Meta, StoryObj} from "@storybook/react"; | ||
|
||
import {View} from "@khanacademy/wonder-blocks-core"; | ||
import {PropsFor, View} from "@khanacademy/wonder-blocks-core"; | ||
import Button from "@khanacademy/wonder-blocks-button"; | ||
import {color, spacing} from "@khanacademy/wonder-blocks-tokens"; | ||
import {LabelLarge} from "@khanacademy/wonder-blocks-typography"; | ||
import {LabelLarge, LabelSmall} from "@khanacademy/wonder-blocks-typography"; | ||
|
||
import SearchField from "@khanacademy/wonder-blocks-search-field"; | ||
|
||
import ComponentInfo from "../../.storybook/components/component-info"; | ||
import packageConfig from "../../packages/wonder-blocks-search-field/package.json"; | ||
import SearchFieldArgtypes from "./search-field.argtypes"; | ||
import {Strut} from "@khanacademy/wonder-blocks-layout"; | ||
|
||
/** | ||
* `SearchField` helps users input text to search for relevant content. It is | ||
|
@@ -52,8 +53,11 @@ export default { | |
|
||
type StoryComponentType = StoryObj<typeof SearchField>; | ||
|
||
const Template = (args: any) => { | ||
const [value, setValue] = React.useState(""); | ||
const Template = (args: PropsFor<typeof SearchField>) => { | ||
const [value, setValue] = React.useState(args?.value || ""); | ||
const [errorMessage, setErrorMessage] = React.useState< | ||
string | null | undefined | ||
>(""); | ||
|
||
const handleChange = (newValue: string) => { | ||
setValue(newValue); | ||
|
@@ -66,15 +70,26 @@ const Template = (args: any) => { | |
}; | ||
|
||
return ( | ||
<SearchField | ||
{...args} | ||
value={value} | ||
onChange={handleChange} | ||
onKeyDown={(e) => { | ||
action("onKeyDown")(e); | ||
handleKeyDown(e); | ||
}} | ||
/> | ||
<View> | ||
<SearchField | ||
{...args} | ||
value={value} | ||
onChange={handleChange} | ||
onKeyDown={(e) => { | ||
action("onKeyDown")(e); | ||
handleKeyDown(e); | ||
}} | ||
onValidate={setErrorMessage} | ||
/> | ||
{(errorMessage || args.error) && ( | ||
<> | ||
<Strut size={spacing.xSmall_8} /> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. suggestion: I guess you could also set On a side note, I feel that There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That makes sense! I wonder if it would be helpful to add a Also, is it preferred we inline the aphrodite styles or create styles in a stylesheet for things like this?
or
Is one more performant? One nice thing about inlining the styles is that if the styling gets removed, it's removed! Unlike the other way, there's a chance we forget to remove the unused style from the stylesheet! This may also change depending on our css approach, curious if anyone has thoughts on this :) |
||
<LabelSmall style={styles.errorMessage}> | ||
{errorMessage || "Error from error prop"} | ||
</LabelSmall> | ||
</> | ||
)} | ||
</View> | ||
); | ||
}; | ||
|
||
|
@@ -217,9 +232,78 @@ export const WithAutofocus: StoryComponentType = { | |
}, | ||
}; | ||
|
||
/** | ||
* The SearchField can be put in an error state using the `error` prop. | ||
*/ | ||
export const Error: StoryComponentType = { | ||
args: { | ||
error: true, | ||
}, | ||
render: Template, | ||
parameters: { | ||
chromatic: { | ||
// Disabling because this is covered by the All Variants stories | ||
disableSnapshot: true, | ||
}, | ||
}, | ||
}; | ||
|
||
/** | ||
* The SearchField supports `validate`, `onValidate`, and `instantValidation` | ||
* props. | ||
* | ||
* See docs for the TextField component for more details around validation | ||
* since SearchField uses TextField internally. | ||
*/ | ||
export const Validation: StoryComponentType = { | ||
args: { | ||
validate(value) { | ||
if (value.length < 5) { | ||
return "Too short. Value should be at least 5 characters"; | ||
} | ||
}, | ||
}, | ||
render: (args) => { | ||
return ( | ||
<View style={{gap: spacing.small_12}}> | ||
<LabelSmall htmlFor="instant-validation-true"> | ||
Validation on mount if there is a value | ||
</LabelSmall> | ||
<Template {...args} id="instant-validation-true" value="T" /> | ||
<LabelSmall htmlFor="instant-validation-true"> | ||
Error shown immediately (instantValidation: true) | ||
</LabelSmall> | ||
<Template | ||
{...args} | ||
id="instant-validation-true" | ||
instantValidation={true} | ||
/> | ||
<LabelSmall htmlFor="instant-validation-false"> | ||
Error shown onBlur (instantValidation: false) | ||
</LabelSmall> | ||
<Template | ||
{...args} | ||
id="instant-validation-false" | ||
instantValidation={false} | ||
/> | ||
</View> | ||
); | ||
}, | ||
parameters: { | ||
chromatic: { | ||
// Disabling because this doesn't test anything visual. | ||
disableSnapshot: true, | ||
}, | ||
}, | ||
}; | ||
|
||
const styles = StyleSheet.create({ | ||
darkBackground: { | ||
background: color.darkBlue, | ||
padding: spacing.medium_16, | ||
}, | ||
errorMessage: { | ||
color: color.red, | ||
paddingLeft: spacing.xxxSmall_4, | ||
}, | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
super-nit:
View
providesflex
by default.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Removed! (🦸 super-nit 😄)