Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,16 @@ and this project adheres to [Semantic Versioning](http://semver.org/).

## [Unreleased]
### Added
- #### Nav Drawer
- The drawer for non-relative positions is now implemented using a native `<dialog>` element, providing built-in modal semantics, focus trapping, and accessibility support.
- Added `keepOpenOnEscape` property — prevents the drawer from closing when the user presses the **Escape** key (non-relative positions only).
- Added `igcClosing` event — emitted just before the drawer is closed by user interaction. Cancelable.
- Added `igcClosed` event — emitted just after the drawer is closed by user interaction.
- The `mini` slot content is now always visible whenever it is provided, regardless of the drawer's position or open state.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Um, what? Not sure I'm getting this correctly, but is this changing the behavior of mini now? Besides being unrelated to the PR scope, it also doesn't make sense to me, mini is an alternative closed state.


### Changed
- #### Nav Drawer
- The `overlay` CSS part has been removed. The native `<dialog>` element's `::backdrop` pseudo-element should be used for backdrop styling instead.
- #### Form controls
- `IgcInput`, `IgcMaskInput`, `IgcDateTimeInput`, `IgcCheckbox`, `IgcRadio`, `IgcSwitch`, `IgcDatePicker`, and `IgcDateRangePicker` now submit their associated form on **Enter** key press, matching native browser behavior.

Expand Down
220 changes: 201 additions & 19 deletions src/components/nav-drawer/nav-drawer.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
import { expect, fixture, html } from '@open-wc/testing';
import {
elementUpdated,
expect,
fixture,
html,
waitUntil,
} from '@open-wc/testing';
import type { TemplateResult } from 'lit';

import { spy } from 'sinon';
import { defineComponents } from '../common/definitions/defineComponents.js';
import { simulateClick } from '../common/utils.spec.js';
import IgcIconComponent from '../icon/icon.js';
import IgcNavDrawerComponent from './nav-drawer.js';

Expand All @@ -12,12 +19,6 @@ describe('Navigation Drawer', () => {

let navDrawer: IgcNavDrawerComponent;

// Workaround since transitionend is not emitted in the tests
async function runWithTransition(awaitable: Promise<boolean>) {
navDrawer.renderRoot.dispatchEvent(new Event('transitionend'));
return await awaitable;
}

describe('Accessibility', () => {
beforeEach(async () => {
navDrawer = await createNavDrawer();
Expand All @@ -29,7 +30,8 @@ describe('Navigation Drawer', () => {
});

it('passes the a11y audit (open state)', async () => {
await runWithTransition(navDrawer.show());
await navDrawer.show();
await elementUpdated(navDrawer);
expect(navDrawer.open).to.be.true;

await expect(navDrawer).dom.to.be.accessible();
Expand Down Expand Up @@ -63,16 +65,34 @@ describe('Navigation Drawer', () => {
expect(navDrawer).to.contain('igc-nav-drawer-item');
});

it('render navigation drawer slots', async () => {
it('renders dialog-based shadow DOM for non-relative position', async () => {
navDrawer = await createNavDrawer();

expect(navDrawer).shadowDom.equal(`
<div part="overlay"></div>
<div inert part="base">
<dialog aria-modal="true" part="base">
<div part="main">
<slot></slot>
</div>
</dialog>
<div part="mini hidden">
<slot name="mini"></slot>
</div>
`);
});

it('renders nav-based shadow DOM for relative position', async () => {
navDrawer = await createNavDrawer(html`
<igc-nav-drawer position="relative">
<igc-nav-drawer-item></igc-nav-drawer-item>
</igc-nav-drawer>
`);

expect(navDrawer).shadowDom.equal(`
<nav inert part="base">
<div part="main">
<slot></slot>
</div>
</nav>
<div part="mini hidden">
<slot name="mini"></slot>
</div>
Expand Down Expand Up @@ -123,25 +143,187 @@ describe('Navigation Drawer', () => {
});

it('`show`', async () => {
await runWithTransition(navDrawer.show());
expect(await navDrawer.show()).to.be.true;
expect(navDrawer.open).to.be.true;
expect(await runWithTransition(navDrawer.show())).to.be.false;
expect(await navDrawer.show()).to.be.false;
});

it('`hide`', async () => {
await runWithTransition(navDrawer.toggle());
await runWithTransition(navDrawer.hide());
await navDrawer.toggle();
expect(await navDrawer.hide()).to.be.true;
expect(navDrawer.open).to.be.false;
expect(await runWithTransition(navDrawer.hide())).to.be.false;
expect(await navDrawer.hide()).to.be.false;
});

it('`toggle`', async () => {
await runWithTransition(navDrawer.toggle());
expect(await navDrawer.toggle()).to.be.true;
expect(navDrawer.open).to.be.true;

expect(await navDrawer.toggle()).to.be.true;
expect(navDrawer.open).to.be.false;
});
});

describe('Events & Behaviors', () => {
let nativeDialog: HTMLDialogElement;

beforeEach(async () => {
navDrawer = await createNavDrawer();
nativeDialog = navDrawer.renderRoot.querySelector('dialog')!;
});

it('should correctly render with initial open state', async () => {
navDrawer = await createNavDrawer(html`
<igc-nav-drawer open>
<igc-nav-drawer-item></igc-nav-drawer-item>
</igc-nav-drawer>
`);
nativeDialog = navDrawer.renderRoot.querySelector('dialog')!;

expect(navDrawer.open).to.be.true;
expect(nativeDialog.open).to.be.true;
});

it('should open dialog when position changes from relative to non-relative while open', async () => {
navDrawer = await createNavDrawer(html`
<igc-nav-drawer position="relative" open>
<igc-nav-drawer-item></igc-nav-drawer-item>
</igc-nav-drawer>
`);

expect(navDrawer.open).to.be.true;
expect(navDrawer.renderRoot.querySelector('dialog')).to.be.null;

navDrawer.position = 'start';
await elementUpdated(navDrawer);

nativeDialog = navDrawer.renderRoot.querySelector('dialog')!;
expect(nativeDialog).to.exist;
expect(nativeDialog.open).to.be.true;
});

it('should close the native dialog when position changes to relative while open', async () => {
await navDrawer.show();
await elementUpdated(navDrawer);

expect(nativeDialog.open).to.be.true;

navDrawer.position = 'relative';
await elementUpdated(navDrawer);

expect(navDrawer.open).to.be.true;
expect(navDrawer.renderRoot.querySelector('dialog')).to.be.null;
});

it('should close when the user presses Escape', async () => {
const eventSpy = spy(navDrawer, 'emitEvent');
await navDrawer.show();
await elementUpdated(navDrawer);

nativeDialog.dispatchEvent(new Event('cancel'));
await elementUpdated(navDrawer);
await waitUntil(() => !navDrawer.open);

expect(eventSpy.getCalls()).lengthOf(2);
expect(eventSpy.firstCall).calledWith('igcClosing');
expect(eventSpy.secondCall).calledWith('igcClosed');
});

it('should not close when Escape is pressed and `keepOpenOnEscape` is set', async () => {
const eventSpy = spy(navDrawer, 'emitEvent');

navDrawer.keepOpenOnEscape = true;
await navDrawer.show();
await elementUpdated(navDrawer);

nativeDialog.dispatchEvent(new Event('cancel'));
await elementUpdated(navDrawer);

await runWithTransition(navDrawer.toggle());
expect(navDrawer.open).to.be.true;
expect(eventSpy.getCalls()).is.empty;
});

it('should close when clicking outside in non-relative position', async () => {
await navDrawer.show();
await elementUpdated(navDrawer);

const eventSpy = spy(navDrawer, 'emitEvent');
const { x, y } = nativeDialog.getBoundingClientRect();
simulateClick(nativeDialog, { clientX: x + 1, clientY: y - 1 });
await elementUpdated(navDrawer);

await waitUntil(() => eventSpy.calledWith('igcClosed'));
expect(navDrawer.open).to.be.false;
});

it('should not close when clicking inside the dialog', async () => {
await navDrawer.show();
await elementUpdated(navDrawer);

const eventSpy = spy(navDrawer, 'emitEvent');
const { x, y } = nativeDialog.getBoundingClientRect();

simulateClick(nativeDialog, { clientX: x + 1, clientY: y + 1 });
await elementUpdated(navDrawer);

expect(eventSpy).not.calledWith('igcClosed');
expect(navDrawer.open).to.be.true;
});

it('should not close when clicking outside in relative position', async () => {
const relativeNavDrawer = await createNavDrawer(html`
<igc-nav-drawer position="relative">
<igc-nav-drawer-item></igc-nav-drawer-item>
</igc-nav-drawer>
`);

await relativeNavDrawer.show();
await elementUpdated(relativeNavDrawer);

const eventSpy = spy(relativeNavDrawer, 'emitEvent');
const { x, y } = relativeNavDrawer.getBoundingClientRect();
simulateClick(relativeNavDrawer, { clientX: x + 1, clientY: y - 1 });
await elementUpdated(relativeNavDrawer);

expect(eventSpy.calledWith('igcClosed')).to.be.false;
expect(relativeNavDrawer.open).to.be.true;
});

it('can cancel `igcClosing` event', async () => {
await navDrawer.show();
await elementUpdated(navDrawer);

const eventSpy = spy(navDrawer, 'emitEvent');
navDrawer.addEventListener('igcClosing', (e) => e.preventDefault(), {
once: true,
});

nativeDialog.dispatchEvent(new Event('cancel'));
await elementUpdated(navDrawer);

expect(eventSpy).calledWith('igcClosing');
expect(eventSpy).not.calledWith('igcClosed');
expect(navDrawer.open).to.be.true;
});

it('does not close when keepOpenOnEscape is true and a non-cancelable close event is fired', async () => {
navDrawer.keepOpenOnEscape = true;
await navDrawer.show();
await elementUpdated(navDrawer);

nativeDialog.dispatchEvent(new Event('close'));
await elementUpdated(navDrawer);

expect(navDrawer.open).to.be.true;
});

it('programmatic hide does not emit events', async () => {
const eventSpy = spy(navDrawer, 'emitEvent');
await navDrawer.show();
await navDrawer.hide();

expect(eventSpy.getCalls()).is.empty;
});
});

async function createNavDrawer(template?: TemplateResult) {
Expand Down
Loading
Loading