diff --git a/libs/collapsible/src/lib/collapsible/collapsible.component.html b/libs/collapsible/src/lib/collapsible/collapsible.component.html
index b050a2de5..e547633c6 100644
--- a/libs/collapsible/src/lib/collapsible/collapsible.component.html
+++ b/libs/collapsible/src/lib/collapsible/collapsible.component.html
@@ -9,8 +9,10 @@
tabindex="0"
>
-
-
+
+
+
+
diff --git a/libs/collapsible/src/lib/collapsible/collapsible.component.less b/libs/collapsible/src/lib/collapsible/collapsible.component.less
index 84829b419..c32ec3d7f 100644
--- a/libs/collapsible/src/lib/collapsible/collapsible.component.less
+++ b/libs/collapsible/src/lib/collapsible/collapsible.component.less
@@ -111,4 +111,8 @@
background: @gray-lighter;
color: @ink;
}
+
+ &-content-inner {
+ display: contents;
+ }
}
diff --git a/libs/collapsible/src/lib/collapsible/collapsible.component.stories.ts b/libs/collapsible/src/lib/collapsible/collapsible.component.stories.ts
index b4de1437c..2b3003ea3 100644
--- a/libs/collapsible/src/lib/collapsible/collapsible.component.stories.ts
+++ b/libs/collapsible/src/lib/collapsible/collapsible.component.stories.ts
@@ -1,12 +1,12 @@
import { Component } from '@angular/core';
import { provideAnimations } from '@angular/platform-browser/animations';
import { applicationConfig, Meta, moduleMetadata } from '@storybook/angular';
-import { CollapsibleComponent } from './collapsible.component';
import { CollapsibleModule } from '../collapsible.module';
+import { CollapsibleComponent } from './collapsible.component';
@Component({
selector: 'spy-story',
- template: ` Collapse Content `,
+ template: ` Collapse Content `,
})
class StoryComponent {
constructor() {
diff --git a/libs/dropdown/src/lib/dropdown/dropdown.component.html b/libs/dropdown/src/lib/dropdown/dropdown.component.html
index 30a9bb243..18295fafb 100644
--- a/libs/dropdown/src/lib/dropdown/dropdown.component.html
+++ b/libs/dropdown/src/lib/dropdown/dropdown.component.html
@@ -6,28 +6,55 @@
[(nzVisible)]="visible"
[nzTrigger]="trigger"
(click)="$event.stopPropagation()"
+ (keydown)="onKeyDown($event)"
(nzVisibleChange)="visibleChange.emit($event)"
+ tabindex="0"
+ [attr.aria-label]="ariaLabel"
>
+
-
-
+
+
-
-
- -
-
- {{ item.title }}
-
- -
-
-
-
+
+ @for (item of items; track $index) {
+ @if (item.subItems) {
+
+ } @else {
+ -
+
+ {{ item.title }}
+
+ }
+ }
diff --git a/libs/dropdown/src/lib/dropdown/dropdown.component.less b/libs/dropdown/src/lib/dropdown/dropdown.component.less
index 38da4f6b7..d7b3368cc 100644
--- a/libs/dropdown/src/lib/dropdown/dropdown.component.less
+++ b/libs/dropdown/src/lib/dropdown/dropdown.component.less
@@ -9,6 +9,11 @@
&-trigger {
display: flex;
height: 100%;
+
+ &:focus-visible {
+ outline: @outline;
+ outline-offset: @outline-offset;
+ }
}
&-menu {
@@ -18,26 +23,32 @@
background-color: @spy-white;
padding: @dropdown-menu-padding;
overflow: hidden;
+ }
+
+ &-menu-submenu-title,
+ &-menu-item {
+ height: @dropdown-menu-item-height;
+ padding: @dropdown-menu-item-padding;
+ font: @font-default;
+ color: @ink;
+ border: @dropdown-menu-item-border;
+ display: flex;
+ align-items: center;
- &-item {
- height: @dropdown-menu-item-height;
- padding: @dropdown-menu-item-padding;
- font: @font-default;
- color: @ink;
- border: @dropdown-menu-item-border;
- display: flex;
- align-items: center;
-
- &:hover {
- background-color: @gray-lighter;
- border-color: @gray-lighter;
- }
-
- &:focus {
- border-radius: @dropdown-menu-item-border-focus-radius;
- border-color: @green;
- }
+ &:hover {
+ background-color: @gray-lighter;
+ border-color: @gray-lighter;
}
+
+ &:focus {
+ border-radius: @dropdown-menu-item-border-focus-radius;
+ border-color: @green;
+ }
+ }
+
+ &-menu-submenu:focus .@{dropdown-prefix-cls}-menu-submenu-title {
+ border-radius: @dropdown-menu-item-border-focus-radius;
+ border-color: @green;
}
.@{menu-prefix-cls}-title-content {
diff --git a/libs/dropdown/src/lib/dropdown/dropdown.component.stories.ts b/libs/dropdown/src/lib/dropdown/dropdown.component.stories.ts
index 9f045d95f..dc2919594 100644
--- a/libs/dropdown/src/lib/dropdown/dropdown.component.stories.ts
+++ b/libs/dropdown/src/lib/dropdown/dropdown.component.stories.ts
@@ -38,7 +38,28 @@ export default {
},
args: {
items: [
- { action: 'action1', title: 'item1' },
+ {
+ action: 'action1',
+ title: 'item1',
+ subItems: [
+ {
+ action: 'action1',
+ title: 'subItem1',
+ subItems: [
+ { action: 'action1', title: 'subItem1' },
+ { action: 'action1', title: 'subItem2' },
+ { action: 'action1', title: 'subItem3' },
+ { action: 'action1', title: 'subItem4' },
+ ],
+ },
+ { action: 'action1', title: 'subItem2' },
+ { action: 'action1', title: 'subItem3' },
+ { action: 'action1', title: 'subItem4' },
+ ],
+ },
+ { action: 'action2', title: 'item2' },
+ { action: 'action2', title: 'item2' },
+ { action: 'action2', title: 'item2' },
{ action: 'action2', title: 'item2' },
],
placement: 'bottomRight',
@@ -56,9 +77,12 @@ export const primary = (args) => ({
[items]="items"
[placement]="placement"
[visible]="visible"
+ [trigger]="trigger"
[disabled]="disabled">
{{ trigger }} me
+
+
`,
});
diff --git a/libs/dropdown/src/lib/dropdown/dropdown.component.ts b/libs/dropdown/src/lib/dropdown/dropdown.component.ts
index 6ecc8ff38..2c3d63b58 100644
--- a/libs/dropdown/src/lib/dropdown/dropdown.component.ts
+++ b/libs/dropdown/src/lib/dropdown/dropdown.component.ts
@@ -2,12 +2,17 @@ import {
booleanAttribute,
ChangeDetectionStrategy,
Component,
+ ElementRef,
EventEmitter,
- HostBinding,
Input,
Output,
+ QueryList,
+ ViewChild,
+ ViewChildren,
ViewEncapsulation,
} from '@angular/core';
+import { NzDropDownDirective } from 'ng-zorro-antd/dropdown';
+import { NzMenuDirective, NzSubMenuComponent } from 'ng-zorro-antd/menu';
export interface DropdownItem {
action: string;
@@ -25,15 +30,111 @@ export type Trigger = 'click' | 'hover';
styleUrls: ['./dropdown.component.less'],
changeDetection: ChangeDetectionStrategy.OnPush,
encapsulation: ViewEncapsulation.None,
+ host: {
+ '[class.spy-dropdown--open]': 'visible',
+ },
})
export class DropdownComponent {
@Input() items: DropdownItem[] = [];
@Input() placement: Placement = 'bottomRight';
@Input() trigger: Trigger = 'hover';
- @HostBinding('class.spy-dropdown--open')
- @Input({ transform: booleanAttribute })
- visible = false;
+ @Input() ariaLabel: string = null;
+ @Input({ transform: booleanAttribute }) visible = true;
@Input({ transform: booleanAttribute }) disabled = false;
@Output() visibleChange = new EventEmitter();
@Output() actionTriggered = new EventEmitter();
+
+ @ViewChild(NzDropDownDirective, { read: ElementRef }) menuTrigger: ElementRef;
+ @ViewChild(NzMenuDirective, { read: ElementRef }) menu: ElementRef;
+
+ @ViewChildren(NzSubMenuComponent) submenuTriggers: QueryList;
+ @ViewChildren('submenu') submenuElements: QueryList>;
+
+ onKeyDown(event: KeyboardEvent): void {
+ if (event.key === 'Tab' && document.activeElement === this.menuTrigger.nativeElement) {
+ this.visible = false;
+
+ return;
+ }
+
+ const events = ['Enter', ' ', 'ArrowDown'];
+
+ if (!events.includes(event.key) || !this.items.length) {
+ return;
+ }
+
+ event.preventDefault();
+
+ if (event.key === 'ArrowDown') {
+ this.getMenuItems(this.menu.nativeElement)[0].focus();
+
+ return;
+ }
+
+ this.visible = !this.visible;
+ }
+
+ onMenuKeyDown(event: KeyboardEvent, submenuIndex?: number): void {
+ const events = ['ArrowUp', 'Tab', 'ArrowDown'];
+ const items = this.getMenuItems(event.currentTarget as HTMLElement);
+ const { length } = items;
+
+ if (!events.includes(event.key) || !length) {
+ return;
+ }
+
+ event.preventDefault();
+
+ if (event.key === 'Tab' && event.shiftKey) {
+ const focusElement =
+ submenuIndex === undefined
+ ? this.menuTrigger.nativeElement
+ : this.getMenuItems(
+ submenuIndex === 0
+ ? this.menu.nativeElement
+ : this.submenuElements.get(submenuIndex - 1).nativeElement,
+ )[0];
+
+ focusElement.focus();
+ this.visible = submenuIndex !== undefined;
+ this.submenuTriggers.get(submenuIndex)?.setOpenStateWithoutDebounce(false);
+
+ return;
+ }
+
+ const currentIndex = items.findIndex((item) => item === document.activeElement);
+ const index = event.key === 'ArrowUp' ? (currentIndex - 1 + length) % length : (currentIndex + 1) % length;
+
+ items[index].focus();
+ }
+
+ onItemKeyDown(event: KeyboardEvent, submenuIndex?: number): void {
+ if (event.key !== ' ' && event.key !== 'Enter') {
+ return;
+ }
+
+ const target = event.target as HTMLElement;
+
+ target.click();
+
+ if (submenuIndex !== undefined) {
+ this.submenuTriggers.get(submenuIndex).setOpenStateWithoutDebounce(true);
+
+ setTimeout(() => {
+ this.getMenuItems(this.submenuElements.get(submenuIndex).nativeElement)[0].focus();
+ });
+
+ return;
+ }
+
+ this.submenuTriggers.forEach((trigger) => trigger.setOpenStateWithoutDebounce(false));
+ }
+
+ onItemClick(action: string): void {
+ this.actionTriggered.emit(action);
+ }
+
+ private getMenuItems(menu: HTMLElement): HTMLElement[] {
+ return Array.from(menu.querySelectorAll('.spy-dropdown-item__item'));
+ }
}
diff --git a/libs/styles/src/lib/themes/default/variables/common.less b/libs/styles/src/lib/themes/default/variables/common.less
index bde6a4b38..3d8efad81 100644
--- a/libs/styles/src/lib/themes/default/variables/common.less
+++ b/libs/styles/src/lib/themes/default/variables/common.less
@@ -17,6 +17,11 @@
@box-shadow-2: var(--spy-box-shadow-2, 1px 3px 18px @ink-effect);
@box-shadow-3: var(--spy-box-shadow-3, -2px 2px 20px @ink-effect);
+@outline-color: var(--spy-outline-color, @green);
+@outline-width: var(--spy-outline-width, 1px);
+@outline: var(--spy-outline, @outline-width solid @outline-color);
+@outline-offset: var(--spy-outline-offset, 2px);
+
// Breakpoints
/* stylelint-disable property-no-unknown */