From 39b2979504c65519c08aba2025793ee9ba52a861 Mon Sep 17 00:00:00 2001 From: Pedro Bonamin Date: Tue, 5 Sep 2023 18:15:22 +0200 Subject: [PATCH 1/6] feat(WCAG): Associate field descriptions to inputs --- .../components/formField/FormFieldHeaderText.tsx | 3 ++- .../form/components/formField/FormFieldSet.tsx | 5 ++++- .../core/form/inputs/PortableText/Compositor.tsx | 4 +++- .../src/core/form/inputs/PortableText/Editor.tsx | 4 ++++ .../form/inputs/ReferenceInput/ReferenceField.tsx | 1 + .../form/inputs/ReferenceInput/ReferenceItem.tsx | 1 + .../members/array/items/ArrayOfObjectsItem.tsx | 7 ++++++- .../members/array/items/ArrayOfPrimitivesItem.tsx | 5 +++++ .../form/members/common/constructDescriptionId.ts | 14 ++++++++++++++ .../core/form/members/object/MemberFieldset.tsx | 1 + .../members/object/fields/ArrayOfObjectsField.tsx | 7 ++++++- .../object/fields/ArrayOfPrimitivesField.tsx | 7 ++++++- .../form/members/object/fields/ObjectField.tsx | 7 ++++++- .../form/members/object/fields/PrimitiveField.tsx | 5 +++++ .../sanity/src/core/form/studio/FormBuilder.tsx | 8 +++++++- .../form/studio/inputResolver/fieldResolver.tsx | 2 ++ packages/sanity/src/core/form/types/inputProps.ts | 2 ++ 17 files changed, 75 insertions(+), 8 deletions(-) create mode 100644 packages/sanity/src/core/form/members/common/constructDescriptionId.ts diff --git a/packages/sanity/src/core/form/components/formField/FormFieldHeaderText.tsx b/packages/sanity/src/core/form/components/formField/FormFieldHeaderText.tsx index f287b1d943b..f756685a273 100644 --- a/packages/sanity/src/core/form/components/formField/FormFieldHeaderText.tsx +++ b/packages/sanity/src/core/form/components/formField/FormFieldHeaderText.tsx @@ -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 */ @@ -45,7 +46,7 @@ export const FormFieldHeaderText = memo(function FormFieldHeaderText( {description && ( - + {description} )} diff --git a/packages/sanity/src/core/form/components/formField/FormFieldSet.tsx b/packages/sanity/src/core/form/components/formField/FormFieldSet.tsx index e77f171c44c..5e7ddb1a77c 100644 --- a/packages/sanity/src/core/form/components/formField/FormFieldSet.tsx +++ b/packages/sanity/src/core/form/components/formField/FormFieldSet.tsx @@ -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' @@ -43,6 +44,7 @@ export interface FormFieldSetProps { * @beta */ validation?: FormNodeValidation[] + inputId: string } function getChildren(children: React.ReactNode | (() => React.ReactNode)): React.ReactNode { @@ -109,6 +111,7 @@ export const FormFieldSet = forwardRef(function FormFieldSet( tabIndex, title, validation = EMPTY_ARRAY, + inputId, ...restProps } = props @@ -170,7 +173,7 @@ export const FormFieldSet = forwardRef(function FormFieldSet( {description && ( - + {description} )} diff --git a/packages/sanity/src/core/form/inputs/PortableText/Compositor.tsx b/packages/sanity/src/core/form/inputs/PortableText/Compositor.tsx index 3525e7868bd..92aff161e58 100644 --- a/packages/sanity/src/core/form/inputs/PortableText/Compositor.tsx +++ b/packages/sanity/src/core/form/inputs/PortableText/Compositor.tsx @@ -351,10 +351,11 @@ export function Compositor(props: Omit ( void setScrollElement: (scrollElement: HTMLElement | null) => void + describedBy: string | undefined } const renderDecorator: RenderDecoratorFunction = (props) => { @@ -84,6 +85,7 @@ export function Editor(props: EditorProps) { scrollElement, setPortalElement, setScrollElement, + describedBy, } = props const {isTopLayer} = useLayer() const editableRef = useRef(null) @@ -148,6 +150,7 @@ export function Editor(props: EditorProps) { selection={initialSelection} style={noOutlineStyle} spellCheck={spellcheck} + aria-describedby={describedBy} /> ), [ @@ -161,6 +164,7 @@ export function Editor(props: EditorProps) { renderPlaceholder, scrollSelectionIntoView, spellcheck, + describedBy, ], ) diff --git a/packages/sanity/src/core/form/inputs/ReferenceInput/ReferenceField.tsx b/packages/sanity/src/core/form/inputs/ReferenceInput/ReferenceField.tsx index a6c1723022e..2eeebc6f66b 100644 --- a/packages/sanity/src/core/form/inputs/ReferenceInput/ReferenceField.tsx +++ b/packages/sanity/src/core/form/inputs/ReferenceInput/ReferenceField.tsx @@ -300,6 +300,7 @@ export function ReferenceField(props: ReferenceFieldProps) { level={props.level} title={props.title} validation={props.validation} + inputId={props.inputId} > {isEditing ? ( {children} diff --git a/packages/sanity/src/core/form/inputs/ReferenceInput/ReferenceItem.tsx b/packages/sanity/src/core/form/inputs/ReferenceInput/ReferenceItem.tsx index 9cc4b9fc112..9ab53889c0a 100644 --- a/packages/sanity/src/core/form/inputs/ReferenceInput/ReferenceItem.tsx +++ b/packages/sanity/src/core/form/inputs/ReferenceInput/ReferenceItem.tsx @@ -369,6 +369,7 @@ export function ReferenceItem {children} diff --git a/packages/sanity/src/core/form/members/array/items/ArrayOfObjectsItem.tsx b/packages/sanity/src/core/form/members/array/items/ArrayOfObjectsItem.tsx index 7423e4e0399..1f2a89fc73b 100644 --- a/packages/sanity/src/core/form/members/array/items/ArrayOfObjectsItem.tsx +++ b/packages/sanity/src/core/form/members/array/items/ArrayOfObjectsItem.tsx @@ -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' /** * @@ -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 => { diff --git a/packages/sanity/src/core/form/members/array/items/ArrayOfPrimitivesItem.tsx b/packages/sanity/src/core/form/members/array/items/ArrayOfPrimitivesItem.tsx index 3e78e1e3caf..5e58083dd85 100644 --- a/packages/sanity/src/core/form/members/array/items/ArrayOfPrimitivesItem.tsx +++ b/packages/sanity/src/core/form/members/array/items/ArrayOfPrimitivesItem.tsx @@ -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' /** * @@ -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, diff --git a/packages/sanity/src/core/form/members/common/constructDescriptionId.ts b/packages/sanity/src/core/form/members/common/constructDescriptionId.ts new file mode 100644 index 00000000000..6724cc3c91f --- /dev/null +++ b/packages/sanity/src/core/form/members/common/constructDescriptionId.ts @@ -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}` +} diff --git a/packages/sanity/src/core/form/members/object/MemberFieldset.tsx b/packages/sanity/src/core/form/members/object/MemberFieldset.tsx index 93f997f6eb9..dbf3c6ff514 100644 --- a/packages/sanity/src/core/form/members/object/MemberFieldset.tsx +++ b/packages/sanity/src/core/form/members/object/MemberFieldset.tsx @@ -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') { diff --git a/packages/sanity/src/core/form/members/object/fields/ArrayOfObjectsField.tsx b/packages/sanity/src/core/form/members/object/fields/ArrayOfObjectsField.tsx index 93b8ba1a541..5d84a9cca3b 100644 --- a/packages/sanity/src/core/form/members/object/fields/ArrayOfObjectsField.tsx +++ b/packages/sanity/src/core/form/members/object/fields/ArrayOfObjectsField.tsx @@ -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 @@ -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) diff --git a/packages/sanity/src/core/form/members/object/fields/ArrayOfPrimitivesField.tsx b/packages/sanity/src/core/form/members/object/fields/ArrayOfPrimitivesField.tsx index af007a1fb88..56c630b0cce 100644 --- a/packages/sanity/src/core/form/members/object/fields/ArrayOfPrimitivesField.tsx +++ b/packages/sanity/src/core/form/members/object/fields/ArrayOfPrimitivesField.tsx @@ -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(arr: T[], from: number, to: number): T[] { const copy = arr.slice() @@ -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( diff --git a/packages/sanity/src/core/form/members/object/fields/ObjectField.tsx b/packages/sanity/src/core/form/members/object/fields/ObjectField.tsx index ff2d88a5191..6c077767175 100644 --- a/packages/sanity/src/core/form/members/object/fields/ObjectField.tsx +++ b/packages/sanity/src/core/form/members/object/fields/ObjectField.tsx @@ -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 @@ -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 => { diff --git a/packages/sanity/src/core/form/members/object/fields/PrimitiveField.tsx b/packages/sanity/src/core/form/members/object/fields/PrimitiveField.tsx index 27be72b0ece..1178c33b1e1 100644 --- a/packages/sanity/src/core/form/members/object/fields/PrimitiveField.tsx +++ b/packages/sanity/src/core/form/members/object/fields/PrimitiveField.tsx @@ -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 @@ -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, diff --git a/packages/sanity/src/core/form/studio/FormBuilder.tsx b/packages/sanity/src/core/form/studio/FormBuilder.tsx index b9a1fdbb499..bc39e2db9af 100644 --- a/packages/sanity/src/core/form/studio/FormBuilder.tsx +++ b/packages/sanity/src/core/form/studio/FormBuilder.tsx @@ -177,7 +177,13 @@ function RootInput() { const rootInputProps: Omit = { 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, diff --git a/packages/sanity/src/core/form/studio/inputResolver/fieldResolver.tsx b/packages/sanity/src/core/form/studio/inputResolver/fieldResolver.tsx index 3de5bdc1976..caf70a01612 100644 --- a/packages/sanity/src/core/form/studio/inputResolver/fieldResolver.tsx +++ b/packages/sanity/src/core/form/studio/inputResolver/fieldResolver.tsx @@ -105,6 +105,7 @@ function ObjectOrArrayField(field: ObjectFieldProps | ArrayFieldProps) { onExpand={field.onExpand} title={field.title} validation={field.validation} + inputId={field.inputId} > {field.children} @@ -152,6 +153,7 @@ function ImageOrFileField(field: ObjectFieldProps) { onExpand={field.onExpand} title={field.title} validation={field.validation} + inputId={field.inputId} > {field.children} diff --git a/packages/sanity/src/core/form/types/inputProps.ts b/packages/sanity/src/core/form/types/inputProps.ts index da712b11821..dc095dab1f9 100644 --- a/packages/sanity/src/core/form/types/inputProps.ts +++ b/packages/sanity/src/core/form/types/inputProps.ts @@ -392,6 +392,7 @@ export interface PrimitiveInputElementProps { onFocus: FocusEventHandler onBlur: FocusEventHandler ref: React.MutableRefObject + 'aria-describedby': string | undefined } /** @@ -402,6 +403,7 @@ export interface ComplexElementProps { onFocus: FocusEventHandler onBlur: FocusEventHandler ref: React.MutableRefObject + 'aria-describedby': string | undefined } /** From 5c7efbf48a3742f284e77b4c42cba2ea006b5ffd Mon Sep 17 00:00:00 2001 From: Pedro Bonamin Date: Fri, 8 Sep 2023 10:46:01 +0200 Subject: [PATCH 2/6] feat(WCAG): Adds aria-describedby to PTE --- packages/@sanity/portable-text-editor/src/editor/Editable.tsx | 4 ++++ .../src/editor/__tests__/PortableTextEditor.test.tsx | 1 + .../src/editor/__tests__/PortableTextEditorTester.tsx | 1 + packages/sanity/src/core/form/inputs/PortableText/Editor.tsx | 2 +- 4 files changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/@sanity/portable-text-editor/src/editor/Editable.tsx b/packages/@sanity/portable-text-editor/src/editor/Editable.tsx index be405c9d081..373b5e506f4 100644 --- a/packages/@sanity/portable-text-editor/src/editor/Editable.tsx +++ b/packages/@sanity/portable-text-editor/src/editor/Editable.tsx @@ -67,6 +67,7 @@ export type PortableTextEditableProps = { scrollSelectionIntoView?: ScrollSelectionIntoViewFunction selection?: EditorSelection spellCheck?: boolean + describedBy?: string } /** @@ -91,6 +92,7 @@ export const PortableTextEditable = forwardRef(function PortableTextEditable( selection: propsSelection, scrollSelectionIntoView, spellCheck, + describedBy, ...restProps } = props @@ -405,6 +407,7 @@ export const PortableTextEditable = forwardRef(function PortableTextEditable( renderLeaf={renderLeaf} style={props.style} scrollSelectionIntoView={scrollSelectionIntoViewToSlate} + aria-describedby={describedBy} /> ), [ @@ -420,6 +423,7 @@ export const PortableTextEditable = forwardRef(function PortableTextEditable( renderElement, renderLeaf, scrollSelectionIntoViewToSlate, + describedBy, ], ) diff --git a/packages/@sanity/portable-text-editor/src/editor/__tests__/PortableTextEditor.test.tsx b/packages/@sanity/portable-text-editor/src/editor/__tests__/PortableTextEditor.test.tsx index c7957a2bc5e..173f4acade2 100644 --- a/packages/@sanity/portable-text-editor/src/editor/__tests__/PortableTextEditor.test.tsx +++ b/packages/@sanity/portable-text-editor/src/editor/__tests__/PortableTextEditor.test.tsx @@ -38,6 +38,7 @@ describe('initialization', () => {
) diff --git a/packages/sanity/src/core/form/inputs/PortableText/Editor.tsx b/packages/sanity/src/core/form/inputs/PortableText/Editor.tsx index 289b5bb7566..b7c9c2ec76f 100644 --- a/packages/sanity/src/core/form/inputs/PortableText/Editor.tsx +++ b/packages/sanity/src/core/form/inputs/PortableText/Editor.tsx @@ -150,7 +150,7 @@ export function Editor(props: EditorProps) { selection={initialSelection} style={noOutlineStyle} spellCheck={spellcheck} - aria-describedby={describedBy} + describedBy={describedBy} /> ), [ From e7355a6745bd7e582f935ac0f322e78183fcff80 Mon Sep 17 00:00:00 2001 From: Pedro Bonamin Date: Tue, 12 Sep 2023 13:02:40 +0200 Subject: [PATCH 3/6] feat(WCAG): Rename PT prop and change constructDescriptionId for createDescriptionId --- .../@sanity/portable-text-editor/src/editor/Editable.tsx | 8 ++++---- .../src/editor/__tests__/PortableTextEditorTester.tsx | 2 +- .../form/components/formField/FormFieldHeaderText.tsx | 4 ++-- .../src/core/form/components/formField/FormFieldSet.tsx | 4 ++-- .../src/core/form/inputs/PortableText/Compositor.tsx | 6 +++--- .../sanity/src/core/form/inputs/PortableText/Editor.tsx | 8 ++++---- .../core/form/members/array/items/ArrayOfObjectsItem.tsx | 7 ++----- .../form/members/array/items/ArrayOfPrimitivesItem.tsx | 7 ++----- .../{constructDescriptionId.ts => createDescriptionId.ts} | 2 +- .../form/members/object/fields/ArrayOfObjectsField.tsx | 7 ++----- .../form/members/object/fields/ArrayOfPrimitivesField.tsx | 7 ++----- .../src/core/form/members/object/fields/ObjectField.tsx | 7 ++----- .../core/form/members/object/fields/PrimitiveField.tsx | 7 ++----- 13 files changed, 29 insertions(+), 47 deletions(-) rename packages/sanity/src/core/form/members/common/{constructDescriptionId.ts => createDescriptionId.ts} (89%) diff --git a/packages/@sanity/portable-text-editor/src/editor/Editable.tsx b/packages/@sanity/portable-text-editor/src/editor/Editable.tsx index 373b5e506f4..089e54eb9cc 100644 --- a/packages/@sanity/portable-text-editor/src/editor/Editable.tsx +++ b/packages/@sanity/portable-text-editor/src/editor/Editable.tsx @@ -67,7 +67,7 @@ export type PortableTextEditableProps = { scrollSelectionIntoView?: ScrollSelectionIntoViewFunction selection?: EditorSelection spellCheck?: boolean - describedBy?: string + 'aria-describedby'?: string } /** @@ -92,7 +92,7 @@ export const PortableTextEditable = forwardRef(function PortableTextEditable( selection: propsSelection, scrollSelectionIntoView, spellCheck, - describedBy, + 'aria-describedby': ariaDescribedBy, ...restProps } = props @@ -407,7 +407,7 @@ export const PortableTextEditable = forwardRef(function PortableTextEditable( renderLeaf={renderLeaf} style={props.style} scrollSelectionIntoView={scrollSelectionIntoViewToSlate} - aria-describedby={describedBy} + aria-describedby={ariaDescribedBy} /> ), [ @@ -423,7 +423,7 @@ export const PortableTextEditable = forwardRef(function PortableTextEditable( renderElement, renderLeaf, scrollSelectionIntoViewToSlate, - describedBy, + ariaDescribedBy, ], ) diff --git a/packages/@sanity/portable-text-editor/src/editor/__tests__/PortableTextEditorTester.tsx b/packages/@sanity/portable-text-editor/src/editor/__tests__/PortableTextEditorTester.tsx index 3bc63cc937f..a301389da17 100644 --- a/packages/@sanity/portable-text-editor/src/editor/__tests__/PortableTextEditorTester.tsx +++ b/packages/@sanity/portable-text-editor/src/editor/__tests__/PortableTextEditorTester.tsx @@ -73,7 +73,7 @@ export const PortableTextEditorTester = forwardRef(function PortableTextEditorTe ) diff --git a/packages/sanity/src/core/form/components/formField/FormFieldHeaderText.tsx b/packages/sanity/src/core/form/components/formField/FormFieldHeaderText.tsx index f756685a273..049b46560f2 100644 --- a/packages/sanity/src/core/form/components/formField/FormFieldHeaderText.tsx +++ b/packages/sanity/src/core/form/components/formField/FormFieldHeaderText.tsx @@ -3,7 +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 {createDescriptionId} from '../../members/common/createDescriptionId' import {FormFieldValidationStatus} from './FormFieldValidationStatus' /** @internal */ @@ -46,7 +46,7 @@ export const FormFieldHeaderText = memo(function FormFieldHeaderText( {description && ( - + {description} )} diff --git a/packages/sanity/src/core/form/components/formField/FormFieldSet.tsx b/packages/sanity/src/core/form/components/formField/FormFieldSet.tsx index 5e7ddb1a77c..81b98067e6e 100644 --- a/packages/sanity/src/core/form/components/formField/FormFieldSet.tsx +++ b/packages/sanity/src/core/form/components/formField/FormFieldSet.tsx @@ -6,7 +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 {createDescriptionId} from '../../members/common/createDescriptionId' import {FormFieldValidationStatus} from './FormFieldValidationStatus' import {FormFieldSetLegend} from './FormFieldSetLegend' import {focusRingStyle} from './styles' @@ -173,7 +173,7 @@ export const FormFieldSet = forwardRef(function FormFieldSet( {description && ( - + {description} )} diff --git a/packages/sanity/src/core/form/inputs/PortableText/Compositor.tsx b/packages/sanity/src/core/form/inputs/PortableText/Compositor.tsx index 92aff161e58..a30221f2e69 100644 --- a/packages/sanity/src/core/form/inputs/PortableText/Compositor.tsx +++ b/packages/sanity/src/core/form/inputs/PortableText/Compositor.tsx @@ -351,11 +351,11 @@ export function Compositor(props: Omit ( void setScrollElement: (scrollElement: HTMLElement | null) => void - describedBy: string | undefined + ariaDescribedBy: string | undefined } const renderDecorator: RenderDecoratorFunction = (props) => { @@ -85,7 +85,7 @@ export function Editor(props: EditorProps) { scrollElement, setPortalElement, setScrollElement, - describedBy, + ariaDescribedBy, } = props const {isTopLayer} = useLayer() const editableRef = useRef(null) @@ -150,7 +150,7 @@ export function Editor(props: EditorProps) { selection={initialSelection} style={noOutlineStyle} spellCheck={spellcheck} - describedBy={describedBy} + aria-describedby={ariaDescribedBy} /> ), [ @@ -164,7 +164,7 @@ export function Editor(props: EditorProps) { renderPlaceholder, scrollSelectionIntoView, spellcheck, - describedBy, + ariaDescribedBy, ], ) diff --git a/packages/sanity/src/core/form/members/array/items/ArrayOfObjectsItem.tsx b/packages/sanity/src/core/form/members/array/items/ArrayOfObjectsItem.tsx index 1f2a89fc73b..6f936fddfac 100644 --- a/packages/sanity/src/core/form/members/array/items/ArrayOfObjectsItem.tsx +++ b/packages/sanity/src/core/form/members/array/items/ArrayOfObjectsItem.tsx @@ -23,7 +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' +import {createDescriptionId} from '../../common/createDescriptionId' /** * @@ -243,10 +243,7 @@ export function ArrayOfObjectsItem(props: MemberItemProps) { onFocus: handleFocus, id: member.item.id, ref: focusRef, - 'aria-describedby': constructDescriptionId( - member.item.id, - member.item.schemaType.description, - ), + 'aria-describedby': createDescriptionId(member.item.id, member.item.schemaType.description), }), [handleBlur, handleFocus, member.item.id, member.item.schemaType.description], ) diff --git a/packages/sanity/src/core/form/members/array/items/ArrayOfPrimitivesItem.tsx b/packages/sanity/src/core/form/members/array/items/ArrayOfPrimitivesItem.tsx index 5e58083dd85..71bf0291d37 100644 --- a/packages/sanity/src/core/form/members/array/items/ArrayOfPrimitivesItem.tsx +++ b/packages/sanity/src/core/form/members/array/items/ArrayOfPrimitivesItem.tsx @@ -13,7 +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' +import {createDescriptionId} from '../../common/createDescriptionId' /** * @@ -112,10 +112,7 @@ 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, - ), + 'aria-describedby': createDescriptionId(member.item.id, member.item.schemaType.description), }), [ handleBlur, diff --git a/packages/sanity/src/core/form/members/common/constructDescriptionId.ts b/packages/sanity/src/core/form/members/common/createDescriptionId.ts similarity index 89% rename from packages/sanity/src/core/form/members/common/constructDescriptionId.ts rename to packages/sanity/src/core/form/members/common/createDescriptionId.ts index 6724cc3c91f..b0fc997bf8e 100644 --- a/packages/sanity/src/core/form/members/common/constructDescriptionId.ts +++ b/packages/sanity/src/core/form/members/common/createDescriptionId.ts @@ -5,7 +5,7 @@ import React from 'react' * and added to the descriptive element id. * @internal */ -export function constructDescriptionId( +export function createDescriptionId( id: string | undefined, description: React.ReactNode | undefined, ): string | undefined { diff --git a/packages/sanity/src/core/form/members/object/fields/ArrayOfObjectsField.tsx b/packages/sanity/src/core/form/members/object/fields/ArrayOfObjectsField.tsx index 5d84a9cca3b..f7b98a0eeba 100644 --- a/packages/sanity/src/core/form/members/object/fields/ArrayOfObjectsField.tsx +++ b/packages/sanity/src/core/form/members/object/fields/ArrayOfObjectsField.tsx @@ -34,7 +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' +import {createDescriptionId} from '../../common/createDescriptionId' /** * Responsible for creating inputProps and fieldProps to pass to ´renderInput´ and ´renderField´ for an array input @@ -299,10 +299,7 @@ export function ArrayOfObjectsField(props: { onFocus: handleFocus, id: member.field.id, ref: focusRef, - 'aria-describedby': constructDescriptionId( - member.field.id, - member.field.schemaType.description, - ), + 'aria-describedby': createDescriptionId(member.field.id, member.field.schemaType.description), }), [handleBlur, handleFocus, member.field.id, member.field.schemaType.description], ) diff --git a/packages/sanity/src/core/form/members/object/fields/ArrayOfPrimitivesField.tsx b/packages/sanity/src/core/form/members/object/fields/ArrayOfPrimitivesField.tsx index 56c630b0cce..ce263e112cd 100644 --- a/packages/sanity/src/core/form/members/object/fields/ArrayOfPrimitivesField.tsx +++ b/packages/sanity/src/core/form/members/object/fields/ArrayOfPrimitivesField.tsx @@ -36,7 +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' +import {createDescriptionId} from '../../common/createDescriptionId' function move(arr: T[], from: number, to: number): T[] { const copy = arr.slice() @@ -302,10 +302,7 @@ export function ArrayOfPrimitivesField(props: { onFocus: handleFocus, id: member.field.id, ref: focusRef, - 'aria-describedby': constructDescriptionId( - member.field.id, - member.field.schemaType.description, - ), + 'aria-describedby': createDescriptionId(member.field.id, member.field.schemaType.description), }), [handleBlur, handleFocus, member.field.id, member.field.schemaType.description], ) diff --git a/packages/sanity/src/core/form/members/object/fields/ObjectField.tsx b/packages/sanity/src/core/form/members/object/fields/ObjectField.tsx index 6c077767175..1b2e1f10d5f 100644 --- a/packages/sanity/src/core/form/members/object/fields/ObjectField.tsx +++ b/packages/sanity/src/core/form/members/object/fields/ObjectField.tsx @@ -19,7 +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' +import {createDescriptionId} from '../../common/createDescriptionId' /** * Responsible for creating inputProps and fieldProps to pass to ´renderInput´ and ´renderField´ for an object input @@ -184,10 +184,7 @@ export const ObjectField = function ObjectField(props: { onFocus: handleFocus, id: member.field.id, ref: focusRef, - 'aria-describedby': constructDescriptionId( - member.field.id, - member.field.schemaType.description, - ), + 'aria-describedby': createDescriptionId(member.field.id, member.field.schemaType.description), }), [handleBlur, handleFocus, member.field.id, member.field.schemaType.description], ) diff --git a/packages/sanity/src/core/form/members/object/fields/PrimitiveField.tsx b/packages/sanity/src/core/form/members/object/fields/PrimitiveField.tsx index 1178c33b1e1..111e562a65e 100644 --- a/packages/sanity/src/core/form/members/object/fields/PrimitiveField.tsx +++ b/packages/sanity/src/core/form/members/object/fields/PrimitiveField.tsx @@ -11,7 +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' +import {createDescriptionId} from '../../common/createDescriptionId' /** * Responsible for creating inputProps and fieldProps to pass to ´renderInput´ and ´renderField´ for a primitive field/input @@ -106,10 +106,7 @@ 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, - ), + 'aria-describedby': createDescriptionId(member.field.id, member.field.schemaType.description), }), [ handleBlur, From f856a7a6ce2e6bbce7bcde26e188b5a8435db302 Mon Sep 17 00:00:00 2001 From: Per-Kristian Nordnes Date: Wed, 13 Sep 2023 13:43:37 +0200 Subject: [PATCH 4/6] fix(portable-text-editor): forward html attributes to editable component This will forward html props for the editable element to the slate editable. It will also get rid of the need of having an own wrapper div element to put the ref on. Point it directly on the editable element. --- .../src/editor/Editable.tsx | 26 ++++++++++--------- 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/packages/@sanity/portable-text-editor/src/editor/Editable.tsx b/packages/@sanity/portable-text-editor/src/editor/Editable.tsx index 089e54eb9cc..d24efd82965 100644 --- a/packages/@sanity/portable-text-editor/src/editor/Editable.tsx +++ b/packages/@sanity/portable-text-editor/src/editor/Editable.tsx @@ -52,7 +52,10 @@ const EMPTY_DECORATORS: BaseRange[] = [] /** * @public */ -export type PortableTextEditableProps = { +export type PortableTextEditableProps = Omit< + React.TextareaHTMLAttributes, + 'onPaste' | 'onCopy' +> & { hotkeys?: HotkeyOptions onBeforeInput?: OnBeforeInputFn onPaste?: OnPasteFn @@ -67,7 +70,6 @@ export type PortableTextEditableProps = { scrollSelectionIntoView?: ScrollSelectionIntoViewFunction selection?: EditorSelection spellCheck?: boolean - 'aria-describedby'?: string } /** @@ -92,7 +94,6 @@ export const PortableTextEditable = forwardRef(function PortableTextEditable( selection: propsSelection, scrollSelectionIntoView, spellCheck, - 'aria-describedby': ariaDescribedBy, ...restProps } = props @@ -389,10 +390,16 @@ export const PortableTextEditable = forwardRef(function PortableTextEditable( return EMPTY_DECORATORS }, [schemaTypes, slateEditor]) + // Set the forwarded ref to be the Slate editable DOM element + useEffect(() => { + ref.current = ReactEditor.toDOMNode(slateEditor, slateEditor) as HTMLDivElement | null + }, [slateEditor, ref]) + // The editor const slateEditable = useMemo( () => ( ), [ @@ -418,21 +425,16 @@ export const PortableTextEditable = forwardRef(function PortableTextEditable( handleOnBlur, handleOnFocus, handlePaste, - props.style, readOnly, renderElement, renderLeaf, + restProps, scrollSelectionIntoViewToSlate, - ariaDescribedBy, ], ) if (!portableTextEditor) { return null } - return ( -
- {hasInvalidValue ? null : slateEditable} -
- ) + return hasInvalidValue ? null : slateEditable }) From e846d0f4422a32a7c63af130c855cb1d6b86761c Mon Sep 17 00:00:00 2001 From: Per-Kristian Nordnes Date: Wed, 13 Sep 2023 13:43:55 +0200 Subject: [PATCH 5/6] test(portable-text-editor): update snapshot --- .../__tests__/PortableTextEditor.test.tsx | 94 +++++++++---------- 1 file changed, 46 insertions(+), 48 deletions(-) diff --git a/packages/@sanity/portable-text-editor/src/editor/__tests__/PortableTextEditor.test.tsx b/packages/@sanity/portable-text-editor/src/editor/__tests__/PortableTextEditor.test.tsx index 173f4acade2..13b4c3d15fc 100644 --- a/packages/@sanity/portable-text-editor/src/editor/__tests__/PortableTextEditor.test.tsx +++ b/packages/@sanity/portable-text-editor/src/editor/__tests__/PortableTextEditor.test.tsx @@ -35,58 +35,56 @@ describe('initialization', () => { expect(onChange).toHaveBeenCalledWith({type: 'ready'}) expect(onChange).toHaveBeenCalledWith({type: 'value', value: undefined}) expect(container).toMatchInlineSnapshot(` +
+
+
+
-
-
+ -
+ + -
-
- - - Jot something down here - - - -  -
-
-
-
-
-
-
-
-
+  +
+ + +
- `) +
+
+
+
+`) }) }) it('takes value from props', async () => { From f1c2e5e2b597496c77db523ee8b7a686bd21a612 Mon Sep 17 00:00:00 2001 From: Per-Kristian Nordnes Date: Wed, 13 Sep 2023 16:16:51 +0200 Subject: [PATCH 6/6] refactor(portable-text-editor): remove unnecessary memo This doesn't need to be memoed, at least not after supporting restProps here. Debugging shows that the nodes inside are not re-rendered by removing this memo, which is what we care about. --- .../src/editor/Editable.tsx | 60 +++++++------------ 1 file changed, 21 insertions(+), 39 deletions(-) diff --git a/packages/@sanity/portable-text-editor/src/editor/Editable.tsx b/packages/@sanity/portable-text-editor/src/editor/Editable.tsx index d24efd82965..e8292fdc256 100644 --- a/packages/@sanity/portable-text-editor/src/editor/Editable.tsx +++ b/packages/@sanity/portable-text-editor/src/editor/Editable.tsx @@ -395,46 +395,28 @@ export const PortableTextEditable = forwardRef(function PortableTextEditable( ref.current = ReactEditor.toDOMNode(slateEditor, slateEditor) as HTMLDivElement | null }, [slateEditor, ref]) - // The editor - const slateEditable = useMemo( - () => ( - - ), - [ - decorate, - handleCopy, - handleKeyDown, - handleOnBeforeInput, - handleOnBlur, - handleOnFocus, - handlePaste, - readOnly, - renderElement, - renderLeaf, - restProps, - scrollSelectionIntoViewToSlate, - ], - ) - if (!portableTextEditor) { return null } - return hasInvalidValue ? null : slateEditable + return hasInvalidValue ? null : ( + + ) })