diff --git a/projects/angular-ui/src/lib/dropdown-menu/dropdown-menu.component.scss b/projects/angular-ui/src/lib/dropdown-menu/dropdown-menu.component.scss
index a048c84f..357d9c84 100644
--- a/projects/angular-ui/src/lib/dropdown-menu/dropdown-menu.component.scss
+++ b/projects/angular-ui/src/lib/dropdown-menu/dropdown-menu.component.scss
@@ -17,8 +17,14 @@ $overlay-max-width: 32rem !default;
background: $white;
box-shadow: 0 0.5rem 2rem 0 rgba(0, 0, 0, 0.1);
border-radius: 0.25rem;
+ margin-top: 0.25rem;
+ margin-bottom: 0.25rem;
padding-top: 0.5rem;
padding-bottom: 0.5rem;
+
+ &:focus-visible {
+ outline: none;
+ }
.bao-overlay-transparent-backdrop {
background-color: $transparent;
display: none;
diff --git a/projects/angular-ui/src/lib/dropdown-menu/dropdown-menu.component.ts b/projects/angular-ui/src/lib/dropdown-menu/dropdown-menu.component.ts
index 8a1a247f..6dddc036 100644
--- a/projects/angular-ui/src/lib/dropdown-menu/dropdown-menu.component.ts
+++ b/projects/angular-ui/src/lib/dropdown-menu/dropdown-menu.component.ts
@@ -269,18 +269,22 @@ export class BaoDropdownMenuComponent
@HostListener('window:keyup.arrowup')
upKeyEvent() {
if (this.isOpen) {
- const index = isNaN(this._activeItemIndex) ? 0 : this._activeItemIndex;
+ const index = isNaN(this._activeItemIndex) ? -1 : this._activeItemIndex;
const nextIndex = this.getNextActivableItemIndex(index, false);
- this.focusNextItem(nextIndex);
+ if (nextIndex > -1) {
+ this.focusNextItem(nextIndex);
+ }
}
}
@HostListener('window:keyup.arrowdown')
downKeyEvent() {
if (this.isOpen) {
- const index = isNaN(this._activeItemIndex) ? 0 : this._activeItemIndex;
+ const index = isNaN(this._activeItemIndex) ? -1 : this._activeItemIndex;
const nextIndex = this.getNextActivableItemIndex(index, true);
- this.focusNextItem(nextIndex);
+ if (nextIndex > -1) {
+ this.focusNextItem(nextIndex);
+ }
}
}
@@ -294,6 +298,19 @@ export class BaoDropdownMenuComponent
}
}
+ @HostListener('window:keyup.tab')
+ tabUpKeyEvent() {
+ if (this.isOpen) {
+ this._activeItemIndex = this._listItems.reduce((acc, element, index) => {
+ if (element.nativeElement === document.activeElement) {
+ acc = index;
+ }
+
+ return acc;
+ }, -1);
+ }
+ }
+
/** Prevents focus to be lost when SHIFT + TAB has reached beginning of menu */
@HostListener('window:keydown.shift.tab')
shiftTabKeyEvent() {
@@ -316,11 +333,6 @@ export class BaoDropdownMenuComponent
);
}
- public focusFirstItem(): void {
- this._activeItemIndex = 0;
- this._listItems.first.nativeElement.focus();
- }
-
public open(): void {
this.isOpen = true;
}
@@ -329,6 +341,11 @@ export class BaoDropdownMenuComponent
this.isOpen = false;
}
+ public focus(): void {
+ this._activeItemIndex = -1;
+ this._menuContent.nativeElement.focus();
+ }
+
/** Move the aria-current attribute to new active page */
public setNavigationAttribute(activePageElement: HTMLElement): void {
const previousActivePage = this._listItems.find(
@@ -364,40 +381,62 @@ export class BaoDropdownMenuComponent
*/
private getNextActivableItemIndex(
currentIndex: number,
- isDown: boolean,
- isBackward = false
+ isDown: boolean
): number {
- if (!this._listItems.get(currentIndex).disabled) {
- if (!this.canMove(currentIndex, isDown)) {
- return currentIndex;
+ const init: number[] = [];
+ // Get all the activable indexes
+ const activableIndexes = this._listItems.reduce((acc, element, index) => {
+ if (!element.disabled) {
+ acc = [...acc, index];
}
- } else {
- if (!this.canMove(currentIndex, isDown)) {
- const previousIndex = isDown ? currentIndex - 1 : currentIndex + 1;
- return this.getNextActivableItemIndex(previousIndex, isDown, true);
+
+ return acc;
+ }, init);
+
+ if (activableIndexes.length) {
+ if (isDown) {
+ // Select the first enabled element
+ if (currentIndex === -1) {
+ return activableIndexes[0];
+ }
+
+ // Select the only enabled element
+ if (activableIndexes.length === 1) {
+ return activableIndexes[0];
+ }
+
+ // Stay on the last enabled element
+ if (currentIndex === activableIndexes[activableIndexes.length - 1]) {
+ return currentIndex;
+ }
+
+ // Select the next enabled element
+ return activableIndexes.find(index => index > currentIndex);
}
- }
- const nextIndex = isDown ? currentIndex + 1 : currentIndex - 1;
- if (this._listItems.get(nextIndex).disabled) {
- if (isBackward) {
- return currentIndex;
+
+ const isUp = !isDown;
+ if (isUp) {
+ // Do nothing whenever nothing is selected
+ if (currentIndex === -1) {
+ return currentIndex;
+ }
+
+ // Select the only enabled element
+ if (activableIndexes.length === 1) {
+ return activableIndexes[0];
+ }
+
+ // Stay on the first enabled element
+ if (currentIndex === activableIndexes[0]) {
+ return currentIndex;
+ }
+
+ // Select the above enabled element
+ return activableIndexes.reverse().find(index => index < currentIndex);
}
- return this.getNextActivableItemIndex(nextIndex, isDown);
- }
- return nextIndex;
- }
- /**
- * Finds if focus has reached end or beginning of list
- * @param currentIndex List item index which currently has focus
- * @param isDown Whether the navigation is going in the down direction or not
- * @returns Can focus move to next item or not
- */
- private canMove(currentIndex: number, isDown: boolean): boolean {
- return !(
- (currentIndex == 0 && !isDown) ||
- (currentIndex == this._listItems.length - 1 && isDown)
- );
+ return -1;
+ }
}
}
/**
@@ -431,16 +470,6 @@ export class BaoDropdownMenuTrigger implements AfterViewInit, OnDestroy {
}
}
- /** Enter key event triggers click event which opens menu,
- * then focus is put on first item in the menu */
- @HostListener('window:keyup.enter', ['$event'])
- enterKeyEvent(event: KeyboardEvent) {
- if (this._isMenuOpen && document.activeElement === this.nativeElement) {
- event.stopImmediatePropagation();
- this.menu.focusFirstItem();
- }
- }
-
@HostListener('click')
onClick() {
this.toggleMenu();
@@ -485,6 +514,7 @@ export class BaoDropdownMenuTrigger implements AfterViewInit, OnDestroy {
overlayRef.attach(this.menu.menuPortal);
this._isMenuOpen = true;
this.menu.open();
+ this.menu.focus();
}
private createOverlay(): OverlayRef {
diff --git a/projects/angular-ui/src/lib/list/list-item.component.html b/projects/angular-ui/src/lib/list/list-item.component.html
index 7144844d..bf55ad25 100644
--- a/projects/angular-ui/src/lib/list/list-item.component.html
+++ b/projects/angular-ui/src/lib/list/list-item.component.html
@@ -11,3 +11,9 @@
>
+
+
+
+
diff --git a/projects/angular-ui/src/lib/list/list.component.scss b/projects/angular-ui/src/lib/list/list.component.scss
index e4c7799b..43618207 100644
--- a/projects/angular-ui/src/lib/list/list.component.scss
+++ b/projects/angular-ui/src/lib/list/list.component.scss
@@ -123,5 +123,10 @@ $size-xx-small: 1rem;
}
}
}
+
+ > .bao-list-item-actions .bao-button {
+ margin-left: 2rem;
+ margin-top: -0.5rem;
+ }
}
}
diff --git a/projects/angular-ui/src/lib/list/list.component.spec.ts b/projects/angular-ui/src/lib/list/list.component.spec.ts
new file mode 100644
index 00000000..e7deaffd
--- /dev/null
+++ b/projects/angular-ui/src/lib/list/list.component.spec.ts
@@ -0,0 +1,103 @@
+/*
+ * Copyright (c) 2024 Ville de Montreal. All rights reserved.
+ * Licensed under the MIT license.
+ * See LICENSE file in the project root for full license information.
+ */
+import { OverlayModule } from '@angular/cdk/overlay';
+import { DebugElement } from '@angular/core';
+import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
+import { By } from '@angular/platform-browser';
+import { BaoListModule } from '.';
+import { BaoButtonModule } from '../button';
+import { BaoDropdownMenuModule } from '../dropdown-menu';
+import { BaoIconModule } from '../icon';
+import { TestListDropdownMenuComponent } from './tests/list.hostcomponent.spec';
+
+describe('BaoList', () => {
+ describe('TestListDropdownMenuComponent', () => {
+ let fixture: ComponentFixture;
+ let TestListDropdownMenuComponentElement: DebugElement;
+
+ beforeEach(waitForAsync(() => {
+ TestBed.configureTestingModule({
+ declarations: [TestListDropdownMenuComponent],
+ imports: [
+ OverlayModule,
+ BaoListModule,
+ BaoDropdownMenuModule,
+ BaoButtonModule,
+ BaoIconModule
+ ]
+ });
+ return TestBed.compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(TestListDropdownMenuComponent);
+ TestListDropdownMenuComponentElement = fixture.debugElement;
+ });
+
+ it('should have a list', () => {
+ const element = TestListDropdownMenuComponentElement.queryAll(
+ By.css('.bao-list')
+ );
+
+ expect(element).toBeTruthy();
+ });
+
+ it('should have an icon', () => {
+ const element = TestListDropdownMenuComponentElement.queryAll(
+ By.css('.bao-list-item-header .bao-icon')
+ );
+
+ expect(element).toBeTruthy();
+ });
+
+ it('should have a title', () => {
+ const element = TestListDropdownMenuComponentElement.queryAll(
+ By.css(
+ '.bao-list-item-content .bao-list-item-text .bao-list-item-title'
+ )
+ );
+
+ expect(element).toBeTruthy();
+ });
+
+ it('should have a description', () => {
+ const element = TestListDropdownMenuComponentElement.queryAll(
+ By.css(
+ '.bao-list-item-content .bao-list-item-text .bao-list-item-description li'
+ )
+ );
+
+ expect(element).toBeTruthy();
+ });
+
+ it('should have a tag', () => {
+ const element = TestListDropdownMenuComponentElement.queryAll(
+ By.css('.bao-list-item-content .bao-list-item-tag .bao-tag')
+ );
+
+ expect(element).toBeTruthy();
+ });
+
+ it('should have a dropdown menu within the list item', () => {
+ const element = TestListDropdownMenuComponentElement.queryAll(
+ By.css('.bao-list-item .bao-dropdown-menu-container')
+ );
+
+ expect(element).toBeTruthy();
+ });
+
+ it('should have as many dropdown menus as the list items', () => {
+ const listItemElement = TestListDropdownMenuComponentElement.queryAll(
+ By.css('.bao-list-item')
+ );
+ const dropDownMenuElement = TestListDropdownMenuComponentElement.queryAll(
+ By.css('.bao-dropdown-menu-container')
+ );
+
+ expect(listItemElement.length).toEqual(dropDownMenuElement.length);
+ });
+ });
+});
diff --git a/projects/angular-ui/src/lib/list/tests/list.hostcomponent.spec.ts b/projects/angular-ui/src/lib/list/tests/list.hostcomponent.spec.ts
new file mode 100644
index 00000000..1cdeabc1
--- /dev/null
+++ b/projects/angular-ui/src/lib/list/tests/list.hostcomponent.spec.ts
@@ -0,0 +1,99 @@
+/*
+ * Copyright (c) 2024 Ville de Montreal. All rights reserved.
+ * Licensed under the MIT license.
+ * See LICENSE file in the project root for full license information.
+ */
+
+import { Component } from '@angular/core';
+
+@Component({
+ template: `
+
+
+
+ Title
+
+ - Description 1
+ - Description 2
+
+
+
+
+
+
+
+
+ Title
+
+ - Description 1
+ - Description 2
+
+
+
+
+
+
+
+ `
+})
+export class TestListDropdownMenuComponent {}
diff --git a/projects/storybook-angular/src/stories/List/List.stories.ts b/projects/storybook-angular/src/stories/List/List.stories.ts
index bad27ee1..e9563329 100644
--- a/projects/storybook-angular/src/stories/List/List.stories.ts
+++ b/projects/storybook-angular/src/stories/List/List.stories.ts
@@ -3,12 +3,20 @@
* Licensed under the MIT license.
* See LICENSE file in the project root for full license information.
*/
+import { OverlayModule } from '@angular/cdk/overlay';
+import { PortalModule } from '@angular/cdk/portal';
+import { CommonModule } from '@angular/common';
import { moduleMetadata } from '@storybook/angular';
import { Meta, Story } from '@storybook/angular';
import {
+ BaoAvatarModule,
+ BaoButtonModule,
+ BaoCheckboxModule,
+ BaoDropdownMenuModule,
BaoIconModule,
BaoListItem,
BaoListModule,
+ BaoRadioModule,
BaoTagModule
} from 'angular-ui';
@@ -23,7 +31,19 @@ export default {
decorators: [
moduleMetadata({
// declarations: [BaoListItem],
- imports: [BaoListModule, BaoIconModule, BaoTagModule]
+ imports: [
+ BaoListModule,
+ BaoIconModule,
+ BaoTagModule,
+ CommonModule,
+ BaoDropdownMenuModule,
+ BaoButtonModule,
+ BaoAvatarModule,
+ BaoCheckboxModule,
+ BaoRadioModule,
+ OverlayModule,
+ PortalModule
+ ]
})
],
component: BaoListItem,
@@ -128,7 +148,7 @@ export const SimpleListWithTagAndIcon: Story = args => ({
Title
- Label
+ Non vérifié
`
@@ -163,7 +183,7 @@ export const SimpleListWithDescription: Story = args => ({
Title
- Label
+ Non vérifié
Description 1
Description 2
@@ -206,7 +226,7 @@ export const SimpleListWithInlineDescription: Story = args => ({
Description 1
Description 2
- Label
+ Non vérifié
`
@@ -215,3 +235,166 @@ SimpleListWithInlineDescription.storyName = 'Simple list - Inline Description';
SimpleListWithInlineDescription.args = {
...Primary.args
};
+
+export const SimpleListWithDropdownMenu: Story = args => ({
+ props: args,
+ template: `
+
+
+
+ Title
+
+ - Description 1
+ - Description 2
+
+ Label
+
+
+
+
+
+
+
+ Title
+
+ - Description 1
+ - Description 2
+
+
+
+ Non vérifié
+
+
+
+
+
+
+
+
+ Title
+
+ - Description 1
+ - Description 2
+
+
+
+
+
+
+
+ `
+});
+SimpleListWithDropdownMenu.storyName = 'Simple list - Dropdown menu';
+SimpleListWithDropdownMenu.args = {
+ ...Primary.args
+};
+
+export const SimpleListWithActionButton: Story = args => ({
+ props: args,
+ template: `
+
+
+
+ Title
+
+ - Description 1
+ - Description 2
+
+ Label
+
+
+
+
+ Title
+
+ - Description 1
+ - Description 2
+
+
+
+ Non vérifié
+
+
+
+
+
+ Title
+
+ - Description 1
+ - Description 2
+
+
+
+
+ `
+});
+SimpleListWithActionButton.storyName = 'Simple list - Action button';
+SimpleListWithActionButton.args = {
+ ...Primary.args
+};