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(core/dropdown): use controller instance to handle dropdown instances #1051

Merged
merged 22 commits into from
Jan 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions packages/angular-test-app/src/app/app-routing.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,11 @@ import Datetimepicker from 'src/preview-examples/datetimepicker';
import Drawer from 'src/preview-examples/drawer';
import DrawerFullHeight from 'src/preview-examples/drawer-full-height';
import Dropdown from 'src/preview-examples/dropdown';
import DropdownButton from 'src/preview-examples/dropdown-button';
import DropdownButtonIcon from 'src/preview-examples/dropdown-button-icon';
import DropdownIcon from 'src/preview-examples/dropdown-icon';
import DropdownQuickActions from 'src/preview-examples/dropdown-quick-actions';
import DropdownSubmenu from 'src/preview-examples/dropdown-submenu';
import EmptyState from 'src/preview-examples/empty-state';
import EmptyStateCompact from 'src/preview-examples/empty-state-compact';
import EmptyStateCompactBreak from 'src/preview-examples/empty-state-compact-break';
Expand Down Expand Up @@ -293,9 +297,13 @@ const routes: Routes = [
path: 'drawer',
component: Drawer,
},
{ path: 'dropdown-button', component: DropdownButton },
{ path: 'dropdown-button-icon', component: DropdownButtonIcon },
{ path: 'dropdown-icon', component: DropdownIcon },

