From 1d777557f55a970d29896239a6ed424026f84abd Mon Sep 17 00:00:00 2001 From: Damla Demir Date: Mon, 21 Nov 2022 14:52:13 +0300 Subject: [PATCH] fix: support of dialog in old versions of browsers (#319) * fix(dialog): add polyfill dialog component * fix: center dialog & position backdrop * fix(dialog): add dialog polyfill operations * fix(dialog): code review and fix * style(dialog): fix style lint error * fix(dialog): fix z-index * refactor(dialog): code review and refactor dialog * refactor(dialog): refactor event listeners * refactor(dialog): revert template and fix fixme comments Co-authored-by: damla.demir1 Co-authored-by: olkeoguz --- playground/template.html | 2 +- src/components/dialog/bl-dialog.css | 75 ++++-- src/components/dialog/bl-dialog.test.ts | 295 ++++++++++++++++-------- src/components/dialog/bl-dialog.ts | 118 ++++------ src/types/index.d.ts | 7 + 5 files changed, 300 insertions(+), 197 deletions(-) create mode 100644 src/types/index.d.ts diff --git a/playground/template.html b/playground/template.html index 25c61b40..5b3dc776 100644 --- a/playground/template.html +++ b/playground/template.html @@ -30,4 +30,4 @@

Baklava Playground

Baklava is ready - + \ No newline at end of file diff --git a/src/components/dialog/bl-dialog.css b/src/components/dialog/bl-dialog.css index 82c86947..4aef7d08 100644 --- a/src/components/dialog/bl-dialog.css +++ b/src/components/dialog/bl-dialog.css @@ -1,24 +1,56 @@ -dialog { - padding: 0; - border: 0; +.container { + display: flex; + flex-direction: column; background: var(--bl-color-primary-background); - border-radius: var(--bl-border-radius-l); max-width: calc(100vw - var(--bl-size-4xl)); max-height: calc(100vh - var(--bl-size-4xl)); - overflow: hidden; + min-width: 424px; + padding: 0; + border: 0; + border-radius: var(--bl-border-radius-l); } -dialog::backdrop { +.dialog, +.dialog-polyfill .container { + padding: 0; + border: 0; + border-radius: var(--bl-border-radius-l); +} + +.dialog-polyfill .container { + position: fixed; + + /* FIXME: Use z-index variable */ + z-index: 999; + } + +.dialog::backdrop { background: #273142; opacity: 0.7; } -dialog .container { - min-width: 424px; - min-height: 178px; +.dialog-polyfill { + display: none; + position: fixed; + left: 0; + top: 0; + width: 100vw; + height: 100vh; + align-items: center; + justify-content: center; + + /* FIXME: Use css variables for alpha colors */ + background: #273142b3; + + /* FIXME: Use z-index variable */ + z-index: 999; + } + + :host([open]) .dialog-polyfill { + display: flex; } -dialog header { +header { display: flex; justify-content: space-between; align-items: center; @@ -26,11 +58,11 @@ dialog header { padding: var(--bl-size-xl) var(--bl-size-xl) 0 var(--bl-size-xl); } -dialog header bl-button { +header bl-button { margin-left: auto; } -dialog header h2 { +header h2 { font: var(--bl-font-title-1-medium); color: var(--bl-color-secondary); white-space: nowrap; @@ -40,38 +72,31 @@ dialog header h2 { padding: 0; } -dialog .content { +.content { padding: var(--bl-size-xl) var(--bl-size-xl) var(--bl-size-m) var(--bl-size-xl); overflow: auto; } -dialog footer { +footer { padding: var(--bl-size-xl); display: flex; flex-flow: row-reverse wrap; gap: var(--bl-size-m); } -dialog footer.sticky { - position: sticky; - bottom: 0; - z-index: 999; /* FIXME */ - background-color: var(--bl-color-primary-background); +footer.shadow { box-shadow: 0 -4px 15px #27314226; } -@media only screen and (max-width: 469px) { - dialog { +@media only screen and (max-width: 471px) { + .container { max-width: calc(100vw - var(--bl-size-2xl)); max-height: calc(100vh - var(--bl-size-2xl)); - } - - dialog .container { min-width: auto; min-height: auto; } - dialog footer { + footer { flex-flow: column wrap; } } diff --git a/src/components/dialog/bl-dialog.test.ts b/src/components/dialog/bl-dialog.test.ts index 2e2132bb..62986627 100644 --- a/src/components/dialog/bl-dialog.test.ts +++ b/src/components/dialog/bl-dialog.test.ts @@ -5,118 +5,208 @@ import { oneEvent, expect, fixtureCleanup, + elementUpdated, } from '@open-wc/testing'; import { sendKeys, sendMouse, resetMouse } from '@web/test-runner-commands'; import BlDialog from './bl-dialog'; - import type typeOfBlDialog from './bl-dialog'; +const htmlDialogElement = window.HTMLDialogElement; + describe('bl-dialog', () => { it('is defined', () => { const el = document.createElement('bl-dialog'); assert.instanceOf(el, BlDialog); }); - it('should render with the default values', async () => { - const el = await fixture(html``); - assert.shadowDom.equal( - el, - ` - -
-
- - -
-
- - -
-
-
- ` - ); - }); - - it('should render the title,the content and the footer if provided', async () => { - const el = await fixture(html` -
-

My Content

-
- Primary - Secondary -
`); + describe('dialog polyfill tests', () => { + before(() => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + window.HTMLDialogElement = false; + }); - const caption = el.shadowRoot?.querySelector('h2') as HTMLElement; - const content = el.shadowRoot?.querySelector('.content') as HTMLElement; - const footer = el.shadowRoot?.querySelector('footer'); + after(() => { + fixtureCleanup(); + window.HTMLDialogElement = htmlDialogElement; + }); - expect(caption).to.exist; - expect(caption?.innerText).to.equal('My title'); + it('should render dialog polyfill component when does not support html dialog', async () => { + const el = await fixture(html``); + const dialogPolyfill = el.shadowRoot?.querySelector('.dialog-polyfill'); + expect(dialogPolyfill).to.be.not.null; + }); - expect(content).to.exist; - expect(content?.innerHTML).to.equal(''); + it('should open the dialog when the change open attribute as true', async () => { + const el = await fixture(html` +
My Content
+
`); - expect(footer).to.exist; - expect(footer?.slot).to.exist; - }); + expect(el.open).to.equal(false); - it('should close the dialog when the close btn is clicked', async () => { - const el = await fixture(html` -
My Content
-
`); + el.open = true; + await elementUpdated(el); - const closeBtn = el?.shadowRoot?.querySelector('bl-button'); + expect(el.open).to.equal(true); + }); - expect(closeBtn).to.exist; - expect(el.open).to.equal(true); + it('should close the dialog when the close btn is clicked', async () => { + const el = await fixture(html` +
My Content
+
`); + const dialogPolyfill = el.shadowRoot?.querySelector('.dialog-polyfill') as HTMLDivElement; + const closeBtn = el?.shadowRoot?.querySelector('bl-button'); - setTimeout(() => { + expect(dialogPolyfill).to.exist; + expect(closeBtn).to.exist; + expect(el.open).to.equal(true); closeBtn?.click(); - expect(el.open).to.equal(false); - fixtureCleanup(); + + setTimeout(() => { + expect(el.open).to.equal(false); + + const visible = !!( + dialogPolyfill.offsetWidth || + dialogPolyfill.offsetHeight || + dialogPolyfill.getClientRects().length + ); + expect(visible).to.be.false; + }); }); }); - it('should close the dialog when user presses "Escape" key ', async () => { - const container = await fixture(html`
- -
`); + describe('dialog tests', () => { + before(() => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + window.HTMLDialogElement = true; + }); - const dialog = container.querySelector('bl-dialog'); - if (dialog) { - await sendKeys({ press: 'Escape' }); - expect(dialog?.getAttribute('open'))?.oneOf(['', null]); + after(() => { fixtureCleanup(); - } - }); + window.HTMLDialogElement = htmlDialogElement; + }); + it('should render html dialog component with the default values when supports html dialog', async () => { + const el = await fixture(html``); - it('should close the dialog on outside click', async () => { - const body = await fixture(html` -
- -

my content

-
-
- `); - - const dialogEl = body.querySelector('bl-dialog') as typeOfBlDialog; - - await sendMouse({ type: 'click', position: [1, 1] }); - expect(dialogEl.getAttribute('open')).oneOf(['', null]); - await resetMouse(); - fixtureCleanup(); - }); + assert.shadowDom.equal( + el, + ` + +
+
+ + +
+
+ + +
+
+
+ ` + ); + }); - it('should add shadow to footer when the content is too long', async () => { - window.innerWidth = 400; + it('should open the dialog when the change open attribute as true', async () => { + const el = await fixture(html` +
My Content
+
`); + + expect(el.open).to.equal(false); + + el.open = true; + await elementUpdated(el); - const el = await fixture(html` + setTimeout(() => { + expect(el.open).to.equal(true); + }); + }); + + it('should close the dialog when the close btn is clicked', async () => { + const el = await fixture(html` +
My Content
+
`); + const dialog = el.shadowRoot?.querySelector('.dialog') as HTMLDivElement; + const closeBtn = el?.shadowRoot?.querySelector('bl-button'); + + expect(dialog).to.exist; + expect(closeBtn).to.exist; + expect(el.open).to.equal(true); + closeBtn?.click(); + + setTimeout(() => { + expect(el.open).to.equal(false); + + const visible = !!( + dialog.offsetWidth || + dialog.offsetHeight || + dialog.getClientRects().length + ); + expect(visible).to.be.false; + }); + }); + + it('should render the title,the content and the footer if provided', async () => { + const el = await fixture(html` +
+

My Content

+
+ Primary + Secondary +
`); + + const caption = el.shadowRoot?.querySelector('h2') as HTMLElement; + const content = el.shadowRoot?.querySelector('.content') as HTMLElement; + const footer = el.shadowRoot?.querySelector('footer'); + + expect(caption).to.exist; + expect(caption?.innerText).to.equal('My title'); + + expect(content).to.exist; + expect(content?.innerHTML).to.equal(''); + + expect(footer).to.exist; + expect(footer?.slot).to.exist; + }); + + it('should close the dialog when user presses "Escape" key', async () => { + const container = await fixture(html`
+ +
`); + + const dialog = container.querySelector('bl-dialog'); + if (dialog) { + await sendKeys({ press: 'Escape' }); + expect(dialog?.getAttribute('open'))?.oneOf(['', null]); + } + }); + + it('should close the dialog on outside click', async () => { + const body = await fixture(html` +
+ +

my content

+
+
+ `); + + const dialogEl = body.querySelector('bl-dialog') as typeOfBlDialog; + + await sendMouse({ type: 'click', position: [1, 1] }); + expect(dialogEl.getAttribute('open')).oneOf(['', null]); + await resetMouse(); + }); + + it('should add shadow to footer when the content is too long', async () => { + window.innerWidth = 400; + + const el = await fixture(html`

Contrary to popular belief, Lorem Ipsum is not simply random text., comes from a line in section 1.10.32. @@ -186,29 +276,30 @@ describe('bl-dialog', () => { `); - const footer = el?.shadowRoot?.querySelector('footer') as HTMLElement; + const footer = el?.shadowRoot?.querySelector('footer') as HTMLElement; - expect(footer.className).to.oneOf(['sticky','']); - }); + expect(footer.className).to.oneOf(['shadow', '']); + }); - describe('Events', () => { - it('should fire bl-dialog-open / close event on dialog open / close', async () => { - const el = await fixture(html` - `); + describe('Events', () => { + it('should fire bl-dialog-open / close event on dialog open / close', async () => { + const el = await fixture(html` + `); - setTimeout(async () => { - const ev = await oneEvent(el, 'bl-dialog-open'); - expect(ev).to.exist; - expect(ev.detail.isOpen).to.equal(true); - }); + setTimeout(async () => { + const ev = await oneEvent(el, 'bl-dialog-open'); + expect(ev).to.exist; + expect(ev.detail.isOpen).to.equal(true); + }); - const closeBtn = el?.shadowRoot?.querySelector('bl-button'); + const closeBtn = el?.shadowRoot?.querySelector('bl-button'); - setTimeout(async () => { - closeBtn?.click(); - const ev = await oneEvent(el, 'bl-dialog-close'); - expect(ev).to.exist; - expect(ev.detail.isOpen).to.equal(false); + setTimeout(async () => { + closeBtn?.click(); + const ev = await oneEvent(el, 'bl-dialog-close'); + expect(ev).to.exist; + expect(ev.detail.isOpen).to.equal(false); + }); }); }); }); diff --git a/src/components/dialog/bl-dialog.ts b/src/components/dialog/bl-dialog.ts index 0b2bea43..21232f56 100644 --- a/src/components/dialog/bl-dialog.ts +++ b/src/components/dialog/bl-dialog.ts @@ -32,17 +32,17 @@ export default class BlDialog extends LitElement { @property({ type: String }) caption?: string; - @query('dialog') - dialog: HTMLDialogElement & DialogElement; + @query('.dialog') + private dialog: HTMLDialogElement & DialogElement; @query('footer') - footer: HTMLElement; + private footer: HTMLElement; @query('.container') - container: HTMLElement; + private container: HTMLElement; @query('.content') - content: HTMLElement; + private content: HTMLElement; /** * Fires when the dialog is opened @@ -54,49 +54,42 @@ export default class BlDialog extends LitElement { */ @event('bl-dialog-close') private onClose: EventDispatcher; - connectedCallback() { - super.connectedCallback(); - - setTimeout(() => { - window?.addEventListener('keydown', event => this.onKeydown(event)); - window?.addEventListener('resize', () => this.resizeHandler()); - this.dialog.addEventListener('click', this.clickOutsideHandler); - }); - } - - disconnectedCallback() { - super.disconnectedCallback(); - window?.removeEventListener('keydown', this.onKeydown); - window?.removeEventListener('resize', this.resizeHandler); - this.dialog.removeEventListener('click', this.clickOutsideHandler); - } - updated(changedProperties: PropertyValues) { if (changedProperties.has('open')) { this.toggleDialogHandler(); } } - private resizeHandler() { - this.changeContentHeight(); - this.toggleFooterSticky(); + private get hasHtmlDialogSupport() { + return !!window.HTMLDialogElement; + } + + private get _hasFooter() { + return [...this.childNodes].some(node => node.nodeName === 'BL-BUTTON'); } private toggleDialogHandler() { if (this.open) { - this.dialog.showModal?.(); + this.dialog?.showModal?.(); this.onOpen({ isOpen: true }); document.body.style.overflow = 'hidden'; + this.toggleFooterShadow(); + window?.addEventListener('keydown', event => this.onKeydown(event)); + window?.addEventListener('resize', () => this.toggleFooterShadow()); } else { - this.dialog.close?.(); + this.dialog?.close?.(); this.onClose({ isOpen: false }); document.body.style.overflow = 'auto'; + window?.removeEventListener('keydown', this.onKeydown); + window?.removeEventListener('resize', this.toggleFooterShadow); } + } - this.resizeHandler(); + private closeDialog() { + this.open = false; } - clickOutsideHandler = (event: MouseEvent) => { + private clickOutsideHandler = (event: MouseEvent) => { const eventPath = event.composedPath() as HTMLElement[]; if (!eventPath.includes(this.container)) { @@ -104,39 +97,20 @@ export default class BlDialog extends LitElement { } }; - private closeDialog() { - this.open = false; - } - private onKeydown = (event: KeyboardEvent): void => { if (event.code === 'Escape' && this.open) { this.closeDialog(); } }; - private toggleFooterSticky() { + private toggleFooterShadow() { if (this.content?.scrollHeight > this.content?.offsetHeight) { - this.footer?.classList?.add('sticky'); + this.footer?.classList?.add('shadow'); } else { - this.footer?.classList?.remove('sticky'); + this.footer?.classList?.remove('shadow'); } } - private changeContentHeight() { - const footerHeight = this.footer?.offsetHeight || 0; - let contentHeight = 144; // 56px(header) + 48px(dialog-margin) + 40px(content-padding) - - if (window.innerWidth < 470) { - contentHeight = 128; // 56px(header) + 32px(dialog-margin) + 40px(content-padding) - } - - this.content.style.maxHeight = `${window.innerHeight - (contentHeight + footerHeight)}px`; - } - - get _hasFooter() { - return [...this.childNodes].some(node => node.nodeName === 'BL-BUTTON'); - } - private renderFooter() { return this._hasFooter ? html`