Skip to content
This repository has been archived by the owner on Sep 11, 2024. It is now read-only.

Commit

Permalink
fix(expandable-section, expandable-text, expandable-panel): Added ari…
Browse files Browse the repository at this point in the history
…a-controls and aria-expanded.

Fixed an issue where the correct aria attributes where not present on accordion like elements like
expandable-section, expandable-text and expandable-panel.

Fixes #788
  • Loading branch information
tomheller committed Apr 6, 2020
1 parent 72d9a35 commit 6d4f590
Show file tree
Hide file tree
Showing 15 changed files with 430 additions and 83 deletions.
33 changes: 33 additions & 0 deletions libs/barista-components/core/src/common-behaviours/id.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/**
* @license
* Copyright 2020 Dynatrace LLC
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import { mixinId } from './id';

describe('MixinId', () => {
it('should augment an existing class with an property', () => {
class EmptyClass {}

const classWithDisabled = mixinId(EmptyClass, 'dt-mixin-test');
const instance = new classWithDisabled();

// Expected the mixed-into class to have an id property
expect(instance.id).toMatch(/dt-mixin-test-\d/);

instance.id = 'my-id';
// Expected the mixed-into class to have an updated id property
expect(instance.id).toBe('my-id');
});
});
55 changes: 55 additions & 0 deletions libs/barista-components/core/src/common-behaviours/id.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/**
* @license
* Copyright 2020 Dynatrace LLC
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import { Constructor } from './constructor';
import { isDefined } from '../util';

/**
* UniqueId counter, will be incremented with every
* instatiation of the ExpandablePanel class
*/
let uniqueId = 0;

export interface HasId {
/** Represents the unique id of the component. */
id: string;
}

