diff --git a/src/components/editorAccessibility.ts b/src/components/editorAccessibility.ts index 376a9a0b..aa5b6471 100644 --- a/src/components/editorAccessibility.ts +++ b/src/components/editorAccessibility.ts @@ -51,6 +51,30 @@ function validateEditorTextDirection( } } +/** Reject runtime `aria-invalid` values outside Inkspan's finite contract. */ +function validateEditorAriaInvalid( + value: EditorAriaInvalid | undefined, +): void { + if ( + value !== undefined && + value !== false && + value !== true && + value !== 'grammar' && + value !== 'spelling' + ) { + throw new RangeError( + 'Editor aria-invalid must be false, true, grammar, or spelling.', + ); + } +} + +/** Reject runtime `aria-required` values outside Inkspan's boolean contract. */ +function validateEditorAriaRequired(value: boolean | undefined): void { + if (value !== undefined && value !== false && value !== true) { + throw new RangeError('Editor aria-required must be false or true.'); + } +} + /** * Normalize the shared visual and semantic empty-editor guidance. * @@ -76,6 +100,8 @@ export function buildEditorAccessibilityAttributes( options: EditorAccessibilityOptions, ): Record { validateEditorTextDirection(options.textDirection); + validateEditorAriaInvalid(options.ariaInvalid); + validateEditorAriaRequired(options.ariaRequired); const placeholder = normalizeEditorPlaceholder(options.placeholder); const languageTag = normalizedAccessibilityValue(options.languageTag); const labelledBy = normalizedAccessibilityValue(options.ariaLabelledBy); diff --git a/src/components/editorAccessibilityRuntime.test.ts b/src/components/editorAccessibilityRuntime.test.ts index 04ce1ea4..b00b5e09 100644 --- a/src/components/editorAccessibilityRuntime.test.ts +++ b/src/components/editorAccessibilityRuntime.test.ts @@ -26,4 +26,56 @@ describe('editor accessibility runtime contracts', () => { new RangeError('Editor text direction must be ltr, rtl, or auto.'), ); }); + + it.each([false, true, 'grammar', 'spelling'] as const)( + 'preserves the valid %s aria-invalid state', + (ariaInvalid) => { + expect( + buildEditorAccessibilityAttributes({ + defaultLabel: 'Editor', + editable: true, + ariaInvalid, + })['aria-invalid'], + ).toBe(String(ariaInvalid)); + }, + ); + + it('rejects a runtime aria-invalid value outside the public states', () => { + expect(() => + buildEditorAccessibilityAttributes({ + defaultLabel: 'Editor', + editable: true, + ariaInvalid: 'unknown' as never, + }), + ).toThrowError( + new RangeError( + 'Editor aria-invalid must be false, true, grammar, or spelling.', + ), + ); + }); + + it.each([false, true] as const)( + 'preserves the valid %s aria-required state', + (ariaRequired) => { + expect( + buildEditorAccessibilityAttributes({ + defaultLabel: 'Editor', + editable: true, + ariaRequired, + })['aria-required'], + ).toBe(String(ariaRequired)); + }, + ); + + it('rejects a runtime aria-required value outside the public states', () => { + expect(() => + buildEditorAccessibilityAttributes({ + defaultLabel: 'Editor', + editable: true, + ariaRequired: 'maybe' as never, + }), + ).toThrowError( + new RangeError('Editor aria-required must be false or true.'), + ); + }); });