diff --git a/packages/angular-test-app/src/preview-examples/dropdown-submenu.ts b/packages/angular-test-app/src/preview-examples/dropdown-submenu.ts
index ccae12e11dc..7b114d92b8a 100644
--- a/packages/angular-test-app/src/preview-examples/dropdown-submenu.ts
+++ b/packages/angular-test-app/src/preview-examples/dropdown-submenu.ts
@@ -14,9 +14,7 @@ import { Component } from '@angular/core';
template: `
Open
-
-
-
+
diff --git a/packages/core/src/components/dropdown/dropdown.tsx b/packages/core/src/components/dropdown/dropdown.tsx
index dbe2793615d..ab06fa42d25 100644
--- a/packages/core/src/components/dropdown/dropdown.tsx
+++ b/packages/core/src/components/dropdown/dropdown.tsx
@@ -147,14 +147,14 @@ export class Dropdown {
return Array.from(this.hostElement.querySelectorAll('ix-dropdown-item'));
}
+ get slotElement() {
+ return this.hostElement.shadowRoot.querySelector('slot');
+ }
+
private addEventListenersFor(triggerEvent: DropdownTriggerEvent) {
switch (triggerEvent) {
case 'click':
- if (this.closeBehavior === 'outside') {
- this.triggerElement.addEventListener('click', this.openBind);
- } else {
- this.triggerElement.addEventListener('click', this.toggleBind);
- }
+ this.triggerElement.addEventListener('click', this.openBind);
break;
case 'hover':
@@ -277,8 +277,13 @@ export class Dropdown {
@Listen('click', {
target: 'window',
})
- clickOutside(event: Event) {
+ clickOutside(event: PointerEvent) {
const target = event.target as HTMLElement;
+
+ if (event.defaultPrevented) {
+ return;
+ }
+
if (
this.show === false ||
this.closeBehavior === false ||
@@ -287,24 +292,27 @@ export class Dropdown {
) {
return;
}
+
+ const clickInsideDropdown = this.isClickInsideDropdown(event);
+
switch (this.closeBehavior) {
case 'outside':
- if (!this.dropdownRef.contains(target)) {
- this.close(event);
+ if (!clickInsideDropdown || this.anchor === target) {
+ this.close();
}
break;
case 'inside':
- if (this.dropdownRef.contains(target) && this.hostElement !== target) {
- this.close(event);
+ if (clickInsideDropdown && this.hostElement !== target) {
+ this.close();
}
break;
case 'both':
if (this.hostElement !== target) {
- this.close(event);
+ this.close();
}
break;
default:
- this.close(event);
+ this.close();
}
}
@@ -330,42 +338,40 @@ export class Dropdown {
return true;
}
- private toggle(event?: Event) {
- event?.preventDefault();
+ private toggle(event: Event) {
+ event.preventDefault();
if (this.isNestedDropdown(event.target as HTMLElement)) {
- event?.stopPropagation();
+ event.stopPropagation();
}
- this.show = !this.show;
- this.showChanged.emit(this.show);
+ const { defaultPrevented } = this.showChanged.emit(this.show);
+
+ if (!defaultPrevented) {
+ this.show = !this.show;
+ }
}
- private open(event?: Event) {
- event?.preventDefault();
+ private open(event: Event) {
+ event.preventDefault();
if (this.isNestedDropdown(event.target as HTMLElement)) {
- event?.stopPropagation();
+ event.stopPropagation();
}
- this.show = true;
- this.showChanged.emit(true);
- }
+ const { defaultPrevented } = this.showChanged.emit(true);
- private close(event?: Event) {
- if (event?.defaultPrevented) {
- const target = event.target as HTMLElement;
- if (
- target.contains(this.anchorElement) ||
- target.contains(this.triggerElement) ||
- target.shadowRoot.contains(this.anchorElement) ||
- target.shadowRoot.contains(this.triggerElement)
- )
- return;
+ if (!defaultPrevented) {
+ this.show = true;
}
+ }
+
+ private close() {
+ const { defaultPrevented } = this.showChanged.emit(false);
- this.show = false;
- this.showChanged.emit(false);
+ if (!defaultPrevented) {
+ this.show = false;
+ }
}
private async applyDropdownPosition() {
@@ -440,9 +446,7 @@ export class Dropdown {
}
async componentDidLoad() {
- if (this.trigger) {
- this.changedTrigger(this.trigger, null);
- }
+ this.changedTrigger(this.trigger, null);
}
async componentDidRender() {
@@ -459,6 +463,16 @@ export class Dropdown {
}
}
+ private isClickInsideDropdown(event: PointerEvent) {
+ const rect = this.dropdownRef.getBoundingClientRect();
+ return (
+ rect.top <= event.clientY &&
+ event.clientY <= rect.top + rect.height &&
+ rect.left <= event.clientX &&
+ event.clientX <= rect.left + rect.width
+ );
+ }
+
disconnectedCallback() {
if (this.autoUpdateCleanup) {
this.autoUpdateCleanup();
diff --git a/packages/core/src/components/dropdown/test/dropdown.ct.ts b/packages/core/src/components/dropdown/test/dropdown.ct.ts
new file mode 100644
index 00000000000..0d7e53c3177
--- /dev/null
+++ b/packages/core/src/components/dropdown/test/dropdown.ct.ts
@@ -0,0 +1,101 @@
+/*
+ * SPDX-FileCopyrightText: 2023 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.
+ */
+
+/*
+ * SPDX-FileCopyrightText: 2023 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 { expect, Locator } from '@playwright/test';
+import { test } from '@utils/test';
+
+test('renders', async ({ mount, page }) => {
+ await mount(`
+
+ Test 1
+
+
+
+ Test 1
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `);
+
+ const sb1 = page.locator('ix-split-button').nth(0);
+ const sb2 = page.locator('ix-split-button').nth(1);
+
+ const g1 = page.locator('ix-group').nth(0);
+ const g2 = page.locator('ix-group').nth(1);
+
+ const sb1Dropdown = sb1.locator('ix-dropdown');
+ const sb2Dropdown = sb2.locator('ix-dropdown');
+ const g1Dropdown = g1.locator('ix-dropdown');
+ const g2Dropdown = g2.locator('ix-dropdown');
+
+ await sb1
+ .getByRole('button')
+ .filter({ hasText: 'context-menu' })
+ .first()
+ .click();
+
+ await expectToBeVisible(
+ [sb1Dropdown, sb2Dropdown, g1Dropdown, g2Dropdown],
+ 0
+ );
+
+ await sb2
+ .getByRole('button')
+ .filter({ hasText: 'context-menu' })
+ .first()
+ .click();
+
+ await expectToBeVisible(
+ [sb1Dropdown, sb2Dropdown, g1Dropdown, g2Dropdown],
+ 1
+ );
+
+ await g2.getByRole('button').filter({ hasText: 'context-menu' }).click();
+
+ await expectToBeVisible(
+ [sb1Dropdown, sb2Dropdown, g1Dropdown, g2Dropdown],
+ 3
+ );
+});
+
+function expectToBeVisible(elements: Locator[], index: number) {
+ return Promise.all(
+ elements.map(async (element, i) => {
+ let ef = expect(element);
+ if (i !== index) {
+ ef = ef.not;
+ }
+ await ef.toBeVisible();
+ })
+ );
+}
diff --git a/packages/core/src/components/pagination/test/pagination.ct.ts b/packages/core/src/components/pagination/test/pagination.ct.ts
new file mode 100644
index 00000000000..10a446059fc
--- /dev/null
+++ b/packages/core/src/components/pagination/test/pagination.ct.ts
@@ -0,0 +1,47 @@
+/*
+ * SPDX-FileCopyrightText: 2023 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 { expect } from '@playwright/test';
+import { test } from '@utils/test';
+
+test('renders', async ({ mount, page }) => {
+ await mount(`
+
+
+ `);
+ const element = page.locator('ix-pagination');
+
+ await expect(element).toHaveClass(/hydrated/);
+});
+
+test('advanced', async ({ mount, page }) => {
+ await mount(`
+
+
+ `);
+ const element = page.locator('ix-pagination[advanced]');
+
+ await expect(element).toHaveClass(/hydrated/);
+});
+
+test('open show number of page dropdown', async ({ mount, page }) => {
+ await mount(`
+
+
+ `);
+ const element = page.locator('ix-pagination[advanced]');
+
+ await element
+ .getByRole('button')
+ .filter({ hasText: 'chevron-down-small' })
+ .click();
+
+ const dropdown = element.locator('ix-dropdown');
+
+ await expect(dropdown).toBeVisible();
+});
diff --git a/packages/core/src/components/select/select.tsx b/packages/core/src/components/select/select.tsx
index 196fb563306..df352d281aa 100644
--- a/packages/core/src/components/select/select.tsx
+++ b/packages/core/src/components/select/select.tsx
@@ -314,12 +314,12 @@ export class Select {
target: 'window',
})
async onKeyDown(event: KeyboardEvent) {
- if (!this.dropdownShow) {
- return;
+ if (event.code === 'ArrowDown' || event.code === 'ArrowUp') {
+ this.onArrowNavigation(event, event.code);
}
- if (event.code === 'ArrowDown' || event.code === 'ArrowUp') {
- this.onArrowNavigation(event);
+ if (!this.dropdownShow) {
+ return;
}
if (event.code === 'Enter' || event.code === 'NumpadEnter') {
@@ -349,32 +349,47 @@ export class Select {
}
}
- private onArrowNavigation(event: KeyboardEvent) {
- event.stopPropagation();
+ private onArrowNavigation(
+ event: KeyboardEvent,
+ key: 'ArrowDown' | 'ArrowUp'
+ ) {
event.preventDefault();
+ event.stopPropagation();
- const focusItem = this.items.find(
- (item) => document.activeElement === item.querySelector('button')
- );
- this.navigationItem = focusItem;
+ this.dropdownShow = true;
- const selectItems = this.items.filter(
- (i) => !i.classList.contains('d-none')
- );
+ const items = this.items.filter((i) => !i.classList.contains('d-none'));
+ if (this.navigationItem === undefined) {
+ this.applyFocusTo(items[0]);
+ return;
+ }
- const index = selectItems.indexOf(this.navigationItem);
+ let indexOfNavigationItem = items.findIndex(
+ (item) => item === this.navigationItem
+ );
- if (event.code === 'ArrowDown' && index < selectItems.length - 1) {
- this.navigationItem = selectItems[index + 1];
- } else if (event.code === 'ArrowUp' && index > 0) {
- this.navigationItem = selectItems[index - 1];
+ if (key === 'ArrowDown') {
+ indexOfNavigationItem++;
+ } else {
+ indexOfNavigationItem--;
}
- this.setHoverEffectForNavigatedSelectItem();
+ const newFocusItem = items[indexOfNavigationItem];
+ this.applyFocusTo(newFocusItem);
}
- private setHoverEffectForNavigatedSelectItem() {
- this.navigationItem?.querySelector('button').focus();
+ private applyFocusTo(element: HTMLIxSelectItemElement) {
+ if (!element) {
+ return;
+ }
+ this.navigationItem = element;
+
+ setTimeout(() => {
+ element.shadowRoot
+ .querySelector('ix-dropdown-item')
+ .shadowRoot.querySelector('button')
+ .focus();
+ });
}
private filterItemsWithTypeahead() {
diff --git a/packages/core/src/components/utils/shadow-dom.ts b/packages/core/src/components/utils/shadow-dom.ts
index e0696287c69..503b8b3e2b9 100644
--- a/packages/core/src/components/utils/shadow-dom.ts
+++ b/packages/core/src/components/utils/shadow-dom.ts
@@ -26,3 +26,13 @@ export function hasSlottedElements(slot: any) {
}
return slot.assignedElements({ flatten: true }).length !== 0;
}
+
+export function containsElement(target: Element, element: Element) {
+ const hasShadowDom = target.shadowRoot;
+
+ if (hasShadowDom) {
+ target.contains(element) || target.shadowRoot.contains(element);
+ }
+
+ return target.contains(element);
+}
diff --git a/packages/html-test-app/src/preview-examples/dropdown-submenu.html b/packages/html-test-app/src/preview-examples/dropdown-submenu.html
index 2aa876a6c64..cfcca8edc08 100644
--- a/packages/html-test-app/src/preview-examples/dropdown-submenu.html
+++ b/packages/html-test-app/src/preview-examples/dropdown-submenu.html
@@ -13,9 +13,7 @@
Open
-
+
diff --git a/packages/react-test-app/src/preview-examples/dropdown-submenu.tsx b/packages/react-test-app/src/preview-examples/dropdown-submenu.tsx
index bb87c077b49..c42ba77698c 100644
--- a/packages/react-test-app/src/preview-examples/dropdown-submenu.tsx
+++ b/packages/react-test-app/src/preview-examples/dropdown-submenu.tsx
@@ -7,12 +7,7 @@
* LICENSE file in the root directory of this source tree.
*/
-import {
- IxButton,
- IxDropdown,
- IxDropdownItem,
- IxIcon,
-} from '@siemens/ix-react';
+import { IxButton, IxDropdown, IxDropdownItem } from '@siemens/ix-react';
import React from 'react';
export default () => {
@@ -20,9 +15,7 @@ export default () => {
<>
Open
-
+