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

feat(typeahead): accessibility, options summary status message #2197

Merged
merged 1 commit into from
Apr 28, 2018

Conversation

ymeine
Copy link
Contributor

@ymeine ymeine commented Feb 28, 2018

Closes #1945


ngOnDestroy() { this._element.remove(); }

async say(message: string): Promise<void> {

Choose a reason for hiding this comment

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

Could you please not use async-await for now? It transpiles to rather verbose code.

Choose a reason for hiding this comment

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

More important - why do we return a Promise here? We don't care about any return value, do we? What would be the use-case for knowing when announcing starts?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think this is pity to deprive ourselves of one of the best standard language features that was brought these last years: now asynchronous code remains explicit and controlled, but is aligned with synchronous code. It's what made me adopt async code widely, because otherwise dealing with Promises chaining is quite verbose.

This is a general comment, and of course here using a Promise is easy, since I even had to create one due to the lack of proper API for creating a delay.

However:

  • first, the code size shouldn't be an issue, especially once compressed
  • then, we should configure the compilation so that it doesn't generate the helpers in each file, but instead relies on the TypeScript library to have this piece of code only once.
  • last, we don't really care about the return value here. It's a Promise because async functions return Promises.


element.classList.add('sr-only');

document.body.appendChild(element);

Choose a reason for hiding this comment

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

Who is going to remove this element? We should have onDestroy, maybe?

Choose a reason for hiding this comment

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

Oh, actually it is here, never mind

@@ -357,9 +358,15 @@ export class NgbTypeahead implements ControlValueAccessor,
// the change detection turn wouldn't be invoked automatically.
this._windowRef.changeDetectorRef.detectChanges();
}

this._live.say(this._srOptionsSummaryTemplate({count: results.length}));

Choose a reason for hiding this comment

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

Why do you pass an object here instead of just a number? It results in the unnecessary object creation and you read only count from it anyway.

Copy link
Member

Choose a reason for hiding this comment

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

or no need in method at all

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I don't know why this appears as an outdated comment, but I'll reply anyways

No need for that indeed. I started with a public API, so I made it generic:

  • users would give a callback, which is now the private method we can see
  • I would give them an object since this is more future-proof in case we want to give more information

I kept this design while making things private.

However I would not be concerned AT ALL with performances. These are micro-optimizations, and I think you are well placed to know that actual performance is more complex than that. In this context, the feature call rate is way too low to worry about extra nanoseconds spent creating an object, calling a method and reading from this object. My concerns would be more about maintainability. But here I can remove both the method and the object as long as it remains private, it's a matter of one line of code so it's perfectly fine.

const element = this._element;

element.textContent = '';
await new Promise(resolve => setTimeout(resolve, 100));

Choose a reason for hiding this comment

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

Why 100? How was this number chosen? Why does it need to be async in the first place? As a very minimum we need documentation that explain this.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is all explained there: material2/live-announcer.ts at 6405da9b8e8532a7e5c854c920ee1815c275d734 · angular/material2.

It seems natural to wait a bit in case, knowing the unpredictability of screen readers. However, choosing a proper amount of delay is purely arbitrary.

Copy link
Member

@maxokorokov maxokorokov left a comment

Choose a reason for hiding this comment

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

Ok for introducing the announcer.

  • checked with NVDA and JAWS 17, seems to work.
  • typeahead is broken for Voiceover, but not because of the announcer

I think we should find a way to remove delay from tests, make it customizeable?

