Skip to content

Commit

Permalink
feat(tree): improve screen reader support (#102)
Browse files Browse the repository at this point in the history
* feat(list): improve screen reader

* feat(list): add aria-multiselectable for screen reader

* test(list): improve key control cases

* test(tree): update snapshots for tree

* feat(tree): add roles to tree and treee-item and add aria-expanded

* fix: add more generic type to default role for extendibility

* refactor: use setAttribute instead of overriding AOM properties

* refactor: aria expanded and selected logics

* feat: add aria level and setsize support

* feat: remove asterick key support

* feat: used aria-checked for multiple mode to indicate indeterminate state

* chore: update lock file

* refactor: simplify aria setting logics

* feat: add generic type to treeitem defaulRole

* fix: handles multiple mode changes

* test(tree): update snapshots

* refactor: reflect aria-selected according to selected state

* refactor: move most of the aria setting logics to tree-item element

* refactor: remove aria-setsize as it's no longer needed

Co-authored-by: Jidapa-Pai <[email protected]>
Co-authored-by: Jidapa-Pai <[email protected]>
  • Loading branch information
3 people authored Jan 12, 2022
1 parent 3a03599 commit 0d6db3b
Show file tree
Hide file tree
Showing 6 changed files with 105 additions and 26 deletions.
6 changes: 6 additions & 0 deletions packages/elements/src/list/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,12 @@ export class List<T extends DataItem = ItemData> extends ControlElement {
@property({ type: Boolean })
public stateless = false;

/**
* Aria indicating that list supports multiple selection
*/
@property({ type: String, reflect: true, attribute: 'aria-multiselectable' })
public ariaMultiselectable = 'false';

/**
* Allow multiple selections
*/
Expand Down
2 changes: 1 addition & 1 deletion packages/elements/src/tree/__snapshots__/Tree.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
```html
<ef-tree
aria-multiselectable="false"
role="listbox"
role="tree"
>
</ef-tree>

Expand Down
118 changes: 94 additions & 24 deletions packages/elements/src/tree/elements/tree-item.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ export class TreeItem<T extends TreeDataItem = TreeDataItem> extends ControlElem
return VERSION;
}

protected readonly defaultRole: string | null = 'treeitem';

/**
* Checked state of the item
*/
Expand Down Expand Up @@ -61,7 +63,7 @@ export class TreeItem<T extends TreeDataItem = TreeDataItem> extends ControlElem
/**
* Depth of the item
*/
@property({ reflect: true, type: Number })
@property({ type: Number, reflect: true })
public depth = 0;

/**
Expand Down Expand Up @@ -103,10 +105,7 @@ export class TreeItem<T extends TreeDataItem = TreeDataItem> extends ControlElem
*/
protected get toggleTemplate (): TemplateResult {
return html`
<div
expand-toggle
part="toggle"
style="pointer-events:all;visibility:${this.parent ? 'visible' : 'hidden'}">
<div expand-toggle part="toggle" style="pointer-events:all;visibility:${this.parent ? 'visible' : 'hidden'}">
<ef-icon part="toggle-icon${this.expanded ? ' toggle-icon-expanded' : ''}" icon="right"></ef-icon>
</div>
`;
Expand All @@ -119,23 +118,25 @@ export class TreeItem<T extends TreeDataItem = TreeDataItem> extends ControlElem
if (!this.multiple) {
return emptyTemplate;
}

return html`
<ef-checkbox
part="checkbox"
tabindex="-1"
.disabled="${this.disabled}"
.readonly="${this.readonly}"
.indeterminate="${this.indeterminate}"
.checked="${this.checked}"
style="pointer-events:none"></ef-checkbox>
part="checkbox"
tabindex="-1"
.disabled="${this.disabled}"
.readonly="${this.readonly}"
.indeterminate="${this.indeterminate}"
.checked="${this.checked}"
style="pointer-events:none">
</ef-checkbox>
`;
}

/**
* Template for rendering the icon
*/
protected get iconTemplate (): TemplateResult {
if(typeof this.icon === 'undefined') {
if (typeof this.icon === 'undefined') {
return emptyTemplate;
}

Expand All @@ -156,24 +157,93 @@ export class TreeItem<T extends TreeDataItem = TreeDataItem> extends ControlElem
return this.checkedState === CheckedState.INDETERMINATE;
}

/**
* Handles aria-checked and aria-selected when mode changes
* aria-checked is used for multiple mode due to tri-state support
* @returns {void}
**/
private multipleChanged (): void {
this.removeAttribute(this.multiple ? 'aria-selected' : 'aria-checked');
this.checkedChanged();
}

/**
* Handles selected and aria attribute changes
* @returns {void}
*/
private checkedChanged (): void {
switch (this.checkedState) {
case CheckedState.CHECKED:
this.setAttribute('selected', '');
this.setAttribute(this.multiple ? 'aria-checked' : 'aria-selected', 'true');
break;
case CheckedState.INDETERMINATE:
this.setAttribute('selected', 'indeterminate');
this.setAttribute('aria-checked', 'mixed');
break;
default:
this.removeAttribute('selected');

// In single mode, only children nodes are selectable
if (this.parent && !this.multiple) {
this.removeAttribute('aria-selected');
}
else {
this.setAttribute(this.multiple ? 'aria-checked' : 'aria-selected', 'false');
}

break;
}
}

/**
* Handles aria-expanded when expanded state changes
* @returns {void}
*/
private expandedChanged () :void {
if (this.parent) {
this.setAttribute('aria-expanded', this.expanded ? 'true' : 'false');
}
}

/**
* Called after the component is first rendered
* @param changedProperties Properties which have changed
* @returns {void}
*/
protected firstUpdated (changedProperties: PropertyValues): void {
super.firstUpdated(changedProperties);
this.setAttribute('aria-level', String(this.depth + 1));
}

/**
* Invoked whenever the element is updated
* @param changedProperties Map of changed properties with old values
* @returns {void}
*/
protected update (changedProperties: PropertyValues): void {
super.update(changedProperties);

if (changedProperties.has('checkedState')) {
switch (this.checkedState) {
case CheckedState.CHECKED:
this.setAttribute('selected', '');
break;
case CheckedState.INDETERMINATE:
this.setAttribute('selected', 'indeterminate');
break;
default:
this.removeAttribute('selected');
}
this.checkedChanged();
}

if (changedProperties.has('multiple') && changedProperties.get('multiple') !== undefined) {
this.multipleChanged();
}

if (changedProperties.has('expanded')) {
this.expandedChanged();
}
}

/**
* A `TemplateResult` that will be used
* to render the updated internal template.
* @returns Render template
*/
protected render (): TemplateResult {
return html `
return html`
${this.indentTemplate}
${this.toggleTemplate}
${this.checkboxTemplate}
Expand Down
2 changes: 2 additions & 0 deletions packages/elements/src/tree/elements/tree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ export class Tree<T extends TreeDataItem = TreeDataItem> extends List<T> {
return VERSION;
}

protected readonly defaultRole: string | null = 'tree';

/**
* Tree manager used for manipulation
*/
Expand Down
2 changes: 2 additions & 0 deletions packages/elements/src/tree/helpers/renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ type RendererScope = {

export class TreeRenderer extends Renderer {
constructor (scope?: unknown) {

let manager: TreeManager<TreeDataItem>;
let currentMode: TreeManagerMode;
let currentComposer: CollectionComposer<TreeDataItem>;
Expand All @@ -36,6 +37,7 @@ export class TreeRenderer extends Renderer {
el.disabled = composer.getItemPropertyValue(item, 'disabled') === true;
el.readonly = composer.getItemPropertyValue(item, 'readonly') === true;
el.highlighted = composer.getItemPropertyValue(item, 'highlighted') === true;

return el;
});
}
Expand Down
1 change: 0 additions & 1 deletion packages/elements/src/tree/managers/tree-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -439,5 +439,4 @@ export class TreeManager<T extends TreeDataItem> {
public uncheckAllItems (): void {
this.editableItems.forEach(item => this.uncheckItem(item));
}

}

0 comments on commit 0d6db3b

Please sign in to comment.