diff --git a/packages/@sanity/portable-text-editor/src/editor/Editable.tsx b/packages/@sanity/portable-text-editor/src/editor/Editable.tsx index be405c9d081..e8292fdc256 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 @@ -387,48 +390,33 @@ export const PortableTextEditable = forwardRef(function PortableTextEditable( return EMPTY_DECORATORS }, [schemaTypes, slateEditor]) - // The editor - const slateEditable = useMemo( - () => ( - - ), - [ - decorate, - handleCopy, - handleKeyDown, - handleOnBeforeInput, - handleOnBlur, - handleOnFocus, - handlePaste, - props.style, - readOnly, - renderElement, - renderLeaf, - scrollSelectionIntoViewToSlate, - ], - ) + // Set the forwarded ref to be the Slate editable DOM element + useEffect(() => { + ref.current = ReactEditor.toDOMNode(slateEditor, slateEditor) as HTMLDivElement | null + }, [slateEditor, ref]) if (!portableTextEditor) { return null } - return ( -
- {hasInvalidValue ? null : slateEditable} -
+ return hasInvalidValue ? null : ( + ) }) 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..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,57 +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 () => { 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 f21bcfef161..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,6 +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 f287b1d943b..049b46560f2 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 {createDescriptionId} from '../../members/common/createDescriptionId' 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..81b98067e6e 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 {createDescriptionId} from '../../members/common/createDescriptionId' 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..a30221f2e69 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 + ariaDescribedBy: string | undefined } const renderDecorator: RenderDecoratorFunction = (props) => { @@ -84,6 +85,7 @@ export function Editor(props: EditorProps) { scrollElement, setPortalElement, setScrollElement, + ariaDescribedBy, } = 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={ariaDescribedBy} /> ), [ @@ -161,6 +164,7 @@ export function Editor(props: EditorProps) { renderPlaceholder, scrollSelectionIntoView, spellcheck, + ariaDescribedBy, ], ) 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..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,6 +23,7 @@ import {createProtoValue} from '../../../utils/createProtoValue' import {isEmptyItem} from '../../../store/utils/isEmptyItem' import {useResolveInitialValueForType} from '../../../../store' import {resolveInitialArrayValues} from '../../common/resolveInitialArrayValues' +import {createDescriptionId} from '../../common/createDescriptionId' /** * @@ -242,8 +243,9 @@ export function ArrayOfObjectsItem(props: MemberItemProps) { onFocus: handleFocus, id: member.item.id, ref: focusRef, + 'aria-describedby': createDescriptionId(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..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,6 +13,7 @@ import { import {insert, PatchArg, PatchEvent, set, unset} from '../../../patch' import {useFormCallbacks} from '../../../studio/contexts/FormCallbacks' import {resolveNativeNumberInputValue} from '../../common/resolveNativeNumberInputValue' +import {createDescriptionId} from '../../common/createDescriptionId' /** * @@ -111,6 +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': createDescriptionId(member.item.id, member.item.schemaType.description), }), [ handleBlur, diff --git a/packages/sanity/src/core/form/members/common/createDescriptionId.ts b/packages/sanity/src/core/form/members/common/createDescriptionId.ts new file mode 100644 index 00000000000..b0fc997bf8e --- /dev/null +++ b/packages/sanity/src/core/form/members/common/createDescriptionId.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 createDescriptionId( + 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..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,6 +34,7 @@ import {resolveInitialArrayValues} from '../../common/resolveInitialArrayValues' import {applyAll} from '../../../patch/applyPatch' import {useFormPublishedId} from '../../../useFormPublishedId' import {DocumentFieldActionNode} from '../../../../config' +import {createDescriptionId} from '../../common/createDescriptionId' /** * Responsible for creating inputProps and fieldProps to pass to ´renderInput´ and ´renderField´ for an array input @@ -298,8 +299,9 @@ export function ArrayOfObjectsField(props: { onFocus: handleFocus, id: member.field.id, ref: focusRef, + 'aria-describedby': createDescriptionId(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..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,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 {createDescriptionId} from '../../common/createDescriptionId' function move(arr: T[], from: number, to: number): T[] { const copy = arr.slice() @@ -301,8 +302,9 @@ export function ArrayOfPrimitivesField(props: { onFocus: handleFocus, id: member.field.id, ref: focusRef, + 'aria-describedby': createDescriptionId(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..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,6 +19,7 @@ import {FormCallbacksProvider, useFormCallbacks} from '../../../studio/contexts/ import {createProtoValue} from '../../../utils/createProtoValue' import {applyAll} from '../../../patch/applyPatch' import {useFormBuilder} from '../../../useFormBuilder' +import {createDescriptionId} from '../../common/createDescriptionId' /** * Responsible for creating inputProps and fieldProps to pass to ´renderInput´ and ´renderField´ for an object input @@ -183,8 +184,9 @@ export const ObjectField = function ObjectField(props: { onFocus: handleFocus, id: member.field.id, ref: focusRef, + 'aria-describedby': createDescriptionId(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..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,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 {createDescriptionId} from '../../common/createDescriptionId' /** * Responsible for creating inputProps and fieldProps to pass to ´renderInput´ and ´renderField´ for a primitive field/input @@ -105,6 +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': createDescriptionId(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 } /**