-
Notifications
You must be signed in to change notification settings - Fork 1.5k
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
feat(typeahead): accessibility, options summary status message #2197
Conversation
src/util/accessibility/live.ts
Outdated
|
||
ngOnDestroy() { this._element.remove(); } | ||
|
||
async say(message: string): Promise<void> { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
src/util/accessibility/live.ts
Outdated
|
||
element.classList.add('sr-only'); | ||
|
||
document.body.appendChild(element); |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
src/typeahead/typeahead.ts
Outdated
@@ -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})); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
src/util/accessibility/live.ts
Outdated
const element = this._element; | ||
|
||
element.textContent = ''; | ||
await new Promise(resolve => setTimeout(resolve, 100)); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this 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?
src/typeahead/typeahead.spec.ts
Outdated
@@ -513,6 +513,7 @@ describe('ngb-typeahead', () => { | |||
changeInput(compiled, 'o'); | |||
fixture.detectChanges(); | |||
tick(250); | |||
tick(100); // for the aria-live announcer |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
src/typeahead/typeahead.ts
Outdated
@@ -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})); |
There was a problem hiding this comment.
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
src/util/accessibility/live.ts
Outdated
export class Live implements OnDestroy { | ||
private _element: HTMLElement; | ||
|
||
constructor(@Inject(DOCUMENT) private _document: any) { this._element = this._createElement(); } |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
fb864d6
to
d3cfdd2
Compare
d3cfdd2
to
cf83d84
Compare
Changes done. |
cf83d84
to
855a31f
Compare
855a31f
to
c156951
Compare
There was a problem hiding this 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
src/util/accessibility/live.ts
Outdated
} | ||
} | ||
|
||
private _createElement(document): HTMLElement { |
There was a problem hiding this comment.
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
src/util/accessibility/live.ts
Outdated
|
||
// 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; |
There was a problem hiding this comment.
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
?
src/util/accessibility/live.ts
Outdated
this._element = this._createElement(document); | ||
} | ||
|
||
ngOnDestroy() { this._element.remove(); } |
There was a problem hiding this comment.
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
src/typeahead/typeahead.ts
Outdated
@@ -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`); |
There was a problem hiding this comment.
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
dc8be3e
to
f39aec9
Compare
@ymeine, could you please fix a failing test and I'll merge:
|
f39aec9
to
2f2ad4e
Compare
2f2ad4e
to
060693f
Compare
Closes #1945