Skip to content

Commit

Permalink
Hds::CodeEditor & hds-code-editor modifer - aria-describedby su…
Browse files Browse the repository at this point in the history
…pport (#2661)
  • Loading branch information
zamoore authored Jan 22, 2025
1 parent 4f56a37 commit 2dbe8b7
Show file tree
Hide file tree
Showing 8 changed files with 138 additions and 19 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,13 @@
SPDX-License-Identifier: MPL-2.0
}}

<Hds::Text::Body class="hds-code-editor__description" @tag="p" @size="100" ...attributes>
<Hds::Text::Body
id={{this._id}}
class="hds-code-editor__description"
@tag="p"
@size="100"
{{did-insert @onInsert}}
...attributes
>
{{yield}}
</Hds::Text::Body>
16 changes: 10 additions & 6 deletions packages/components/src/components/hds/code-editor/description.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,22 @@
* SPDX-License-Identifier: MPL-2.0
*/

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

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

type HdsCodeEditorDescriptionElement = HdsTextBodySignature['Element'];
export interface HdsCodeEditorDescriptionSignature {
Args: {
editorId: string;
onInsert: (element: HdsCodeEditorDescriptionElement) => void;
};
Blocks: {
default: [];
};
Element: HdsTextBodySignature['Element'];
Element: HdsCodeEditorDescriptionElement;
}

const HdsCodeEditorDescription =
TemplateOnlyComponent<HdsCodeEditorDescriptionSignature>();