{ path: 'dropdown', component: Dropdown },
{ path: 'dropdown-quick-actions', component: DropdownQuickActions },
{ path: 'dropdown-submenu', component: DropdownSubmenu },
{ path: 'event-list-compact', component: EventListCompact },
{
path: 'event-list-custom-item-height',
Expand Down
8 changes: 8 additions & 0 deletions packages/angular-test-app/src/app/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,11 @@ import Datetimepicker from 'src/preview-examples/datetimepicker';
import Drawer from 'src/preview-examples/drawer';
import DrawerFullHeight from 'src/preview-examples/drawer-full-height';
import Dropdown from 'src/preview-examples/dropdown';
import DropdownButton from 'src/preview-examples/dropdown-button';
import DropdownButtonIcon from 'src/preview-examples/dropdown-button-icon';
import DropdownIcon from 'src/preview-examples/dropdown-icon';
import DropdownQuickActions from 'src/preview-examples/dropdown-quick-actions';
import DropdownSubmenu from 'src/preview-examples/dropdown-submenu';
import EmptyState from 'src/preview-examples/empty-state';
import EmptyStateCompact from 'src/preview-examples/empty-state-compact';
import EmptyStateCompactBreak from 'src/preview-examples/empty-state-compact-break';
Expand Down Expand Up @@ -182,8 +186,12 @@ import { NavigationTestComponent } from './components/navigation-test.component'
Datetimepicker,
DrawerFullHeight,
Drawer,
DropdownButton,
DropdownButtonIcon,
DropdownIcon,
Dropdown,
DropdownQuickActions,
DropdownSubmenu,
EventListCompact,
EventListCustomItemHeight,
EventListSelected,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,4 @@ import { Component } from '@angular/core';
</ix-dropdown-button>
`,
})
export class Dropdown {}
export default class Dropdown {}
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,4 @@ import { Component } from '@angular/core';
</ix-dropdown-button>
`,
})
export class Dropdown {}
export default class Dropdown {}
38 changes: 25 additions & 13 deletions packages/core/component-doc.json
Original file line number Diff line number Diff line change
Expand Up @@ -5024,9 +5024,15 @@
"name": "closeBehavior",
"type": "\"both\" | \"inside\" | \"outside\" | boolean",
"complexType": {
"original": "'inside' | 'outside' | 'both' | boolean",
"original": "CloseBehaviour",
"resolved": "\"both\" | \"inside\" | \"outside\" | boolean",
"references": {}
"references": {
"CloseBehaviour": {
"location": "import",
"path": "dropdown-controller",
"id": "src/components/dropdown/dropdown-controller.ts::CloseBehaviour"
}
}
},
"mutable": false,
"attr": "close-behavior",
Expand Down Expand Up @@ -5288,7 +5294,13 @@
"styles": [],
"slots": [],
"parts": [],
"listeners": []
"listeners": [
{
"event": "ix-assign-sub-menu",
"capture": false,
"passive": false
}
]
},
{
"dirPath": "src/components/dropdown-button",
Expand Down Expand Up @@ -16831,21 +16843,16 @@
"docstring": "",
"path": "src/components/datetime-picker/datetime-picker.tsx"
},
"src/components/dropdown/placement.ts::AlignedPlacement": {
"declaration": "\"bottom-start\" | \"top-start\" | \"top-end\" | \"right-start\" | \"right-end\" | \"bottom-end\" | \"left-start\" | \"left-end\"",
"docstring": "",
"path": "src/components/dropdown/placement.ts"
},
"src/components/dropdown/dropdown.tsx::DropdownTriggerEvent": {
"declaration": "export type DropdownTriggerEvent = 'click' | 'hover' | 'focus';",
"docstring": "",
"path": "src/components/dropdown/dropdown.tsx"
},
"src/components/dropdown-button/dropdown-button.tsx::DropdownButtonVariant": {
"declaration": "export type ButtonVariant = 'primary' | 'secondary';",
"docstring": "",
"path": "src/components/dropdown-button/dropdown-button.tsx"
},
"src/components/dropdown/placement.ts::AlignedPlacement": {
"declaration": "\"bottom-start\" | \"top-start\" | \"top-end\" | \"right-start\" | \"right-end\" | \"bottom-end\" | \"left-start\" | \"left-end\"",
"docstring": "",
"path": "src/components/dropdown/placement.ts"
},
"src/components/empty-state/empty-state.tsx::EmptyStateLayout": {
"declaration": "export type EmptyStateLayout = 'large' | 'compact' | 'compactBreak';",
"docstring": "",
Expand Down Expand Up @@ -16936,6 +16943,11 @@
"docstring": "",
"path": "src/components/category-filter/input-state.ts"
},
"src/components/dropdown/dropdown-controller.ts::CloseBehaviour": {
"declaration": "export type CloseBehaviour = 'inside' | 'outside' | 'both' | boolean;",
"docstring": "",
"path": "src/components/dropdown/dropdown-controller.ts"
},
"src/components/flip-tile/flip-tile-state.ts::FlipTileState": {
"declaration": "export enum FlipTileState {\n None = 'none',\n Info = 'info',\n Warning = 'warning',\n Alarm = 'alarm',\n Primary = 'primary',\n}",
"docstring": "",
Expand Down
16 changes: 4 additions & 12 deletions packages/core/src/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ import { DateTimeCardCorners } from "./components/date-time-card/date-time-card"
import { DateChangeEvent } from "./components/date-picker/date-picker";
import { DateTimeCardCorners as DateTimeCardCorners1 } from "./components/date-time-card/date-time-card";
import { DateTimeDateChangeEvent, DateTimeSelectEvent } from "./components/datetime-picker/datetime-picker";
import { CloseBehaviour } from "./components/dropdown/dropdown-controller";
import { AlignedPlacement, Side } from "./components/dropdown/placement";
import { DropdownTriggerEvent } from "./components/dropdown/dropdown";
import { DropdownButtonVariant } from "./components/dropdown-button/dropdown-button";
import { EmptyStateLayout } from "./components/empty-state/empty-state";
import { FlipTileState } from "./components/flip-tile/flip-tile-state";
Expand Down Expand Up @@ -60,8 +60,8 @@ export { DateTimeCardCorners } from "./components/date-time-card/date-time-card"
export { DateChangeEvent } from "./components/date-picker/date-picker";
export { DateTimeCardCorners as DateTimeCardCorners1 } from "./components/date-time-card/date-time-card";
export { DateTimeDateChangeEvent, DateTimeSelectEvent } from "./components/datetime-picker/datetime-picker";
export { CloseBehaviour } from "./components/dropdown/dropdown-controller";
export { AlignedPlacement, Side } from "./components/dropdown/placement";
export { DropdownTriggerEvent } from "./components/dropdown/dropdown";
export { DropdownButtonVariant } from "./components/dropdown-button/dropdown-button";
export { EmptyStateLayout } from "./components/empty-state/empty-state";
export { FlipTileState } from "./components/flip-tile/flip-tile-state";
Expand Down Expand Up @@ -784,7 +784,7 @@ 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": 'inside' | 'outside' | 'both' | boolean;
"closeBehavior": CloseBehaviour;
/**
* An optional header shown at the top of the dropdown
*/
Expand Down Expand Up @@ -822,10 +822,6 @@ export namespace Components {
* Define an element that triggers the dropdown. A trigger can either be a string that will be interpreted as id attribute or a DOM element.
*/
"trigger": string | HTMLElement | Promise<HTMLElement>;
/**
* Define one or more events to open dropdown
*/
"triggerEvent": DropdownTriggerEvent | DropdownTriggerEvent[];
/**
* Update position of dropdown
*/
Expand Down Expand Up @@ -4725,7 +4721,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"?: 'inside' | 'outside' | 'both' | boolean;
"closeBehavior"?: CloseBehaviour;
/**
* An optional header shown at the top of the dropdown
*/
Expand Down Expand Up @@ -4767,10 +4763,6 @@ declare namespace LocalJSX {
* Define an element that triggers the dropdown. A trigger can either be a string that will be interpreted as id attribute or a DOM element.
*/
"trigger"?: string | HTMLElement | Promise<HTMLElement>;
/**
* Define one or more events to open dropdown
*/
"triggerEvent"?: DropdownTriggerEvent | DropdownTriggerEvent[];
}
/**
* @since 1.3.0
Expand Down
147 changes: 147 additions & 0 deletions packages/core/src/components/dropdown/dropdown-controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
/*
* SPDX-FileCopyrightText: 2024 Siemens AG
*
* SPDX-License-Identifier: MIT
*
* 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;
}

export type CloseBehaviour = 'inside' | 'outside' | 'both' | boolean;

export interface DropdownInterface extends IxComponent {
closeBehavior: CloseBehaviour;

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

isPresent(): boolean;

willPresent?(): boolean;
willDismiss?(): boolean;

present(): void;
dismiss(): void;
}

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

class DropdownController {
private dropdowns = new Set<DropdownInterface>();
private dropdownRules: DropdownRule = {};

private isWindowListenerActive = false;

connected(dropdown: DropdownInterface) {
if (!this.isWindowListenerActive) {
this.addOverlayListeners();
}
this.dropdowns.add(dropdown);
}

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

present(dropdown: DropdownInterface) {
this.dropdownRules[dropdown.getId()] = dropdown.getAssignedSubmenuIds();
if (!dropdown.isPresent() && dropdown.willPresent()) {
dropdown.present();
this.dismissPath(dropdown.getId());
}
}

dismiss(dropdown: DropdownInterface) {
if (dropdown.isPresent() && dropdown.willDismiss()) {
dropdown.dismiss();
}
}

dismissAll() {
for (const dropdown of this.dropdowns) {
if (
dropdown.closeBehavior === 'inside' ||
dropdown.closeBehavior === false
) {
continue;
}

this.dismiss(dropdown);
}
}

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

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

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

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

return path;
}

private addOverlayListeners() {
this.isWindowListenerActive = true;

window.addEventListener('click', () => {
this.dismissAll();
});

window.addEventListener('keydown', (event: KeyboardEvent) => {
if (event.key === 'Escape') {
this.dismissAll();
}
});
}
}

export const addDisposableEventListener = (
element: Element | Window | Document,
eventType: string,
callback: EventListenerOrEventListenerObject
) => {
element.addEventListener(eventType, callback);

return () => {
element.removeEventListener(eventType, callback);
};
};

export const addDisposableEventListenerAsArray = (
listener: {
element: Element | Window | Document;
eventType: string;
callback: EventListenerOrEventListenerObject;
}[]
) => {
const disposables = listener.map(({ callback, element, eventType }) =>
addDisposableEventListener(element, eventType, callback)
);

return () => disposables.forEach((dispose) => dispose());
};

export const dropdownController = new DropdownController();
Loading
Loading