-
Notifications
You must be signed in to change notification settings - Fork 0
feat(accessibility): expose editor placeholder semantics #126
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
Closed
Changes from all commits
Commits
Show all changes
19 commits
Select commit
Hold shift + click to select a range
ca25ed4
test(accessibility): define editor placeholder RED
seonghobae f231385
feat(accessibility): expose normalized editor placeholder
seonghobae d533be4
feat(accessibility): bind visual placeholder to textbox semantics
seonghobae d2b1b7a
feat(accessibility): align collaborative placeholder semantics
seonghobae 0b70ec5
test(accessibility): verify placeholder semantics on both editor surf…
seonghobae 553e21d
docs(accessibility): record editor placeholder semantics
seonghobae c5d4312
test(docs): bind accessible placeholder authority
seonghobae 2e38faa
test(accessibility): bind placeholder name separation semantically
seonghobae afc1757
test(accessibility): expose visual placeholder normalization drift
seonghobae 7aa01ab
fix(accessibility): share placeholder normalization
seonghobae 79c3003
fix(accessibility): normalize standalone visual placeholder
seonghobae e4b3182
fix(accessibility): normalize collaborative visual placeholder
seonghobae a2bd2cc
test(package): smoke public placeholder semantics
seonghobae 5af4bb6
fix(accessibility): keep placeholder callback live
seonghobae e9571b2
fix(accessibility): refresh standalone visual placeholder
seonghobae fe77e5a
fix(accessibility): refresh collaborative visual placeholder
seonghobae 0b40373
test(package): run public placeholder smoke verification
seonghobae 0c1915d
docs(accessibility): align placeholder verification authority
seonghobae 3a4803b
test(package): bind placeholder smoke to npm pack inventory
seonghobae File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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. | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
|
|
||
| ## 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/ | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
102 changes: 102 additions & 0 deletions
102
src/components/CwlEditor.accessiblePlaceholder.test.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<CwlEditorHandle>(); | ||
| const { rerender } = render( | ||
| <CwlEditor | ||
| ref={editorRef} | ||
| ariaLabel="Report editor" | ||
| placeholder=" Start the report… " | ||
| />, | ||
| ); | ||
|
|
||
| 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( | ||
| <CwlEditor | ||
| ref={editorRef} | ||
| ariaLabel="Report editor" | ||
| placeholder="Continue with evidence…" | ||
| />, | ||
| ); | ||
| await waitFor(() => | ||
| expect(textbox).toHaveAttribute( | ||
| 'aria-placeholder', | ||
| 'Continue with evidence…', | ||
| ), | ||
| ); | ||
| expect(visualPlaceholder(textbox)).toBe('Continue with evidence…'); | ||
| expect(editorRef.current!.getEditor()).toBe(editor); | ||
|
|
||
| rerender( | ||
| <CwlEditor | ||
| ref={editorRef} | ||
| ariaLabel="Report editor" | ||
| placeholder=" " | ||
| />, | ||
| ); | ||
| 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<CwlEditorHandle>(); | ||
| try { | ||
| const { rerender } = render( | ||
| <CollaborativeCwlEditor | ||
| ref={editorRef} | ||
| document={collaborationDocument} | ||
| ariaLabel="Shared report editor" | ||
| placeholder=" Shared report… " | ||
| hideToolbar | ||
| />, | ||
| ); | ||
|
|
||
| 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( | ||
| <CollaborativeCwlEditor | ||
| ref={editorRef} | ||
| document={collaborationDocument} | ||
| ariaLabel="Shared report editor" | ||
| placeholder="Review together…" | ||
| hideToolbar | ||
| />, | ||
| ); | ||
| 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(); | ||
| } | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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…', | ||
| }); | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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).'); | ||
| }); | ||
| }); |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.