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

Expandables Added aria-controls and aria-expanded #820

Merged
merged 1 commit into from
Apr 7, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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<{}>>(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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