2022-07-13 00:08:54 +03:00
|
|
|
/**
|
|
|
|
* @license
|
|
|
|
* Copyright 2022 Google LLC
|
|
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
|
|
*/
|
|
|
|
|
|
|
|
import {html} from 'lit';
|
2022-08-24 21:00:50 +03:00
|
|
|
import {customElement} from 'lit/decorators.js';
|
|
|
|
import {ifDefined} from 'lit/directives/if-defined.js';
|
2022-07-13 00:08:54 +03:00
|
|
|
|
2023-05-31 05:23:42 +03:00
|
|
|
import {Environment} from '../../testing/environment.js';
|
2022-10-18 19:28:12 +03:00
|
|
|
import {NavigationTabHarness} from '../navigationtab/harness.js';
|
|
|
|
import {MdNavigationTab} from '../navigationtab/navigation-tab.js';
|
2022-10-04 19:32:06 +03:00
|
|
|
|
|
|
|
import {NavigationBarHarness} from './harness.js';
|
2022-08-24 21:00:50 +03:00
|
|
|
import {MdNavigationBar} from './navigation-bar.js';
|
2022-07-13 00:08:54 +03:00
|
|
|
|
|
|
|
@customElement('md-test-navigation-bar')
|
|
|
|
class TestMdNavigationBar extends MdNavigationBar {
|
|
|
|
}
|
2022-08-12 20:50:55 +03:00
|
|
|
@customElement('md-test-navigation-bar-tab')
|
2022-07-13 00:08:54 +03:00
|
|
|
class TestMdNavigationTab extends MdNavigationTab {
|
|
|
|
}
|
|
|
|
|
|
|
|
declare global {
|
|
|
|
interface HTMLElementTagNameMap {
|
|
|
|
'md-test-navigation-bar': TestMdNavigationBar;
|
2022-08-12 20:50:55 +03:00
|
|
|
'md-test-navigation-bar-tab': TestMdNavigationTab;
|
2022-07-13 00:08:54 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-08-12 20:50:55 +03:00
|
|
|
interface NavigationBarProps {
|
|
|
|
activeIndex: number;
|
|
|
|
hideInactiveLabels: boolean;
|
|
|
|
ariaLabel?: string;
|
|
|
|
}
|
|
|
|
|
2022-10-04 19:32:06 +03:00
|
|
|
const defaultNavBar = html`
|
|
|
|
<md-test-navigation-bar>
|
|
|
|
<md-test-navigation-bar-tab label="One"></md-test-navigation-bar-tab>
|
|
|
|
</md-test-navigation-bar>
|
|
|
|
`;
|
|
|
|
|
2022-08-12 20:50:55 +03:00
|
|
|
const navBarWithNavTabsElement = (propsInit: Partial<NavigationBarProps>) => {
|
2022-07-13 00:08:54 +03:00
|
|
|
return html`
|
|
|
|
<md-test-navigation-bar
|
|
|
|
.activeIndex="${propsInit.activeIndex ?? 0}"
|
|
|
|
.hideInactiveLabels="${propsInit.hideInactiveLabels === true}"
|
|
|
|
aria-label="${ifDefined(propsInit.ariaLabel)}">
|
2022-08-12 20:50:55 +03:00
|
|
|
<md-test-navigation-bar-tab label="One"></md-test-navigation-bar-tab>
|
|
|
|
<md-test-navigation-bar-tab label="Two"></md-test-navigation-bar-tab>
|
2022-07-13 00:08:54 +03:00
|
|
|
</md-test-navigation-bar>
|
|
|
|
`;
|
|
|
|
};
|
|
|
|
|
|
|
|
// The following is a Navbar with the tabs being out of sync with the bar.
|
|
|
|
const navBarWithIncorrectTabsElement = html`
|
|
|
|
<md-test-navigation-bar activeIndex="0">
|
2022-08-12 20:50:55 +03:00
|
|
|
<md-test-navigation-bar-tab label="One" hideInactiveLabel></md-test-navigation-bar-tab>
|
|
|
|
<md-test-navigation-bar-tab label="One" active></md-test-navigation-bar-tab>
|
2022-07-13 00:08:54 +03:00
|
|
|
</md-test-navigation-bar>`;
|
|
|
|
|
|
|
|
describe('md-navigation-bar', () => {
|
2022-10-04 19:32:06 +03:00
|
|
|
const env = new Environment();
|
2022-07-13 00:08:54 +03:00
|
|
|
|
2022-10-04 19:32:06 +03:00
|
|
|
async function setupTest(template = defaultNavBar) {
|
|
|
|
const element =
|
|
|
|
env.render(template).querySelector('md-test-navigation-bar');
|
|
|
|
if (!element) {
|
|
|
|
throw new Error('Could not query rendered <md-test-navigation-bar>.');
|
|
|
|
}
|
2022-07-13 00:08:54 +03:00
|
|
|
|
2022-10-04 19:32:06 +03:00
|
|
|
await env.waitForStability();
|
2022-07-13 00:08:54 +03:00
|
|
|
|
2022-10-04 19:32:06 +03:00
|
|
|
return {
|
|
|
|
harness: new NavigationBarHarness(element),
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
describe('basic', () => {
|
|
|
|
it('initializes as a md-navigation-bar', async () => {
|
|
|
|
const {harness} = await setupTest();
|
2022-07-13 00:08:54 +03:00
|
|
|
const navBarBase =
|
2022-10-04 19:32:06 +03:00
|
|
|
harness.element.shadowRoot!.querySelector('.md3-navigation-bar')!;
|
|
|
|
expect(harness.element).toBeInstanceOf(MdNavigationBar);
|
|
|
|
expect(harness.element.activeIndex).toEqual(0);
|
|
|
|
expect(harness.element.hideInactiveLabels).toBeFalse();
|
2022-07-13 00:08:54 +03:00
|
|
|
expect(navBarBase.getAttribute('aria-label')).toEqual(null);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
describe('activeIndex', () => {
|
|
|
|
it('on change emits activated event', async () => {
|
2022-10-04 19:32:06 +03:00
|
|
|
const {harness} =
|
|
|
|
await setupTest(navBarWithNavTabsElement({activeIndex: 1}));
|
2022-07-20 21:00:07 +03:00
|
|
|
const activatedHandler = jasmine.createSpy();
|
2022-10-04 19:32:06 +03:00
|
|
|
harness.element.addEventListener(
|
|
|
|
'navigation-bar-activated', activatedHandler);
|
|
|
|
harness.element.activeIndex = 0;
|
|
|
|
await env.waitForStability();
|
2022-07-20 21:00:07 +03:00
|
|
|
expect(activatedHandler).toHaveBeenCalled();
|
2022-07-13 00:08:54 +03:00
|
|
|
});
|
|
|
|
|
|
|
|
it('activated event detail contains the tab and activeIndex', async () => {
|
2022-10-04 19:32:06 +03:00
|
|
|
const {harness} =
|
|
|
|
await setupTest(navBarWithNavTabsElement({activeIndex: 1}));
|
2022-07-13 00:08:54 +03:00
|
|
|
const navigationBarActivatedSpy =
|
|
|
|
jasmine.createSpy('navigationBarActivated');
|
2022-10-04 19:32:06 +03:00
|
|
|
harness.element.addEventListener(
|
2022-07-13 00:08:54 +03:00
|
|
|
'navigation-bar-activated', navigationBarActivatedSpy);
|
|
|
|
|
2022-10-04 19:32:06 +03:00
|
|
|
const tab = harness.element.tabs[0];
|
|
|
|
harness.element.activeIndex = 0;
|
2022-07-13 00:08:54 +03:00
|
|
|
|
2022-10-04 19:32:06 +03:00
|
|
|
await env.waitForStability();
|
2022-07-13 00:08:54 +03:00
|
|
|
expect(navigationBarActivatedSpy)
|
|
|
|
.toHaveBeenCalledWith(jasmine.any(CustomEvent));
|
|
|
|
expect(navigationBarActivatedSpy)
|
|
|
|
.toHaveBeenCalledWith(jasmine.objectContaining({
|
|
|
|
detail: jasmine.objectContaining({tab, activeIndex: 0}),
|
|
|
|
}));
|
|
|
|
});
|
|
|
|
|
|
|
|
it('#handleNavigationTabInteraction () updates on navigation tab click',
|
|
|
|
async () => {
|
2022-10-04 19:32:06 +03:00
|
|
|
const {harness} =
|
|
|
|
await setupTest(navBarWithNavTabsElement({activeIndex: 1}));
|
|
|
|
const tab1Harness = new NavigationTabHarness(harness.element.tabs[0]);
|
|
|
|
const tab2Harness = new NavigationTabHarness(harness.element.tabs[1]);
|
2022-07-13 00:08:54 +03:00
|
|
|
|
|
|
|
await tab1Harness.clickWithMouse();
|
2022-10-04 19:32:06 +03:00
|
|
|
expect(harness.element.activeIndex).toEqual(0);
|
2022-07-13 00:08:54 +03:00
|
|
|
await tab2Harness.clickWithMouse();
|
2022-10-04 19:32:06 +03:00
|
|
|
expect(harness.element.activeIndex).toEqual(1);
|
2022-07-13 00:08:54 +03:00
|
|
|
});
|
|
|
|
|
|
|
|
it('#onActiveIndexChange() sets tab at activeIndex to active', async () => {
|
2022-10-04 19:32:06 +03:00
|
|
|
const {harness} =
|
|
|
|
await setupTest(navBarWithNavTabsElement({activeIndex: 1}));
|
|
|
|
const tab = harness.element.tabs[0];
|
2022-07-13 00:08:54 +03:00
|
|
|
expect(tab.active).toBeFalse();
|
2022-10-04 19:32:06 +03:00
|
|
|
harness.element.activeIndex = 0;
|
|
|
|
harness.element.requestUpdate();
|
|
|
|
await env.waitForStability();
|
2022-07-13 00:08:54 +03:00
|
|
|
expect(tab.active).toBeTrue();
|
|
|
|
});
|
|
|
|
|
|
|
|
it('#onActiveIndexChange() sets previously active tab to inactive',
|
|
|
|
async () => {
|
2022-10-04 19:32:06 +03:00
|
|
|
const {harness} =
|
|
|
|
await setupTest(navBarWithNavTabsElement({activeIndex: 1}));
|
|
|
|
const tab = harness.element.tabs[1];
|
2022-07-13 00:08:54 +03:00
|
|
|
expect(tab.active).toBeTrue();
|
2022-10-04 19:32:06 +03:00
|
|
|
harness.element.activeIndex = 0;
|
|
|
|
harness.element.requestUpdate();
|
|
|
|
await env.waitForStability();
|
2022-07-13 00:08:54 +03:00
|
|
|
expect(tab.active).toBeFalse();
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
describe('hideInactiveLabels', () => {
|
|
|
|
it('#onHideInactiveLabelsChange() affects navigation tabs hideInactiveLabel state',
|
|
|
|
async () => {
|
2022-10-04 19:32:06 +03:00
|
|
|
const {harness} = await setupTest(
|
|
|
|
navBarWithNavTabsElement({hideInactiveLabels: true}));
|
|
|
|
const tab1 = harness.element.tabs[0];
|
|
|
|
const tab2 = harness.element.tabs[1];
|
2022-07-13 00:08:54 +03:00
|
|
|
expect(tab1.hideInactiveLabel).toBeTrue();
|
|
|
|
expect(tab2.hideInactiveLabel).toBeTrue();
|
2022-10-04 19:32:06 +03:00
|
|
|
harness.element.hideInactiveLabels = false;
|
|
|
|
harness.element.requestUpdate();
|
|
|
|
await env.waitForStability();
|
2022-07-13 00:08:54 +03:00
|
|
|
expect(tab1.hideInactiveLabel).toBeFalse();
|
|
|
|
expect(tab2.hideInactiveLabel).toBeFalse();
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
describe('aria-label', () => {
|
2022-10-04 19:32:06 +03:00
|
|
|
it('sets the root aria-label property', async () => {
|
|
|
|
const {harness} =
|
|
|
|
await setupTest(navBarWithNavTabsElement({ariaLabel: 'foo'}));
|
2022-07-13 00:08:54 +03:00
|
|
|
const navBarBase =
|
2022-10-04 19:32:06 +03:00
|
|
|
harness.element.shadowRoot!.querySelector('.md3-navigation-bar')!;
|
2022-07-13 00:08:54 +03:00
|
|
|
expect(navBarBase.getAttribute('aria-label')).toEqual('foo');
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
describe('#onTabsChange()', () => {
|
|
|
|
it('syncs tabs\' hideInactiveLabel state with the navigation bar\'s ' +
|
|
|
|
'hideInactiveLabels state',
|
2022-10-04 19:32:06 +03:00
|
|
|
async () => {
|
|
|
|
const {harness} = await setupTest(navBarWithIncorrectTabsElement);
|
|
|
|
const tab1 = harness.element.tabs[0];
|
|
|
|
const tab2 = harness.element.tabs[1];
|
|
|
|
expect(harness.element.hideInactiveLabels).toBeFalse();
|
2022-07-13 00:08:54 +03:00
|
|
|
expect(tab1.hideInactiveLabel).toBeFalse();
|
|
|
|
expect(tab2.hideInactiveLabel).toBeFalse();
|
|
|
|
});
|
|
|
|
|
|
|
|
it('syncs tabs\' active state with the navigation bar\'s activeIndex state',
|
2022-10-04 19:32:06 +03:00
|
|
|
async () => {
|
|
|
|
const {harness} = await setupTest(navBarWithIncorrectTabsElement);
|
|
|
|
const tab1 = harness.element.tabs[0];
|
|
|
|
const tab2 = harness.element.tabs[1];
|
|
|
|
expect(harness.element.activeIndex).toBe(0);
|
2022-07-13 00:08:54 +03:00
|
|
|
expect(tab1.active).toBeTrue();
|
|
|
|
expect(tab2.active).toBeFalse();
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
describe('#handleKeydown', () => {
|
2022-10-04 19:32:06 +03:00
|
|
|
let element: MdNavigationBar;
|
2022-07-13 00:08:54 +03:00
|
|
|
let bar: HTMLElement;
|
|
|
|
let tab1: HTMLElement;
|
|
|
|
let tab2: HTMLElement;
|
|
|
|
|
|
|
|
beforeEach(async () => {
|
2022-10-04 19:32:06 +03:00
|
|
|
const {harness} =
|
|
|
|
await setupTest(navBarWithNavTabsElement({activeIndex: 0}));
|
|
|
|
element = harness.element;
|
|
|
|
bar = harness.element.shadowRoot!.querySelector('.md3-navigation-bar')!;
|
|
|
|
tab1 = harness.element.children[0] as HTMLElement;
|
|
|
|
tab2 = harness.element.children[1] as HTMLElement;
|
2022-07-13 00:08:54 +03:00
|
|
|
});
|
|
|
|
|
|
|
|
it('(Enter) activates the focused tab', async () => {
|
|
|
|
const eventRight =
|
2022-08-12 20:50:55 +03:00
|
|
|
new KeyboardEvent('keydown', {key: 'ArrowRight', bubbles: true});
|
2022-07-13 00:08:54 +03:00
|
|
|
const eventEnter =
|
2022-08-12 20:50:55 +03:00
|
|
|
new KeyboardEvent('keydown', {key: 'Enter', bubbles: true});
|
2022-07-13 00:08:54 +03:00
|
|
|
tab1.focus();
|
|
|
|
expect(element.activeIndex).toBe(0);
|
|
|
|
bar.dispatchEvent(eventRight);
|
|
|
|
bar.dispatchEvent(eventEnter);
|
|
|
|
element.requestUpdate();
|
2022-10-04 19:32:06 +03:00
|
|
|
await env.waitForStability();
|
2022-07-13 00:08:54 +03:00
|
|
|
expect(element.activeIndex).toBe(1);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('(Spacebar) activates the focused tab', async () => {
|
|
|
|
const eventRight =
|
2022-08-12 20:50:55 +03:00
|
|
|
new KeyboardEvent('keydown', {key: 'ArrowRight', bubbles: true});
|
2022-07-13 00:08:54 +03:00
|
|
|
const eventSpacebar =
|
2022-10-05 19:35:12 +03:00
|
|
|
new KeyboardEvent('keydown', {key: ' ', bubbles: true});
|
2022-07-13 00:08:54 +03:00
|
|
|
tab1.focus();
|
|
|
|
expect(element.activeIndex).toBe(0);
|
|
|
|
bar.dispatchEvent(eventRight);
|
|
|
|
bar.dispatchEvent(eventSpacebar);
|
|
|
|
element.requestUpdate();
|
2022-10-04 19:32:06 +03:00
|
|
|
await env.waitForStability();
|
2022-07-13 00:08:54 +03:00
|
|
|
expect(element.activeIndex).toBe(1);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('(Home) sets focus on the first tab', () => {
|
2022-08-12 20:50:55 +03:00
|
|
|
const event = new KeyboardEvent('keydown', {key: 'Home', bubbles: true});
|
2022-07-13 00:08:54 +03:00
|
|
|
tab2.focus();
|
2022-10-04 19:32:06 +03:00
|
|
|
expect(tab1.matches(':focus-within')).toBeFalse();
|
2022-07-13 00:08:54 +03:00
|
|
|
bar.dispatchEvent(event);
|
2022-10-04 19:32:06 +03:00
|
|
|
expect(tab1.matches(':focus-within')).toBeTrue();
|
2022-07-13 00:08:54 +03:00
|
|
|
});
|
|
|
|
|
|
|
|
it('(End) sets focus on the last tab', () => {
|
2022-08-12 20:50:55 +03:00
|
|
|
const event = new KeyboardEvent('keydown', {key: 'End', bubbles: true});
|
2022-07-13 00:08:54 +03:00
|
|
|
bar.dispatchEvent(event);
|
2022-10-04 19:32:06 +03:00
|
|
|
expect(tab2.matches(':focus-within')).toBeTrue();
|
2022-07-13 00:08:54 +03:00
|
|
|
});
|
|
|
|
|
|
|
|
describe('(ArrowLeft)', () => {
|
|
|
|
// Use the same key for all tests
|
2022-08-12 20:50:55 +03:00
|
|
|
const key = 'ArrowLeft';
|
2022-07-13 00:08:54 +03:00
|
|
|
|
|
|
|
it(`sets focus on previous tab`, () => {
|
|
|
|
const event = new KeyboardEvent('keydown', {key, bubbles: true});
|
|
|
|
tab2.focus();
|
|
|
|
bar.dispatchEvent(event);
|
2022-10-04 19:32:06 +03:00
|
|
|
expect(tab1.matches(':focus-within')).toBeTrue();
|
2022-07-13 00:08:54 +03:00
|
|
|
});
|
|
|
|
|
|
|
|
it(`sets focus to last tab when focus is on the first tab`, () => {
|
|
|
|
const event = new KeyboardEvent('keydown', {key, bubbles: true});
|
|
|
|
tab1.focus();
|
|
|
|
bar.dispatchEvent(event);
|
2022-10-04 19:32:06 +03:00
|
|
|
expect(tab2.matches(':focus-within')).toBeTrue();
|
2022-07-13 00:08:54 +03:00
|
|
|
});
|
|
|
|
|
|
|
|
it(`sets focus on next tab in RTL`, () => {
|
|
|
|
document.body.style.direction = 'rtl';
|
|
|
|
const event = new KeyboardEvent('keydown', {key, bubbles: true});
|
|
|
|
tab1.focus();
|
|
|
|
bar.dispatchEvent(event);
|
2022-10-04 19:32:06 +03:00
|
|
|
expect(tab2.matches(':focus-within')).toBeTrue();
|
2022-07-13 00:08:54 +03:00
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
describe('(ArrowRight)', () => {
|
|
|
|
// Use the same key for all tests
|
2022-08-12 20:50:55 +03:00
|
|
|
const key = 'ArrowRight';
|
2022-07-13 00:08:54 +03:00
|
|
|
|
|
|
|
it(`sets focus on next tab`, () => {
|
|
|
|
const event = new KeyboardEvent('keydown', {key, bubbles: true});
|
|
|
|
tab1.focus();
|
|
|
|
bar.dispatchEvent(event);
|
2022-10-04 19:32:06 +03:00
|
|
|
expect(tab2.matches(':focus-within')).toBeTrue();
|
2022-07-13 00:08:54 +03:00
|
|
|
});
|
|
|
|
|
|
|
|
it(`sets focus to first tab when focus is on the last tab`, () => {
|
|
|
|
const event = new KeyboardEvent('keydown', {key, bubbles: true});
|
|
|
|
tab2.focus();
|
|
|
|
bar.dispatchEvent(event);
|
2022-10-04 19:32:06 +03:00
|
|
|
expect(tab1.matches(':focus-within')).toBeTrue();
|
2022-07-13 00:08:54 +03:00
|
|
|
});
|
|
|
|
|
|
|
|
it(`sets focus on previous tab in RTL`, () => {
|
|
|
|
document.body.style.direction = 'rtl';
|
|
|
|
const event = new KeyboardEvent('keydown', {key, bubbles: true});
|
|
|
|
tab2.focus();
|
|
|
|
bar.dispatchEvent(event);
|
2022-10-04 19:32:06 +03:00
|
|
|
expect(tab1.matches(':focus-within')).toBeTrue();
|
2022-07-13 00:08:54 +03:00
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|