diff --git a/components/side-drawer/src/vwc-side-drawer-base.ts b/components/side-drawer/src/vwc-side-drawer-base.ts index fad8d4e9e2..3ceff3bfb4 100644 --- a/components/side-drawer/src/vwc-side-drawer-base.ts +++ b/components/side-drawer/src/vwc-side-drawer-base.ts @@ -6,7 +6,6 @@ import { } from 'lit-element'; import { ifDefined } from 'lit-html/directives/if-defined'; import { classMap } from 'lit-html/directives/class-map'; -import { observer } from '@material/mwc-base/observer'; import { DocumentWithBlockingElements } from 'blocking-elements'; const blockingElements = @@ -18,7 +17,10 @@ export class VWCSideDrawerBase extends LitElement { * accepts boolean value * @public * */ - @property({ type: Boolean, reflect: true }) + @property({ + type: Boolean, + reflect: true + }) alternate = false; /** @@ -26,7 +28,10 @@ export class VWCSideDrawerBase extends LitElement { * accepts boolean value * @public * */ - @property({ type: Boolean, reflect: true }) + @property({ + type: Boolean, + reflect: true + }) hasTopBar?: boolean; /** @@ -34,7 +39,10 @@ export class VWCSideDrawerBase extends LitElement { * accepts String value * @public * */ - @property({ type: String, reflect: true }) + @property({ + type: String, + reflect: true + }) type = ''; /** @@ -42,40 +50,18 @@ export class VWCSideDrawerBase extends LitElement { * accepts Boolean value * @public * */ - @property({ type: Boolean, reflect: true }) + @property({ + type: Boolean, + reflect: true + }) absolute = false; - @property({ type: Boolean, reflect: true }) - @observer(function ( - this: VWCSideDrawerBase, - isOpen: boolean, - wasOpen: boolean - ) { - if (isOpen) { - this.show(); - // wasOpen helps with first render (when it is `undefined`) perf - } else if (wasOpen !== undefined) { - this.close(); - } - this.openChanged(isOpen); + @property({ + type: Boolean, + reflect: true }) open = false; - /** - * Invoked when the element open state is updated. - * - * Expressions inside this method will trigger upon open state change. - * - * @param _isOpen Boolean of open state - */ openChanged( - // eslint-disable-next-line @typescript-eslint/no-unused-vars - _isOpen: boolean - // eslint-disable-next-line @typescript-eslint/no-empty-function - ): void {} - constructor() { - super(); - this.addEventListener('transitionend', () => this.onTransitionEnd()); - } /** * Opens the side drawer from the closed state. * @public @@ -88,175 +74,131 @@ export class VWCSideDrawerBase extends LitElement { * Closes the side drawer from the open state. * @public */ - close(): void { + hide(): void { this.open = false; } - /** - * Side drawer finished open animation. - */ - #opened(): void { - this.#trapFocus(); - this.#notifyOpen(); + connectedCallback() { + super.connectedCallback(); + this.addEventListener('transitionend', this.#onTransitionEnd); + this.addEventListener('keydown', this.#onKeydown); } - /** - * Side drawer finished close animation. - */ - #closed(): void { + disconnectedCallback(): void { + super.disconnectedCallback(); this.#releaseFocus(); - this.#notifyClose(); + this.removeEventListener('transitionend', this.#onTransitionEnd); + this.removeEventListener('keydown', this.#onKeydown); } - /** - * DispatchEvent creator. - * @param eventName - */ - #createDispatchEvent(eventName: string) { - const init: CustomEventInit = { bubbles: true, composed: true }; - const ev = new CustomEvent(eventName, init); - this.dispatchEvent(ev); - } + protected render(): TemplateResult { + const dismissible = this.type === 'dismissible' || this.type === 'modal'; + const modal = this.type === 'modal'; + const topBar = this.hasTopBar ? this.renderTopBar() : ''; + const scrim = this.type === 'modal' && this.open ? this.renderScrim() : ''; + const alternate = this.alternate ? 'vvd-scheme-alternate' : undefined; + const absolute = this.type === 'modal' && this.absolute; - /** - * Notify close. - * - * @fires SideDrawer#closed - */ - #notifyClose(): void { - this.#createDispatchEvent('closed'); - } + const classes = { + 'vvd-side-drawer--alternate': this.alternate, + 'vvd-side-drawer--dismissible': dismissible, + 'vvd-side-drawer--modal': modal, + 'vvd-side-drawer--open': this.open, + 'vvd-side-drawer--absolute': absolute, + }; - /** - * Notify open. - * - * @fires SideDrawer#opened - */ - #notifyOpen(): void { - this.#createDispatchEvent('opened'); - } + return html` + + + ${scrim} + `; } - /** - * Traps focus on root element and focuses the active navigation element. - * - * Notify trap focus. - * @fires SideDrawer#trapFocus - */ - #trapFocus(): void { - blockingElements.push(this); - this.#createDispatchEvent('trapFocus'); + private renderTopBar(): TemplateResult { + return html` +
`; } - /** - * Releases focus trap from root element which was set by `trapFocus`. - * - * Notify release focus. - * @fires SideDrawer#releaseFocus - */ - #releaseFocus(): void { - blockingElements.remove(this); - this.#createDispatchEvent('releaseFocus'); + private renderScrim(): TemplateResult { + return html` + `; } - /** - * Click handler to close side drawer when scrim is clicked. - */ - handleScrimClick(): void { + #handleScrimClick(): void { if (this.type === 'modal' && this.open) { - this.close(); + this.hide(); } } - /** - * Keydown handler to close side drawer when key is escape. - */ - onKeydown({ key }: KeyboardEvent): void { - console.log(this.type, this.open, key); + #onKeydown = ({ key }: KeyboardEvent): void => { if (this.type === 'modal' && this.open && key === 'Escape') { - this.close(); + this.hide(); } - } + }; - /** - * Handles the `transitionend` event when the side drawer finishes opening/closing. - */ - onTransitionEnd(): void { + #onTransitionEnd = (): void => { if (this.type === 'modal') { // when side drawer finishes open animation if (this.open) { this.#opened(); } else { - // when side drawer finishes close animation + // when side drawer finishes hide animation this.#closed(); } } - } + }; - /** - * renderTopBar - * @slot top-bar - * @returns TemplateResult - */ - private renderTopBar(): TemplateResult { - return html` `; + #opened(): void { + this.#trapFocus(); + this.#notifyOpen(); } - /** - * renderScrim - * @returns TemplateResult - */ - private renderScrim(): TemplateResult { - // eslint-disable-next-line lit-a11y/click-events-have-key-events - return html``; + #closed(): void { + this.#releaseFocus(); + this.#notifyClose(); } - /** - * the html markup - * @internal - * */ - protected render(): TemplateResult { - const dismissible = this.type === 'dismissible' || this.type === 'modal'; - const modal = this.type === 'modal'; - const topBar = this.hasTopBar ? this.renderTopBar() : ''; - const scrim = this.type === 'modal' && this.open ? this.renderScrim() : ''; - const alternate = this.alternate ? 'vvd-scheme-alternate' : undefined; - const absolute = this.type === 'modal' && this.absolute; - - const classes = { - 'vvd-side-drawer--alternate': this.alternate, - 'vvd-side-drawer--dismissible': dismissible, - 'vvd-side-drawer--modal': modal, - 'vvd-side-drawer--open': this.open, - 'vvd-side-drawer--absolute': absolute, + #createDispatchEvent(eventName: string) { + const init: CustomEventInit = { + bubbles: true, + composed: true }; + const ev = new CustomEvent(eventName, init); + this.dispatchEvent(ev); + } - return html` - + #notifyOpen(): void { + this.#createDispatchEvent('opened'); + } - ${scrim} - `; + #trapFocus(): void { + blockingElements.push(this); + this.#createDispatchEvent('trapFocus'); + } + + #releaseFocus(): void { + blockingElements.remove(this); + this.#createDispatchEvent('releaseFocus'); } } diff --git a/components/side-drawer/test/side-drawer.test.js b/components/side-drawer/test/side-drawer.test.js index b787e2600d..82e4e5139c 100644 --- a/components/side-drawer/test/side-drawer.test.js +++ b/components/side-drawer/test/side-drawer.test.js @@ -10,9 +10,11 @@ import { chaiDomDiff } from '@open-wc/semantic-dom-diff'; chai.use(chaiDomDiff); const COMPONENT_NAME = 'vwc-side-drawer'; -const COMPONENT_PROPERTIES = ['open', 'alternate', 'hasTopBar', 'absolute']; -const COMPONENT_TYPES = ['', 'dismissible', 'modal']; +function animateModal(drawerElement) { + const event = new Event('transitionend'); + drawerElement.dispatchEvent(event); +} describe('Side-drawer', () => { let addElement = isolatedElementsCreation(); @@ -36,7 +38,7 @@ describe('Side-drawer', () => { describe('Side drawer default init', () => { it('should reflect from attribute to property', async () => { - const [actualElement] = ( + const [actualElement] = addElement( textToDomToParent(`<${COMPONENT_NAME}>${COMPONENT_NAME}>`) ); @@ -59,34 +61,44 @@ describe('Side-drawer', () => { describe('Side drawer attributes', () => { it('should reflect from attribute to property', async () => { + const COMPONENT_PROPERTIES = ['open', 'alternate', 'hasTopBar', 'absolute']; for await (const property of COMPONENT_PROPERTIES) { const [actualElement] = addElement( textToDomToParent(`<${COMPONENT_NAME} ${property}>${COMPONENT_NAME}>`) ); await actualElement.updateComplete; - expect(actualElement[property]).to.equal(true); + expect(actualElement[property]) + .to + .equal(true); } }); it('should reflect (type) from attribute to property', async () => { + const COMPONENT_TYPES = ['', 'dismissible', 'modal']; for await (const type of COMPONENT_TYPES) { const [actualElement] = addElement( textToDomToParent(`<${COMPONENT_NAME} type=${type}>${COMPONENT_NAME}>`) ); await actualElement.updateComplete; - expect(actualElement.type).to.equal(type); + expect(actualElement.type) + .to + .equal(type); } }); }); describe('Modal side drawer events', () => { - it('should fire opened event after changing from close to open', async () => { - const onOpened = chai.spy(); - const onFocusTrapped = chai.spy(); + let sideDrawerEl; - const [sideDrawerEl] = addElement( + beforeEach(function () { + [sideDrawerEl] = addElement( textToDomToParent(`<${COMPONENT_NAME} type="modal">${COMPONENT_NAME}>`) ); + }); + it('should fire opened event after animation completes and open is true', async () => { + const onOpened = chai.spy(); + const onFocusTrapped = chai.spy(); + await sideDrawerEl.updateComplete; const eventListenerPromise = new Promise((res) => { @@ -100,22 +112,19 @@ describe('Side-drawer', () => { }); }); - sideDrawerEl.show(); - const event = new Event('transitionend'); - sideDrawerEl.dispatchEvent(event); + sideDrawerEl.open = true; + animateModal(sideDrawerEl); await eventListenerPromise; onOpened.should.have.been.called(); onFocusTrapped.should.have.been.called(); }); - it('should fire closed event after changing from open to close', async () => { + it('should fire closed event after animation completes and open is false', async () => { const onClosed = chai.spy(); const onFocusReleased = chai.spy(); - const [sideDrawerEl] = addElement( - textToDomToParent(`<${COMPONENT_NAME} type="modal" open>${COMPONENT_NAME}>`) - ); + sideDrawerEl.open = true; await sideDrawerEl.updateComplete; const eventListenerPromise = new Promise((res) => { @@ -129,9 +138,8 @@ describe('Side-drawer', () => { }); }); - sideDrawerEl.close(); - const event = new Event('transitionend'); - sideDrawerEl.dispatchEvent(event); + sideDrawerEl.open = false; + animateModal(sideDrawerEl); await eventListenerPromise; onClosed.should.have.been.called(); @@ -141,9 +149,7 @@ describe('Side-drawer', () => { it('should fire closed event after clicking on scrim', async () => { const onClosed = chai.spy(); - const [sideDrawerEl] = addElement( - textToDomToParent(`<${COMPONENT_NAME} type="modal" open>${COMPONENT_NAME}>`) - ); + sideDrawerEl.open = true; await sideDrawerEl.updateComplete; const scrim = sideDrawerEl.shadowRoot.querySelector('.vvd-side-drawer--scrim'); @@ -156,11 +162,59 @@ describe('Side-drawer', () => { }); scrim?.click(); - const event = new Event('transitionend'); - sideDrawerEl.dispatchEvent(event); + animateModal(sideDrawerEl); await eventListenerPromise; onClosed.should.have.been.called(); }); + + it('should fire closed event after pressing escape on the drawer', async () => { + const onClosed = chai.spy(); + + sideDrawerEl.open = true; + await sideDrawerEl.updateComplete; + + + const eventListenerPromise = new Promise((res) => { + sideDrawerEl.addEventListener('closed', () => { + onClosed(); + res(); + }); + }); + + const keyboardEvent = new KeyboardEvent('keydown', { key: 'Escape' }); + sideDrawerEl.dispatchEvent(keyboardEvent); + + animateModal(sideDrawerEl); + + await eventListenerPromise; + onClosed.should.have.been.called(); + }); + }); + + describe(`show`, function () { + it(`should set "open" to true`, function () { + const [actualElement] = addElement( + textToDomToParent(`<${COMPONENT_NAME}>${COMPONENT_NAME}>`) + ); + actualElement.open = false; + actualElement.show(); + expect(actualElement.open) + .to + .equal(true); + }); + }); + + describe(`hide`, function () { + it(`should set "open" to false`, function () { + const [actualElement] = addElement( + textToDomToParent(`<${COMPONENT_NAME}>${COMPONENT_NAME}>`) + ); + actualElement.open = true; + actualElement.hide(); + expect(actualElement.open) + .to + .equal(false); + }); }); });