Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding basic support for Narrators on Dropdown #433

Merged
merged 6 commits into from Apr 10, 2018
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
8 changes: 8 additions & 0 deletions src/ng-select/id.ts
@@ -0,0 +1,8 @@
export function newId() {
// First character is an 'a', it's good practice to tag id to begin with a letter
return 'axxxxxxxxxxx'.replace(/[x]/g, function (_) {
// tslint:disable-next-line:no-bitwise
const val = Math.random() * 16 | 0;
return val.toString(16);
});
}
5 changes: 4 additions & 1 deletion src/ng-select/items-list.ts
Expand Up @@ -2,6 +2,7 @@ import { NgOption } from './ng-select.types';
import * as searchHelper from './search-helper';
import { NgSelectComponent } from './ng-select.component';
import { isObject, isDefined } from './value-utils';
import { newId } from './id';

type OptionGroups = Map<string, NgOption[]>;

Expand Down Expand Up @@ -213,6 +214,7 @@ export class ItemsList {
label: isDefined(label) ? label.toString() : '',
value: item,
disabled: item.disabled,
htmlId: newId()
};
}

Expand Down Expand Up @@ -281,7 +283,8 @@ export class ItemsList {
label: key,
hasChildren: true,
index: i,
disabled: !this._ngSelect.selectableGroup
disabled: !this._ngSelect.selectableGroup,
htmlId: newId()
};
parent.value = {};
parent.value[this._ngSelect.groupBy] = key;
Expand Down
13 changes: 9 additions & 4 deletions src/ng-select/ng-select.component.html
Expand Up @@ -31,7 +31,10 @@
(focus)="onInputFocus()"
(blur)="onInputBlur()"
(change)="$event.stopPropagation()"
role="combobox">
role="combobox"
[attr.aria-expanded]="isOpen"
[attr.aria-owns]="isOpen ? dropdownId : null"
[attr.aria-activedescendant]="isOpen ? itemsList?.markedItem?.htmlId : null">
</div>
</div>

Expand Down Expand Up @@ -61,17 +64,19 @@
[items]="itemsList.filteredItems"
(update)="viewPortItems = $event"
(scrollToEnd)="scrollToEnd.emit($event)"
[ngClass]="{'multiple': multiple}">
[ngClass]="{'multiple': multiple}"
[id]="dropdownId">

<ng-container>
<div class="ng-option" role="option" (click)="toggleItem(item)" (mousedown)="$event.preventDefault()" (mouseover)="onItemHover(item)"
<div class="ng-option" [attr.role]="item.hasChildren ? 'group' : 'option'" (click)="toggleItem(item)" (mousedown)="$event.preventDefault()" (mouseover)="onItemHover(item)"
*ngFor="let item of viewPortItems"
[class.disabled]="item.disabled"
[class.selected]="item.selected"
[class.ng-optgroup]="item.hasChildren"
[class.ng-option]="!item.hasChildren"
[class.ng-option-child]="!!item.parent"
[class.marked]="item === itemsList.markedItem">
[class.marked]="item === itemsList.markedItem"
id="{{item?.htmlId || null}}">

