diff --git a/packages/web-react/src/components/Checkbox/Checkbox.tsx b/packages/web-react/src/components/Checkbox/Checkbox.tsx index 523e92999e..00de2cc8a1 100644 --- a/packages/web-react/src/components/Checkbox/Checkbox.tsx +++ b/packages/web-react/src/components/Checkbox/Checkbox.tsx @@ -3,7 +3,7 @@ import classNames from 'classnames'; import { useStyleProps } from '../../hooks'; import { SpiritCheckboxProps } from '../../types'; import { HelperText, ValidationText } from '../Field'; -import useAriaDescribedBy from '../Field/useAriaDescribedBy'; +import useAriaIds from '../Field/useAriaIds'; import { useCheckboxStyleProps } from './useCheckboxStyleProps'; /* We need an exception for components exported with forwardRef */ @@ -25,18 +25,13 @@ const _Checkbox = (props: SpiritCheckboxProps, ref: ForwardedRef {label} {helperText && ( - + {helperText} )} @@ -57,8 +57,9 @@ const _Checkbox = (props: SpiritCheckboxProps, ref: ForwardedRef )} diff --git a/packages/web-react/src/components/Field/HelperText.tsx b/packages/web-react/src/components/Field/HelperText.tsx index 11210710ed..e781e49b27 100644 --- a/packages/web-react/src/components/Field/HelperText.tsx +++ b/packages/web-react/src/components/Field/HelperText.tsx @@ -1,20 +1,35 @@ -import React, { ElementType } from 'react'; +import React, { ElementType, useEffect } from 'react'; +import { RegisterType } from './useAriaIds'; interface Props { children: React.ReactNode; className?: string; elementType?: ElementType; id?: string; + registerAria?: RegisterType; } const defaultProps = { className: undefined, elementType: 'div', id: undefined, + registerAria: undefined, }; const HelperText = (props: Props) => { - const { children, className, elementType: ElementTag = 'div', id } = props; + const { children, className, elementType: ElementTag = 'div', id, registerAria } = props; + + useEffect(() => { + if (!registerAria) return; + + registerAria({ add: id }); + + return () => { + registerAria({ remove: id }); + }; + + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); return ( diff --git a/packages/web-react/src/components/Field/ValidationText.tsx b/packages/web-react/src/components/Field/ValidationText.tsx index 0ef2228307..485228d26e 100644 --- a/packages/web-react/src/components/Field/ValidationText.tsx +++ b/packages/web-react/src/components/Field/ValidationText.tsx @@ -1,20 +1,35 @@ -import React, { ElementType } from 'react'; +import React, { ElementType, useEffect } from 'react'; import { ValidationTextProp } from '../../types'; +import { RegisterType } from './useAriaIds'; export interface ValidationTextProps extends ValidationTextProp { className?: string; elementType?: ElementType; id?: string; + registerAria?: RegisterType; } const defaultProps = { className: undefined, elementType: 'div', id: undefined, + registerAria: undefined, }; export const ValidationText = (props: ValidationTextProps) => { - const { className, validationText, elementType: ElementTag = 'div', id } = props; + const { className, validationText, elementType: ElementTag = 'div', id, registerAria } = props; + + useEffect(() => { + if (!registerAria) return; + + registerAria({ add: id }); + + return () => { + registerAria({ remove: id }); + }; + + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); return Array.isArray(validationText) ? (
    diff --git a/packages/web-react/src/components/Field/index.ts b/packages/web-react/src/components/Field/index.ts index 66dba1ff66..8a366da225 100644 --- a/packages/web-react/src/components/Field/index.ts +++ b/packages/web-react/src/components/Field/index.ts @@ -1,5 +1,5 @@ export * from './ValidationText'; export * from './HelperText'; -export * from './useAriaDescribedBy'; +export * from './useAriaIds'; export { default as ValidationText } from './ValidationText'; export { default as HelperText } from './HelperText'; diff --git a/packages/web-react/src/components/Field/useAriaDescribedBy.tsx b/packages/web-react/src/components/Field/useAriaDescribedBy.tsx deleted file mode 100644 index e10fcddba2..0000000000 --- a/packages/web-react/src/components/Field/useAriaDescribedBy.tsx +++ /dev/null @@ -1,18 +0,0 @@ -type UseAriaDescribedByProps = { - id?: string; - helperText?: string; - validationText?: string | string[]; - ariaDescribedBy?: string; -}; - -const useAriaDescribedBy = ({ id, helperText, validationText, ariaDescribedBy }: UseAriaDescribedByProps) => { - if (!id) return { validationTextId: undefined, helperTextId: undefined, joinedAriaDescribedBy: ariaDescribedBy }; - - const validationTextId = id && validationText ? `${id}__validationText` : undefined; - const helperTextId = id && helperText ? `${id}__helperText` : undefined; - const joinedAriaDescribedBy = [ariaDescribedBy, helperTextId, validationTextId].join(' ').trim(); - - return { validationTextId, helperTextId, joinedAriaDescribedBy }; -}; - -export default useAriaDescribedBy; diff --git a/packages/web-react/src/components/Field/useAriaIds.tsx b/packages/web-react/src/components/Field/useAriaIds.tsx new file mode 100644 index 0000000000..28ad3600cd --- /dev/null +++ b/packages/web-react/src/components/Field/useAriaIds.tsx @@ -0,0 +1,29 @@ +import { useCallback, useState } from 'react'; + +export type RegisterParams = { add?: string; remove?: string }; +export type RegisterType = (params: RegisterParams) => void; +export type UseAriaIdsHook = (otherAriaIds?: string) => [string[], RegisterType]; + +const useAriaIds: UseAriaIdsHook = (otherAriaIds) => { + const [ids, setIds] = useState(otherAriaIds ? otherAriaIds.split(' ') : []); + + const register = useCallback(function registerFn({ add, remove }: RegisterParams) { + setIds((prevIds) => { + let newIds = [...prevIds]; + + if (remove) { + newIds = newIds.filter((item) => item !== remove); + } + + if (add) { + newIds = [...newIds, add]; + } + + return newIds; + }); + }, []); + + return [ids, register]; +}; + +export default useAriaIds; diff --git a/packages/web-react/src/components/FieldGroup/FieldGroup.tsx b/packages/web-react/src/components/FieldGroup/FieldGroup.tsx index a4d66a825c..1a1f8d0a91 100644 --- a/packages/web-react/src/components/FieldGroup/FieldGroup.tsx +++ b/packages/web-react/src/components/FieldGroup/FieldGroup.tsx @@ -2,9 +2,9 @@ import React from 'react'; import classNames from 'classnames'; import { SpiritFieldGroupProps } from '../../types'; import { useStyleProps } from '../../hooks'; -import useAriaDescribedBy from '../Field/useAriaDescribedBy'; import HelperText from '../Field/HelperText'; import ValidationText from '../Field/ValidationText'; +import useAriaIds from '../Field/useAriaIds'; import { useFieldGroupStyleProps } from './useFieldGroupStyleProps'; const FieldGroup = (props: SpiritFieldGroupProps) => { @@ -26,18 +26,13 @@ const FieldGroup = (props: SpiritFieldGroupProps) => { const { classProps } = useFieldGroupStyleProps({ isFluid, isRequired, validationState }); const { styleProps, props: transferProps } = useStyleProps(rest); - const { validationTextId, helperTextId, joinedAriaDescribedBy } = useAriaDescribedBy({ - id, - helperText, - validationText, - ariaDescribedBy, - }); + const [ids, register] = useAriaIds(ariaDescribedBy); return (
    @@ -49,12 +44,17 @@ const FieldGroup = (props: SpiritFieldGroupProps) => { )}
    {children}
    {helperText && ( - + {helperText} )} {validationState && validationText && ( - + )}
    ); diff --git a/packages/web-react/src/components/FileUploader/FileUploaderInput.tsx b/packages/web-react/src/components/FileUploader/FileUploaderInput.tsx index 61f063e2d2..7c9da5f597 100644 --- a/packages/web-react/src/components/FileUploader/FileUploaderInput.tsx +++ b/packages/web-react/src/components/FileUploader/FileUploaderInput.tsx @@ -3,11 +3,11 @@ import classNames from 'classnames'; import { SpiritFileUploaderInputProps } from '../../types'; import { useStyleProps } from '../../hooks'; import { HelperText, ValidationText } from '../Field'; +import { Icon } from '../Icon'; +import useAriaIds from '../Field/useAriaIds'; import { DEFAULT_FILE_QUEUE_LIMIT, DEFAULT_FILE_SIZE_LIMIT } from './constants'; import { useFileUploaderStyleProps } from './useFileUploaderStyleProps'; import { useFileUploaderInput } from './useFileUploaderInput'; -import { Icon } from '../Icon'; -import useAriaDescribedBy from '../Field/useAriaDescribedBy'; const FileUploaderInput = (props: SpiritFileUploaderInputProps) => { const { @@ -66,12 +66,7 @@ const FileUploaderInput = (props: SpiritFileUploaderInputProps) => { }); const { styleProps, props: transferProps } = useStyleProps(restProps); - const { validationTextId, helperTextId, joinedAriaDescribedBy } = useAriaDescribedBy({ - id, - helperText, - validationText, - ariaDescribedBy, - }); + const [ids, register] = useAriaIds(ariaDescribedBy); return (
    { {label} { {labelText} {helperText && ( - + {helperText} )} @@ -116,8 +111,9 @@ const FileUploaderInput = (props: SpiritFileUploaderInputProps) => { )}
    diff --git a/packages/web-react/src/components/Radio/Radio.tsx b/packages/web-react/src/components/Radio/Radio.tsx index dc859e2a64..7a4377ccd5 100644 --- a/packages/web-react/src/components/Radio/Radio.tsx +++ b/packages/web-react/src/components/Radio/Radio.tsx @@ -3,7 +3,7 @@ import classNames from 'classnames'; import { useStyleProps } from '../../hooks'; import { SpiritRadioProps } from '../../types'; import { HelperText } from '../Field'; -import useAriaDescribedBy from '../Field/useAriaDescribedBy'; +import useAriaIds from '../Field/useAriaIds'; import { useRadioStyleProps } from './useRadioStyleProps'; /* We need an exception for components exported with forwardRef */ @@ -23,18 +23,13 @@ const _Radio = (props: SpiritRadioProps, ref: ForwardedRef): J } = modifiedProps; const { styleProps, props: otherProps } = useStyleProps(restProps); - const { helperTextId, joinedAriaDescribedBy } = useAriaDescribedBy({ - id, - helperText, - validationText: undefined, - ariaDescribedBy, - }); + const [ids, register] = useAriaIds(ariaDescribedBy); return (