Skip to content

Commit 2dbe8b7

Browse files
authored
Hds::CodeEditor & hds-code-editor modifer - aria-describedby support (#2661)
1 parent 4f56a37 commit 2dbe8b7

File tree

8 files changed

+138
-19
lines changed

8 files changed

+138
-19
lines changed

packages/components/src/components/hds/code-editor/description.hbs

+8-1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,13 @@
33
SPDX-License-Identifier: MPL-2.0
44
}}
55

6-
<Hds::Text::Body class="hds-code-editor__description" @tag="p" @size="100" ...attributes>
6+
<Hds::Text::Body
7+
id={{this._id}}
8+
class="hds-code-editor__description"
9+
@tag="p"
10+
@size="100"
11+
{{did-insert @onInsert}}
12+
...attributes
13+
>
714
{{yield}}
815
</Hds::Text::Body>

packages/components/src/components/hds/code-editor/description.ts

+10-6
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,22 @@
33
* SPDX-License-Identifier: MPL-2.0
44
*/
55

6-
import TemplateOnlyComponent from '@ember/component/template-only';
6+
import Component from '@glimmer/component';
77

88
import type { HdsTextBodySignature } from '../text/body';
99

10+
type HdsCodeEditorDescriptionElement = HdsTextBodySignature['Element'];
1011
export interface HdsCodeEditorDescriptionSignature {
12+
Args: {
13+
editorId: string;
14+
onInsert: (element: HdsCodeEditorDescriptionElement) => void;
15+
};
1116
Blocks: {
1217
default: [];
1318
};
14-
Element: HdsTextBodySignature['Element'];
19+
Element: HdsCodeEditorDescriptionElement;
1520
}
1621

17-
const HdsCodeEditorDescription =
18-
TemplateOnlyComponent<HdsCodeEditorDescriptionSignature>();
19-
20-
export default HdsCodeEditorDescription;
22+
export default class HdsCodeEditorDescription extends Component<HdsCodeEditorDescriptionSignature> {
23+
private _id = `${this.args.editorId}-description`;
24+
}

packages/components/src/components/hds/code-editor/index.hbs

