You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@myfaces.apache.org by hn...@apache.org on 2020/03/02 08:23:22 UTC
[myfaces-tobago] 01/06: tobago-dropdown: custom elements,
remove jQuery
This is an automated email from the ASF dual-hosted git repository.
hnoeth pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/myfaces-tobago.git
commit 9f6eb87e36722a9a7b08d719887b3fc59d2895a2
Author: Henning Noeth <hn...@apache.org>
AuthorDate: Wed Feb 26 13:21:43 2020 +0100
tobago-dropdown: custom elements, remove jQuery
* implement key navigation
issue: TOBAGO-1633: TS refactoring
---
.../myfaces/tobago/renderkit/css/TobagoClass.java | 2 +
tobago-core/src/main/resources/scss/_tobago.scss | 19 +-
.../src/main/npm/ts/tobago-dropdown.ts | 205 +++++++++++++++++++--
3 files changed, 202 insertions(+), 24 deletions(-)
diff --git a/tobago-core/src/main/java/org/apache/myfaces/tobago/renderkit/css/TobagoClass.java b/tobago-core/src/main/java/org/apache/myfaces/tobago/renderkit/css/TobagoClass.java
index bc2ab9a..b0b4d16 100644
--- a/tobago-core/src/main/java/org/apache/myfaces/tobago/renderkit/css/TobagoClass.java
+++ b/tobago-core/src/main/java/org/apache/myfaces/tobago/renderkit/css/TobagoClass.java
@@ -121,6 +121,8 @@ public enum TobagoClass implements CssItem {
BUTTONS("tobago-buttons"),
COLLAPSED("tobago-collapsed"),
DATE("tobago-date"),
+ DROPDOWN__OPEN("tobago-dropdown-open"),
+ DROPDOWN__SELECTED("tobago-dropdown-selected"),
FILE("tobago-file"),
FILE__PLACEHOLDER("tobago-file-placeholder"),
FLEX_LAYOUT("tobago-flexLayout"),
diff --git a/tobago-core/src/main/resources/scss/_tobago.scss b/tobago-core/src/main/resources/scss/_tobago.scss
index ef9638a..dd11466 100644
--- a/tobago-core/src/main/resources/scss/_tobago.scss
+++ b/tobago-core/src/main/resources/scss/_tobago.scss
@@ -74,6 +74,7 @@ $input-focus-box-shadow: $input-btn-focus-box-shadow !default;
$form-check-inline-input-margin-x: 0.75rem;
$input-placeholder-color: $gray-600 !default;
$grid-gutter-width: 30px !default;
+$dropdown-link-hover-color: darken($gray-900, 5%) !default;
$page-padding-top: 1rem;
@@ -600,6 +601,14 @@ tobago-dropdown {
display: inline-block;
}
+tobago-dropdown, .tobago-page-menuStore {
+ .tobago-dropdown-selected {
+ color: $dropdown-link-hover-color;
+ text-decoration: none;
+ @include gradient-bg($dropdown-link-hover-bg);
+ }
+}
+
ul > tobago-dropdown {
display: list-item;
}
@@ -647,11 +656,13 @@ ul > tobago-dropdown {
margin-right: -10px;
}
- &:hover > .dropdown-menu {
- display: block;
+ &:hover, &.tobago-dropdown-open {
+ > .dropdown-menu {
+ display: block;
- > a:after {
- border-left-color: $dropdown-bg;
+ > a:after {
+ border-left-color: $dropdown-bg;
+ }
}
}
diff --git a/tobago-theme/tobago-theme-standard/src/main/npm/ts/tobago-dropdown.ts b/tobago-theme/tobago-theme-standard/src/main/npm/ts/tobago-dropdown.ts
index 61e0898..f026821 100644
--- a/tobago-theme/tobago-theme-standard/src/main/npm/ts/tobago-dropdown.ts
+++ b/tobago-theme/tobago-theme-standard/src/main/npm/ts/tobago-dropdown.ts
@@ -19,39 +19,90 @@ import Popper from "popper.js";
class Dropdown extends HTMLElement {
- private closeFlag: boolean = false;
+ private dropdownEntries: DropdownEntry[] = [];
+ private activeDropdownEntry: DropdownEntry;
constructor() {
super();
+ if (!this.classList.contains("tobago-dropdown-submenu")) { // ignore submenus
+ const root = this.getRootNode() as ShadowRoot | Document;
- this.toggleButton.addEventListener("mouseup", this.toggleDropdown.bind(this));
- this.toggleButton.addEventListener("blur", this.setCloseFlag.bind(this));
- window.addEventListener("mouseup", this.deselectComponent.bind(this));
+ this.createDropdownEntries(this.dropdownMenu, null);
+
+ this.toggleButton.addEventListener("click", this.toggleDropdown.bind(this));
+ root.addEventListener("mouseup", this.mouseupOnDocument.bind(this));
+ root.addEventListener("keydown", this.keydownOnDocument.bind(this));
+ root.addEventListener("keyup", this.keyupOnDocument.bind(this));
+ }
}
connectedCallback(): void {
- //TODO add keyboard support
}
toggleDropdown(event: Event): void {
- this.resetCloseFlag();
-
- const visible: boolean = this.dropdownMenu.classList.contains("show");
- if (visible) {
+ event.preventDefault();
+ event.stopPropagation();
+ if (this.dropdownVisible()) {
this.closeDropdown();
} else {
this.openDropdown();
}
}
- deselectComponent(event: Event): void {
- const visible: boolean = this.dropdownMenu.classList.contains("show");
- if (this.closeFlag && visible) {
- this.resetCloseFlag();
+ mouseupOnDocument(event: MouseEvent): void {
+ if (!this.toggleButtonSelected() && this.dropdownVisible()) {
+ this.closeDropdown();
+ }
+ }
- const target: HTMLElement = event.target as HTMLElement;
+ keydownOnDocument(event: KeyboardEvent): void {
+ if (this.dropdownVisible() && event.code === "Escape") {
+ event.preventDefault();
+ event.stopPropagation();
this.closeDropdown();
- target.dispatchEvent(new MouseEvent("click", {bubbles: true}));
+ } else if ((this.toggleButtonSelected() || this.dropdownVisible())
+ && (event.code === "ArrowUp" || event.code === "ArrowDown"
+ || event.code === "ArrowLeft" || event.code === "ArrowRight")) {
+ // prevent scrolling with arrow keys
+ event.preventDefault();
+ event.stopPropagation();
+ }
+ }
+
+ keyupOnDocument(event: KeyboardEvent): void {
+ const root = this.getRootNode() as ShadowRoot | Document;
+
+ if (this.toggleButtonSelected() && !this.dropdownVisible()
+ && (event.code === "ArrowUp" || event.code === "ArrowDown")) {
+ event.preventDefault();
+ event.stopPropagation();
+ this.openDropdown();
+ this.activeDropdownEntry = this.dropdownEntries[0];
+
+ const interval = setInterval(() => {
+ if (this.dropdownVisible()) {
+ this.activeDropdownEntry.focus();
+ clearInterval(interval);
+ }
+ }, 0);
+ } else if (this.activeDropdownEntry && this.dropdownVisible()) {
+ if (event.code === "ArrowUp" && this.activeDropdownEntry.previous) {
+ this.activeDropdownEntry.clearCss();
+ this.activeDropdownEntry = this.activeDropdownEntry.previous;
+ this.activeDropdownEntry.focus();
+ } else if (event.code === "ArrowDown" && this.activeDropdownEntry.next) {
+ this.activeDropdownEntry.clearCss();
+ this.activeDropdownEntry = this.activeDropdownEntry.next;
+ this.activeDropdownEntry.focus();
+ } else if (event.code === "ArrowRight" && this.activeDropdownEntry.children.length > 0) {
+ this.activeDropdownEntry = this.activeDropdownEntry.children[0];
+ this.activeDropdownEntry.focus();
+ } else if (event.code === "ArrowLeft" && this.activeDropdownEntry.parent) {
+ this.activeDropdownEntry.clearCss();
+ this.activeDropdownEntry = this.activeDropdownEntry.parent;
+ this.activeDropdownEntry.clearCss();
+ this.activeDropdownEntry.focus();
+ }
}
}
@@ -63,6 +114,10 @@ class Dropdown extends HTMLElement {
});
}
+ for (const dropdownEntry of this.dropdownEntries) {
+ dropdownEntry.clearCss();
+ }
+
this.dropdownMenu.classList.add("show");
}
@@ -75,9 +130,14 @@ class Dropdown extends HTMLElement {
return this.querySelector(":scope > button[data-toggle='dropdown']");
}
+ private toggleButtonSelected(): boolean {
+ const root = this.getRootNode() as ShadowRoot | Document;
+ return root.activeElement === this.toggleButton;
+ }
+
private inStickyHeader(): boolean {
const root = this.getRootNode() as ShadowRoot | Document;
- return root.querySelector("header.tobago-header.sticky-top tobago-dropdown[id='" + this.id + "']") !== null;
+ return Boolean(root.querySelector("header.tobago-header.sticky-top tobago-dropdown[id='" + this.id + "']"));
}
private get dropdownMenu(): HTMLDivElement {
@@ -85,17 +145,122 @@ class Dropdown extends HTMLElement {
return root.querySelector(".dropdown-menu[name='" + this.id + "']");
}
+ private dropdownVisible(): boolean {
+ return this.dropdownMenu.classList.contains("show");
+ }
+
private get menuStore(): HTMLDivElement {
const root = this.getRootNode() as ShadowRoot | Document;
return root.querySelector(".tobago-page-menuStore");
}
- setCloseFlag(event: Event): void {
- this.closeFlag = true;
+ private createDropdownEntries(dropdownMenu: HTMLDivElement, parent: DropdownEntry): void {
+ let lastDropdownEntry: DropdownEntry = null;
+
+ for (const dropdownItem of dropdownMenu.children) {
+ if (dropdownItem.classList.contains("dropdown-item")) {
+ const entry = this.createDropdownEntry(dropdownItem as HTMLElement, parent, lastDropdownEntry);
+
+ lastDropdownEntry = entry;
+ this.dropdownEntries.push(entry);
+
+ if (dropdownItem.classList.contains("tobago-dropdown-submenu")) {
+ this.createDropdownEntries(dropdownItem.querySelector(".dropdown-menu"), entry);
+ }
+ } else {
+ const dropdownItems: NodeListOf<HTMLElement> = dropdownItem.querySelectorAll(".dropdown-item");
+ for (const dropdownItem of dropdownItems) {
+ const entry = this.createDropdownEntry(dropdownItem, parent, lastDropdownEntry);
+
+ lastDropdownEntry = entry;
+ this.dropdownEntries.push(entry);
+ }
+ }
+ }
+ }
+
+ private createDropdownEntry(
+ dropdownItem: HTMLElement, parent: DropdownEntry, previous: DropdownEntry): DropdownEntry {
+
+ const entry = new DropdownEntry(dropdownItem);
+ if (parent) {
+ entry.parent = parent;
+ parent.children.push(entry);
+ }
+
+ if (previous) {
+ previous.next = entry;
+ entry.previous = previous;
+ }
+
+ return entry;
+ }
+}
+
+class DropdownEntry {
+
+ private _previous: DropdownEntry;
+ private _next: DropdownEntry;
+ private _parent: DropdownEntry;
+ private _children: DropdownEntry[] = [];
+ private readonly _baseElement: HTMLElement;
+ private readonly focusElement: HTMLElement;
+
+ constructor(dropdownItem: HTMLElement) {
+ this._baseElement = dropdownItem;
+ if (dropdownItem.classList.contains("tobago-dropdown-submenu")) {
+ this.focusElement = dropdownItem.querySelector(".tobago-link");
+ } else if (dropdownItem.tagName === "LABEL") {
+ this.focusElement = dropdownItem.querySelector("input");
+ } else {
+ this.focusElement = dropdownItem;
+ }
+ }
+
+ get previous(): DropdownEntry {
+ return this._previous;
+ }
+
+ set previous(value: DropdownEntry) {
+ this._previous = value;
+ }
+
+ get next(): DropdownEntry {
+ return this._next;
+ }
+
+ set next(value: DropdownEntry) {
+ this._next = value;
+ }
+
+ get parent(): DropdownEntry {
+ return this._parent;
+ }
+
+ set parent(value: DropdownEntry) {
+ this._parent = value;
+ }
+
+ get children(): DropdownEntry[] {
+ return this._children;
+ }
+
+ set children(value: DropdownEntry[]) {
+ this._children = value;
+ }
+
+ public focus(): void {
+ if (this.parent) {
+ this.parent._baseElement.classList.add("tobago-dropdown-open");
+ }
+
+ this._baseElement.classList.add("tobago-dropdown-selected");
+ this.focusElement.focus();
}
- resetCloseFlag(): void {
- this.closeFlag = false;
+ public clearCss(): void {
+ this._baseElement.classList.remove("tobago-dropdown-open");
+ this._baseElement.classList.remove("tobago-dropdown-selected");
}
}