Skip to content

Commit

Permalink
feat(WCAG): Associate field descriptions to inputs
Browse files Browse the repository at this point in the history
  • Loading branch information
pedrobonamin committed Sep 8, 2023
1 parent c27ee3e commit 39b2979
Show file tree
Hide file tree
Showing 17 changed files with 75 additions and 8 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import {FormNodeValidation} from '@sanity/types'
import {Box, Flex, Stack, Text} from '@sanity/ui'
import React, {memo} from 'react'
import {constructDescriptionId} from '../../members/common/constructDescriptionId'
import {FormFieldValidationStatus} from './FormFieldValidationStatus'

/** @internal */
Expand Down Expand Up @@ -45,7 +46,7 @@ export const FormFieldHeaderText = memo(function FormFieldHeaderText(
</Flex>

{description && (
<Text muted size={1}>
<Text muted size={1} id={constructDescriptionId(inputId, description)}>
{description}
</Text>
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {FormNodeValidation} from '@sanity/types'
import {FormNodePresence} from '../../../presence'
import {DocumentFieldActionNode} from '../../../config'
import {useFieldActions} from '../../field'
import {constructDescriptionId} from '../../members/common/constructDescriptionId'
import {FormFieldValidationStatus} from './FormFieldValidationStatus'
import {FormFieldSetLegend} from './FormFieldSetLegend'
import {focusRingStyle} from './styles'
Expand Down Expand Up @@ -43,6 +44,7 @@ export interface FormFieldSetProps {
* @beta
*/
validation?: FormNodeValidation[]
inputId: string
}

function getChildren(children: React.ReactNode | (() => React.ReactNode)): React.ReactNode {
Expand Down Expand Up @@ -109,6 +111,7 @@ export const FormFieldSet = forwardRef(function FormFieldSet(
tabIndex,
title,
validation = EMPTY_ARRAY,
inputId,
...restProps
} = props

Expand Down Expand Up @@ -170,7 +173,7 @@ export const FormFieldSet = forwardRef(function FormFieldSet(
</Flex>

{description && (
<Text muted size={1}>
<Text muted size={1} id={constructDescriptionId(inputId, description)}>
{description}
</Text>
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -351,10 +351,11 @@ export function Compositor(props: Omit<InputProps, 'schemaType' | 'arrayFunction
renderPreview,
],
)

const describedBy = props.elementProps['aria-describedby']
const editorNode = useMemo(
() => (
<Editor
describedBy={describedBy}
hasFocus={hasFocus}
hotkeys={editorHotkeys}
isActive={isActive}
Expand Down Expand Up @@ -390,6 +391,7 @@ export function Compositor(props: Omit<InputProps, 'schemaType' | 'arrayFunction
onPaste,
path,
readOnly,
describedBy,
],
)

Expand Down
4 changes: 4 additions & 0 deletions packages/sanity/src/core/form/inputs/PortableText/Editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ interface EditorProps {
scrollElement: HTMLElement | null
setPortalElement?: (portalElement: HTMLDivElement | null) => void
setScrollElement: (scrollElement: HTMLElement | null) => void
describedBy: string | undefined
}

const renderDecorator: RenderDecoratorFunction = (props) => {
Expand Down Expand Up @@ -84,6 +85,7 @@ export function Editor(props: EditorProps) {
scrollElement,
setPortalElement,
setScrollElement,
describedBy,
} = props
const {isTopLayer} = useLayer()
const editableRef = useRef<HTMLDivElement | null>(null)
Expand Down Expand Up @@ -148,6 +150,7 @@ export function Editor(props: EditorProps) {
selection={initialSelection}
style={noOutlineStyle}
spellCheck={spellcheck}
aria-describedby={describedBy}
/>
),
[
Expand All @@ -161,6 +164,7 @@ export function Editor(props: EditorProps) {
renderPlaceholder,
scrollSelectionIntoView,
spellcheck,
describedBy,
],
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,7 @@ export function ReferenceField(props: ReferenceFieldProps) {
level={props.level}
title={props.title}
validation={props.validation}
inputId={props.inputId}
>
{isEditing ? (
<Box>{children}</Box>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -369,6 +369,7 @@ export function ReferenceItem<Item extends ReferenceItemValue = ReferenceItemVal
description={schemaType.description}
__unstable_presence={presence}
validation={validation}
inputId={inputId}
>
{children}
</FormFieldSet>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {createProtoValue} from '../../../utils/createProtoValue'
import {isEmptyItem} from '../../../store/utils/isEmptyItem'
import {useResolveInitialValueForType} from '../../../../store'
import {resolveInitialArrayValues} from '../../common/resolveInitialArrayValues'
import {constructDescriptionId} from '../../common/constructDescriptionId'

/**
*
Expand Down Expand Up @@ -242,8 +243,12 @@ export function ArrayOfObjectsItem(props: MemberItemProps) {
onFocus: handleFocus,
id: member.item.id,
ref: focusRef,
'aria-describedby': constructDescriptionId(
member.item.id,
member.item.schemaType.description,
),
}),
[handleBlur, handleFocus, member.item.id],
[handleBlur, handleFocus, member.item.id, member.item.schemaType.description],
)

const inputProps = useMemo((): Omit<ObjectInputProps, 'renderDefault'> => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
import {insert, PatchArg, PatchEvent, set, unset} from '../../../patch'
import {useFormCallbacks} from '../../../studio/contexts/FormCallbacks'
import {resolveNativeNumberInputValue} from '../../common/resolveNativeNumberInputValue'
import {constructDescriptionId} from '../../common/constructDescriptionId'

/**
*
Expand Down Expand Up @@ -111,6 +112,10 @@ export function ArrayOfPrimitivesItem(props: PrimitiveMemberItemProps) {
value: resolveNativeInputValue(member.item.schemaType, member.item.value, localValue),
readOnly: Boolean(member.item.readOnly),
placeholder: member.item.schemaType.placeholder,
'aria-describedby': constructDescriptionId(
member.item.id,
member.item.schemaType.description,
),
}),
[
handleBlur,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import React from 'react'

/**
* Creates a description id from a field id, for use with aria-describedby in the field,
* and added to the descriptive element id.
* @internal
*/
export function constructDescriptionId(
id: string | undefined,
description: React.ReactNode | undefined,
): string | undefined {
if (!description || !id) return undefined
return `desc_${id}`
}
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ export const MemberFieldSet = memo(function MemberFieldSet(props: {
onExpand={handleExpand}
columns={member?.fieldSet?.columns}
data-testid={`fieldset-${member.fieldSet.name}`}
inputId={member.fieldSet.name}
>
{member.fieldSet.members.map((fieldsetMember) => {
if (fieldsetMember.kind === 'error') {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import {resolveInitialArrayValues} from '../../common/resolveInitialArrayValues'
import {applyAll} from '../../../patch/applyPatch'
import {useFormPublishedId} from '../../../useFormPublishedId'
import {DocumentFieldActionNode} from '../../../../config'
import {constructDescriptionId} from '../../common/constructDescriptionId'

/**
* Responsible for creating inputProps and fieldProps to pass to ´renderInput´ and ´renderField´ for an array input
Expand Down Expand Up @@ -298,8 +299,12 @@ export function ArrayOfObjectsField(props: {
onFocus: handleFocus,
id: member.field.id,
ref: focusRef,
'aria-describedby': constructDescriptionId(
member.field.id,
member.field.schemaType.description,
),
}),
[handleBlur, handleFocus, member.field.id],
[handleBlur, handleFocus, member.field.id, member.field.schemaType.description],
)

const client = useClient(DEFAULT_STUDIO_CLIENT_OPTIONS)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import {readAsText} from '../../../studio/uploads/file/readAsText'
import {accepts} from '../../../studio/uploads/accepts'
import {applyAll} from '../../../patch/applyPatch'
import {useFormBuilder} from '../../../useFormBuilder'
import {constructDescriptionId} from '../../common/constructDescriptionId'

function move<T>(arr: T[], from: number, to: number): T[] {
const copy = arr.slice()
Expand Down Expand Up @@ -301,8 +302,12 @@ export function ArrayOfPrimitivesField(props: {
onFocus: handleFocus,
id: member.field.id,
ref: focusRef,
'aria-describedby': constructDescriptionId(
member.field.id,
member.field.schemaType.description,
),
}),
[handleBlur, handleFocus, member.field.id],
[handleBlur, handleFocus, member.field.id, member.field.schemaType.description],
)

const plainTextUploader = useMemo(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {FormCallbacksProvider, useFormCallbacks} from '../../../studio/contexts/
import {createProtoValue} from '../../../utils/createProtoValue'
import {applyAll} from '../../../patch/applyPatch'
import {useFormBuilder} from '../../../useFormBuilder'
import {constructDescriptionId} from '../../common/constructDescriptionId'

/**
* Responsible for creating inputProps and fieldProps to pass to ´renderInput´ and ´renderField´ for an object input
Expand Down Expand Up @@ -183,8 +184,12 @@ export const ObjectField = function ObjectField(props: {
onFocus: handleFocus,
id: member.field.id,
ref: focusRef,
'aria-describedby': constructDescriptionId(
member.field.id,
member.field.schemaType.description,
),
}),
[handleBlur, handleFocus, member.field.id],
[handleBlur, handleFocus, member.field.id, member.field.schemaType.description],
)

const inputProps = useMemo((): Omit<ObjectInputProps, 'renderDefault'> => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {FormPatch, PatchEvent, set, unset} from '../../../patch'
import {useFormCallbacks} from '../../../studio/contexts/FormCallbacks'
import {resolveNativeNumberInputValue} from '../../common/resolveNativeNumberInputValue'
import {useFormBuilder} from '../../../useFormBuilder'
import {constructDescriptionId} from '../../common/constructDescriptionId'

/**
* Responsible for creating inputProps and fieldProps to pass to ´renderInput´ and ´renderField´ for a primitive field/input
Expand Down Expand Up @@ -105,6 +106,10 @@ export function PrimitiveField(props: {
value: resolveNativeNumberInputValue(member.field.schemaType, member.field.value, localValue),
readOnly: Boolean(member.field.readOnly),
placeholder: member.field.schemaType.placeholder,
'aria-describedby': constructDescriptionId(
member.field.id,
member.field.schemaType.description,
),
}),
[
handleBlur,
Expand Down
8 changes: 7 additions & 1 deletion packages/sanity/src/core/form/studio/FormBuilder.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,13 @@ function RootInput() {

const rootInputProps: Omit<ObjectInputProps, 'renderDefault'> = {
focusPath,
elementProps: {ref: focusRef, id, onBlur: handleBlur, onFocus: handleFocus},
elementProps: {
ref: focusRef,
id,
onBlur: handleBlur,
onFocus: handleFocus,
'aria-describedby': undefined, // Root input should not have any aria-describedby
},
changed: members.some((m) => m.kind === 'field' && m.field.changed),
focused,
groups,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ function ObjectOrArrayField(field: ObjectFieldProps | ArrayFieldProps) {
onExpand={field.onExpand}
title={field.title}
validation={field.validation}
inputId={field.inputId}
>
{field.children}
</FormFieldSet>
Expand Down Expand Up @@ -152,6 +153,7 @@ function ImageOrFileField(field: ObjectFieldProps) {
onExpand={field.onExpand}
title={field.title}
validation={field.validation}
inputId={field.inputId}
>
{field.children}
</FormFieldSet>
Expand Down
2 changes: 2 additions & 0 deletions packages/sanity/src/core/form/types/inputProps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -392,6 +392,7 @@ export interface PrimitiveInputElementProps {
onFocus: FocusEventHandler
onBlur: FocusEventHandler
ref: React.MutableRefObject<any>
'aria-describedby': string | undefined
}

/**
Expand All @@ -402,6 +403,7 @@ export interface ComplexElementProps {
onFocus: FocusEventHandler
onBlur: FocusEventHandler
ref: React.MutableRefObject<any>
'aria-describedby': string | undefined
}

/**
Expand Down

0 comments on commit 39b2979

Please sign in to comment.