From e724cb027c7749d2371bfaf298dc7bf88e5fabab Mon Sep 17 00:00:00 2001 From: nikitaorliak-cengage Date: Fri, 24 Jan 2025 17:42:05 +0100 Subject: [PATCH] fix(Input): Clarification that developers should use `ref` with `Input`. Add a new example for Storybook. (#1526) Co-authored-by: Nikita Orliak --- .../a11y-input-error-message-not-read.md | 5 + .../Alert/__snapshots__/Alert.test.js.snap | 48 ++++++- .../src/components/AlertBase/index.tsx | 6 +- .../src/components/Form/index.tsx | 5 +- .../src/components/Input/Input.stories.tsx | 111 +++++++++++++++- .../react-magma-docs/src/pages/api/form.mdx | 122 +++++++++++++++++- .../react-magma-docs/src/pages/api/input.mdx | 8 +- 7 files changed, 291 insertions(+), 14 deletions(-) create mode 100644 .changeset/a11y-input-error-message-not-read.md diff --git a/.changeset/a11y-input-error-message-not-read.md b/.changeset/a11y-input-error-message-not-read.md new file mode 100644 index 0000000000..d996b451d0 --- /dev/null +++ b/.changeset/a11y-input-error-message-not-read.md @@ -0,0 +1,5 @@ +--- +'react-magma-docs': patch +--- + +fix(Input): Clarification that developers should use `ref` with `Input`. Add a new example for Storybook. diff --git a/packages/react-magma-dom/src/components/Alert/__snapshots__/Alert.test.js.snap b/packages/react-magma-dom/src/components/Alert/__snapshots__/Alert.test.js.snap index 31bb651f6e..c50f9e4128 100644 --- a/packages/react-magma-dom/src/components/Alert/__snapshots__/Alert.test.js.snap +++ b/packages/react-magma-dom/src/components/Alert/__snapshots__/Alert.test.js.snap @@ -389,6 +389,10 @@ exports[`Alert Variants should render an alert with danger variant 1`] = ` } } +.emotion-8 { + white-space: pre-line; +} + .emotion-0 { -webkit-align-items: stretch; -webkit-box-align: stretch; @@ -492,6 +496,10 @@ exports[`Alert Variants should render an alert with danger variant 1`] = ` } } +.emotion-8 { + white-space: pre-line; +} +
- + Test Alert Text
@@ -634,6 +644,10 @@ exports[`Alert Variants should render an alert with info variant 1`] = ` } } +.emotion-8 { + white-space: pre-line; +} + .emotion-0 { -webkit-align-items: stretch; -webkit-box-align: stretch; @@ -737,6 +751,10 @@ exports[`Alert Variants should render an alert with info variant 1`] = ` } } +.emotion-8 { + white-space: pre-line; +} +
- + Test Alert Text
@@ -879,6 +899,10 @@ exports[`Alert Variants should render an alert with warning variant 1`] = ` } } +.emotion-8 { + white-space: pre-line; +} + .emotion-0 { -webkit-align-items: stretch; -webkit-box-align: stretch; @@ -982,6 +1006,10 @@ exports[`Alert Variants should render an alert with warning variant 1`] = ` } } +.emotion-8 { + white-space: pre-line; +} +
- + Test Alert Text
@@ -1124,6 +1154,10 @@ exports[`Alert should render an alert with default variant 1`] = ` } } +.emotion-8 { + white-space: pre-line; +} + .emotion-0 { -webkit-align-items: stretch; -webkit-box-align: stretch; @@ -1227,6 +1261,10 @@ exports[`Alert should render an alert with default variant 1`] = ` } } +.emotion-8 { + white-space: pre-line; +} +
- + Test Alert Text
diff --git a/packages/react-magma-dom/src/components/AlertBase/index.tsx b/packages/react-magma-dom/src/components/AlertBase/index.tsx index 109febb36d..1380bb3060 100644 --- a/packages/react-magma-dom/src/components/AlertBase/index.tsx +++ b/packages/react-magma-dom/src/components/AlertBase/index.tsx @@ -379,6 +379,10 @@ const DismissButton = styled(IconButton, { shouldForwardProp })<{ } `; +const AlertSpan = styled.span` + white-space: pre-line; +` + function renderIcon(variant = 'info', isToast?: boolean, theme?: any) { const Icon = VARIANT_ICON[variant]; @@ -479,7 +483,7 @@ export const AlertBase = React.forwardRef( isDismissible={isDismissible} theme={theme} > - {children} + {children} {additionalContent && ( {additionalContent} diff --git a/packages/react-magma-dom/src/components/Form/index.tsx b/packages/react-magma-dom/src/components/Form/index.tsx index 9544c18a49..014b140ae0 100644 --- a/packages/react-magma-dom/src/components/Form/index.tsx +++ b/packages/react-magma-dom/src/components/Form/index.tsx @@ -8,6 +8,7 @@ import { ThemeInterface } from '../../theme/magma'; import { InverseContext, useIsInverse } from '../../inverse'; import styled from '@emotion/styled'; import { TypographyContextVariant, TypographyVisualStyle } from '../Typography'; +import { Announce } from '../Announce'; /** * @children required @@ -108,7 +109,9 @@ export const Form = React.forwardRef( {description && {description}} {errorMessage && ( - {errorMessage} + + {errorMessage} + )}
{children}
{actions} diff --git a/packages/react-magma-dom/src/components/Input/Input.stories.tsx b/packages/react-magma-dom/src/components/Input/Input.stories.tsx index 4329385726..71725aff1e 100644 --- a/packages/react-magma-dom/src/components/Input/Input.stories.tsx +++ b/packages/react-magma-dom/src/components/Input/Input.stories.tsx @@ -2,13 +2,14 @@ import { Meta, Story } from '@storybook/react/types-6-0'; import React from 'react'; import { HelpIcon, NotificationsIcon, WorkIcon } from 'react-magma-icons'; import { Input, InputProps } from '.'; -import { ButtonSize, ButtonType, ButtonVariant } from '../Button'; +import { Button, ButtonSize, ButtonType, ButtonVariant } from '../Button'; import { Card, CardBody } from '../Card'; import { IconButton } from '../IconButton'; import { InputIconPosition, InputSize, InputType } from '../InputBase'; import { LabelPosition } from '../Label'; import { Tooltip } from '../Tooltip'; import { Spacer } from '../Spacer'; +import { ButtonGroup } from '../ButtonGroup'; const Template: Story = args => ( <> @@ -211,7 +212,11 @@ export const HelpLink = args => { - + { + const [inputValues, setInputValues] = React.useState({ + firstName: '', + lastName: '', + emailAddress: '', + }); + const [hasErrors, setHasErrors] = React.useState({ + firstName: false, + lastName: false, + emailAddress: false, + }); + + const firstNameInputRef = React.useRef(); + const lastNameInputRef = React.useRef(); + const emailAddressInputRef = React.useRef(); + + const submit = () => { + setHasErrors({ + firstName: false, + lastName: false, + emailAddress: false, + }); + + if (!inputValues.emailAddress) { + setHasErrors(prev => ({ ...prev, emailAddress: true })); + emailAddressInputRef.current.focus(); + } + + if (!inputValues.lastName) { + setHasErrors(prev => ({ ...prev, lastName: true })); + lastNameInputRef.current.focus(); + } + + if (!inputValues.firstName) { + setHasErrors(prev => ({ ...prev, firstName: true })); + firstNameInputRef.current.focus(); + } + }; + + const reset = () => { + setHasErrors({ + firstName: false, + lastName: false, + emailAddress: false, + }); + setInputValues({ + firstName: '', + lastName: '', + emailAddress: '', + }); + + firstNameInputRef.current.focus(); + }; + + return ( + <> + + setInputValues(prev => ({ ...prev, firstName: event.target.value })) + } + required + value={inputValues.firstName} + ref={firstNameInputRef} + /> +
+ + setInputValues(prev => ({ ...prev, lastName: event.target.value })) + } + required + value={inputValues.lastName} + ref={lastNameInputRef} + /> +
+ + setInputValues(prev => ({ ...prev, emailAddress: event.target.value })) + } + required + value={inputValues.emailAddress} + ref={emailAddressInputRef} + /> +
+ + + + + + ); +}; diff --git a/website/react-magma-docs/src/pages/api/form.mdx b/website/react-magma-docs/src/pages/api/form.mdx index 2d84b46d52..7a94181fe8 100644 --- a/website/react-magma-docs/src/pages/api/form.mdx +++ b/website/react-magma-docs/src/pages/api/form.mdx @@ -27,23 +27,135 @@ import { } from 'react-magma-dom'; export function Example() { + const [state, setState] = React.useState({ + firstName: '', + lastName: '', + email: '', + }); + + const [errors, setErrors] = React.useState({ + firstName: false, + lastName: false, + email: false, + }); + + const resetErrors = () => { + setErrors({ + firstName: false, + lastName: false, + email: false, + }); + }; + + const onSubmit = event => { + event.preventDefault(); + resetErrors(); + + if (!state.firstName) { + setErrors(prevErrors => ({ ...prevErrors, firstName: true })); + } + + if (!state.lastName) { + setErrors(prevErrors => ({ ...prevErrors, lastName: true })); + } + + if (!state.email) { + setErrors(prevErrors => ({ ...prevErrors, email: true })); + } + }; + + const cancel = () => { + setState({ + firstName: '', + lastName: '', + email: '', + }); + setErrors({ + firstName: false, + lastName: false, + email: false, + }); + }; + + const errorMessage = React.useMemo(() => { + let message = ''; + + if (Object.values(errors).some(error => error)) { + message += 'Please fix the following errors:\n'; + } + + for (const error in errors) { + if (errors[error]) { + switch (error) { + case 'firstName': + message += '· First Name is required\n'; + break; + case 'lastName': + message += '· Last Name is required\n'; + break; + case 'email': + message += '· Email is required\n'; + break; + default: + return; + } + } + } + + return message; + }, [errors]); + return (
alert('form submitted')} + onSubmit={onSubmit} header="Form Heading" description="Some Form Description" - errorMessage="Some Form Error" + errorMessage={errorMessage} actions={ - + } > <> - + + setState(prevState => ({ + ...prevState, + firstName: event.target.value, + })) + } + errorMessage={errors.firstName && 'First Name is required'} + /> + + + setState(prevState => ({ + ...prevState, + lastName: event.target.value, + })) + } + errorMessage={errors.lastName && 'Last Name is required'} + /> - + + setState(prevState => ({ + ...prevState, + email: event.target.value, + })) + } + errorMessage={errors.email && 'Email is required'} + /> diff --git a/website/react-magma-docs/src/pages/api/input.mdx b/website/react-magma-docs/src/pages/api/input.mdx index 9a1936a8de..49240f6a1e 100644 --- a/website/react-magma-docs/src/pages/api/input.mdx +++ b/website/react-magma-docs/src/pages/api/input.mdx @@ -170,7 +170,9 @@ If an error message is present, it will replace the helper text. Can be a node o The `required` prop can be used to indicate when a field is required. It is also important to indicate to the user whenever a field is required. -While React Magma provides the error styling, it is up to the consumer app to handle the validation. +While React Magma provides the error styling, it is up to the consumer app to handle the validation. We recommend using a `ref` on the input for accessibility. + +For short forms with an error, clicking submit should bring the focus back to the input with an error. For long forms, we recommend using an alert to combine the errors and focus should be moved to the alert. See example in Form. ```tsx import React from 'react'; @@ -178,10 +180,12 @@ import { Input, Button, ButtonGroup, Spacer } from 'react-magma-dom'; export function Example() { const [hasError, setHasError] = React.useState(false); const [nameValue, setNameValue] = React.useState(''); + const inputRef = React.useRef(); function submit() { if (nameValue === '') { setHasError(true); + inputRef.current.focus(); } else { setHasError(false); } @@ -190,6 +194,7 @@ export function Example() { function reset() { setHasError(false); setNameValue(''); + inputRef.current.focus(); } return ( @@ -201,6 +206,7 @@ export function Example() { onChange={event => setNameValue(event.target.value)} required value={nameValue} + ref={inputRef} />