Skip to content

Commit

Permalink
fix(menu): fix menu tapping behaviors on iOS and do not close on anch…
Browse files Browse the repository at this point in the history
…or click

fixes #5036
fixes #5015

iOS has some really bad behaviors when it comes to clicking and focus. clicking on the body on a non-clickable item (like anything outside a menu) does not tirgger a click event that bubbles to menu. Also, clicking on a button does not cause it to be focused, thus depriving us of information on focusout's related target. Therefore we have to assume the anchor is the trigger.

PiperOrigin-RevId: 572137765
  • Loading branch information
Elliott Marquez authored and copybara-github committed Oct 10, 2023
1 parent 2e72802 commit 8bbb4b4
Showing 1 changed file with 56 additions and 34 deletions.
90 changes: 56 additions & 34 deletions menu/internal/menu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,6 @@ export abstract class Menu extends LitElement {
* The event path of the last window pointerdown event.
*/
private pointerPath: EventTarget[] = [];
private isPointerDown = false;
private readonly openCloseAnimationSignal = createAnimationSignal();

private readonly listController = new ListController<MenuItem>({
Expand Down Expand Up @@ -303,7 +302,7 @@ export abstract class Menu extends LitElement {
super();
if (!isServer) {
this.internals.role = 'menu';
this.addEventListener('keydown', this.listController.handleKeydown);
this.addEventListener('keydown', this.handleKeydown);
// Capture so that we can grab the event before it reaches the menu item
// istelf. Specifically useful for the case where typeahead encounters a
// space and we don't want the menu item to close the menu.
Expand Down Expand Up @@ -358,6 +357,30 @@ export abstract class Menu extends LitElement {
this.setAttribute('aria-hidden', 'true');
}

override update(changed: PropertyValues<Menu>) {
if (changed.has('open')) {
if (this.open) {
this.setUpGlobalEventListeners();
} else {
this.cleanUpGlobalEventListeners();
}
}

super.update(changed);
}

override connectedCallback() {
super.connectedCallback();
if (this.open) {
this.setUpGlobalEventListeners();
}
}

override disconnectedCallback() {
super.disconnectedCallback();
this.cleanUpGlobalEventListeners();
}

protected override render() {
return this.renderSurface();
}
Expand Down Expand Up @@ -411,25 +434,23 @@ export abstract class Menu extends LitElement {
}

private readonly handleFocusout = async (event: FocusEvent) => {
if (this.stayOpenOnFocusout || !this.open) {
const anchorEl = this.anchorElement!;
// Do not close if we focused out by clicking on the anchor element. We
// can't assume anchor buttons can be the related target because of iOS does
// not focus buttons.
if (this.stayOpenOnFocusout || !this.open ||
this.pointerPath.includes(anchorEl)) {
return;
}

if (event.relatedTarget) {
// Don't close the menu if we are switching focus between menu,
// md-menu-item, and md-list
if (isElementInSubtree(event.relatedTarget, this)) {
return;
}

const anchorEl = this.anchorElement!;
const wasAnchorClickFocused =
isElementInSubtree(event.relatedTarget, anchorEl) &&
this.isPointerDown;
if (wasAnchorClickFocused) {
// md-menu-item, and md-list or if the anchor was click focused.
if (isElementInSubtree(event.relatedTarget, this) ||
isElementInSubtree(event.relatedTarget, anchorEl)) {
return;
}
} else if (this.isPointerDown && this.pointerPath.includes(this)) {
} else if (this.pointerPath.includes(this)) {
// If menu tabindex == -1 and the user clicks on the menu or a divider, we
// want to keep the menu open.
return;
Expand Down Expand Up @@ -752,34 +773,35 @@ export abstract class Menu extends LitElement {
return animationEnded;
}

override connectedCallback() {
super.connectedCallback();
if (!isServer) {
window.addEventListener('click', this.onWindowClick, {capture: true});
window.addEventListener('pointerdown', this.onWindowPointerdown);
window.addEventListener('pointerup', this.onWindowPointerup);
}
private handleKeydown(event: KeyboardEvent) {
// At any key event, the pointer interaction is done so we need to clear our
// cached pointerpath. This handles the case where the user clicks on the
// anchor, and then hits shift+tab
this.pointerPath = [];
this.listController.handleKeydown(event);
}

override disconnectedCallback() {
super.disconnectedCallback();
if (!isServer) {
window.removeEventListener('click', this.onWindowClick, {capture: true});
window.removeEventListener('pointerdown', this.onWindowPointerdown);
window.removeEventListener('pointerup', this.onWindowPointerup);
}
private setUpGlobalEventListeners() {
document.addEventListener('click', this.onDocumentClick, {capture: true});
window.addEventListener('pointerdown', this.onWindowPointerdown);
}

private cleanUpGlobalEventListeners() {
document.removeEventListener(
'click', this.onDocumentClick, {capture: true});
window.removeEventListener('pointerdown', this.onWindowPointerdown);
}

private readonly onWindowPointerdown = (event: PointerEvent) => {
this.isPointerDown = true;
this.pointerPath = event.composedPath();
};

private readonly onWindowPointerup = () => {
this.isPointerDown = false;
};

private readonly onWindowClick = (event: MouseEvent) => {
/**
* We cannot listen to window click because Safari on iOS will not bubble a
* click event on window if the item clicked is not a "clickable" item such as
* <body>
*/
private readonly onDocumentClick = (event: Event) => {
if (!this.open) {
return;
}
Expand Down

0 comments on commit 8bbb4b4

Please sign in to comment.