/** Mixin to augment a directive with a `id` property. */
export function mixinId<T extends Constructor<{}>>(
base: T,
idPreset: string,
): Constructor<HasId> & T {
return class extends base {
/** Sets a unique id for the expandable section. */
get id(): string {
return this._id;
}
set id(value: string) {
if (isDefined(value)) {
this._id = value;
} else {
this._id = `${idPreset}-${uniqueId++}`;
}
}
private _id = `${idPreset}-${uniqueId++}`;

// tslint:disable-next-line
constructor(...args: any[]) {
super(...args); // tslint:disable-line:no-inferred-empty-object-type
}
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,4 @@ export * from './error-state';
export * from './progress';
export * from './tabindex';
export * from './dom-exit';
export * from './id';
9 changes: 5 additions & 4 deletions libs/barista-components/expandable-panel/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,11 @@ element.

## Inputs

| Name | Type | Default | Description |
| ---------- | --------- | ------- | ---------------------------------------- |
| `expanded` | `boolean` | `false` | Sets or gets the panel's expanded state. |
| `disabled` | `boolean` | `false` | Sets or gets the panel's disabled state. |
| Name | Type | Default | Description |
| ---------- | --------- | ------------------------------------- | ---------------------------------------- |
| `expanded` | `boolean` | `false` | Sets or gets the panel's expanded state. |
| `disabled` | `boolean` | `false` | Sets or gets the panel's disabled state. |
| `id` | `string` | `dt-expandable-panel-{rollingNumber}` | Sets the id of the expandable panel. |

## Outputs

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@

import { DOWN_ARROW, UP_ARROW } from '@angular/cdk/keycodes';
import { ChangeDetectorRef, Directive, Input, OnDestroy } from '@angular/core';
import { Subscription } from 'rxjs';
import { Subscription, merge } from 'rxjs';

import { _readKeyCode } from '@dynatrace/barista-components/core';

Expand All @@ -33,6 +33,8 @@ import { DtExpandablePanel } from './expandable-panel';
'[attr.disabled]':
'dtExpandablePanel && dtExpandablePanel.disabled ? true: null',
'[attr.aria-disabled]': 'dtExpandablePanel && dtExpandablePanel.disabled',
'[attr.aria-expanded]': 'dtExpandablePanel && dtExpandablePanel.expanded',
'[attr.aria-controls]': 'dtExpandablePanel && dtExpandablePanel.id',
'[tabindex]': 'dtExpandablePanel && dtExpandablePanel.disabled ? -1 : 0',
'(click)': '_handleClick()',
'(keydown)': '_handleKeydown($event)',
Expand All @@ -47,11 +49,12 @@ export class DtExpandablePanelTrigger implements OnDestroy {
set dtExpandablePanel(value: DtExpandablePanel) {
this._panel = value;
this._expandedSubscription.unsubscribe();
this._expandedSubscription = this.dtExpandablePanel.expandChange.subscribe(
() => {
this._changeDetectorRef.markForCheck();
},
);
this._expandedSubscription = merge(
this.dtExpandablePanel.expandChange,
this.dtExpandablePanel._id,
).subscribe(() => {
this._changeDetectorRef.markForCheck();
});
}
private _panel: DtExpandablePanel;
private _expandedSubscription: Subscription = Subscription.EMPTY;
Expand Down
162 changes: 110 additions & 52 deletions libs/barista-components/expandable-panel/src/expandable-panel.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
// tslint:disable deprecation

import { Component, DebugElement } from '@angular/core';
import { TestBed, async } from '@angular/core/testing';
import { TestBed, async, ComponentFixture } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';

Expand Down Expand Up @@ -121,94 +121,150 @@ describe('DtExpandablePanel', () => {
expect(instanceElement.classList).toContain('dt-expandable-panel-opened');
});

// check CSS class of trigger when expanded
it('should have correctly styled trigger button when expanded', () => {
const panelFixture = createComponent(ExpandablePanelWithTriggerComponent);
const panelDebugElement = panelFixture.debugElement.query(
By.directive(DtExpandablePanel),
// check expanded and expandChange outputs
it('should fire expanded and expandChange events on open', () => {
const expandedSpy = jest.fn();
const changedSpy = jest.fn();
const instance = instanceDebugElement.componentInstance;
const expandedSubscription = instance._panelExpanded.subscribe(
expandedSpy,
);
const triggerInstanceElement = panelFixture.debugElement.query(
const changedSubscription = instance.expandChange.subscribe(changedSpy);

expandablePanelInstance.open();
fixture.detectChanges();
expect(expandedSpy).toHaveBeenCalled();
expect(changedSpy).toHaveBeenCalled();

expandedSubscription.unsubscribe();
changedSubscription.unsubscribe();
});

// check collapsed and expandChange outputs
it('should fire collapsed and expandChange events on close', () => {
expandablePanelInstance.expanded = true;
const collapsedSpy = jest.fn();
const changedSpy = jest.fn();
const instance = instanceDebugElement.componentInstance;
const collapsedSubscription = instance._panelCollapsed.subscribe(
collapsedSpy,
);
const changedSubscription = instance.expandChange.subscribe(changedSpy);

expandablePanelInstance.close();
fixture.detectChanges();
expect(collapsedSpy).toHaveBeenCalled();
expect(changedSpy).toHaveBeenCalled();

collapsedSubscription.unsubscribe();
changedSubscription.unsubscribe();
});
});

describe('dt-expandable-panel with trigger', () => {
let fixture: ComponentFixture<ExpandablePanelWithTriggerComponent>;
let triggerInstanceElement: HTMLElement;
let panelDebugElement: DebugElement;
let panelInstance: DtExpandablePanel;
let panelInstanceElement: HTMLElement;

beforeEach(() => {
fixture = createComponent(ExpandablePanelWithTriggerComponent);
triggerInstanceElement = fixture.debugElement.query(
By.css('.dt-expandable-panel-trigger'),
).nativeElement;
const panelInstance = panelDebugElement.injector.get<DtExpandablePanel>(
DtExpandablePanel,
panelInstance = fixture.debugElement.query(
By.directive(DtExpandablePanel),
).componentInstance;
panelDebugElement = fixture.debugElement.query(
By.directive(DtExpandablePanel),
);
panelInstanceElement = panelDebugElement.nativeElement;
});

// check CSS class of trigger when expanded
it('should have correctly styled trigger button when expanded', () => {
expect(triggerInstanceElement.classList).toContain(
'dt-expandable-panel-trigger',
);
expect(triggerInstanceElement.classList).not.toContain(
'dt-expandable-panel-trigger-open',
);
panelInstance.expanded = true;
panelFixture.detectChanges();
fixture.detectChanges();
expect(triggerInstanceElement.classList).toContain(
'dt-expandable-panel-trigger-open',
);
});

// check attributes of panel and trigger when disabled
it('should have correct attributes when disabled', () => {
const panelFixture = createComponent(ExpandablePanelWithTriggerComponent);
const panelDebugElement = panelFixture.debugElement.query(
By.directive(DtExpandablePanel),
);
const triggerInstanceElement = panelFixture.debugElement.query(
By.css('.dt-expandable-panel-trigger'),
).nativeElement;
const panelInstanceElement = panelDebugElement.nativeElement;
const panelInstance = panelDebugElement.injector.get<DtExpandablePanel>(
DtExpandablePanel,
);

expect(panelInstanceElement.getAttribute('aria-disabled')).toBe('false');
expect(triggerInstanceElement.getAttribute('tabindex')).toBe('0');
expect(triggerInstanceElement.getAttribute('disabled')).toBe(null);

panelInstance.disabled = true;
panelFixture.detectChanges();
fixture.detectChanges();
expect(panelInstanceElement.getAttribute('aria-disabled')).toBe('true');
expect(triggerInstanceElement.getAttribute('tabindex')).toBe('-1');
expect(triggerInstanceElement.getAttribute('disabled')).toBe('true');
});

// check expanded and expandChange outputs
it('should fire expanded and expandChange events on open', () => {
const expandedSpy = jest.fn();
const changedSpy = jest.fn();
const instance = instanceDebugElement.componentInstance;
const expandedSubscription = instance._panelExpanded.subscribe(
expandedSpy,
// check aria-controls attribute
it('should have the correct aria-controls attribute', () => {
expect(triggerInstanceElement.getAttribute('aria-controls')).toMatch(
/dt-expandable-panel-\d/,
);
const changedSubscription = instance.expandChange.subscribe(changedSpy);
});

expandablePanelInstance.open();
// check aria-controls attribute
it('should have the correct aria-controls attribute when using ID input', () => {
fixture.componentInstance.id = 'my-panel';
fixture.detectChanges();
expect(expandedSpy).toHaveBeenCalled();
expect(changedSpy).toHaveBeenCalled();

expandedSubscription.unsubscribe();
changedSubscription.unsubscribe();
triggerInstanceElement = fixture.debugElement.query(
By.css('.dt-expandable-panel-trigger'),
).nativeElement;
panelInstanceElement = fixture.debugElement.query(
By.css('.dt-expandable-panel'),
).nativeElement;

expect(panelInstanceElement.getAttribute('id')).toBe('my-panel');
expect(triggerInstanceElement.getAttribute('aria-controls')).toBe(
'my-panel',
);
});

// check collapsed and expandChange outputs
it('should fire collapsed and expandChange events on close', () => {
expandablePanelInstance.expanded = true;
const collapsedSpy = jest.fn();
const changedSpy = jest.fn();
const instance = instanceDebugElement.componentInstance;
const collapsedSubscription = instance._panelCollapsed.subscribe(
collapsedSpy,
// check aria-controls attribute
it('should fall back to the default id when ID is unset', () => {
fixture.componentInstance.id = 'my-panel';
fixture.detectChanges();

fixture.componentInstance.id = null;
fixture.detectChanges();

triggerInstanceElement = fixture.debugElement.query(
By.css('.dt-expandable-panel-trigger'),
).nativeElement;

expect(triggerInstanceElement.getAttribute('aria-controls')).toMatch(
/dt-expandable-panel-\d/,
);
const changedSubscription = instance.expandChange.subscribe(changedSpy);
});

expandablePanelInstance.close();
// check if it has the correct aria-expanded attribute
it('should have the correct aria-expanded attribute', () => {
expect(triggerInstanceElement.getAttribute('aria-expanded')).toBe(
'false',
);
});

// check if it has the correct aria-expanded attribute is set after expanding
it('should have the correct aria-expanded attribute after opening the expandable', () => {
panelInstance.expanded = true;
fixture.detectChanges();
expect(collapsedSpy).toHaveBeenCalled();
expect(changedSpy).toHaveBeenCalled();

collapsedSubscription.unsubscribe();
changedSubscription.unsubscribe();
expect(triggerInstanceElement.getAttribute('aria-expanded')).toBe('true');
});
});
});
Expand All @@ -224,8 +280,10 @@ class ExpandablePanelComponent {}
@Component({
selector: 'dt-test-app',
template: `
<dt-expandable-panel #panel>text</dt-expandable-panel>
<dt-expandable-panel #panel [id]="id">text</dt-expandable-panel>
<button [dtExpandablePanel]="panel">trigger</button>
`,
})
class ExpandablePanelWithTriggerComponent {}
class ExpandablePanelWithTriggerComponent {
id: string | null;
}
Loading

0 comments on commit 6d4f590

Please sign in to comment.