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 nav keyboard handling #4634

Merged
merged 3 commits into from
Nov 22, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
12 changes: 4 additions & 8 deletions demo/src/app/components/nav/overview/nav-overview.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -150,8 +150,8 @@ <h4>Accessibility</h4>
<ngbd-api-docs-badge [since]="{ version: '6.1.0' }"></ngbd-api-docs-badge>

<p>
Keyboard support is not activated by default and can be turned on via the <code>[keyboard]</code> input. Depending
on the input value, we'll either focus or switch to a specific nav.
Keyboard support is activated by default and can be changed via the <code>[keyboard]</code> input. Depending on the
input value, we'll either focus or switch to a specific nav.
</p>

<p>Please refer to the <a routerLink="../api" fragment="NgbNav">NgbNav API</a></p>
Expand All @@ -160,15 +160,11 @@ <h4>Accessibility</h4>
<tbody>
<tr>
<td><code>Arrow(Left|Up)</code></td>
<td>
Moves focus or switches to the previous non-disabled nav. Loops through navs and depends on nav orientation.
</td>
<td>Moves focus or switches to the previous non-disabled nav</td>
</tr>
<tr>
<td><code>Arrow(Right|Down)</code></td>
<td>
Moves focus or switches to the next non-disabled nav. Loops through navs and depends on nav orientation.
</td>
<td>Moves focus or switches to the next non-disabled nav</td>
</tr>
<tr>
<td><code>Home</code></td>
Expand Down
5 changes: 5 additions & 0 deletions e2e-app/src/app/app.routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import { OffcanvasNestingComponent } from './offcanvas/nesting/offcanvas-nesting
import { OffcanvasStackConfirmationComponent } from './offcanvas/stack-confirmation/offcanvas-stack-confirmation.component';
import { DropdownShadowComponent } from './dropdown/shadow/dropdown-shadow.component';
import { TooltipTriggersComponent } from './tooltip/triggers/tooltip-triggers.component';
import { NavFocusComponent } from './nav/focus/nav-focus.component';

