Skip to content

Commit 42e310e

Browse files
committed
code editor accessibility
1 parent d31d331 commit 42e310e

4 files changed

Lines changed: 105 additions & 8 deletions

File tree

‎package-lock.json‎

Lines changed: 5 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "solid-ui",
3-
"version": "5.0.2-8",
3+
"version": "5.0.2-9",
44
"description": "UI library for Solid applications",
55
"main": "dist/index.cjs.js",
66
"types": "dist/index.d.ts",

‎src/lib/code-editor/CodeEditor.ts‎

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,16 +18,27 @@ export class CodeEditor {
1818
private _view: EditorView | null = null
1919
private _languageCompartment: Compartment | null = null
2020
private _editableCompartment: Compartment | null = null
21+
private _readOnlyCompartment: Compartment | null = null
22+
private _accessibilityCompartment: Compartment | null = null
2123
private _onDirtyChange?: (dirty: boolean) => void
2224
private _isDirty = false
2325

24-
async initialize (container: HTMLElement, initialDoc = '', contentType: string = 'text/turtle', theme: ThemeMode = 'dark', onDirtyChange?: (dirty: boolean) => void) {
26+
async initialize (
27+
container: HTMLElement,
28+
initialDoc = '',
29+
contentType: string = 'text/turtle',
30+
theme: ThemeMode = 'dark',
31+
onDirtyChange?: (dirty: boolean) => void,
32+
accessibility?: { label?: string, description?: string }
33+
) {
2534
if (this._view) {
2635
this._view.destroy()
2736
}
2837

2938
this._languageCompartment = new Compartment()
3039
this._editableCompartment = new Compartment()
40+
this._readOnlyCompartment = new Compartment()
41+
this._accessibilityCompartment = new Compartment()
3142
this._onDirtyChange = onDirtyChange
3243
this._isDirty = false
3344
const languageExtension = await this._getLanguageExtension(contentType)
@@ -43,6 +54,11 @@ export class CodeEditor {
4354
}),
4455
this._languageCompartment.of(languageExtension),
4556
this._editableCompartment.of(EditorView.editable.of(true)),
57+
this._readOnlyCompartment.of(EditorState.readOnly.of(false)),
58+
this._accessibilityCompartment.of(EditorView.contentAttributes.of({
59+
...(accessibility?.label ? { 'aria-label': accessibility.label } : {}),
60+
...(accessibility?.description ? { 'aria-description': accessibility.description } : {})
61+
})),
4662
syntaxHighlighting(defaultHighlightStyle, { fallback: true }),
4763
lineNumbers(),
4864
history(),
@@ -94,9 +110,23 @@ export class CodeEditor {
94110
setReadOnly (readOnly: boolean) {
95111
if (!this._view) return
96112
const editableCompartment = this._editableCompartment
97-
if (!editableCompartment) return
113+
const readOnlyCompartment = this._readOnlyCompartment
114+
if (!editableCompartment || !readOnlyCompartment) return
98115
this._view.dispatch({
99-
effects: editableCompartment.reconfigure(EditorView.editable.of(!readOnly))
116+
effects: [
117+
editableCompartment.reconfigure(EditorView.editable.of(!readOnly)),
118+
readOnlyCompartment.reconfigure(EditorState.readOnly.of(readOnly))
119+
]
120+
})
121+
}
122+
123+
setAccessibility (accessibility: { label?: string, description?: string }) {
124+
if (!this._view || !this._accessibilityCompartment) return
125+
this._view.dispatch({
126+
effects: this._accessibilityCompartment.reconfigure(EditorView.contentAttributes.of({
127+
...(accessibility.label ? { 'aria-label': accessibility.label } : {}),
128+
...(accessibility.description ? { 'aria-description': accessibility.description } : {})
129+
}))
100130
})
101131
}
102132

‎test/unit/codeEditor.test.js‎

Lines changed: 66 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,10 @@ vi.mock('@codemirror/state', () => {
1212
}
1313

1414
const EditorState = {
15-
create: vi.fn((config) => ({ ...config }))
15+
create: vi.fn((config) => ({ ...config })),
16+
readOnly: {
17+
of: vi.fn((value) => ({ type: 'readOnly', value }))
18+
}
1619
}
1720

1821
return { Compartment, EditorState }
@@ -47,6 +50,9 @@ vi.mock('@codemirror/view', () => {
4750
EditorView.editable = {
4851
of: vi.fn((value) => ({ type: 'editable', value }))
4952
}
53+
EditorView.contentAttributes = {
54+
of: vi.fn((value) => ({ type: 'contentAttributes', value }))
55+
}
5056
EditorView.theme = vi.fn((spec, options) => ({ type: 'theme', spec, options }))
5157
EditorView.lineWrapping = { type: 'lineWrapping' }
5258
EditorView.updateListener = {
@@ -162,7 +168,10 @@ describe('CodeEditor', () => {
162168

163169
editor.setReadOnly(true)
164170
expect(view.dispatch).toHaveBeenLastCalledWith({
165-
effects: { type: 'reconfigure', value: { type: 'editable', value: false } }
171+
effects: [
172+
{ type: 'reconfigure', value: { type: 'editable', value: false } },
173+
{ type: 'reconfigure', value: { type: 'readOnly', value: true } }
174+
]
166175
})
167176

168177
editor.focusEditor()
@@ -201,4 +210,59 @@ describe('CodeEditor', () => {
201210
)
202211
expect(keymapExtension.value[0]).toMatchObject({ key: 'Tab' })
203212
})
213+
214+
it('exposes an accessible name and description on the editing surface', async () => {
215+
const editor = new CodeEditor()
216+
const container = document.createElement('div')
217+
document.body.appendChild(container)
218+
219+
await editor.initialize(
220+
container,
221+
'generated content',
222+
'text/n3',
223+
'dark',
224+
undefined,
225+
{
226+
label: 'Generated N3 for https://pod.example/card',
227+
description: 'Generated N3 serialization. This content is read-only.'
228+
}
229+
)
230+
231+
const attributes = createdViews[0].state.extensions.find(
232+
extension => extension.type === 'compartment-of' &&
233+
extension.value.type === 'contentAttributes'
234+
).value
235+
expect(attributes.value).toEqual({
236+
'aria-label': 'Generated N3 for https://pod.example/card',
237+
'aria-description': 'Generated N3 serialization. This content is read-only.'
238+
})
239+
})
240+
241+
it('updates the accessible description when editor mode changes', async () => {
242+
const editor = new CodeEditor()
243+
const container = document.createElement('div')
244+
document.body.appendChild(container)
245+
246+
await editor.initialize(container, 'content', 'text/plain', 'dark', undefined, {
247+
label: 'Source editor for https://pod.example/file.txt',
248+
description: 'Read-only source content.'
249+
})
250+
editor.setAccessibility({
251+
label: 'Source editor for https://pod.example/file.txt',
252+
description: 'Editable source content.'
253+
})
254+
255+
expect(createdViews[0].dispatch).toHaveBeenLastCalledWith({
256+
effects: {
257+
type: 'reconfigure',
258+
value: {
259+
type: 'contentAttributes',
260+
value: {
261+
'aria-label': 'Source editor for https://pod.example/file.txt',
262+
'aria-description': 'Editable source content.'
263+
}
264+
}
265+
}
266+
})
267+
})
204268
})

0 commit comments

Comments
 (0)