export default HdsCodeEditorDescription;
export default class HdsCodeEditorDescription extends Component<HdsCodeEditorDescriptionSignature> {
private _id = `${this.args.editorId}-description`;
}
5 changes: 4 additions & 1 deletion packages/components/src/components/hds/code-editor/index.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@
{{yield
(hash
Title=(component "hds/code-editor/title" editorId=this._id onInsert=this.registerTitleElement)
Description=(component "hds/code-editor/description")
Description=(component
"hds/code-editor/description" editorId=this._id onInsert=this.registerDescriptionElement
)
Generic=(component "hds/code-editor/generic")
)
}}
Expand Down Expand Up @@ -50,6 +52,7 @@
<div
class="hds-code-editor__editor"
{{hds-code-editor
ariaDescribedBy=this.ariaDescribedBy
ariaLabel=@ariaLabel
ariaLabelledBy=this.ariaLabelledBy
value=@value
Expand Down
22 changes: 13 additions & 9 deletions packages/components/src/components/hds/code-editor/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,20 +15,12 @@ import type { HdsCodeEditorTitleSignature } from './title';
import type { HdsCodeEditorGenericSignature } from './generic';
import type { EditorView } from '@codemirror/view';
import { guidFor } from '@ember/object/internals';

export interface HdsCodeEditorSignature {
Args: {
ariaLabel?: string;
ariaLabelledBy?: string;
hasCopyButton?: boolean;
hasFullScreenButton?: boolean;
isStandalone?: boolean;
language?: HdsCodeEditorModifierSignature['Args']['Named']['language'];
value?: HdsCodeEditorModifierSignature['Args']['Named']['value'];
onBlur?: HdsCodeEditorModifierSignature['Args']['Named']['onBlur'];
onInput?: HdsCodeEditorModifierSignature['Args']['Named']['onInput'];
onSetup?: HdsCodeEditorModifierSignature['Args']['Named']['onSetup'];
};
} & HdsCodeEditorModifierSignature['Args']['Named'];
Blocks: {
default: [
{
Expand All @@ -46,6 +38,7 @@ export default class HdsCodeEditor extends Component<HdsCodeEditorSignature> {
@tracked private _isSetupComplete = false;
@tracked private _value;
@tracked private _titleId: string | undefined;
@tracked private _descriptionId: string | undefined;

private _id = guidFor(this);

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

get ariaDescribedBy(): string | undefined {
return this.args.ariaDescribedBy ?? this._descriptionId;
}

get hasActions(): boolean {
return (this.args.hasCopyButton || this.args.hasFullScreenButton) ?? false;
}
Expand Down Expand Up @@ -110,6 +107,13 @@ export default class HdsCodeEditor extends Component<HdsCodeEditorSignature> {
this._titleId = element.id;
}

@action
registerDescriptionElement(
element: HdsCodeEditorDescriptionSignature['Element']
): void {
this._descriptionId = element.id;
}

@action
toggleFullScreen(): void {
this._isFullScreen = !this._isFullScreen;
Expand Down
38 changes: 36 additions & 2 deletions packages/components/src/modifiers/hds-code-editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ type HdsCodeEditorBlurHandler = (editor: EditorView, event: FocusEvent) => void;
export interface HdsCodeEditorSignature {
Args: {
Named: {
ariaDescribedBy?: string;
ariaLabel?: string;
ariaLabelledBy?: string;
language?: HdsCodeEditorLanguages;
Expand Down Expand Up @@ -155,7 +156,7 @@ export default class HdsCodeEditorModifier extends Modifier<HdsCodeEditorSignatu
(inputElement as HTMLElement).addEventListener('blur', this.blurHandler);
}

private _setupEditorAriaAttributes(
private _setupEditorAriaLabel(
editor: EditorView,
{
ariaLabel,
Expand All @@ -181,6 +182,34 @@ export default class HdsCodeEditorModifier extends Modifier<HdsCodeEditorSignatu
}
}

private _setupEditorAriaDescribedBy(
editor: EditorView,
ariaDescribedBy?: string
) {
if (ariaDescribedBy === undefined) {
return;
}

editor.dom
.querySelector('[role="textbox"]')
?.setAttribute('aria-describedby', ariaDescribedBy);
}

private _setupEditorAriaAttributes(
editor: EditorView,
{
ariaDescribedBy,
ariaLabel,
ariaLabelledBy,
}: Pick<
HdsCodeEditorSignature['Args']['Named'],
'ariaDescribedBy' | 'ariaLabel' | 'ariaLabelledBy'
>
) {
this._setupEditorAriaLabel(editor, { ariaLabel, ariaLabelledBy });
this._setupEditorAriaDescribedBy(editor, ariaDescribedBy);
}

private _loadLanguageTask = task(
{ drop: true },
async (language?: HdsCodeEditorLanguages) => {
Expand Down Expand Up @@ -319,6 +348,7 @@ export default class HdsCodeEditorModifier extends Modifier<HdsCodeEditorSignatu
onBlur,
onInput,
onSetup,
ariaDescribedBy,
ariaLabel,
ariaLabelledBy,
language,
Expand All @@ -345,7 +375,11 @@ export default class HdsCodeEditorModifier extends Modifier<HdsCodeEditorSignatu
this._setupEditorBlurHandler(element, onBlur);
}

this._setupEditorAriaAttributes(editor, { ariaLabel, ariaLabelledBy });
this._setupEditorAriaAttributes(editor, {
ariaDescribedBy,
ariaLabel,
ariaLabelledBy,
});

onSetup?.(this.editor);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: MPL-2.0
*/

import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';
import { render } from '@ember/test-helpers';
import { hbs } from 'ember-cli-htmlbars';
import sinon from 'sinon';

module(
'Integration | Component | hds/code-editor/description',
function (hooks) {
setupRenderingTest(hooks);

test('it should render the component with a CSS class that matches the component name', async function (assert) {
this.set('noop', () => {});

await render(
hbs`<Hds::CodeEditor::Description @editorId="test" @onInsert={{this.noop}} />`
);

assert.dom('.hds-code-editor__description').exists();
});

// @onInsert
test('it should call the `@onInsert` action when the description is inserted', async function (assert) {
const onInsert = sinon.spy();
this.set('onInsert', onInsert);

await render(
hbs`<Hds::CodeEditor::Description @editorId="test" @onInsert={{this.onInsert}}>Test description</Hds::CodeEditor::Description>`
);

assert.true(onInsert.calledOnce);
});
}
);
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,14 @@ module('Integration | Component | hds/code-editor/index', function (hooks) {
await setupCodeEditor(hbs`<Hds::CodeEditor @ariaLabel="code editor" />`);
assert.dom('hds-code-editor__description').doesNotExist();
});
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) {
await setupCodeEditor(
hbs`<Hds::CodeEditor @ariaLabel="code editor" as |CE|><CE.Description id="test-description">Test Description</CE.Description></Hds::CodeEditor>`
);
assert
.dom('.hds-code-editor__editor .cm-editor [role="textbox"]')
.hasAttribute('aria-describedby', 'test-description');
});

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

// @ariaDescribedBy
test('it should render the component with an aria-describedby when provided', async function (assert) {
await setupCodeEditor(
hbs`<Hds::CodeEditor @ariaLabel="code editor" @ariaDescribedBy="test-description" />`
);
assert
.dom('.hds-code-editor__editor .cm-editor [role="textbox"]')
.hasAttribute('aria-describedby', 'test-description');
});

// @ariaLabel
test('it should render the component with an aria-label when provided', async function (assert) {
await setupCodeEditor(
Expand Down
10 changes: 10 additions & 0 deletions showcase/tests/integration/modifiers/hds-code-editor-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,16 @@ module('Integration | Modifier | hds-code-editor', function (hooks) {
assert.ok(inputSpy.calledOnceWith('Test string'));
});

// ariaDescribedBy
test('it should render the editor with an aria-describedby when provided', async function (assert) {
await setupCodeEditor(
hbs`<div id="code-editor-wrapper" {{hds-code-editor ariaLabel="test" ariaDescribedBy="test-description"}} />`
);
assert
.dom('#code-editor-wrapper .cm-editor [role="textbox"]')
.hasAttribute('aria-describedby', 'test-description');
});

// ariaLabel
test('it should render the editor with an aria-label when provided', async function (assert) {
await setupCodeEditor(
Expand Down

0 comments on commit 2dbe8b7

Please sign in to comment.