@@ -513,6 +513,7 @@ describe('ngb-typeahead', () => {
changeInput(compiled, 'o');
fixture.detectChanges();
tick(250);
tick(100); // for the aria-live announcer
Copy link
Member

Choose a reason for hiding this comment

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

I think we should use a different announcer for tests or inject delay value 0 so it would become synchronous

Copy link
Contributor Author

Choose a reason for hiding this comment

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

ANYTHING to get rid of these weird fake things like tick would please me. I'll explore the injection of a different delay, seems the simplest and most flexible, that could even be useful for users.

@@ -357,9 +358,15 @@ export class NgbTypeahead implements ControlValueAccessor,
// the change detection turn wouldn't be invoked automatically.
this._windowRef.changeDetectorRef.detectChanges();
}

this._live.say(this._srOptionsSummaryTemplate({count: results.length}));
Copy link
Member

Choose a reason for hiding this comment

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

or no need in method at all

export class Live implements OnDestroy {
private _element: HTMLElement;

constructor(@Inject(DOCUMENT) private _document: any) { this._element = this._createElement(); }
Copy link
Member

Choose a reason for hiding this comment

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

nitpick: no need to store document privately

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Why would I make it public? It's an internal thing, it's not part of the API. Otherwise this removes the whole purpose of having visibility specifiers in the typing system.

@ymeine ymeine force-pushed the pr/feat/accessibility/1945 branch from fb864d6 to d3cfdd2 Compare March 29, 2018 14:09
@ymeine ymeine force-pushed the pr/feat/accessibility/1945 branch from d3cfdd2 to cf83d84 Compare April 6, 2018 16:17
@ymeine
Copy link
Contributor Author

ymeine commented Apr 6, 2018

Changes done.

Copy link
Member

@maxokorokov maxokorokov left a comment

Choose a reason for hiding this comment

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

Thanks, LGTM, just 2 things:

  • fix failing tests in IE
  • rename DELAY and related types/constants → something more precise

Will merge after fixes

}
}

private _createElement(document): HTMLElement {
Copy link
Member

Choose a reason for hiding this comment

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

nitpick: this doesn't have to be private, can be a static outside function


// usefulness (and default value) of delay documented in Material's CDK
// https://github.com/angular/material2/blob/6405da9b8e8532a7e5c854c920ee1815c275d734/src/cdk/a11y/live-announcer/live-announcer.ts#L50
export type DELAY_TYPE = number | null;
Copy link
Member

Choose a reason for hiding this comment

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

Could we name them more precisely, please? {provide: DELAY, ...} - not clear which delay.

Maybe something like ARIA_LIVE_DELAY ?

this._element = this._createElement(document);
}

ngOnDestroy() { this._element.remove(); }
Copy link
Member

Choose a reason for hiding this comment

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

Apparently .remove() is failing in IE 10-11 tests

@@ -354,6 +355,8 @@ export class NgbTypeahead implements ControlValueAccessor,

this._showHint();
}
const count = results.length;
this._live.say(count === 0 ? 'No result available' : `${count} options available`);
Copy link
Member

Choose a reason for hiding this comment

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

0 → No results available
1 → 1 result available
n → n results available

@ymeine ymeine force-pushed the pr/feat/accessibility/1945 branch 2 times, most recently from dc8be3e to f39aec9 Compare April 20, 2018 10:16
@maxokorokov
Copy link
Member

@ymeine, could you please fix a failing test and I'll merge:

Firefox 59.0.0 (Linux 0.0.0) LiveAnnouncer live announcer should remove the used element from the DOM on destroy FAILED
	TypeError: this._element.parentElement is null in node_modules/systemjs/dist/system.src.js line 1556 > eval (line 32)
	Live.prototype.ngOnDestroy@src/util/accessibility/live.ts:37:18 <- temp/util/accessibility/live.js:32:48
	@src/util/accessibility/live.spec.ts:33:20 <- temp/util/accessibility/live.spec.js:34:40

@ymeine ymeine force-pushed the pr/feat/accessibility/1945 branch from f39aec9 to 2f2ad4e Compare April 27, 2018 14:01
@ymeine ymeine force-pushed the pr/feat/accessibility/1945 branch from 2f2ad4e to 060693f Compare April 27, 2018 15:14
@maxokorokov maxokorokov merged commit f65b8a4 into ng-bootstrap:master Apr 28, 2018
@maxokorokov maxokorokov modified the milestones: 2.1.0, 2.0.0 Apr 28, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants