diff --git a/.changeset/breezy-bears-teach.md b/.changeset/breezy-bears-teach.md new file mode 100644 index 000000000..501c08ac6 --- /dev/null +++ b/.changeset/breezy-bears-teach.md @@ -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 diff --git a/__docs__/wonder-blocks-search-field/search-field-variants.stories.tsx b/__docs__/wonder-blocks-search-field/search-field-variants.stories.tsx new file mode 100644 index 000000000..e821674f9 --- /dev/null +++ b/__docs__/wonder-blocks-search-field/search-field-variants.stories.tsx @@ -0,0 +1,166 @@ +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; + +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 ( + + + {props.label} + + + {states.map((scenario) => { + return ( + + + {scenario.label} + + {}} + {...props} + {...scenario.props} + /> + + ); + })} + + + ); +}; + +const AllVariants = () => ( + + {[false, true].map((light) => { + return ( + + + + + + + + + + ); + })} + +); + +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: { + flexDirection: "row", + alignItems: "center", + gap: spacing.xxxLarge_64, + flexWrap: "wrap", + }, + scenario: { + gap: spacing.small_12, + overflow: "hidden", + }, +}); diff --git a/__docs__/wonder-blocks-search-field/search-field.stories.tsx b/__docs__/wonder-blocks-search-field/search-field.stories.tsx index bf9e16c4f..9b15c8567 100644 --- a/__docs__/wonder-blocks-search-field/search-field.stories.tsx +++ b/__docs__/wonder-blocks-search-field/search-field.stories.tsx @@ -3,10 +3,10 @@ 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"; @@ -52,8 +52,11 @@ export default { type StoryComponentType = StoryObj; -const Template = (args: any) => { - const [value, setValue] = React.useState(""); +const Template = (args: PropsFor) => { + const [value, setValue] = React.useState(args?.value || ""); + const [errorMessage, setErrorMessage] = React.useState< + string | null | undefined + >(""); const handleChange = (newValue: string) => { setValue(newValue); @@ -66,15 +69,23 @@ const Template = (args: any) => { }; return ( - { - action("onKeyDown")(e); - handleKeyDown(e); - }} - /> + + { + action("onKeyDown")(e); + handleKeyDown(e); + }} + onValidate={setErrorMessage} + /> + {(errorMessage || args.error) && ( + + {errorMessage || "Error from error prop"} + + )} + ); }; @@ -217,9 +228,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 ( + + + Validation on mount if there is a value + +