Skip to content

Commit

Permalink
#2571 [Fixed] Dropdown Menu: Oneindige loop
Browse files Browse the repository at this point in the history
  • Loading branch information
tfrijsewijk committed Mar 6, 2024
1 parent e61ee10 commit 349faaa
Show file tree
Hide file tree
Showing 2 changed files with 49 additions and 57 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Expand Up @@ -5,6 +5,11 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) a

## Next

## 🦷 62.11.1 - 06-03-2024

### Fixed
* Dropdown Menu: Oneindige loop ([#2571](https://github.com/dso-toolkit/dso-toolkit/issues/2571))

## 🌉 62.11.0 - 07-02-2024

### Added
Expand Down
101 changes: 44 additions & 57 deletions packages/core/src/components/dropdown-menu/dropdown-menu.tsx
@@ -1,18 +1,16 @@
import { createPopper, Placement, Instance as PopperInstance } from "@popperjs/core";
import { h, Component, Element, Host, Prop, Watch } from "@stencil/core";
import { h, Component, Element, Host, Prop, Watch, Listen } from "@stencil/core";
import { FocusableElement, tabbable } from "tabbable";
import { v4 as uuidv4 } from "uuid";

import { hasOverflow } from "../../utils/has-overflow";
import { createFocusTrap, FocusTrap } from "focus-trap";

@Component({
tag: "dso-dropdown-menu",
styleUrl: "dropdown-menu.scss",
shadow: true,
})
export class DropdownMenu {
private trap?: FocusTrap;
/**
* Whether the menu is open or closed.
* This attribute is reflected and mutable.
Expand Down Expand Up @@ -89,32 +87,6 @@ export class DropdownMenu {
this.setStrategy();
}

private createTrap() {
this.trap = createFocusTrap(this.host, {
clickOutsideDeactivates: () => {
this.open = false;
this.trap?.deactivate();
delete this.trap;

return true;
},
escapeDeactivates: (event) => {
if (event instanceof KeyboardEvent) {
this.open = false;
this.trap?.deactivate();
delete this.trap;
}

return true;
},
tabbableOptions: {
getShadowRoot: true,
},
setReturnFocus: this.button ?? false,
initialFocus: this.button ?? false,
}).activate();
}

private setStrategy() {
if (!this.popper) {
return;
Expand Down Expand Up @@ -163,8 +135,10 @@ export class DropdownMenu {
return button;
}

get tabbables(): FocusableElement[] {
return tabbable(this.host).filter((e) => e !== this.button);
private tabbables(withButton: boolean): FocusableElement[] {
const tabbables = tabbable(this.host);

return withButton ? tabbables : tabbables.filter((element) => element !== this.button);
}

componentDidLoad() {
Expand Down Expand Up @@ -227,11 +201,6 @@ export class DropdownMenu {
this.setStrategy();
if (this.open) {
this.popper?.update();
this.createTrap();
} else {
this.trap?.deactivate();

delete this.trap;
}

for (const li of Array.from(this.host.getElementsByTagName("li"))) {
Expand All @@ -244,36 +213,55 @@ export class DropdownMenu {
}
}

this.host.removeEventListener("keydown", this.keyDownListener);
this.button.setAttribute("aria-expanded", this.open ? "true" : "false");
if (this.open) {
this.host.addEventListener("keydown", this.keyDownListener);
}

@Listen("click", { target: "window" })
onClick(event: MouseEvent) {
if (!(event.target instanceof HTMLElement)) {
return;
}

this.tabbables.forEach((focusableElement) => {
focusableElement.removeEventListener("click", this.escape);
if (this.open) {
focusableElement.addEventListener("click", this.escape);
}
});
if (this.open && (!this.host.contains(event.target) || this.tabbables(false).includes(event.target))) {
this.open = false;
}
}

disconnectedCallback() {
this.popper?.destroy();
}

private keyDownListener = (event: KeyboardEvent) => {
if (event.defaultPrevented) {
private focusOutListener = (event: FocusEvent) => {
if (!(event.relatedTarget instanceof HTMLElement)) {
return;
}

if (!this.tabbables(true).includes(event.relatedTarget)) {
this.open = false;
}
};

@Listen("keydown", { target: "window" })
keyDownListener(event: KeyboardEvent) {
if (event.defaultPrevented || !this.open) {
return;
}

switch (event.key) {
case "Tab":
if (event.shiftKey) {
this.tabInPopup(this.tabbables(true), -1);
} else {
this.tabInPopup(this.tabbables(true), 1);
}

break;
case "ArrowDown":
this.tabInPopup(1);
this.tabInPopup(this.tabbables(false), 1);
break;

case "ArrowUp":
this.tabInPopup(-1);
this.tabInPopup(this.tabbables(false), -1);
break;

case "Escape":
Expand All @@ -292,7 +280,7 @@ export class DropdownMenu {
}

event.preventDefault();
};
}

private getActiveElement(root: Document | ShadowRoot = document): Element | null {
const activeEl = root.activeElement;
Expand All @@ -308,18 +296,17 @@ export class DropdownMenu {
return activeEl;
}

private tabInPopup(direction: number) {
const tabs = this.tabbables;
const currentIndex = tabs.findIndex((e) => e === this.getActiveElement());
private tabInPopup(tabbables: FocusableElement[], direction: number) {
const currentIndex = tabbables.findIndex((e) => e === this.getActiveElement());

let nextIndex = currentIndex + direction;
if (nextIndex >= tabs.length) {
if (nextIndex >= tabbables.length) {
nextIndex = 0;
} else if (nextIndex < 0) {
nextIndex = tabs.length - 1;
nextIndex = tabbables.length - 1;
}

tabs[nextIndex]?.focus();
tabbables[nextIndex]?.focus();
}

private escape = () => {
Expand All @@ -329,7 +316,7 @@ export class DropdownMenu {

render() {
return (
<Host tabindex={this.open ? "-1" : undefined}>
<Host onFocusout={this.focusOutListener}>
<slot name="toggle" />
<div hidden={!this.open}>
<slot />
Expand Down

0 comments on commit 349faaa

Please sign in to comment.