From e361b3ccdc245c0b618525c1deb5ac62e7201fbb Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 16:06:57 +0900 Subject: [PATCH 01/13] feat(accessibility): expose normalized editor placeholder semantics --- src/components/editorAccessibility.ts | 28 +++++++++++++++++++++------ 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/src/components/editorAccessibility.ts b/src/components/editorAccessibility.ts index 54ff4684..f2cc1779 100644 --- a/src/components/editorAccessibility.ts +++ b/src/components/editorAccessibility.ts @@ -7,13 +7,15 @@ export type EditorAriaInvalid = boolean | 'grammar' | 'spelling'; export interface EditorAccessibilityOptions { /** Fallback accessible name when no host label reference is supplied. */ defaultLabel: string; + /** Visual empty-editor guidance mirrored to `aria-placeholder` when non-blank. */ + placeholder?: string; /** BCP 47 language tag for the authored document. */ languageTag?: string; /** Base writing direction for the authored document. */ textDirection?: EditorTextDirection; /** Explicit string accessible name for the editable surface. */ ariaLabel?: string; - /** Space-separated IDs of visible elements that label the surface. */ + /** Space-separated IDs of elements that label the surface. */ ariaLabelledBy?: string; /** Space-separated IDs of elements that describe the surface. */ ariaDescribedBy?: string; @@ -27,7 +29,7 @@ export interface EditorAccessibilityOptions { editable: boolean; } -/** Normalize a host-supplied language, accessible-name, or ID-reference string. */ +/** Normalize an optional host-supplied accessibility string. */ function normalizedAccessibilityValue( value: string | undefined, ): string | undefined { @@ -35,18 +37,31 @@ function normalizedAccessibilityValue( return normalized ? normalized : undefined; } +/** + * Normalize the shared visual and semantic empty-editor guidance. + * + * Returning `undefined` for blank input lets callers omit both the visual + * Placeholder extension text and `aria-placeholder` from the same source. + */ +export function normalizeEditorPlaceholder( + value: string | undefined, +): string | undefined { + return normalizedAccessibilityValue(value); +} + /** * Build the complete semantic attribute contract shared by standalone and * collaborative editor surfaces. * - * A visible label referenced with `aria-labelledby` takes precedence over the - * fallback string label. Optional language and ID-reference values are omitted - * when blank so browsers and assistive technologies never receive empty - * metadata relationships. + * A non-blank `aria-labelledby` reference takes precedence over the fallback + * string label. Optional placeholder, language, and ID-reference values are + * omitted when blank. Placeholder guidance remains supplemental and never + * replaces the accessible name. */ export function buildEditorAccessibilityAttributes( options: EditorAccessibilityOptions, ): Record { + const placeholder = normalizeEditorPlaceholder(options.placeholder); const languageTag = normalizedAccessibilityValue(options.languageTag); const labelledBy = normalizedAccessibilityValue(options.ariaLabelledBy); const describedBy = normalizedAccessibilityValue(options.ariaDescribedBy); @@ -59,6 +74,7 @@ export function buildEditorAccessibilityAttributes( 'aria-readonly': String(!options.editable), }; + if (placeholder) attributes['aria-placeholder'] = placeholder; if (languageTag) attributes.lang = languageTag; if (options.textDirection) attributes.dir = options.textDirection; if (labelledBy) { From d4fd95934025816e480d0c06cd09031ff69cb03b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 16:07:55 +0900 Subject: [PATCH 02/13] fix(accessibility): keep standalone placeholder semantics live --- src/components/CwlEditor.tsx | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/components/CwlEditor.tsx b/src/components/CwlEditor.tsx index c0a1e596..598ac948 100644 --- a/src/components/CwlEditor.tsx +++ b/src/components/CwlEditor.tsx @@ -10,7 +10,10 @@ import type { ClipboardSanitizationError } from '../extensions/SafeClipboard.js' import { buildExtensions } from '../extensions/kit.js'; import type { CwlEditorHandle, CwlEditorProps } from '../types.js'; import { EditorFrame } from './EditorFrame.js'; -import { buildEditorAccessibilityAttributes } from './editorAccessibility.js'; +import { + buildEditorAccessibilityAttributes, + normalizeEditorPlaceholder, +} from './editorAccessibility.js'; import { createEditorDocumentSnapshot } from './editorDocumentSnapshot.js'; import { applyEditorFormReset } from './editorFormReset.js'; import { editorHtmlToValue, editorValueToHtml } from './editorSerialization.js'; @@ -87,10 +90,16 @@ export const CwlEditor = forwardRef( }, [onClipboardErrorRef], ); + const normalizedPlaceholder = useMemo( + () => normalizeEditorPlaceholder(placeholder), + [placeholder], + ); + const placeholderRef = useLatestRef(normalizedPlaceholder ?? ''); const editorAttributes = useMemo( () => buildEditorAccessibilityAttributes({ defaultLabel: 'Rich text editor', + placeholder: normalizedPlaceholder, languageTag, textDirection, ariaLabel, @@ -102,6 +111,7 @@ export const CwlEditor = forwardRef( editable, }), [ + normalizedPlaceholder, languageTag, textDirection, ariaLabel, @@ -118,7 +128,7 @@ export const CwlEditor = forwardRef( immediatelyRender: false, editable, extensions: buildExtensions({ - placeholder, + placeholder: () => placeholderRef.current, image, clipboard, onImageError: reportImageError, From 285c92a44a6c306c20f84bb52abe682a9911d5f3 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 16:08:57 +0900 Subject: [PATCH 03/13] fix(accessibility): keep collaborative placeholder semantics live --- src/collaboration/CollaborativeCwlEditor.tsx | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/collaboration/CollaborativeCwlEditor.tsx b/src/collaboration/CollaborativeCwlEditor.tsx index 3a77ff4d..eea89b6c 100644 --- a/src/collaboration/CollaborativeCwlEditor.tsx +++ b/src/collaboration/CollaborativeCwlEditor.tsx @@ -10,7 +10,10 @@ import { useState, } from 'react'; import { EditorFrame } from '../components/EditorFrame.js'; -import { buildEditorAccessibilityAttributes } from '../components/editorAccessibility.js'; +import { + buildEditorAccessibilityAttributes, + normalizeEditorPlaceholder, +} from '../components/editorAccessibility.js'; import { createEditorDocumentSnapshot } from '../components/editorDocumentSnapshot.js'; import { applyEditorFormReset } from '../components/editorFormReset.js'; import { editorHtmlToValue } from '../components/editorSerialization.js'; @@ -106,6 +109,10 @@ export const CollaborativeCwlEditor = forwardRef< } const normalizedField = field.trim(); + const normalizedPlaceholder = useMemo( + () => normalizeEditorPlaceholder(placeholder), + [placeholder], + ); const cursorUser = user ? serializeCollaborationUser(user) : undefined; const presenceEnabled = provider !== undefined && cursorUser !== undefined; const scopedProvider = useMemo( @@ -134,6 +141,7 @@ export const CollaborativeCwlEditor = forwardRef< const onReadyRef = useLatestRef(onReady); const onDestroyRef = useLatestRef(onDestroy); const onFormResetRef = useLatestRef(onFormReset); + const placeholderRef = useLatestRef(normalizedPlaceholder ?? ''); const reportImageError = useCallback((error: Error) => { onImageErrorRef.current?.(error); }, [onImageErrorRef]); @@ -147,6 +155,7 @@ export const CollaborativeCwlEditor = forwardRef< () => buildEditorAccessibilityAttributes({ defaultLabel: 'Collaborative rich text editor', + placeholder: normalizedPlaceholder, languageTag, textDirection, ariaLabel, @@ -158,6 +167,7 @@ export const CollaborativeCwlEditor = forwardRef< editable, }), [ + normalizedPlaceholder, languageTag, textDirection, ariaLabel, @@ -175,7 +185,7 @@ export const CollaborativeCwlEditor = forwardRef< immediatelyRender: false, editable, extensions: buildExtensions({ - placeholder, + placeholder: () => placeholderRef.current, image, clipboard, onImageError: reportImageError, From 689d1a9c3b961ad8cfc487261dd4958ead4cb226 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 16:39:00 +0900 Subject: [PATCH 04/13] docs(accessibility): restore placeholder doctoring on refreshed branch --- .../editor-placeholder-accessibility.md | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 docs/doctoring/editor-placeholder-accessibility.md diff --git a/docs/doctoring/editor-placeholder-accessibility.md b/docs/doctoring/editor-placeholder-accessibility.md new file mode 100644 index 00000000..60000299 --- /dev/null +++ b/docs/doctoring/editor-placeholder-accessibility.md @@ -0,0 +1,39 @@ +# Editor placeholder accessibility + +Status: Implemented on active PR + +## Purpose + +Inkspan's visual empty-editor hint is rendered by the TipTap Placeholder extension. The active accessibility change mirrors that same host-supplied placeholder into the ProseMirror textbox's `aria-placeholder` attribute so assistive-technology users can receive equivalent entry guidance without requiring every embedding host to duplicate the text in a separate description element. + +The placeholder remains **supplemental guidance**, not the editor's accessible name. Inkspan's existing accessible-name precedence remains unchanged: `aria-labelledby` whenever a host supplies a non-blank label reference, otherwise an explicit `aria-label`, otherwise the product fallback label. + +## WAI-ARIA authority + +WAI-ARIA 1.2 defines `aria-placeholder` as a short hint intended to aid data entry when a control has no value and allows it on the `textbox` role. The Recommendation also states that placeholder text must not be used instead of a label because users still need to understand the input's purpose once a value is present. + +Inkspan therefore exposes the placeholder only after trimming surrounding whitespace and omits the attribute when the configured visual placeholder is blank or whitespace-only. It never promotes the placeholder to `aria-label` and never removes the existing textbox name. + +## Lifecycle and ownership + +Standalone and provider-neutral collaborative surfaces use the same `buildEditorAccessibilityAttributes()` contract. A changed React `placeholder` prop updates the semantic textbox attribute and the visual TipTap placeholder from one normalized value without replacing the current TipTap editor or Yjs document binding. The change introduces no live region, network call, model call, persistence field, telemetry event, tenant identifier, authorization state, or collaboration-provider behavior. + +`aria-placeholder` does not assert that the document is editable. `aria-readonly` and the TipTap editable state remain the authority for editability. A read-only empty surface can still expose its configured placeholder guidance, but that guidance grants no editing capability. + +## Verification + +The active test line includes: + +- a focused historical RED proving the accessibility builder had no placeholder input or attribute contract; +- normalized non-empty placeholder plus `aria-labelledby` name precedence; +- standalone DOM verification and live placeholder-prop update without editor recreation; +- collaborative DOM verification and live placeholder-prop update without editor or Yjs-fragment replacement; +- blank/whitespace-only placeholder omission; +- package-distribution verification through `pnpm build && pnpm verify:package`, whose npm-pack inventory check binds `dist/cwl-editor.js` to the publishable package and whose `node ./tests/package/verify-editor-placeholder-package.mjs` smoke verifies the public `CwlEditor.placeholder` visual and `aria-placeholder` semantics from that built entry; and +- repository-wide exact production coverage, package, CI, security, and SAST gates before protected integration. + +## References — APA 7th + +World Wide Web Consortium. (2023, June 6). *Accessible Rich Internet Applications (WAI-ARIA) 1.2* (W3C Recommendation). https://www.w3.org/TR/wai-aria-1.2/ + +W3C Web Accessibility Initiative. (n.d.). *Providing accessible names and descriptions*. ARIA Authoring Practices Guide. Retrieved August 10, 2026, from https://www.w3.org/WAI/ARIA/apg/practices/names-and-descriptions/ From 0e602234703b6d43735ea046b6b260f37754d5a9 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 16:39:21 +0900 Subject: [PATCH 05/13] test(accessibility): restore placeholder component coverage on refresh --- .../CwlEditor.accessiblePlaceholder.test.tsx | 102 ++++++++++++++++++ 1 file changed, 102 insertions(+) create mode 100644 src/components/CwlEditor.accessiblePlaceholder.test.tsx diff --git a/src/components/CwlEditor.accessiblePlaceholder.test.tsx b/src/components/CwlEditor.accessiblePlaceholder.test.tsx new file mode 100644 index 00000000..ec28f2a8 --- /dev/null +++ b/src/components/CwlEditor.accessiblePlaceholder.test.tsx @@ -0,0 +1,102 @@ +import { cleanup, render, screen, waitFor } from '@testing-library/react'; +import { createRef } from 'react'; +import { afterEach, describe, expect, it } from 'vitest'; +import * as Y from 'yjs'; +import { CollaborativeCwlEditor } from '../collaboration/CollaborativeCwlEditor.js'; +import type { CwlEditorHandle } from '../types.js'; +import { CwlEditor } from './CwlEditor.js'; + +afterEach(cleanup); + +const visualPlaceholder = (textbox: HTMLElement): string | null => + textbox.querySelector('[data-placeholder]')?.getAttribute('data-placeholder') ?? + null; + +describe('accessible editor placeholder semantics', () => { + it('keeps standalone visual and semantic placeholder guidance normalized together', async () => { + const editorRef = createRef(); + const { rerender } = render( + , + ); + + const textbox = await screen.findByRole('textbox', { name: 'Report editor' }); + await waitFor(() => expect(editorRef.current?.getEditor()).not.toBeNull()); + const editor = editorRef.current!.getEditor(); + expect(textbox).toHaveAttribute('aria-placeholder', 'Start the report…'); + expect(visualPlaceholder(textbox)).toBe('Start the report…'); + + rerender( + , + ); + await waitFor(() => + expect(textbox).toHaveAttribute( + 'aria-placeholder', + 'Continue with evidence…', + ), + ); + expect(visualPlaceholder(textbox)).toBe('Continue with evidence…'); + expect(editorRef.current!.getEditor()).toBe(editor); + + rerender( + , + ); + await waitFor(() => expect(textbox).not.toHaveAttribute('aria-placeholder')); + expect(visualPlaceholder(textbox)).toBeNull(); + expect(editorRef.current!.getEditor()).toBe(editor); + }); + + it('keeps collaborative visual and semantic placeholder updates Yjs-preserving', async () => { + const collaborationDocument = new Y.Doc(); + const editorRef = createRef(); + try { + const { rerender } = render( + , + ); + + const textbox = await screen.findByRole('textbox', { + name: 'Shared report editor', + }); + await waitFor(() => expect(editorRef.current?.getEditor()).not.toBeNull()); + const editor = editorRef.current!.getEditor(); + const sharedFragment = collaborationDocument.getXmlFragment('default'); + expect(textbox).toHaveAttribute('aria-placeholder', 'Shared report…'); + expect(visualPlaceholder(textbox)).toBe('Shared report…'); + + rerender( + , + ); + await waitFor(() => + expect(textbox).toHaveAttribute('aria-placeholder', 'Review together…'), + ); + expect(visualPlaceholder(textbox)).toBe('Review together…'); + expect(editorRef.current!.getEditor()).toBe(editor); + expect(collaborationDocument.getXmlFragment('default')).toBe(sharedFragment); + } finally { + collaborationDocument.destroy(); + } + }); +}); From e1c5f1f29e9bfd205a661144ed1e19fe94582b7f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 16:39:39 +0900 Subject: [PATCH 06/13] test(accessibility): restore placeholder attribute contract --- .../editorAccessibilityPlaceholder.test.ts | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 src/components/editorAccessibilityPlaceholder.test.ts diff --git a/src/components/editorAccessibilityPlaceholder.test.ts b/src/components/editorAccessibilityPlaceholder.test.ts new file mode 100644 index 00000000..88e612b6 --- /dev/null +++ b/src/components/editorAccessibilityPlaceholder.test.ts @@ -0,0 +1,22 @@ +import { describe, expect, it } from 'vitest'; +import { buildEditorAccessibilityAttributes } from './editorAccessibility.js'; + +describe('editor accessible placeholder contract', () => { + it('exposes normalized placeholder guidance without replacing the accessible name', () => { + expect( + buildEditorAccessibilityAttributes({ + defaultLabel: 'Rich text editor', + ariaLabelledBy: 'editor-label', + placeholder: ' Start writing… ', + editable: true, + }), + ).toEqual({ + class: 'cwl-editor__content', + role: 'textbox', + 'aria-multiline': 'true', + 'aria-readonly': 'false', + 'aria-labelledby': 'editor-label', + 'aria-placeholder': 'Start writing…', + }); + }); +}); From 31300e2bab865a21afb8c8b39784613524e6db4e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 16:39:59 +0900 Subject: [PATCH 07/13] test(docs): restore placeholder accessibility doctoring contract --- src/editorPlaceholderDocumentation.test.ts | 25 ++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 src/editorPlaceholderDocumentation.test.ts diff --git a/src/editorPlaceholderDocumentation.test.ts b/src/editorPlaceholderDocumentation.test.ts new file mode 100644 index 00000000..c5402ca5 --- /dev/null +++ b/src/editorPlaceholderDocumentation.test.ts @@ -0,0 +1,25 @@ +import { readFileSync } from 'node:fs'; +import { resolve } from 'node:path'; + +import { describe, expect, it } from 'vitest'; + +const repositoryFile = (path: string): string => + readFileSync(resolve(process.cwd(), path), 'utf8'); + +describe('editor placeholder accessibility documentation', () => { + it('keeps placeholder guidance separate from accessible-name authority', () => { + const doctoring = repositoryFile( + 'docs/doctoring/editor-placeholder-accessibility.md', + ); + + expect(doctoring).toContain('Status: Implemented on active PR'); + expect(doctoring).toContain('aria-placeholder'); + expect(doctoring).toContain('supplemental guidance'); + expect(doctoring).toContain('aria-labelledby'); + expect(doctoring).toContain( + 'It never promotes the placeholder to `aria-label`', + ); + expect(doctoring).toContain('WAI-ARIA 1.2'); + expect(doctoring).toContain('World Wide Web Consortium. (2023, June 6).'); + }); +}); From fbede7fe811aa264bbea85d20d8c32fb2cf3e9b8 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 16:40:21 +0900 Subject: [PATCH 08/13] test(package): restore packed placeholder accessibility smoke --- .../verify-editor-placeholder-package.mjs | 68 +++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 tests/package/verify-editor-placeholder-package.mjs diff --git a/tests/package/verify-editor-placeholder-package.mjs b/tests/package/verify-editor-placeholder-package.mjs new file mode 100644 index 00000000..7beceb64 --- /dev/null +++ b/tests/package/verify-editor-placeholder-package.mjs @@ -0,0 +1,68 @@ +import assert from 'node:assert/strict'; +import { execFileSync } from 'node:child_process'; +import { JSDOM } from 'jsdom'; +import React from 'react'; + +const packResult = JSON.parse( + execFileSync( + 'npm', + ['pack', '--dry-run', '--json', '--ignore-scripts'], + { encoding: 'utf8', stdio: ['ignore', 'pipe', 'inherit'] }, + ), +)[0]; +assert.ok( + packResult.files.some(({ path }) => path === 'dist/cwl-editor.js'), + 'npm package must include the exact root ESM entry exercised by this smoke test', +); + +const dom = new JSDOM('', { + url: 'https://inkspan.invalid/', +}); + +for (const name of [ + 'window', + 'document', + 'navigator', + 'HTMLElement', + 'Element', + 'Node', + 'DOMParser', + 'MutationObserver', + 'getComputedStyle', +]) { + Object.defineProperty(globalThis, name, { + configurable: true, + value: + name === 'getComputedStyle' + ? dom.window.getComputedStyle.bind(dom.window) + : dom.window[name], + }); +} + +globalThis.requestAnimationFrame = (callback) => + setTimeout(() => callback(Date.now()), 0); +globalThis.cancelAnimationFrame = (handle) => clearTimeout(handle); +globalThis.IS_REACT_ACT_ENVIRONMENT = true; + +const { render, screen, cleanup } = await import('@testing-library/react'); +const { CwlEditor } = await import('../../dist/cwl-editor.js'); + +try { + render( + React.createElement(CwlEditor, { + ariaLabel: 'Packed editor', + placeholder: ' Packed guidance… ', + hideToolbar: true, + }), + ); + + const textbox = await screen.findByRole('textbox', { name: 'Packed editor' }); + assert.equal(textbox.getAttribute('aria-placeholder'), 'Packed guidance…'); + assert.equal( + textbox.querySelector('[data-placeholder]')?.getAttribute('data-placeholder'), + 'Packed guidance…', + ); +} finally { + cleanup(); + dom.window.close(); +} From a5b55a5a0916362516c192a74022b00ada0b985f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 16:40:43 +0900 Subject: [PATCH 09/13] refactor(placeholder): keep visual guidance lazy on refreshed branch --- src/extensions/kit.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/extensions/kit.ts b/src/extensions/kit.ts index f287e880..71554bc2 100644 --- a/src/extensions/kit.ts +++ b/src/extensions/kit.ts @@ -20,7 +20,8 @@ import type { ImageConfig } from '../types.js'; /** Options for constructing the shared Inkspan extension collection. */ export interface BuildExtensionsOptions { - placeholder?: string; + /** Static or lazily resolved visual empty-editor guidance. */ + placeholder?: string | (() => string); image?: ImageConfig; /** Bounded rich-HTML paste policy shared by all editor surfaces. */ clipboard?: ClipboardConfig; From 3fe7e3e09c6c569770254ee70e10198337f785e1 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 16:42:42 +0900 Subject: [PATCH 10/13] test(package): bind packed placeholder accessibility smoke --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d4261f22..9a6354ff 100644 --- a/package.json +++ b/package.json @@ -108,7 +108,7 @@ "test:watch": "vitest", "coverage": "vitest run --coverage", "test:package-config": "node --test ./scripts/revision-evidence-consumer-config.test.mjs ./scripts/release-metadata.test.mjs ./scripts/javascript-runtime-authority.test.mjs", - "verify:package": "pnpm run test:package-config && node ./tests/package/verify-package.mjs && node ./scripts/verify-canonical-envelope-package.mjs && node ./scripts/verify-revision-evidence-package.mjs && node ./scripts/verify-framework-free-revision-evidence-package.mjs && node ./scripts/verify-framework-free-envelope-identity-package.mjs && node ./tests/package/verify-framework-free-autosave-package.mjs && node ./scripts/verify-text-position-selector-package.mjs && node ./scripts/verify-text-position-selector-subpath-package.mjs && node ./scripts/verify-markdown-subpath-package.mjs" + "verify:package": "pnpm run test:package-config && node ./tests/package/verify-package.mjs && node ./tests/package/verify-editor-placeholder-package.mjs && node ./scripts/verify-canonical-envelope-package.mjs && node ./scripts/verify-revision-evidence-package.mjs && node ./scripts/verify-framework-free-revision-evidence-package.mjs && node ./scripts/verify-framework-free-envelope-identity-package.mjs && node ./tests/package/verify-framework-free-autosave-package.mjs && node ./scripts/verify-text-position-selector-package.mjs && node ./scripts/verify-text-position-selector-subpath-package.mjs && node ./scripts/verify-markdown-subpath-package.mjs" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0", From 610ca26ff84cc98e1ce440d6b98719b43283c79e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 17:15:42 +0900 Subject: [PATCH 11/13] test(accessibility): assert effective placeholder guidance --- src/components/CwlEditor.accessiblePlaceholder.test.tsx | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/components/CwlEditor.accessiblePlaceholder.test.tsx b/src/components/CwlEditor.accessiblePlaceholder.test.tsx index ec28f2a8..d65cc3cd 100644 --- a/src/components/CwlEditor.accessiblePlaceholder.test.tsx +++ b/src/components/CwlEditor.accessiblePlaceholder.test.tsx @@ -8,9 +8,12 @@ import { CwlEditor } from './CwlEditor.js'; afterEach(cleanup); -const visualPlaceholder = (textbox: HTMLElement): string | null => - textbox.querySelector('[data-placeholder]')?.getAttribute('data-placeholder') ?? - null; +const visualPlaceholder = (textbox: HTMLElement): string | null => { + const placeholder = textbox + .querySelector('[data-placeholder]') + ?.getAttribute('data-placeholder'); + return placeholder?.trim() ? placeholder : null; +}; describe('accessible editor placeholder semantics', () => { it('keeps standalone visual and semantic placeholder guidance normalized together', async () => { From 19c2aa6e23a3c7c5fdd470c4a4cc8ab4b34da70b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 17:27:34 +0900 Subject: [PATCH 12/13] test(accessibility): cover collaborative absent placeholder --- .../CwlEditor.accessiblePlaceholder.test.tsx | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/components/CwlEditor.accessiblePlaceholder.test.tsx b/src/components/CwlEditor.accessiblePlaceholder.test.tsx index d65cc3cd..c7c6a830 100644 --- a/src/components/CwlEditor.accessiblePlaceholder.test.tsx +++ b/src/components/CwlEditor.accessiblePlaceholder.test.tsx @@ -102,4 +102,28 @@ describe('accessible editor placeholder semantics', () => { collaborationDocument.destroy(); } }); + + it('omits collaborative placeholder guidance when the host supplies none', async () => { + const collaborationDocument = new Y.Doc(); + const editorRef = createRef(); + try { + render( + , + ); + + const textbox = await screen.findByRole('textbox', { + name: 'Shared report editor', + }); + await waitFor(() => expect(editorRef.current?.getEditor()).not.toBeNull()); + expect(textbox).not.toHaveAttribute('aria-placeholder'); + expect(visualPlaceholder(textbox)).toBeNull(); + } finally { + collaborationDocument.destroy(); + } + }); }); From fd5311b4d5406dd15561e8108998414b7c179564 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 17:37:03 +0900 Subject: [PATCH 13/13] test(accessibility): cover collaborative blank placeholder --- src/components/CwlEditor.accessiblePlaceholder.test.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/CwlEditor.accessiblePlaceholder.test.tsx b/src/components/CwlEditor.accessiblePlaceholder.test.tsx index c7c6a830..2c510488 100644 --- a/src/components/CwlEditor.accessiblePlaceholder.test.tsx +++ b/src/components/CwlEditor.accessiblePlaceholder.test.tsx @@ -103,7 +103,7 @@ describe('accessible editor placeholder semantics', () => { } }); - it('omits collaborative placeholder guidance when the host supplies none', async () => { + it('omits whitespace-only collaborative placeholder guidance', async () => { const collaborationDocument = new Y.Doc(); const editorRef = createRef(); try { @@ -112,6 +112,7 @@ describe('accessible editor placeholder semantics', () => { ref={editorRef} document={collaborationDocument} ariaLabel="Shared report editor" + placeholder=" " hideToolbar />, );