Skip to content

Commit

Permalink
fix(core/dropdown): fix submenu discovery (#1060)
Browse files Browse the repository at this point in the history
Co-authored-by: Lukas Maurer <lukas.maurer@siemens.com>
  • Loading branch information
danielleroux and nuke-ellington committed Feb 6, 2024
1 parent 317e009 commit 4a95af8
Show file tree
Hide file tree
Showing 17 changed files with 273 additions and 59 deletions.
2 changes: 1 addition & 1 deletion packages/core/component-doc.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"timestamp": "",
"compiler": {
"name": "@stencil/core",
"version": "4.11.0",
"version": "4.12.0",
"typescriptVersion": "5.3.3"
},
"components": [
Expand Down
2 changes: 1 addition & 1 deletion packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@
},
"dependencies": {
"@floating-ui/dom": "^1.5.1",
"@stencil/core": "^4.11.0",
"@stencil/core": "^4.12.0",
"@types/luxon": "^3.3.7",
"animejs": "~3.2.1",
"hyperlist": "^1.0.0",
Expand Down
3 changes: 1 addition & 2 deletions packages/core/scss/_z-index.scss
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,10 @@
*/

:root {
--theme-z-index-dropdown: 1000;
--theme-z-index-sticky: 1020;
--theme-z-index-fixed: 1030;
--theme-z-index-modal-backdrop: 1040;
--theme-z-index-modal: 1050;
--theme-z-index-popover: 1060;
--theme-z-index-dropdown: 1060;
--theme-z-index-tooltip: 1070;
}
6 changes: 6 additions & 0 deletions packages/core/src/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -785,6 +785,8 @@ export namespace Components {
* Controls if the dropdown will be closed in response to a click event depending on the position of the event relative to the dropdown.
*/
"closeBehavior": CloseBehaviour;
"discoverAllSubmenus": boolean;
"discoverSubmenu": () => Promise<void>;
/**
* An optional header shown at the top of the dropdown
*/
Expand Down Expand Up @@ -888,6 +890,7 @@ export namespace Components {
* Internal usage only
*/
"emitItemClick": () => Promise<void>;
"getDropdownItemElement": () => Promise<HTMLIxDropdownItemElement>;
/**
* Display hover state
*/
Expand Down Expand Up @@ -1455,6 +1458,7 @@ export namespace Components {
"top": string;
}
interface IxMenuAvatarItem {
"getDropdownItemElement": () => Promise<HTMLIxDropdownItemElement>;
/**
* Avatar dropdown icon
*/
Expand Down Expand Up @@ -1900,6 +1904,7 @@ export namespace Components {
* @deprecated since 2.0.0. Use the `ix-dropdown-item` component instead.
*/
interface IxSplitButtonItem {
"getDropdownItemElement": () => Promise<HTMLIxDropdownItemElement>;
/**
* Dropdown icon
*/
Expand Down Expand Up @@ -4722,6 +4727,7 @@ declare namespace LocalJSX {
* Controls if the dropdown will be closed in response to a click event depending on the position of the event relative to the dropdown.
*/
"closeBehavior"?: CloseBehaviour;
"discoverAllSubmenus"?: boolean;
/**
* An optional header shown at the top of the dropdown
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,7 @@ export class ApplicationHeader {
<ix-dropdown
data-overflow-dropdown
class="dropdown"
discoverAllSubmenus
trigger={this.resolveContextMenuButton()}
>
<div class="dropdown-content">
Expand Down
9 changes: 8 additions & 1 deletion packages/core/src/components/dropdown-item/dropdown-item.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,14 @@ import {
Method,
Prop,
} from '@stencil/core';
import { DropdownItemWrapper } from '../dropdown/dropdown-controller';

@Component({
tag: 'ix-dropdown-item',
styleUrl: 'dropdown-item.scss',
shadow: true,
})
export class DropdownItem {
export class DropdownItem implements DropdownItemWrapper {
@Element() hostElement!: HTMLIxDropdownItemElement;

/**
Expand Down Expand Up @@ -68,6 +69,12 @@ export class DropdownItem {
this.itemClick.emit(this.hostElement);
}

/** @internal */
@Method()
async getDropdownItemElement() {
return this.hostElement;
}

private isIconOnly() {
return (
this.label === undefined &&
Expand Down
44 changes: 34 additions & 10 deletions packages/core/src/components/dropdown/dropdown-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,19 @@
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import { HTMLStencilElement } from '@stencil/core/internal';

export interface IxComponent {
hostElement: HTMLStencilElement;
}

import { IxComponent } from '../utils/internal';
export type CloseBehaviour = 'inside' | 'outside' | 'both' | boolean;

export interface DropdownInterface extends IxComponent {
closeBehavior: CloseBehaviour;
discoverAllSubmenus: boolean;

getAssignedSubmenuIds(): string[];
getId(): string;

discoverSubmenu(): void;

isPresent(): boolean;

willPresent?(): boolean;
Expand All @@ -29,6 +28,19 @@ export interface DropdownInterface extends IxComponent {
dismiss(): void;
}

export function hasDropdownItemWrapperImplemented(
item: unknown
): item is DropdownItemWrapper {
return (
(item as DropdownItemWrapper).getDropdownItemElement !== undefined &&
typeof (item as DropdownItemWrapper).getDropdownItemElement === 'function'
);
}

export interface DropdownItemWrapper {
getDropdownItemElement(): Promise<HTMLIxDropdownItemElement>;
}

type DropdownRule = Record<string, string[]>;

class DropdownController {
Expand All @@ -42,12 +54,22 @@ class DropdownController {
this.addOverlayListeners();
}
this.dropdowns.add(dropdown);

if (dropdown.discoverAllSubmenus) {
this.discoverSubmenus();
}
}

disconnected(dropdown: DropdownInterface) {
this.dropdowns.delete(dropdown);
}

discoverSubmenus() {
this.dropdowns.forEach((dropdown) => {
dropdown.discoverSubmenu();
});
}

present(dropdown: DropdownInterface) {
this.dropdownRules[dropdown.getId()] = dropdown.getAssignedSubmenuIds();
if (!dropdown.isPresent() && dropdown.willPresent()) {
Expand All @@ -59,6 +81,7 @@ class DropdownController {
dismiss(dropdown: DropdownInterface) {
if (dropdown.isPresent() && dropdown.willDismiss()) {
dropdown.dismiss();
delete this.dropdownRules[dropdown.getId()];
}
}

Expand All @@ -76,27 +99,28 @@ class DropdownController {
}

dismissPath(uid: string) {
let path = this.buildComposedPath(uid, []);
let path = this.buildComposedPath(uid, new Set<string>());

for (const dropdown of this.dropdowns) {
if (
dropdown.isPresent() &&
dropdown.closeBehavior !== 'inside' &&
dropdown.closeBehavior !== false &&
!path.includes(dropdown.getId())
!path.has(dropdown.getId())
) {
this.dismiss(dropdown);
}
}
}

private buildComposedPath(id: string, path: string[]): string[] {
private buildComposedPath(id: string, path: Set<string>): Set<string> {
if (this.dropdownRules[id]) {
path.push(id);
path.add(id);
}

for (const ruleKey of Object.keys(this.dropdownRules)) {
if (this.dropdownRules[ruleKey].includes(id)) {
return this.buildComposedPath(ruleKey, path);
this.buildComposedPath(ruleKey, path).forEach((key) => path.add(key));
}
}

Expand Down
84 changes: 62 additions & 22 deletions packages/core/src/components/dropdown/dropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import {
CloseBehaviour,
dropdownController,
DropdownInterface,
hasDropdownItemWrapperImplemented,
} from './dropdown-controller';
import { AlignedPlacement } from './placement';

Expand Down Expand Up @@ -109,6 +110,13 @@ export class Dropdown implements ComponentInterface, DropdownInterface {
triggerRef?: HTMLElement;
}) => Promise<Partial<CSSStyleDeclaration>>;

/**
* @internal
* If initialisation of this dropdown is expected to be defered submenu discovery will have to be re-run globally by the controller.
* This property indicates the need for that to the controller.
*/
@Prop() discoverAllSubmenus = false;

/**
* Fire event after visibility of dropdown has changed
*/
Expand All @@ -128,8 +136,15 @@ export class Dropdown implements ComponentInterface, DropdownInterface {
}

@Listen('ix-assign-sub-menu')
cacheSubmenuId({ detail }: CustomEvent<string>) {
this.assignedSubmenu.push(detail);
cacheSubmenuId(event: CustomEvent<string>) {
event.stopImmediatePropagation();
event.preventDefault();

const { detail } = event;

if (this.assignedSubmenu.indexOf(detail) === -1) {
this.assignedSubmenu.push(detail);
}
}

disconnectedCallback() {
Expand Down Expand Up @@ -211,25 +226,55 @@ export class Dropdown implements ComponentInterface, DropdownInterface {
this.triggerElement.setAttribute('data-ix-dropdown-trigger', this.localUId);
}

/** @internal */
@Method()
async discoverSubmenu() {
this.triggerElement?.dispatchEvent(
new CustomEvent('ix-assign-sub-menu', {
bubbles: true,
composed: false,
cancelable: true,
detail: this.localUId,
})
);
}

private async registerListener(
element: string | HTMLElement | Promise<HTMLElement>
) {
this.triggerElement = await this.resolveElement(element);
if (this.triggerElement) {
this.addEventListenersFor();
this.discoverSubmenu();
}
}

this.triggerElement.dispatchEvent(
new CustomEvent('ix-assign-sub-menu', {
bubbles: true,
composed: false,
cancelable: true,
detail: this.localUId,
})
);
private async resolveElement(
element: string | HTMLElement | Promise<HTMLElement>
) {
const el = await this.findElement(element);

return this.checkForSubmenuAnchor(el);
}

private async checkForSubmenuAnchor(element: Element) {
if (hasDropdownItemWrapperImplemented(element)) {
const dropdownItem = await element.getDropdownItemElement();
dropdownItem.isSubMenu = true;
this.hostElement.style.zIndex = `var(--theme-z-index-dropdown)`;

return dropdownItem;
}

if (element.tagName === 'IX-DROPDOWN-ITEM') {
(element as HTMLIxDropdownItemElement).isSubMenu = true;
this.hostElement.style.zIndex = `var(--theme-z-index-dropdown)`;
}

return element;
}

private resolveElement(
private findElement(
element: string | HTMLElement | Promise<HTMLElement>
): Promise<Element> {
if (element instanceof Promise) {
Expand Down Expand Up @@ -269,6 +314,7 @@ export class Dropdown implements ComponentInterface, DropdownInterface {

if (this.anchorElement) {
this.applyDropdownPosition();
// await this.checkForSubmenuAnchor();
}
}
}
Expand All @@ -278,10 +324,11 @@ export class Dropdown implements ComponentInterface, DropdownInterface {
this.registerListener(newTriggerValue);
}

private isAnchorSubmenu() {
const anchor = this.anchorElement?.closest('ix-dropdown-item');
if (!anchor) {
return false;
private isAnchorSubmenu(): boolean {
if (!hasDropdownItemWrapperImplemented(this.anchorElement)) {
// Is no official dropdown-item, but check if any dropdown-item
// is placed somewhere up the DOM
return !!this.anchorElement?.closest('ix-dropdown-item');
}

return true;
Expand Down Expand Up @@ -381,13 +428,6 @@ export class Dropdown implements ComponentInterface, DropdownInterface {
this.anchorElement = await (this.anchor
? this.resolveElement(this.anchor)
: this.resolveElement(this.trigger));

if (
this.isAnchorSubmenu() &&
this.anchorElement?.tagName === 'IX-DROPDOWN-ITEM'
) {
(this.anchorElement as HTMLIxDropdownItemElement).isSubMenu = true;
}
}

private onDropdownClick(event: PointerEvent) {
Expand Down
Loading

0 comments on commit 4a95af8

Please sign in to comment.