<ng-template #defaultOptionTemplate>
<span class="ng-option-label" [innerHTML]="item.label" [ngOptionHighlight]="filterValue"></span>
Expand Down
80 changes: 79 additions & 1 deletion src/ng-select/ng-select.component.spec.ts
Expand Up @@ -1724,7 +1724,7 @@ describe('NgSelectComponent', function () {
[searchFn]="searchFn"
[(ngModel)]="selectedCity">
</ng-select>`);

fixture.componentInstance.searchFn = (term: string, item: any) => {
return item.name.indexOf(term) > -1 || item.id === 2;
};
Expand Down Expand Up @@ -1967,6 +1967,84 @@ describe('NgSelectComponent', function () {
});
});

describe('aria', () => {
let fixture: ComponentFixture<NgSelectTestCmp>;
let select: NgSelectComponent;
let input: HTMLInputElement;

beforeEach(fakeAsync(() => {
fixture = createTestingModule(
NgSelectTestCmp,
`<ng-select [items]="cities"
(change)="onChange($event)"
bindLabel="name">
</ng-select>`);
select = fixture.componentInstance.select;
input = fixture.debugElement.query(By.css('input')).nativeElement;
}));

it('should set aria-activedescendant absent at start', fakeAsync(() => {
expect(input.hasAttribute('aria-activedescendant'))
.toBe(false);
}));

it('should set aria-owns absent at start', fakeAsync(() => {
expect(input.hasAttribute('aria-owns'))
.toBe(false);
}));

it('should set aria-owns be set to dropdownId on open', fakeAsync(() => {
triggerKeyDownEvent(getNgSelectElement(fixture), KeyCode.Space);
tickAndDetectChanges(fixture);

expect(input.getAttribute('aria-owns'))
.toBe(select.dropdownId);
}));

it('should set aria-activedecendant equal to chosen item on open', fakeAsync(() => {
triggerKeyDownEvent(getNgSelectElement(fixture), KeyCode.Space);
tickAndDetectChanges(fixture);
expect(input.getAttribute('aria-activedescendant'))
.toBe(select.itemsList.markedItem.htmlId);
}));

it('should set aria-activedecendant equal to chosen item on arrow down', fakeAsync(() => {
triggerKeyDownEvent(getNgSelectElement(fixture), KeyCode.Space);
tickAndDetectChanges(fixture);
triggerKeyDownEvent(getNgSelectElement(fixture), KeyCode.ArrowDown);
tickAndDetectChanges(fixture);
expect(input.getAttribute('aria-activedescendant'))
.toBe(select.itemsList.markedItem.htmlId);
}));

it('should set aria-activedecendant equal to chosen item on arrow up', fakeAsync(() => {
triggerKeyDownEvent(getNgSelectElement(fixture), KeyCode.Space);
tickAndDetectChanges(fixture);
triggerKeyDownEvent(getNgSelectElement(fixture), KeyCode.ArrowUp);
tickAndDetectChanges(fixture);
expect(input.getAttribute('aria-activedescendant'))
.toBe(select.itemsList.markedItem.htmlId);
}));

it('should set aria-activedescendant absent on dropdown close', fakeAsync(() => {
triggerKeyDownEvent(getNgSelectElement(fixture), KeyCode.Space);
tickAndDetectChanges(fixture);
triggerKeyDownEvent(getNgSelectElement(fixture), KeyCode.Enter);
tickAndDetectChanges(fixture);
expect(input.hasAttribute('aria-activedescendant'))
.toBe(false);
}));

it('should set aria-owns absent on dropdown close', fakeAsync(() => {
triggerKeyDownEvent(getNgSelectElement(fixture), KeyCode.Space);
tickAndDetectChanges(fixture);
triggerKeyDownEvent(getNgSelectElement(fixture), KeyCode.Enter);
tickAndDetectChanges(fixture);
expect(input.hasAttribute('aria-owns'))
.toBe(false);
}));
});

describe('Output events', () => {
it('fire open event once', fakeAsync(() => {
const fixture = createTestingModule(
Expand Down
4 changes: 3 additions & 1 deletion src/ng-select/ng-select.component.ts
Expand Up @@ -45,6 +45,7 @@ import { NgOptionComponent } from './ng-option.component';
import { NgDropdownPanelComponent } from './ng-dropdown-panel.component';
import { isDefined, isFunction, isPromise, isObject } from './value-utils';
import { ConsoleService } from './console.service';
import { newId } from './id';

export const NG_SELECT_DEFAULT_CONFIG = new InjectionToken<NgSelectConfig>('ng-select-default-options');
export type DropdownPosition = 'bottom' | 'top' | 'auto';
Expand Down Expand Up @@ -153,7 +154,8 @@ export class NgSelectComponent implements OnDestroy, OnChanges, AfterViewInit, C
private readonly _destroy$ = new Subject<void>();
private _onChange = (_: NgOption) => { };
private _onTouched = () => { };

dropdownId = newId();
selectedItemId = 0;
clearItem = (item: any) => {
const option = this.selectedItems.find(x => x.value === item);
this.unselect(option);
Expand Down
1 change: 1 addition & 0 deletions src/ng-select/ng-select.types.ts
@@ -1,6 +1,7 @@
export interface NgOption {
[name: string]: any;
index?: number;
htmlId?: string;
selected?: boolean;
disabled?: boolean;
marked?: boolean;
Expand Down