+4-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@
1818
{{yield
1919
(hash
2020
Title=(component "hds/code-editor/title" editorId=this._id onInsert=this.registerTitleElement)
21-
Description=(component "hds/code-editor/description")
21+
Description=(component
22+
"hds/code-editor/description" editorId=this._id onInsert=this.registerDescriptionElement
23+
)
2224
Generic=(component "hds/code-editor/generic")
2325
)
2426
}}
@@ -50,6 +52,7 @@
5052
<div
5153
class="hds-code-editor__editor"
5254
{{hds-code-editor
55+
ariaDescribedBy=this.ariaDescribedBy
5356
ariaLabel=@ariaLabel
5457
ariaLabelledBy=this.ariaLabelledBy
5558
value=@value

packages/components/src/components/hds/code-editor/index.ts

+13-9
Original file line numberDiff line numberDiff line change
@@ -15,20 +15,12 @@ import type { HdsCodeEditorTitleSignature } from './title';
1515
import type { HdsCodeEditorGenericSignature } from './generic';
1616
import type { EditorView } from '@codemirror/view';
1717
import { guidFor } from '@ember/object/internals';
18-
1918
export interface HdsCodeEditorSignature {
2019
Args: {
21-
ariaLabel?: string;
22-
ariaLabelledBy?: string;
2320
hasCopyButton?: boolean;
2421
hasFullScreenButton?: boolean;
2522
isStandalone?: boolean;
26-
language?: HdsCodeEditorModifierSignature['Args']['Named']['language'];
27-
value?: HdsCodeEditorModifierSignature['Args']['Named']['value'];
28-
onBlur?: HdsCodeEditorModifierSignature['Args']['Named']['onBlur'];
29-
onInput?: HdsCodeEditorModifierSignature['Args']['Named']['onInput'];
30-
onSetup?: HdsCodeEditorModifierSignature['Args']['Named']['onSetup'];
31-
};
23+
} & HdsCodeEditorModifierSignature['Args']['Named'];
3224
Blocks: {
3325
default: [
3426
{
@@ -46,6 +38,7 @@ export default class HdsCodeEditor extends Component<HdsCodeEditorSignature> {
4638
@tracked private _isSetupComplete = false;
4739
@tracked private _value;
4840
@tracked private _titleId: string | undefined;
41+
@tracked private _descriptionId: string | undefined;
4942

5043
private _id = guidFor(this);
5144

@@ -81,6 +74,10 @@ export default class HdsCodeEditor extends Component<HdsCodeEditorSignature> {
8174
return this.args.ariaLabelledBy ?? this._titleId;
8275
}
8376

77+
get ariaDescribedBy(): string | undefined {
78+
return this.args.ariaDescribedBy ?? this._descriptionId;
79+
}
80+
8481
get hasActions(): boolean {
8582
return (this.args.hasCopyButton || this.args.hasFullScreenButton) ?? false;
8683
}
@@ -110,6 +107,13 @@ export default class HdsCodeEditor extends Component<HdsCodeEditorSignature> {
110107
this._titleId = element.id;
111108
}
112109

110+
@action
111+
registerDescriptionElement(
112+
element: HdsCodeEditorDescriptionSignature['Element']
113+
): void {
114+
this._descriptionId = element.id;
115+
}
116+
113117
@action
114118
toggleFullScreen(): void {
115119
this._isFullScreen = !this._isFullScreen;

packages/components/src/modifiers/hds-code-editor.ts

+36-2
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ type HdsCodeEditorBlurHandler = (editor: EditorView, event: FocusEvent) => void;
2727
export interface HdsCodeEditorSignature {
2828
Args: {
2929
Named: {
30+
ariaDescribedBy?: string;
3031
ariaLabel?: string;
3132
ariaLabelledBy?: string;
3233
language?: HdsCodeEditorLanguages;
@@ -155,7 +156,7 @@ export default class HdsCodeEditorModifier extends Modifier<HdsCodeEditorSignatu
155156
(inputElement as HTMLElement).addEventListener('blur', this.blurHandler);
156157
}
157158

158-
private _setupEditorAriaAttributes(
159+
private _setupEditorAriaLabel(
159160
editor: EditorView,
160161
{
161162
ariaLabel,
@@ -181,6 +182,34 @@ export default class HdsCodeEditorModifier extends Modifier<HdsCodeEditorSignatu
181182
}
182183
}
183184

185+
private _setupEditorAriaDescribedBy(
186+
editor: EditorView,
187+
ariaDescribedBy?: string
188+
) {
189+
if (ariaDescribedBy === undefined) {
190+
return;
191+
}
192+
193+
editor.dom
194+
.querySelector('[role="textbox"]')
195+
?.setAttribute('aria-describedby', ariaDescribedBy);
196+
}
197+
198+
private _setupEditorAriaAttributes(
199+
editor: EditorView,
200+
{
201+
ariaDescribedBy,
202+
ariaLabel,
203+
ariaLabelledBy,
204+
}: Pick<
205+
HdsCodeEditorSignature['Args']['Named'],
206+
'ariaDescribedBy' | 'ariaLabel' | 'ariaLabelledBy'
207+
>
208+
) {
209+
this._setupEditorAriaLabel(editor, { ariaLabel, ariaLabelledBy });
210+
this._setupEditorAriaDescribedBy(editor, ariaDescribedBy);
211+
}
212+
184213
private _loadLanguageTask = task(
185214
{ drop: true },
186215
async (language?: HdsCodeEditorLanguages) => {
@@ -319,6 +348,7 @@ export default class HdsCodeEditorModifier extends Modifier<HdsCodeEditorSignatu
319348
onBlur,
320349
onInput,
321350
onSetup,
351+
ariaDescribedBy,
322352
ariaLabel,
323353
ariaLabelledBy,
324354
language,
@@ -345,7 +375,11 @@ export default class HdsCodeEditorModifier extends Modifier<HdsCodeEditorSignatu
345375
this._setupEditorBlurHandler(element, onBlur);
346376
}
347377

348-
this._setupEditorAriaAttributes(editor, { ariaLabel, ariaLabelledBy });
378+
this._setupEditorAriaAttributes(editor, {
379+
ariaDescribedBy,
380+
ariaLabel,
381+
ariaLabelledBy,
382+
});
349383

350384
onSetup?.(this.editor);
351385
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/**
2+
* Copyright (c) HashiCorp, Inc.
3+
* SPDX-License-Identifier: MPL-2.0
4+
*/
5+
6+
import { module, test } from 'qunit';
7+
import { setupRenderingTest } from 'ember-qunit';
8+
import { render } from '@ember/test-helpers';
9+
import { hbs } from 'ember-cli-htmlbars';
10+
import sinon from 'sinon';
11+
12+
module(
13+
'Integration | Component | hds/code-editor/description',
14+
function (hooks) {
15+
setupRenderingTest(hooks);
16+
17+
test('it should render the component with a CSS class that matches the component name', async function (assert) {
18+
this.set('noop', () => {});
19+
20+
await render(
21+
hbs`<Hds::CodeEditor::Description @editorId="test" @onInsert={{this.noop}} />`
22+
);
23+
24+
assert.dom('.hds-code-editor__description').exists();
25+
});
26+
27+
// @onInsert
28+
test('it should call the `@onInsert` action when the description is inserted', async function (assert) {
29+
const onInsert = sinon.spy();
30+
this.set('onInsert', onInsert);
31+
32+
await render(
33+
hbs`<Hds::CodeEditor::Description @editorId="test" @onInsert={{this.onInsert}}>Test description</Hds::CodeEditor::Description>`
34+
);
35+
36+
assert.true(onInsert.calledOnce);
37+
});
38+
}
39+
);

showcase/tests/integration/components/hds/code-editor/index-test.js

+18
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,14 @@ module('Integration | Component | hds/code-editor/index', function (hooks) {
6464
await setupCodeEditor(hbs`<Hds::CodeEditor @ariaLabel="code editor" />`);
6565
assert.dom('hds-code-editor__description').doesNotExist();
6666
});
67+
test('when aria-describedby is not provided and the `Description` contextual component is yielded, it should use the description element id as the aria-describedby value', async function (assert) {
68+
await setupCodeEditor(
69+
hbs`<Hds::CodeEditor @ariaLabel="code editor" as |CE|><CE.Description id="test-description">Test Description</CE.Description></Hds::CodeEditor>`
70+
);
71+
assert
72+
.dom('.hds-code-editor__editor .cm-editor [role="textbox"]')
73+
.hasAttribute('aria-describedby', 'test-description');
74+
});
6775

6876
// yielded block content
6977
test('it should render custom content in the toolbar when provided', async function (assert) {
@@ -191,6 +199,16 @@ module('Integration | Component | hds/code-editor/index', function (hooks) {
191199
sinon.restore();
192200
});
193201

202+
// @ariaDescribedBy
203+
test('it should render the component with an aria-describedby when provided', async function (assert) {
204+
await setupCodeEditor(
205+
hbs`<Hds::CodeEditor @ariaLabel="code editor" @ariaDescribedBy="test-description" />`
206+
);
207+
assert
208+
.dom('.hds-code-editor__editor .cm-editor [role="textbox"]')
209+
.hasAttribute('aria-describedby', 'test-description');
210+
});
211+
194212
// @ariaLabel
195213
test('it should render the component with an aria-label when provided', async function (assert) {
196214
await setupCodeEditor(

showcase/tests/integration/modifiers/hds-code-editor-test.js

+10
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,16 @@ module('Integration | Modifier | hds-code-editor', function (hooks) {
8383
assert.ok(inputSpy.calledOnceWith('Test string'));
8484
});
8585

86+
// ariaDescribedBy
87+
test('it should render the editor with an aria-describedby when provided', async function (assert) {
88+
await setupCodeEditor(
89+
hbs`<div id="code-editor-wrapper" {{hds-code-editor ariaLabel="test" ariaDescribedBy="test-description"}} />`
90+
);
91+
assert
92+
.dom('#code-editor-wrapper .cm-editor [role="textbox"]')
93+
.hasAttribute('aria-describedby', 'test-description');
94+
});
95+
8696
// ariaLabel
8797
test('it should render the editor with an aria-label when provided', async function (assert) {
8898
await setupCodeEditor(

0 commit comments

Comments
 (0)