diff --git a/packages/odyssey-react-mui/src/Field.tsx b/packages/odyssey-react-mui/src/Field.tsx new file mode 100644 index 0000000000..b27f8586f4 --- /dev/null +++ b/packages/odyssey-react-mui/src/Field.tsx @@ -0,0 +1,112 @@ +/*! + * Copyright (c) 2022-present, Okta, Inc. and/or its affiliates. All rights reserved. + * The Okta software accompanied by this notice is provided pursuant to the Apache License, Version 2.0 (the "License.") + * + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0. + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * + * See the License for the specific language governing permissions and limitations under the License. + */ + +import { memo, ReactElement, useMemo } from "react"; + +import { + FieldError, + FieldHint, + FieldLabel, + FormControl, + useUniqueId, +} from "./"; + +export type FieldProps = { + /** + * If `error` is not undefined, the `input` will indicate an error. + */ + errorMessage?: string; + hasVisibleLabel: boolean; + /** + * The helper text content. + */ + hint?: string; + /** + * The id of the `input` element. + */ + id?: string; + /** + * If `true`, the component is disabled. + */ + isDisabled?: boolean; + /** + * If `true`, the `input` element is required. + */ + isRequired?: boolean; + /** + * The label for the `input` element. + */ + label: string; + /** + * The label for the `input` element if the it's not optional + */ + optionalLabel?: string; + /** + * The short hint displayed in the `input` before the user enters a value. + */ + placeholder?: string; + renderFieldComponent: ({ + ariaDescribedBy, + id, + }: { + ariaDescribedBy?: string; + id: string; + }) => ReactElement; +}; + +const Field = ({ + errorMessage, + hasVisibleLabel, + hint, + id: idOverride, + isDisabled = false, + isRequired = true, + label, + optionalLabel, + renderFieldComponent, +}: FieldProps) => { + const id = useUniqueId(idOverride); + const hintId = hint ? `${id}-hint` : undefined; + const errorId = errorMessage ? `${id}-error` : undefined; + const labelId = `${id}-label`; + + const ariaDescribedBy = useMemo( + () => [hintId, errorId].join(" ").trim() || undefined, + [errorId, hintId] + ); + + return ( + + + + {hint && } + + {renderFieldComponent({ + ariaDescribedBy, + id, + })} + + {errorMessage && } + + ); +}; + +const MemoizedField = memo(Field); + +export { MemoizedField as Field }; diff --git a/packages/odyssey-react-mui/src/FieldError.tsx b/packages/odyssey-react-mui/src/FieldError.tsx new file mode 100644 index 0000000000..4edadc8dc2 --- /dev/null +++ b/packages/odyssey-react-mui/src/FieldError.tsx @@ -0,0 +1,34 @@ +/*! + * Copyright (c) 2023-present, Okta, Inc. and/or its affiliates. All rights reserved. + * The Okta software accompanied by this notice is provided pursuant to the Apache License, Version 2.0 (the "License.") + * + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0. + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * + * See the License for the specific language governing permissions and limitations under the License. + */ + +import { memo } from "react"; + +import { FormHelperText } from "."; +import { ScreenReaderText } from "./ScreenReaderText"; + +export type FieldErrorProps = { + id?: string; + text: string; +}; + +const FieldError = ({ id, text }: FieldErrorProps) => { + return ( + + Error: + {text} + + ); +}; + +const MemoizedFieldError = memo(FieldError); + +export { MemoizedFieldError as FieldError }; diff --git a/packages/odyssey-react-mui/src/FieldHint.tsx b/packages/odyssey-react-mui/src/FieldHint.tsx new file mode 100644 index 0000000000..76196b5d65 --- /dev/null +++ b/packages/odyssey-react-mui/src/FieldHint.tsx @@ -0,0 +1,28 @@ +/*! + * Copyright (c) 2023-present, Okta, Inc. and/or its affiliates. All rights reserved. + * The Okta software accompanied by this notice is provided pursuant to the Apache License, Version 2.0 (the "License.") + * + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0. + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * + * See the License for the specific language governing permissions and limitations under the License. + */ + +import { memo } from "react"; + +import { FormHelperText } from "./"; + +export type FieldHintProps = { + id?: string; + text: string; +}; + +const FieldHint = ({ id, text }: FieldHintProps) => { + return {text}; +}; + +const MemoizedFieldHint = memo(FieldHint); + +export { MemoizedFieldHint as FieldHint }; diff --git a/packages/odyssey-react-mui/src/FieldLabel.tsx b/packages/odyssey-react-mui/src/FieldLabel.tsx new file mode 100644 index 0000000000..c59cec3574 --- /dev/null +++ b/packages/odyssey-react-mui/src/FieldLabel.tsx @@ -0,0 +1,57 @@ +/*! + * Copyright (c) 2023-present, Okta, Inc. and/or its affiliates. All rights reserved. + * The Okta software accompanied by this notice is provided pursuant to the Apache License, Version 2.0 (the "License.") + * + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0. + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * + * See the License for the specific language governing permissions and limitations under the License. + */ + +import { InputLabel } from "@mui/material"; +import { memo, useMemo } from "react"; + +import { ScreenReaderText } from "./ScreenReaderText"; +import { Typography } from "."; + +export type FieldLabelProps = { + hasVisibleLabel: boolean; + id: string; + inputId: string; + isRequired: boolean; + optionalText?: string; + text: string; +}; + +const FieldLabel = ({ + hasVisibleLabel, + id, + inputId, + isRequired, + optionalText, + text, +}: FieldLabelProps) => { + const inputLabel = useMemo( + () => ( + + {text} + {!isRequired && ( + {optionalText} + )} + + ), + [id, inputId, isRequired, optionalText, text] + ); + + return hasVisibleLabel ? ( + inputLabel + ) : ( + {inputLabel} + ); +}; + +const MemoizedFieldLabel = memo(FieldLabel); + +export { MemoizedFieldLabel as FieldLabel }; diff --git a/packages/odyssey-react-mui/src/PasswordField.tsx b/packages/odyssey-react-mui/src/PasswordField.tsx new file mode 100644 index 0000000000..315d9dcd54 --- /dev/null +++ b/packages/odyssey-react-mui/src/PasswordField.tsx @@ -0,0 +1,174 @@ +/*! + * Copyright (c) 2022-present, Okta, Inc. and/or its affiliates. All rights reserved. + * The Okta software accompanied by this notice is provided pursuant to the Apache License, Version 2.0 (the "License.") + * + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0. + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * + * See the License for the specific language governing permissions and limitations under the License. + */ + +import { InputAdornment, InputBase } from "@mui/material"; +import { + ChangeEventHandler, + FocusEventHandler, + forwardRef, + memo, + useCallback, + useState, +} from "react"; + +import { EyeIcon, EyeOffIcon, IconButton } from "./"; +import { Field } from "./Field"; + +export type PasswordFieldProps = { + /** + * If `true`, the component will receive focus automatically. + */ + autoFocus?: boolean; + /** + * This prop helps users to fill forms faster, especially on mobile devices. + * The name can be confusing, as it's more like an autofill. + * You can learn more about it [following the specification](https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#autofill). + */ + autoCompleteType?: "current-password" | "new-password"; + /** + * If `error` is not undefined, the `input` will indicate an error. + */ + errorMessage?: string; + /** + * The helper text content. + */ + hint?: string; + /** + * The id of the `input` element. + */ + id?: string; + /** + * If `true`, the component is disabled. + */ + isDisabled?: boolean; + /** + * It prevents the user from changing the value of the field + */ + isReadOnly?: boolean; + /** + * If `true`, the `input` element is required. + */ + isRequired?: boolean; + /** + * The label for the `input` element. + */ + label: string; + /** + * Callback fired when the `input` element loses focus. + */ + onBlur?: FocusEventHandler; + /** + * Callback fired when the value is changed. + */ + onChange?: ChangeEventHandler; + /** + * Callback fired when the `input` element get focus. + */ + onFocus?: FocusEventHandler; + /** + * The short hint displayed in the `input` before the user enters a value. + */ + placeholder?: string; + /** + * The value of the `input` element, required for a controlled component. + */ + value?: string; +}; + +const PasswordField = forwardRef( + ( + { + autoCompleteType, + autoFocus, + errorMessage, + hint, + id: idOverride, + isDisabled = false, + isReadOnly, + label, + onChange, + onFocus, + onBlur, + placeholder, + value, + }, + ref + ) => { + const [inputType, setInputType] = useState("password"); + + const togglePasswordVisibility = useCallback(() => { + setInputType((inputType) => + inputType === "password" ? "text" : "password" + ); + }, []); + + const renderFieldComponent = useCallback( + ({ ariaDescribedBy, id }) => ( + + + {inputType === "password" ? : } + + + } + id={id} + onChange={onChange} + onFocus={onFocus} + onBlur={onBlur} + placeholder={placeholder} + readOnly={isReadOnly} + ref={ref} + type={inputType} + value={value} + /> + ), + [ + autoCompleteType, + autoFocus, + togglePasswordVisibility, + inputType, + onChange, + onFocus, + onBlur, + placeholder, + isReadOnly, + ref, + value, + ] + ); + + return ( + + ); + } +); + +const MemoizedPasswordField = memo(PasswordField); + +export { MemoizedPasswordField as PasswordField }; diff --git a/packages/odyssey-react-mui/src/PasswordInput.test.tsx b/packages/odyssey-react-mui/src/PasswordInput.test.tsx deleted file mode 100644 index 5d4170452a..0000000000 --- a/packages/odyssey-react-mui/src/PasswordInput.test.tsx +++ /dev/null @@ -1,51 +0,0 @@ -/*! - * Copyright (c) 2022-present, Okta, Inc. and/or its affiliates. All rights reserved. - * The Okta software accompanied by this notice is provided pursuant to the Apache License, Version 2.0 (the "License.") - * - * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0. - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * - * See the License for the specific language governing permissions and limitations under the License. - */ - -import { render, screen } from "@testing-library/react"; - -import { a11yCheck } from "./a11yCheck"; -import { PasswordInput } from "./PasswordInput"; - -const label = "Password"; -const tooltipOnLabel = "Show password"; -const tooltipOffLabel = "Hide password"; - -describe("PasswordInput", () => { - it("renders into the document", () => { - render(); - expect(screen.getByLabelText(label)).toBeVisible(); - }); - - it("has a button that changes the type when clicked", () => { - const tooltipLabel = (isHidden: boolean) => { - return isHidden ? tooltipOnLabel : tooltipOffLabel; - }; - render( - - ); - const inputElement = screen.getByLabelText(label); - expect(inputElement).toHaveValue("Imma password"); - expect(inputElement).toHaveAttribute("type", "password"); - const eyeButton = screen.getByRole("button"); - expect(eyeButton).toBeInTheDocument(); - eyeButton.click(); - expect(inputElement).toHaveAttribute("type", "text"); - }); - - a11yCheck(() => - render() - ); -}); diff --git a/packages/odyssey-react-mui/src/PasswordInput.tsx b/packages/odyssey-react-mui/src/PasswordInput.tsx deleted file mode 100644 index 08ae1d8f2e..0000000000 --- a/packages/odyssey-react-mui/src/PasswordInput.tsx +++ /dev/null @@ -1,111 +0,0 @@ -/*! - * Copyright (c) 2022-present, Okta, Inc. and/or its affiliates. All rights reserved. - * The Okta software accompanied by this notice is provided pursuant to the Apache License, Version 2.0 (the "License.") - * - * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0. - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * - * See the License for the specific language governing permissions and limitations under the License. - */ - -import { forwardRef, useState, useMemo } from "react"; -import type { Ref, ChangeEvent, MouseEvent } from "react"; -import type { OutlinedInputProps, TooltipProps } from "@mui/material"; -import { - Tooltip, - IconButton, - Box, - InputLabel, - OutlinedInput, - InputAdornment, -} from "@mui/material"; -import { Visibility, VisibilityOff } from "@mui/icons-material"; -import { useUniqueId } from "./useUniqueId"; - -interface State { - password: string; - showPassword: boolean; -} - -export interface PasswordInputProps - extends Omit { - ref?: Ref; - defaultValue?: string; - label: string; - tooltipLabel?: - | TooltipProps["title"] - | ((isHidden: boolean) => TooltipProps["title"]); -} - -export const PasswordInput = forwardRef( - (props, ref) => { - const { - tooltipLabel, - id, - label, - defaultValue: password = "", - inputProps, - ...rest - } = props; - - const [values, setValues] = useState({ - password, - showPassword: false, - }); - - const handlePasswordChange = (event: ChangeEvent) => { - setValues({ ...values, password: event.target.value }); - props.onChange?.(event); - }; - - const handleClickShowPassword = () => { - setValues({ - ...values, - showPassword: !values.showPassword, - }); - }; - - const handleMouseDownPassword = (event: MouseEvent) => { - event.preventDefault(); - }; - - const tooltipTitle = useMemo(() => { - return typeof tooltipLabel === "function" - ? tooltipLabel(values.showPassword === false) - : tooltipLabel; - }, [values, tooltipLabel]); - - const uniqueId = useUniqueId(id); - - return ( - - {label} - - - - {values.showPassword ? : } - - - - } - /> - - ); - } -); diff --git a/packages/odyssey-react-mui/src/SearchField.tsx b/packages/odyssey-react-mui/src/SearchField.tsx new file mode 100644 index 0000000000..84c168e108 --- /dev/null +++ b/packages/odyssey-react-mui/src/SearchField.tsx @@ -0,0 +1,136 @@ +/*! + * Copyright (c) 2022-present, Okta, Inc. and/or its affiliates. All rights reserved. + * The Okta software accompanied by this notice is provided pursuant to the Apache License, Version 2.0 (the "License.") + * + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0. + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * + * See the License for the specific language governing permissions and limitations under the License. + */ + +import { InputAdornment, InputBase } from "@mui/material"; +import { + ChangeEventHandler, + FocusEventHandler, + forwardRef, + InputHTMLAttributes, + memo, + useCallback, +} from "react"; + +import { SearchIcon } from "./"; +import { Field } from "./Field"; + +export type SearchFieldProps = { + /** + * If `true`, the component will receive focus automatically. + */ + autoFocus?: boolean; + /** + * This prop helps users to fill forms faster, especially on mobile devices. + * The name can be confusing, as it's more like an autofill. + * You can learn more about it [following the specification](https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#autofill). + */ + autoCompleteType?: InputHTMLAttributes["autoComplete"]; + /** + * The id of the `input` element. + */ + id?: string; + /** + * If `true`, the component is disabled. + */ + isDisabled?: boolean; + /** + * This label won't show up visually, but it's required for accessibility. + */ + label: string; + /** + * Callback fired when the `input` element loses focus. + */ + onBlur?: FocusEventHandler; + /** + * Callback fired when the value is changed. + */ + onChange?: ChangeEventHandler; + /** + * Callback fired when the `input` element get focus. + */ + onFocus?: FocusEventHandler; + /** + * The short hint displayed in the `input` before the user enters a value. + */ + placeholder?: string; + /** + * The value of the `input` element, required for a controlled component. + */ + value?: string; +}; + +const SearchField = forwardRef( + ( + { + autoCompleteType, + autoFocus, + id: idOverride, + isDisabled = false, + label, + onChange, + onFocus, + onBlur, + placeholder, + value, + }, + ref + ) => { + const renderFieldComponent = useCallback( + ({ ariaDescribedBy, id }) => ( + + + + } + type="search" + value={value} + /> + ), + [ + autoCompleteType, + autoFocus, + onChange, + onFocus, + onBlur, + placeholder, + ref, + value, + ] + ); + + return ( + + ); + } +); + +const MemoizedSearchField = memo(SearchField); + +export { MemoizedSearchField as SearchField }; diff --git a/packages/odyssey-react-mui/src/TextField.tsx b/packages/odyssey-react-mui/src/TextField.tsx index 3fd474b87d..47e330dbca 100644 --- a/packages/odyssey-react-mui/src/TextField.tsx +++ b/packages/odyssey-react-mui/src/TextField.tsx @@ -10,12 +10,7 @@ * See the License for the specific language governing permissions and limitations under the License. */ -import { - InputAdornment, - InputBase, - InputBaseProps, - InputLabel, -} from "@mui/material"; +import { InputBase } from "@mui/material"; import { ChangeEventHandler, FocusEventHandler, @@ -24,22 +19,9 @@ import { memo, ReactNode, useCallback, - useEffect, - useMemo, - useState, } from "react"; -import { - EyeIcon, - EyeOffIcon, - FormControl, - FormHelperText, - IconButton, - SearchIcon, - Typography, - useUniqueId, - ScreenReaderText, -} from "./"; +import { Field } from "./Field"; export type TextFieldProps = { /** @@ -68,10 +50,6 @@ export type TextFieldProps = { * The id of the `input` element. */ id?: string; - /** - * Props that go onto the HTML `input` element. - */ - inputProps?: InputBaseProps["inputProps"]; /** * If `true`, the component is disabled. */ @@ -91,7 +69,7 @@ export type TextFieldProps = { /** * The label for the `input` element. */ - label?: string; + label: string; /** * Callback fired when the `input` element loses focus. */ @@ -119,11 +97,11 @@ export type TextFieldProps = { /** * Type of the `input` element. It should be [a valid HTML5 input type](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#Form_%3Cinput%3E_types). */ - type?: string; + type?: "email" | "number" | "tel" | "text" | "url"; /** * The value of the `input` element, required for a controlled component. */ - value?: unknown; + value?: string; }; const TextField = forwardRef( @@ -135,15 +113,14 @@ const TextField = forwardRef( errorMessage, hint, id: idOverride, - inputProps = {}, isDisabled = false, isMultiline = false, isReadOnly, isRequired = true, label, + onBlur, onChange, onFocus, - onBlur, optionalLabel, placeholder, startAdornment, @@ -152,91 +129,56 @@ const TextField = forwardRef( }, ref ) => { - const [inputType, setInputType] = useState(type); - - useEffect(() => { - setInputType(type); - }, [type]); - - const togglePasswordVisibility = useCallback(() => { - setInputType((currentType) => - currentType === "password" ? "text" : "password" - ); - }, []); - - const id = useUniqueId(idOverride); - const hintId = hint ? `${id}-hint` : undefined; - const errorId = errorMessage ? `${id}-error` : undefined; - const labelId = label ? `${id}-label` : undefined; - - const localInputProps = useMemo(() => { - const ariaDescribedBy = - errorId && hintId ? `${hintId} ${errorId}` : errorId || hintId; - - return { - ...inputProps, - "aria-describedby": - inputProps["aria-describedby"]?.concat(` ${ariaDescribedBy}`) ?? - ariaDescribedBy, - }; - }, [errorId, hintId, inputProps]); - - return ( - - - {label} - {!isRequired && ( - {optionalLabel} - )} - - {hint && {hint}} + const renderFieldComponent = useCallback( + ({ ariaDescribedBy, id }) => ( - - {inputType === "password" ? : } - - - ) : ( - endAdornment - ) - } + endAdornment={endAdornment} id={id} - inputProps={localInputProps} multiline={isMultiline} + onBlur={onBlur} onChange={onChange} onFocus={onFocus} - onBlur={onBlur} placeholder={placeholder} readOnly={isReadOnly} ref={ref} - startAdornment={ - inputType === "search" ? ( - - - - ) : ( - startAdornment - ) - } - type={inputType} + startAdornment={startAdornment} + type={type} value={value} /> - {errorMessage && ( - - Error: - {errorMessage} - - )} - + ), + [ + autoCompleteType, + autoFocus, + endAdornment, + isMultiline, + onChange, + onFocus, + onBlur, + placeholder, + isReadOnly, + ref, + startAdornment, + type, + value, + ] + ); + + return ( + ); } ); diff --git a/packages/odyssey-react-mui/src/index.ts b/packages/odyssey-react-mui/src/index.ts index 25259e1c4d..ba941e406a 100644 --- a/packages/odyssey-react-mui/src/index.ts +++ b/packages/odyssey-react-mui/src/index.ts @@ -127,6 +127,9 @@ export * from "./Checkbox"; export * from "./CheckboxGroup"; export * from "./CircularProgress"; export * from "./createUniqueId"; +export * from "./FieldError"; +export * from "./FieldHint"; +export * from "./FieldLabel"; export * from "./Icon"; export * from "./iconDictionary"; export * from "./Infobox"; @@ -135,10 +138,11 @@ export * from "./MenuButton"; export * from "./MenuItem"; export * from "./OdysseyCacheProvider"; export * from "./OdysseyThemeProvider"; -export * from "./PasswordInput"; +export * from "./PasswordField"; export * from "./Radio"; export * from "./RadioGroup"; export * from "./ScreenReaderText"; +export * from "./SearchField"; export * from "./Status"; export * from "./TextField"; export * from "./theme"; diff --git a/packages/odyssey-storybook/src/components/odyssey-mui/PasswordField/PasswordField.mdx b/packages/odyssey-storybook/src/components/odyssey-mui/PasswordField/PasswordField.mdx new file mode 100644 index 0000000000..af241daf1d --- /dev/null +++ b/packages/odyssey-storybook/src/components/odyssey-mui/PasswordField/PasswordField.mdx @@ -0,0 +1,11 @@ +import { ArgsTable, Canvas, Meta, Story } from "@storybook/addon-docs"; + + + +# Password Field + +Password inputs ensure that sensitive content is safely obscured. + + + + diff --git a/packages/odyssey-storybook/src/components/odyssey-mui/PasswordField/PasswordField.stories.tsx b/packages/odyssey-storybook/src/components/odyssey-mui/PasswordField/PasswordField.stories.tsx new file mode 100644 index 0000000000..30cc26cd24 --- /dev/null +++ b/packages/odyssey-storybook/src/components/odyssey-mui/PasswordField/PasswordField.stories.tsx @@ -0,0 +1,90 @@ +/*! + * Copyright (c) 2023-present, Okta, Inc. and/or its affiliates. All rights reserved. + * The Okta software accompanied by this notice is provided pursuant to the Apache License, Version 2.0 (the "License.") + * + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0. + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * + * See the License for the specific language governing permissions and limitations under the License. + */ + +import { ComponentMeta, ComponentStory } from "@storybook/react"; +import { PasswordField } from "@okta/odyssey-react-mui"; + +import { MuiThemeDecorator } from "../../../../.storybook/components"; +import PasswordFieldMdx from "./PasswordField.mdx"; + +const storybookMeta: ComponentMeta = { + title: `MUI Components/Forms/PasswordField`, + component: PasswordField, + parameters: { + docs: { + page: PasswordFieldMdx, + }, + }, + argTypes: { + autoCompleteType: { + control: "text", + defaultValue: "name", + }, + autoFocus: { + control: "boolean", + defaultValue: false, + }, + isDisabled: { + control: "boolean", + defaultValue: false, + }, + errorMessage: { + control: "text", + }, + hint: { + control: "text", + }, + id: { + control: "text", + }, + label: { + control: "text", + defaultValue: "Destination", + }, + onBlur: { + control: "function", + }, + onChange: { + control: "function", + }, + onFocus: { + control: "function", + }, + placeholder: { + control: "text", + }, + isReadOnly: { + control: "boolean", + defaultValue: false, + }, + isRequired: { + control: "boolean", + defaultValue: true, + }, + value: { + control: "text", + }, + }, + decorators: [MuiThemeDecorator], +}; + +export default storybookMeta; + +const Template: ComponentStory = (args) => { + return ; +}; + +export const Default = Template.bind({}); +Default.args = { + autoCompleteType: "current-password", + label: "Password", +}; diff --git a/packages/odyssey-storybook/src/components/odyssey-mui/SearchField/SearchField.mdx b/packages/odyssey-storybook/src/components/odyssey-mui/SearchField/SearchField.mdx new file mode 100644 index 0000000000..29417cfc04 --- /dev/null +++ b/packages/odyssey-storybook/src/components/odyssey-mui/SearchField/SearchField.mdx @@ -0,0 +1,11 @@ +import { ArgsTable, Canvas, Meta, Story } from "@storybook/addon-docs"; + + + +# Search Field + +Search inputs allow the user to submit queries. This is the only input type where `placeholder` may be used. + + + + diff --git a/packages/odyssey-storybook/src/components/odyssey-mui/SearchField/SearchField.stories.tsx b/packages/odyssey-storybook/src/components/odyssey-mui/SearchField/SearchField.stories.tsx new file mode 100644 index 0000000000..4623c4ade0 --- /dev/null +++ b/packages/odyssey-storybook/src/components/odyssey-mui/SearchField/SearchField.stories.tsx @@ -0,0 +1,76 @@ +/*! + * Copyright (c) 2023-present, Okta, Inc. and/or its affiliates. All rights reserved. + * The Okta software accompanied by this notice is provided pursuant to the Apache License, Version 2.0 (the "License.") + * + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0. + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * + * See the License for the specific language governing permissions and limitations under the License. + */ + +import { ComponentMeta, ComponentStory } from "@storybook/react"; +import { SearchField } from "@okta/odyssey-react-mui"; + +import { MuiThemeDecorator } from "../../../../.storybook/components"; +import SearchFieldMdx from "./SearchField.mdx"; + +const storybookMeta: ComponentMeta = { + title: `MUI Components/Forms/SearchField`, + component: SearchField, + parameters: { + docs: { + page: SearchFieldMdx, + }, + }, + argTypes: { + autoCompleteType: { + control: "text", + defaultValue: "name", + }, + autoFocus: { + control: "boolean", + defaultValue: false, + }, + isDisabled: { + control: "boolean", + defaultValue: false, + }, + id: { + control: "text", + }, + label: { + control: "text", + defaultValue: "Destination", + }, + onBlur: { + control: "function", + }, + onChange: { + control: "function", + }, + onFocus: { + control: "function", + }, + placeholder: { + control: "text", + }, + value: { + control: "text", + }, + }, + decorators: [MuiThemeDecorator], +}; + +export default storybookMeta; + +const Template: ComponentStory = (args) => { + return ; +}; + +export const Default = Template.bind({}); +Default.args = { + label: "Search", + placeholder: "Search planets", +}; diff --git a/packages/odyssey-storybook/src/components/odyssey-mui/TextField/TextField.mdx b/packages/odyssey-storybook/src/components/odyssey-mui/TextField/TextField.mdx index 4563e37dbe..c8f229d572 100644 --- a/packages/odyssey-storybook/src/components/odyssey-mui/TextField/TextField.mdx +++ b/packages/odyssey-storybook/src/components/odyssey-mui/TextField/TextField.mdx @@ -84,12 +84,6 @@ The values of read-only inputs will be submitted. -### Optional - -Odyssey assumes inputs are required by default. Optional inputs should be used to indicate when data is not required for the user to complete a task. - -**STORY MISSING** - ### Invalid The invalid state is for inputs with incorrect values or values of the wrong format. @@ -119,53 +113,3 @@ Unlike email fields, tel inputs are not automatically validated because global f - -### Password - -Passwords inputs ensure that sensitive content is safely obscured. - - - - - -### Search - -Search inputs allow the user to submit queries. This is the only input type where `placeholder` may be used. - - - - - -## Accessibility - -### Placeholders - -Except for Search inputs, we advise against using placeholder text for inputs. - -#### Translation - -To prevent triggering a change in page layout, browsers don't translate certain attributes. Because of this, users will see untranslated placeholder text. - -#### Recall - -Placeholder text disappears when a field is interacted with. For this reason, it's not suitable for formatting guidelines or necessary context. - -#### Utility - -Placeholder content is limited to static text. Additionally, placeholder text is truncated beyond the width of its input. - -#### Field value confusion - -Low-contrast placeholders may be illegible for some users. Yet, placeholders with compliant contrast can be mistaken for field values. High Contrast Mode will make placeholders and values appear identical. - -Finally, Users with low digital literacy may not understand the purpose or behavior of placeholder text. - -### Purpose - -When collecting an individual's personal data, you must define the input's purpose via the `autocomplete` attribute. This allows users to automate the filling of fields and ensures the purpose is known, regardless of the label. A complete list of fields this is required for may be found in the WCAG spec. - -### Autofocus - -Except for very specific cases, we advise against using the `autoFocus` prop; unless used considerately, the sense of focus being "teleported" to an unexpected part of the page can be jarring to users, especially those using screen readers. - -More details can be found [on MDN's `autofocus` page](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/autofocus#accessibility_considerations). diff --git a/packages/odyssey-storybook/src/components/odyssey-mui/TextField/TextField.stories.tsx b/packages/odyssey-storybook/src/components/odyssey-mui/TextField/TextField.stories.tsx index 57e1dd2033..5638f328fe 100644 --- a/packages/odyssey-storybook/src/components/odyssey-mui/TextField/TextField.stories.tsx +++ b/packages/odyssey-storybook/src/components/odyssey-mui/TextField/TextField.stories.tsx @@ -86,7 +86,7 @@ const storybookMeta: ComponentMeta = { }, type: { control: "select", - options: ["text", "email", "search", "tel", "password"], + options: ["email", "number", "tel", "text", "url"], defaultValue: "text", }, value: { @@ -141,7 +141,6 @@ Adornment.args = { }; // Types - export const Email = Template.bind({}); Email.args = { autoCompleteType: "work email", @@ -156,20 +155,6 @@ Multiline.args = { isMultiline: true, }; -export const Password = Template.bind({}); -Password.args = { - autoCompleteType: "current-password", - label: "Password", - type: "password", -}; - -export const Search = Template.bind({}); -Search.args = { - label: "Search", - placeholder: "Search planets", - type: "search", -}; - export const Tel = Template.bind({}); Tel.args = { autoCompleteType: "mobile tel", diff --git a/packages/odyssey-storybook/src/components/customization/CustomTheme.mdx b/packages/odyssey-storybook/src/contributing/CustomTheme.mdx similarity index 100% rename from packages/odyssey-storybook/src/components/customization/CustomTheme.mdx rename to packages/odyssey-storybook/src/contributing/CustomTheme.mdx diff --git a/packages/odyssey-storybook/src/components/customization/CustomTheme.stories.tsx b/packages/odyssey-storybook/src/contributing/CustomTheme.stories.tsx similarity index 97% rename from packages/odyssey-storybook/src/components/customization/CustomTheme.stories.tsx rename to packages/odyssey-storybook/src/contributing/CustomTheme.stories.tsx index 6ce723cc93..77591ac5ff 100644 --- a/packages/odyssey-storybook/src/components/customization/CustomTheme.stories.tsx +++ b/packages/odyssey-storybook/src/contributing/CustomTheme.stories.tsx @@ -70,7 +70,7 @@ export const TextFieldStory: StoryFn = () => { return (
- +
); diff --git a/packages/odyssey-storybook/src/guidelines/Form Field Accessibility.stories.mdx b/packages/odyssey-storybook/src/guidelines/Form Field Accessibility.stories.mdx new file mode 100644 index 0000000000..8a74300511 --- /dev/null +++ b/packages/odyssey-storybook/src/guidelines/Form Field Accessibility.stories.mdx @@ -0,0 +1,37 @@ +import { Meta } from "@storybook/addon-docs"; + + + +## Form Field Accessibility + +### Placeholders + +Except for Search inputs, we advise against using placeholder text for inputs. + +#### Translation + +To prevent triggering a change in page layout, browsers don't translate certain attributes. Because of this, users will see untranslated placeholder text. + +#### Recall + +Placeholder text disappears when a field is interacted with. For this reason, it's not suitable for formatting guidelines or necessary context. + +#### Utility + +Placeholder content is limited to static text. Additionally, placeholder text is truncated beyond the width of its input. + +#### Field value confusion + +Low-contrast placeholders may be illegible for some users. Yet, placeholders with compliant contrast can be mistaken for field values. High Contrast Mode will make placeholders and values appear identical. + +Finally, Users with low digital literacy may not understand the purpose or behavior of placeholder text. + +### Purpose + +When collecting an individual's personal data, you must define the input's purpose via the `autocomplete` attribute. This allows users to automate the filling of fields and ensures the purpose is known, regardless of the label. A complete list of fields this is required for may be found in the WCAG spec. + +### Autofocus + +Except for very specific cases, we advise against using the `autoFocus` prop; unless used considerately, the sense of focus being "teleported" to an unexpected part of the page can be jarring to users, especially those using screen readers. + +More details can be found [on MDN's `autofocus` page](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/autofocus#accessibility_considerations).