export const ROUTES: Routes = [
{
Expand All @@ -49,6 +50,10 @@ export const ROUTES: Routes = [
{ path: 'stack-confirmation', component: ModalStackConfirmationComponent },
],
},
{
path: 'nav',
children: [{ path: 'focus', component: NavFocusComponent }],
},
{
path: 'offcanvas',
children: [
Expand Down
11 changes: 3 additions & 8 deletions e2e-app/src/app/datepicker/focus/datepicker-focus.e2e-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,9 +118,7 @@ test.describe('Datepicker', () => {
).toBeFocused();
});

test(`should trap focus inside opened popup (Tab)`, async ({ browserName }) => {
test.skip(browserName === 'webkit');

test(`should trap focus inside opened popup (Tab)`, async () => {
await openDatepicker();

// today -> prev. month -> month -> year -> next month -> today -> ...
Expand All @@ -142,9 +140,7 @@ test.describe('Datepicker', () => {
await expect(getPage().locator(SELECTOR_DAY(new Date())), `Today's date should be focused`).toBeFocused();
});

test(`should trap focus inside opened popup with (Shift+Tab)`, async ({ browserName }) => {
test.skip(browserName === 'webkit');

test(`should trap focus inside opened popup with (Shift+Tab)`, async () => {
await openDatepicker();

// today -> next month -> year -> month -> prev. month -> today -> ...
Expand Down Expand Up @@ -203,8 +199,7 @@ test.describe('Datepicker', () => {
await expect(getPage().locator(SELECTOR_DAY(new Date())), `Today's date should stay focused`).toBeFocused();
});

test(`should allow focusing datepicker input`, async ({ browserName }) => {
test.skip(browserName === 'webkit');
test(`should allow focusing datepicker input`, async () => {
await openDatepicker();

// focus input
Expand Down
11 changes: 4 additions & 7 deletions e2e-app/src/app/dropdown/focus/dropdown-focus.e2e-spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { expect } from '@playwright/test';
import { isDropdownOpened, openDropdown } from '../dropdown.po';
import { test, getPage, setPage } from '../../../../baseTest';
import { getPage, setPage, test } from '../../../../baseTest';
import { sendKey } from '../../tools.po';

const SELECTOR_DROPDOWN = '[ngbDropdown]';
Expand Down Expand Up @@ -144,15 +144,12 @@ test.describe(`Dropdown focus`, () => {
await expect(getPage().locator(SELECTOR_DROPDOWN_TOGGLE), `Toggling element should be focused`).toBeFocused();
});

test(`should focus dropdown first item with Tab when dropdown is opened (toggle was focused)`, async ({
browserName,
}) => {
test.skip(browserName === 'webkit');
test(`should focus dropdown first item with Tab when dropdown is opened (toggle was focused)`, async () => {
await openDropdown('Dropdown should be opened', SELECTOR_DROPDOWN, container === 'body');
await expect(getPage().locator(SELECTOR_DROPDOWN_TOGGLE), `Toggling element should be focused`).toBeFocused();

// Tab -> first
await sendKey('Tab');
await sendKey('Tab', false);
await expect(
getPage().locator(SELECTOR_DROPDOWN_ITEM(1)),
`first dropdown item should be focused`,
Expand All @@ -178,7 +175,7 @@ test.describe(`Dropdown focus`, () => {
).toBeFocused();

// Tab -> another element
await sendKey('Tab');
await sendKey('Tab', false);
expect(await isDropdownOpened(), `Dropdown should be closed`).toBeFalsy();
});
});
Expand Down
43 changes: 43 additions & 0 deletions e2e-app/src/app/nav/focus/nav-focus.component.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<h3>Nav focus tests</h3>

<div ngbDropdown class="d-inline-block me-3">
<button class="btn btn-outline-secondary" id="keyboard-dropdown" ngbDropdownToggle
>Keyboard = '{{ keyboard }}'</button
>
<div ngbDropdownMenu aria-labelledby="dropdown">
<button ngbDropdownItem id="keyboard-true" (click)="keyboard = true">True</button>
<button ngbDropdownItem id="keyboard-false" (click)="keyboard = false">False</button>
<button ngbDropdownItem id="keyboard-arrows" (click)="keyboard = 'changeWithArrows'">Change with Arrows</button>
</div>
</div>

<hr />

<input id="before" type="text" placeholder="before" />

<ul ngbNav #nav="ngbNav" [(activeId)]="active" class="nav-tabs" [keyboard]="keyboard">
<li [ngbNavItem]="1" domId="nav-1">
<button ngbNavLink>One</button>
<ng-template ngbNavContent>One</ng-template>
</li>
<li [ngbNavItem]="2" domId="nav-2">
<button ngbNavLink>Two</button>
<ng-template ngbNavContent>Two</ng-template>
</li>
<li [ngbNavItem]="3" domId="nav-3">
<button ngbNavLink>Three</button>
<ng-template ngbNavContent>Three</ng-template>
</li>
<li [ngbNavItem]="4" domId="nav-4" [disabled]="true">
<button ngbNavLink>Four</button>
<ng-template ngbNavContent>Four</ng-template>
</li>
</ul>

<div [ngbNavOutlet]="nav" class="mt-2"></div>

<input id="after" type="text" placeholder="after" />

<hr />

<pre>Active: {{ active }}</pre>
13 changes: 13 additions & 0 deletions e2e-app/src/app/nav/focus/nav-focus.component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Component } from '@angular/core';
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';

@Component({
selector: 'nav-focus-component',
standalone: true,
imports: [NgbModule],
templateUrl: './nav-focus.component.html',
})
export class NavFocusComponent {
active = 2;
keyboard: boolean | 'changeWithArrows' = true;
}
216 changes: 216 additions & 0 deletions e2e-app/src/app/nav/focus/nav-focus.e2e-spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
import { expect } from '@playwright/test';
import { getPage, setPage, test } from '../../../../baseTest';
import { sendKey } from '../../tools.po';

const expectFocused = async (selector: string) => {
await expect(getPage().locator(`${selector}`), `${selector} should be focused`).toBeFocused();
};

const selectKeyboard = async (type: string) => {
await getPage().click('#keyboard-dropdown');
await getPage().click(`#keyboard-${type}`);
await getPage().focus('#before');
};

const expectFocusedNav = async (index: number) => {
await expectFocused(`#nav-${index}`);
};

const expectSelectedNav = async (index: number) => {
expect(await getPage().getAttribute(`#nav-${index}`, 'class'), `nav-${index} should be selected`).toContain('active');
await expect(getPage().locator(`#nav-${index}-panel`), `nav-${index}-panel should be visible`).toBeVisible();
};

test.use({ testURL: 'nav/focus', testSelector: 'h3:text("Nav focus")' });
test.beforeEach(async ({ page }) => setPage(page));

test.describe(`Nav focus`, () => {
test(`should work with keyboard = true`, async () => {
await selectKeyboard('true');
await expectSelectedNav(2);
await expectFocused('#before');

// Tab to select nav 2
await sendKey('Tab');
await expectFocusedNav(2);
await expectSelectedNav(2);

// Arrow right to focus nav 3
await sendKey('ArrowRight');
await expectFocusedNav(3);
await expectSelectedNav(2);

// Arrow down to focus nav 1
await sendKey('ArrowDown');
await expectFocusedNav(1);
await expectSelectedNav(2);

// Arrow left to focus nav 3
await sendKey('ArrowLeft');
await expectFocusedNav(3);
await expectSelectedNav(2);

// Arrow up to focus nav 2
await sendKey('ArrowUp');
await expectFocusedNav(2);
await expectSelectedNav(2);

// Home to focus nav 1
await sendKey('Home');
await expectFocusedNav(1);
await expectSelectedNav(2);

// End to focus nav 3
await sendKey('End');
await expectFocusedNav(3);

// Enter to select nav 3
await sendKey('Enter');
await expectFocusedNav(3);
await expectSelectedNav(3);

// Arrow left to focus nav 2
await sendKey('ArrowLeft');
await expectFocusedNav(2);
await expectSelectedNav(3);

// Space to select nav 2
await sendKey(' ');
await expectFocusedNav(2);
await expectSelectedNav(2);

// Arrow left to focus nav 1
await sendKey('ArrowLeft');
await expectFocusedNav(1);
await expectSelectedNav(2);

// Tab to exit to 'after'
await sendKey('Tab');
await expectFocused('#after');

// Shift-Tab to return to nav 2
await sendKey('Shift+Tab');
await expectFocusedNav(2);
await expectSelectedNav(2);
});

test(`should work with keyboard = false`, async () => {
await selectKeyboard('false');
await expectFocused('#before');
await expectSelectedNav(2);

// Tab to focus nav 1
await sendKey('Tab');
await expectFocusedNav(1);
await expectSelectedNav(2);

// Arrow right does nothing
await sendKey('ArrowRight');
await expectFocusedNav(1);
await expectSelectedNav(2);

// Arrow left does nothing
await sendKey('ArrowLeft');
await expectFocusedNav(1);
await expectSelectedNav(2);

// Arrow down does nothing
await sendKey('ArrowDown');
await expectFocusedNav(1);
await expectSelectedNav(2);

// Arrow up does nothing
await sendKey('ArrowUp');
await expectFocusedNav(1);
await expectSelectedNav(2);

// Home does nothing
await sendKey('Home');
await expectFocusedNav(1);
await expectSelectedNav(2);

// End does nothing
await sendKey('End');
await expectFocusedNav(1);
await expectSelectedNav(2);

// Enter selects nav 1
await sendKey('Enter');
await expectFocusedNav(1);
await expectSelectedNav(1);

// Tab moves to focus the nav 2
await sendKey('Tab');
await expectFocusedNav(2);
await expectSelectedNav(1);

// Space selects nav 2
await sendKey(' ');
await expectFocusedNav(2);
await expectSelectedNav(2);

// Tab moves to focus the nav 3
await sendKey('Tab');
await expectFocusedNav(3);
await expectSelectedNav(2);

// Tab moves to focus the 'after'
await sendKey('Tab');
await expectFocused('#after');

// Shift tab moves to focus back to nav 3
await sendKey('Shift+Tab');
await expectFocusedNav(3);
await expectSelectedNav(2);
});

test(`should work with keyboard = 'changeWithArrows'`, async () => {
await selectKeyboard('arrows');
await expectFocused('#before');
await expectSelectedNav(2);

// Tab to focus nav 2
await sendKey('Tab');
await expectFocusedNav(2);
await expectSelectedNav(2);

// Arrow right to select nav 3
await sendKey('ArrowRight');
await expectFocusedNav(3);
await expectSelectedNav(3);

// Arrow down to select nav 1
await sendKey('ArrowDown');
await expectFocusedNav(1);
await expectSelectedNav(1);

// Arrow left to select nav 3
await sendKey('ArrowLeft');
await expectFocusedNav(3);
await expectSelectedNav(3);

// Arrow up to select nav 2
await sendKey('ArrowUp');
await expectFocusedNav(2);
await expectSelectedNav(2);

// Home to select nav 1
await sendKey('Home');
await expectFocusedNav(1);
await expectSelectedNav(1);

// End to select nav 3
await sendKey('End');
await expectFocusedNav(3);
await expectSelectedNav(3);

// Tab to exit to 'after'
await sendKey('Tab');
await expectFocused('#after');

// Shift-Tab to return to nav 3
await sendKey('Shift+Tab');
await expectFocusedNav(3);
await expectSelectedNav(3);
});
});
1 change: 1 addition & 0 deletions e2e-app/src/app/nav/nav.po.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
const SELECTOR_DROPDOWN = '[ngbDropdown]';