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

fix(select): Improve the a11y of select component by making overlay #11806

Closed
wants to merge 1 commit into from

Conversation

tinayuangao
Copy link
Contributor

always exist in the DOM

@googlebot googlebot added the cla: yes PR author has agreed to Google's Contributor License Agreement label Jun 15, 2018
@@ -165,6 +165,9 @@ export class CdkConnectedOverlay implements OnDestroy, OnChanges {
@Input('cdkConnectedOverlayScrollStrategy') scrollStrategy: ScrollStrategy =
this._scrollStrategy();

/** Strategy to detach the overlay panel on ESCAPE keydown */
@Input('cdkConnectedOverlayDetachOnEscape') detachOnEscape: boolean = true;
Copy link
Member

Choose a reason for hiding this comment

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

I would invert this property so that it's just disableClose with a description:

/** Whether the overlay closes when the user pressed the Escape key. */

and also using coerceBooleanProperty

Copy link
Member

Choose a reason for hiding this comment

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

In that case the same check would have to be added to the backdropClick so it's consistent with all the other overlays.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@crisbeto backdropClick emits an event and developers decide what will happen after backdropClick. (Here in select we close the panel, but other things can be done)

@@ -19,31 +19,31 @@
cdk-connected-overlay
cdkConnectedOverlayLockPosition
cdkConnectedOverlayHasBackdrop
[cdkConnectedOverlayOpen]="true"
Copy link
Member

Choose a reason for hiding this comment

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

I don't think this needs the square braces any more

Also add a comment that explains that why the overlay is always "open"?

Copy link
Member

Choose a reason for hiding this comment

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

My concern with this one is how it would perform on a page with a lot of selects. I wonder whether we couldn't get away with not rendering it on init, but doing it when the user focuses on a select or hovers over it. That way we should have a split second to get it into the DOM before the user decides to open the panel.

Copy link
Member

Choose a reason for hiding this comment

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

Not mentioned here, but the second half of this change is going to be support the native <select> in mat-form-field for situations where mat-select is too heavy. The user can then decide to make the trade-off- of having the Material Design style panel or the much more performant native dropdown.

@@ -396,6 +395,7 @@ describe('MatSelect', () => {
it('should open the panel when pressing a vertical arrow key on a closed multiple select',
fakeAsync(() => {
fixture.destroy();
flush();
Copy link
Member

Choose a reason for hiding this comment

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

I'm worried that if we need to add flush in these tests, there will be a ton of downstream apps that will need to do the same

Copy link
Member

Choose a reason for hiding this comment

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

Could you elaborate on what makes these flush operations necessary now?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The flush is because something is run outside the angular zone when the overlay is attached: https://github.com/angular/material2/blob/master/src/cdk/overlay/overlay-ref.ts#L304

We can attach backdrop when the select panel is opened.

Copy link
Member

Choose a reason for hiding this comment

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

We can see how it affects google3 on presubmit

// Defer setting the value in order to void the "Expression has changed after it was checked"
// errors from Angular
Promise.resolve().then(() => {
this._initializePanel();
Copy link
Member

Choose a reason for hiding this comment

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

Does this have to happen in ngAfterContentInit, or can it happen once the panel is visually displayed?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@jelbourn How can we know the panel is visually displayed?

@@ -554,20 +563,32 @@ export class MatSelect extends _MatSelectMixinBase implements AfterContentInit,
this._keyManager.withHorizontalOrientation(null);
this._calculateOverlayPosition();
this._highlightCorrectOption();
this._showBackdrop();
this.overlayDir.overlayRef.overlayElement.style.minWidth = `${this._triggerRect.width}px`;
Copy link
Member

Choose a reason for hiding this comment

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

Why are setting this minWidth and the direction necessary now when they weren't before?

I'm also concerned about setting any geometry properties directly to the overlay panel, since that's supposed to be the responsibility of the overlay code itself.


this.overlayDir.overlayRef.updatePosition();

this._changeDetectorRef.detectChanges();
Copy link
Member

Choose a reason for hiding this comment

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

Why is the extra detectChanges necessary?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Otherwise we cannot calculate the correct scrollTop and offsetX values in the next steps

@@ -194,7 +195,7 @@ export class MatSelectTrigger {}
'[attr.aria-required]': 'required.toString()',
'[attr.aria-disabled]': 'disabled.toString()',
'[attr.aria-invalid]': 'errorState',
'[attr.aria-owns]': 'panelOpen ? _optionIds : null',
'[attr.aria-owns]': '_optionIds',
Copy link
Member

Choose a reason for hiding this comment

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

Wasn't part of this change doing to make the select role="combobox"? That part is separate from where the options live, and I think it still reflects the interaction here more accurately.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Combobox requires a textbox inside it. I think it means there's an editable textbox and user may choose to enter something other than the list of options, which doesn't match our select component.

Copy link
Contributor

Choose a reason for hiding this comment

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

If #7835 ends up being merged, it would be nice to be able to use role="combobox" in correlation with the editable textbox that is focus-trapped in the mat-select-header.

Copy link
Member

Choose a reason for hiding this comment

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

The text input is optional for combobox; this prototype uses it without one:
https://stackblitz.com/edit/angular-a8j36n?file=app%2Fmat-select%2Fmat-select.component.html

As well as the deque select:
https://pattern-library.dequelabs.com/components/selects

Copy link
Contributor Author

Choose a reason for hiding this comment

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

In aria 1.0 it's not required and in aria 1.1 textbox or searchbox is required.

https://www.w3.org/TR/wai-aria-1.1/#combobox

Copy link
Member

Choose a reason for hiding this comment

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

Ah, interesting. We'll have to explore using combobox for a select with a search input in the future

@@ -165,6 +165,9 @@ export class CdkConnectedOverlay implements OnDestroy, OnChanges {
@Input('cdkConnectedOverlayScrollStrategy') scrollStrategy: ScrollStrategy =
this._scrollStrategy();

/** Strategy to detach the overlay panel on ESCAPE keydown */
@Input('cdkConnectedOverlayDetachOnEscape') detachOnEscape: boolean = true;
Copy link
Member

Choose a reason for hiding this comment

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

In that case the same check would have to be added to the backdropClick so it's consistent with all the other overlays.

[ngClass]="panelClass"
[@transformPanel]="multiple ? 'showing-multiple' : 'showing'"
[@transformPanel]="panelOpen ? (multiple ? 'showing-multiple' : 'showing') : 'void'"
Copy link
Member

Choose a reason for hiding this comment

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

Would be a good idea to move this into a function rather than having the nested ternary.

@@ -396,6 +395,7 @@ describe('MatSelect', () => {
it('should open the panel when pressing a vertical arrow key on a closed multiple select',
fakeAsync(() => {
fixture.destroy();
Copy link
Member

Choose a reason for hiding this comment

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

We should have a test that verifies that the select panel is in the DOM and with the correct amount of options on init.

@@ -758,6 +782,45 @@ export class MatSelect extends _MatSelectMixinBase implements AfterContentInit,
return !this._selectionModel || this._selectionModel.isEmpty();
}

/** Show the backdrop of the overlay. */
private _showBackdrop() {
Copy link
Member

Choose a reason for hiding this comment

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

Not necessarily something for this PR, but rather than adding something special for the select, we can add the ability to detach and re-attach the backdrop via the OverlayRef.

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 tried detach and re-attach the backdrop via the OverlayRef and I think there's a risk of detaching other overlay's backdrop accidentally or failed to detach correctly since there're some async activities outside angular zone. It's more reliable to make the backdrop visible/invisible.

Copy link
Contributor

Choose a reason for hiding this comment

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

If there's a risk that that detaching one overlay could unintentionally detach another, that sounds like a larger problem with the cdk or its implementation here.

Copy link
Member

Choose a reason for hiding this comment

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

I'm not sure I see how that could happen; each OverlayRef has a reference to its own specific backdrop element

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Each OverlayRef has its own backdrop. The detach backdrop is an async call
this._ngZone.runOutsideAngular(() => setTimeout(finishDetach, 500));
When user click on the backdrop, it takes 500ms to fully detach the overlay. If in that 500ms user open the select panel again, a backdrop stilll exists so we cannot reattach it, and is removed after 500ms which means click outside the select panel will not close the panel.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

There's transition.

backdropToDetach.classList.remove('cdk-overlay-backdrop-showing'); This triggers transition and finishDetach is triggered when the transitionend.
https://github.com/angular/material2/blob/master/src/cdk/overlay/overlay-ref.ts#L348

Copy link
Contributor

Choose a reason for hiding this comment

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

I see. So would you say the deeper issue here is that this._ngZone.runOutsideAngular(() => setTimeout(finishDetach, 500)); is meant to accommodate situations where there is no transitionend but currently it runs regardless of whether there's a transition or not?

Copy link
Member

Choose a reason for hiding this comment

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

What we were trying to avoid with that setTimeout is a call to getComputedStyle just so we can figure out whether the backdrop has a transition. I had a PR a while ago that switched the backdrop to be a component which would've given us better control over the animation, however we never managed to get it presubmitted (see #6627).

Copy link
Contributor

Choose a reason for hiding this comment

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

@crisbeto Do you think it would work better if, instead of having a 500ms timeout, there was a 10ms timeout that gets cleared with clearTimeout() if transitionstart is fired?

Copy link
Member

Choose a reason for hiding this comment

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

If I remember correctly, transitionstart was flaky between browsers which means that if we started depending on it, people's tests would start flaking as well.

this._changeDetectorRef.markForCheck();

// Set the font size on the panel element once it exists.
this._ngZone.onStable.asObservable().pipe(take(1)).subscribe(() => {
Copy link
Member

Choose a reason for hiding this comment

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

Rather than waiting for onStable and then having a null check for the overlayRef, we should be able to use the attach on the overlay directive to know when it was attached.

@@ -19,31 +19,31 @@
cdk-connected-overlay
cdkConnectedOverlayLockPosition
cdkConnectedOverlayHasBackdrop
[cdkConnectedOverlayOpen]="true"
Copy link
Member

Choose a reason for hiding this comment

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

My concern with this one is how it would perform on a page with a lot of selects. I wonder whether we couldn't get away with not rendering it on init, but doing it when the user focuses on a select or hovers over it. That way we should have a split second to get it into the DOM before the user decides to open the panel.

@@ -512,6 +513,14 @@ export class MatSelect extends _MatSelectMixinBase implements AfterContentInit,
this._resetOptions();
this._initializeSelection();
});

this._triggerRect = this.trigger.nativeElement.getBoundingClientRect();
Copy link
Member

Choose a reason for hiding this comment

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

It seems like you're recalculating the _triggerRect here and right afterwards inside _initializePanel. We should be able to get away with only doing it in _initializePanel.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The trigger's bounding rectangle may change after the panel is initialized.
The panel is only initialized once when the component is loaded, but if the trigger's rect changed after that, we still need to get the correct scrollTop, offsetX, offsetY based on the new values.

@googlebot

This comment has been minimized.

@googlebot googlebot added cla: no PR author must sign Google's Contributor License Agreement: https://opensource.google.com/docs/cla and removed cla: yes PR author has agreed to Google's Contributor License Agreement labels Jun 18, 2018
@tinayuangao
Copy link
Contributor Author

@crisbeto We discussed the possibility of loading options when user focus on the select, and we think it's too late.

@jelbourn
Copy link
Member

Looks like there are just some lint errors now

@googlebot
Copy link

CLAs look good, thanks!

@googlebot googlebot added cla: yes PR author has agreed to Google's Contributor License Agreement and removed cla: no PR author must sign Google's Contributor License Agreement: https://opensource.google.com/docs/cla labels Jun 20, 2018
@Input('cdkConnectedOverlayOpen') open: boolean = false;
@Input('cdkConnectedOverlayOpen')
get open() { return this._open; }
set open(value: any) { this._open = coerceBooleanProperty(value); }
Copy link
Contributor

Choose a reason for hiding this comment

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

Just curious: any reason to not use boolean (in getter/setter)?

Copy link
Member

@jelbourn jelbourn left a comment

Choose a reason for hiding this comment

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

LGTM

@jelbourn jelbourn added pr: lgtm action: merge The PR is ready for merge by the caretaker target: patch This PR is targeted for the next patch release merge: caretaker note Alert the caretaker performing the merge to check the PR for an out of normal action needed or note labels Jun 21, 2018
@jelbourn
Copy link
Member

Caretaker note: may need to change this from patch to something else depending on how many tests need to be updated in google

@ngbot

This comment has been minimized.

@ngbot
Copy link

ngbot bot commented Aug 23, 2018

Hi @tinayuangao! This PR has merge conflicts due to recent upstream merges.
Please help to unblock it by resolving these conflicts. Thanks!

@Splaktar
Copy link
Member

Splaktar commented Oct 5, 2018

Just wanted to link this over to the related issue: #11083.

@josephperrott josephperrott added pr: needs rebase and removed action: merge The PR is ready for merge by the caretaker labels Oct 19, 2018
@mmalerba mmalerba added aaa and removed aaa labels Apr 25, 2019
@Splaktar Splaktar added the presubmit failures This PR has failures in Google's internal presubmit process and cannot be immediately merged label Jul 19, 2019
@Splaktar
Copy link
Member

Splaktar commented Jul 19, 2019

@jelbourn I think that this one is a good candidate for closing or reviewing for closing at the next meeting?

It hasn't been stated here, but #11083 (comment) mentions that

Attempts at fixing this were all just too much of a breaking change, such that it was impossible to roll out to applications in Google already using MatSelect.

@jelbourn jelbourn closed this Jul 19, 2019
@angular-automatic-lock-bot
Copy link

This issue has been automatically locked due to inactivity.
Please file a new issue if you are encountering a similar or related problem.

Read more about our automatic conversation locking policy.

This action has been performed automatically by a bot.

@angular-automatic-lock-bot angular-automatic-lock-bot bot locked and limited conversation to collaborators Sep 11, 2019
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
cla: yes PR author has agreed to Google's Contributor License Agreement merge: caretaker note Alert the caretaker performing the merge to check the PR for an out of normal action needed or note presubmit failures This PR has failures in Google's internal presubmit process and cannot be immediately merged target: patch This PR is targeted for the next patch release
Projects
None yet
Development

Successfully merging this pull request may close these issues.

9 participants