From 246fda511b55c972d7c23c500289076d26b094fe Mon Sep 17 00:00:00 2001 From: Damla Demir Date: Thu, 27 Oct 2022 14:42:40 +0300 Subject: [PATCH] feat: dialog component (#306) * feat(dialog): create component structure * feat(dialog): improve dialog component * style(dialog): fix content box-shadow * refactor(dialog): refactor open-close dialog operations * style(dialog): add truncate to title * test(dialog): add unit tests * test(dialog): fix close btn test * docs(dialog): add storybook * test(dialog): complete uncovered test blogs * style(dialog): fix container size * docs(dialog): refactor storybook * style(dialog): fix content max height on mobile view * fix(dialog): storybook bug fix * fix(dialog): fix storybook bug and add layout center * fix(dialog): change viewport for chromatic * refactor(dialog): revert story parameters * fix(dialog): fix dialog dimensions * docs(dialog): remove content margin Co-authored-by: olkeoguz --- commitlint.config.js | 3 +- src/baklava.ts | 1 + src/components/dialog/bl-dialog.css | 77 +++++++ src/components/dialog/bl-dialog.stories.mdx | 181 ++++++++++++++++ src/components/dialog/bl-dialog.test.ts | 215 ++++++++++++++++++++ src/components/dialog/bl-dialog.ts | 177 ++++++++++++++++ 6 files changed, 653 insertions(+), 1 deletion(-) create mode 100644 src/components/dialog/bl-dialog.css create mode 100644 src/components/dialog/bl-dialog.stories.mdx create mode 100644 src/components/dialog/bl-dialog.test.ts create mode 100644 src/components/dialog/bl-dialog.ts diff --git a/commitlint.config.js b/commitlint.config.js index 538274b3..88293ac3 100644 --- a/commitlint.config.js +++ b/commitlint.config.js @@ -18,7 +18,8 @@ module.exports = { 'checkbox', 'alert', 'select', - 'pagination' + 'pagination', + 'dialog' ], ], }, diff --git a/src/baklava.ts b/src/baklava.ts index b6a383a1..8b415697 100644 --- a/src/baklava.ts +++ b/src/baklava.ts @@ -12,4 +12,5 @@ export { default as BlAlert } from './components/alert/bl-alert'; export { default as BlSelect } from './components/select/bl-select'; export { default as BlSelectOption } from './components/select/option/bl-select-option'; export { default as BlPagination } from './components/pagination/bl-pagination'; +export { default as BlDialog } from './components/dialog/bl-dialog'; export { getIconPath, setIconPath } from './utilities/asset-paths'; diff --git a/src/components/dialog/bl-dialog.css b/src/components/dialog/bl-dialog.css new file mode 100644 index 00000000..82c86947 --- /dev/null +++ b/src/components/dialog/bl-dialog.css @@ -0,0 +1,77 @@ +dialog { + padding: 0; + border: 0; + 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; +} + +dialog::backdrop { + background: #273142; + opacity: 0.7; +} + +dialog .container { + min-width: 424px; + min-height: 178px; +} + +dialog header { + display: flex; + justify-content: space-between; + align-items: center; + gap: var(--bl-size-2xs); + padding: var(--bl-size-xl) var(--bl-size-xl) 0 var(--bl-size-xl); +} + +dialog header bl-button { + margin-left: auto; +} + +dialog header h2 { + font: var(--bl-font-title-1-medium); + color: var(--bl-color-secondary); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + margin: 0; + padding: 0; +} + +dialog .content { + padding: var(--bl-size-xl) var(--bl-size-xl) var(--bl-size-m) var(--bl-size-xl); + overflow: auto; +} + +dialog 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); + box-shadow: 0 -4px 15px #27314226; +} + +@media only screen and (max-width: 469px) { + dialog { + 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 { + flex-flow: column wrap; + } +} diff --git a/src/components/dialog/bl-dialog.stories.mdx b/src/components/dialog/bl-dialog.stories.mdx new file mode 100644 index 00000000..afcfd0e2 --- /dev/null +++ b/src/components/dialog/bl-dialog.stories.mdx @@ -0,0 +1,181 @@ +import { html } from 'lit'; +import { ifDefined } from 'lit/directives/if-defined.js'; +import { styleMap } from 'lit/directives/style-map.js'; +import { unsafeHTML } from 'lit/directives/unsafe-html.js'; +import { Meta, Canvas, ArgsTable, Story, Preview, Source } from '@storybook/addon-docs'; +import { userEvent } from '@storybook/testing-library'; + + + +export const dialogOpener = async (event,dialogClass) => { + const target= event.target; + const isCanvas = !event.target || target.parentNode.parentNode.getAttribute("id") === "root"; + let selector=`#docs-root .${dialogClass}`; + if(isCanvas){ + selector = `#root .${dialogClass}`; + } + const dialog = document.querySelector(selector); + dialog.setAttribute("open",true); +} + +export const closeAllDialog = async () => { + const dialog = document.querySelector(`#root bl-dialog[open]`); + dialog?.removeAttribute("open"); +} + +export const BasicTemplate = (args) => html` +Open Dialog + + Let us help determine location. This means sending anonymous location data to us. + Agree + Disagree + Cancel + + +` + +export const TemplateWithStickyFooter = (args) => html` +Open Dialog + + Please read all terms and conditions. +

Lorem ipsum dolor sit amet

+

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt
ut labore et dolore magna aliqua. + Ut enim ad minim veniam, quis nostrud exercitation
ullamco laboris nisi ut aliquip ex ea commodo consequat.

+
    +
  • At vero eos et accusamus et iusto odio dignissimos ducimus qui blanditiis
  • +
  • Et harum quidem rerum facilis est et expedita distinctio. Nam libero tempore.
  • +
  • Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit
  • +
  • Et harum quidem rerum facilis est et expedita distinctio. Nam libero tempore.
  • +
  • At vero eos et accusamus et iusto odio dignissimos ducimus qui blanditiis
  • +
  • Et harum quidem rerum facilis est et expedita distinctio. Nam libero tempore.
  • +
  • Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit
  • +
  • Et harum quidem rerum facilis est et expedita distinctio. Nam libero tempore.
  • +
+

The standard chunk of Lorem Ipsum used since the 1500s is reproduced
below for those interested. + Sections 1.10.32 and 1.10.33 from "de Finibus Bonorum et
Malorum" by Cicero are also reproduced in their exact original form,
+ accompanied by English versions from the 1914 translation by H. Rackham.

+

Quis autem vel eum iure reprehenderit qui

+

Lorem Ipsum is simply dummy text of the printing and typesetting industry.
+ Lorem Ipsum has been the industry's standard dummy text ever since the 1500s,
when an unknown printer took a galley of type and scrambled
+ it to make a type specimen book.

+

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt
ut labore et dolore magna aliqua. + Ut enim ad minim veniam, quis nostrud exercitation
ullamco laboris nisi ut aliquip ex ea commodo consequat.

+

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt
ut labore et dolore magna aliqua. + Ut enim ad minim veniam, quis nostrud exercitation
ullamco laboris nisi ut aliquip ex ea commodo consequat.

+
    +
  • At vero eos et accusamus et iusto odio dignissimos ducimus qui blanditiis
  • +
  • Et harum quidem rerum facilis est et expedita distinctio. Nam libero tempore.
  • +
  • Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit
  • +
  • Et harum quidem rerum facilis est et expedita distinctio. Nam libero tempore.
  • +
  • At vero eos et accusamus et iusto odio dignissimos ducimus qui blanditiis
  • +
  • Et harum quidem rerum facilis est et expedita distinctio. Nam libero tempore.
  • +
  • Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit
  • +
  • Et harum quidem rerum facilis est et expedita distinctio. Nam libero tempore.
  • +
+

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt
ut labore et dolore magna aliqua. + Ut enim ad minim veniam, quis nostrud exercitation
ullamco laboris nisi ut aliquip ex ea commodo consequat.
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt
ut labore et dolore magna aliqua. + Ut enim ad minim veniam, quis nostrud exercitation
ullamco laboris nisi ut aliquip ex ea commodo consequat.
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt
ut labore et dolore magna aliqua. + Ut enim ad minim veniam, quis nostrud exercitation
ullamco laboris nisi ut aliquip ex ea commodo consequat.

+

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt
ut labore et dolore magna aliqua. + Ut enim ad minim veniam, quis nostrud exercitation
ullamco laboris nisi ut aliquip ex ea commodo consequat.
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt
ut labore et dolore magna aliqua. + Ut enim ad minim veniam, quis nostrud exercitation
ullamco laboris nisi ut aliquip ex ea commodo consequat.
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt
ut labore et dolore magna aliqua. + Ut enim ad minim veniam, quis nostrud exercitation
ullamco laboris nisi ut aliquip ex ea commodo consequat.

+ Got It + Cancel +
+` + +export const SizingTemplate = (args) => html` +Open Dialog + +

Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, + when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, + remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing + software like Aldus PageMaker including versions of Lorem Ipsum. Let us help determine location. This means sending anonymous location data to us. + Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots in a piece of classical Latin literature from 45 BC, making it over 2000 years old. Richard McClintock, + a Latin professor at Hampden-Sydney College in Virginia, looked up one of the more obscure Latin words, consectetur, from a Lorem Ipsum passage, and going through the cites + of the word in classical literature, discovered the undoubtable source. Lorem Ipsum comes from sections 1.10.32 and 1.10.33 of "de Finibus Bonorum et Malorum" + (The Extremes of Good and Evil) by Cicero, written in 45 BC. This book is a treatise on the theory of ethics, very popular during the Renaissance. The first line of Lorem Ipsum, + "Lorem ipsum dolor sit amet..", comes from a line in section 1.10.32. +

+ Agree + Disagree + Cancel +
+` + +# Dialog + +Dialogs inform users about a task ansd can contain critical information, require decisions, or involve multiple tasks. + +Inline styles in examples are only for **demo purposes**. Use regular CSS classes or tag selectors to set styles. + +### Design Rules + +* By default a dialog contains a close button. +* A dialog should contain at least one content (text, image etc.). +* Dialogs are always centered on the page, with an overlay behind them that hides the page content. +* Only large buttons can be used in the action bar and there can be maximum 3 buttons (**primary**, **secondary** and **tertiary**). +To maintain usability level of dialog component: +* When the dialog sticks to the page edges and does not fit in its minimum size, it switches to mobile view and the buttons are lined up one after. +* Dialog can be dismissed by clicking backdrop or pressing Esc. + + +## Basic Usage + + + dialogOpener(event,"basic-dialog")} + args={{ caption: "Use location service?"}}> + {BasicTemplate.bind({})} + + + +## Dialog With Sticky Footer + +For long content that does not fit on the page, the dialog action area remains sticky at the bottom of the dialog so that it appears on the page. + + + dialogOpener(event,"dialog-with-sticky-footer")} + args={{ caption: "Terms And Conditions"}}> + {TemplateWithStickyFooter.bind({})} + + + +## Dialog Sizing + +The dialog doesn't have any size, it will be fluidly sized regarding its content. You can give your own width and height style to your content. + + + dialogOpener(event,"dialog-sizing")}> + {SizingTemplate.bind({})} + + + +## Reference + + diff --git a/src/components/dialog/bl-dialog.test.ts b/src/components/dialog/bl-dialog.test.ts new file mode 100644 index 00000000..2e2132bb --- /dev/null +++ b/src/components/dialog/bl-dialog.test.ts @@ -0,0 +1,215 @@ +import { + assert, + fixture, + html, + oneEvent, + expect, + fixtureCleanup, +} from '@open-wc/testing'; +import { sendKeys, sendMouse, resetMouse } from '@web/test-runner-commands'; +import BlDialog from './bl-dialog'; + +import type typeOfBlDialog from './bl-dialog'; + +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 +
`); + + 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 the close btn is clicked', async () => { + const el = await fixture(html` +
My Content
+
`); + + const closeBtn = el?.shadowRoot?.querySelector('bl-button'); + + expect(closeBtn).to.exist; + expect(el.open).to.equal(true); + + setTimeout(() => { + closeBtn?.click(); + expect(el.open).to.equal(false); + fixtureCleanup(); + }); + }); + + 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]); + fixtureCleanup(); + } + }); + + 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(); + }); + + 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. +

+

+ Contrary to popular belief, Lorem Ipsum is not simply random text., comes from a line in + section 1.10.32. +

+

+ Contrary to popular belief, Lorem Ipsum is not simply random text., comes from a line in + section 1.10.32. +

+

+ Contrary to popular belief, Lorem Ipsum is not simply random text., comes from a line in + section 1.10.32. +

+

+ Contrary to popular belief, Lorem Ipsum is not simply random text., comes from a line in + section 1.10.32. +

+

+ Contrary to popular belief, Lorem Ipsum is not simply random text., comes from a line in + section 1.10.32. +

+

+ Contrary to popular belief, Lorem Ipsum is not simply random text., comes from a line in + section 1.10.32. +

+

+ Contrary to popular belief, Lorem Ipsum is not simply random text., comes from a line in + section 1.10.32. +

+

+ Contrary to popular belief, Lorem Ipsum is not simply random text., comes from a line in + section 1.10.32. +

+

+ Contrary to popular belief, Lorem Ipsum is not simply random text., comes from a line in + section 1.10.32. +

+

+ Contrary to popular belief, Lorem Ipsum is not simply random text., comes from a line in + section 1.10.32. +

+

+ Contrary to popular belief, Lorem Ipsum is not simply random text., comes from a line in + section 1.10.32. +

+

+ Contrary to popular belief, Lorem Ipsum is not simply random text., comes from a line in + section 1.10.32. +

+

+ Contrary to popular belief, Lorem Ipsum is not simply random text., comes from a line in + section 1.10.32. +

+

+ Contrary to popular belief, Lorem Ipsum is not simply random text., comes from a line in + section 1.10.32. +

+

+ Contrary to popular belief, Lorem Ipsum is not simply random text., comes from a line in + section 1.10.32. +

+ Primary + Secondary +
+ `); + + const footer = el?.shadowRoot?.querySelector('footer') as HTMLElement; + + expect(footer.className).to.oneOf(['sticky','']); + }); + + 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); + }); + + 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); + }); + }); + }); +}); diff --git a/src/components/dialog/bl-dialog.ts b/src/components/dialog/bl-dialog.ts new file mode 100644 index 00000000..0b2bea43 --- /dev/null +++ b/src/components/dialog/bl-dialog.ts @@ -0,0 +1,177 @@ +import { CSSResultGroup, html, LitElement, PropertyValues, TemplateResult } from 'lit'; +import { customElement, property, query } from 'lit/decorators.js'; +import { event, EventDispatcher } from '../../utilities/event'; +import '../button/bl-button'; +import style from './bl-dialog.css'; + +/** + * @tag bl-dialog + * @summary Baklava Dialog component + */ + +type DialogElement = { + showModal: () => void; + close: () => void; +}; + +@customElement('bl-dialog') +export default class BlDialog extends LitElement { + static get styles(): CSSResultGroup { + return [style]; + } + + /** + * Sets dialog open-close status + */ + @property({ type: Boolean, reflect: true }) + open = false; + + /** + * Sets the dialog title + */ + @property({ type: String }) + caption?: string; + + @query('dialog') + dialog: HTMLDialogElement & DialogElement; + + @query('footer') + footer: HTMLElement; + + @query('.container') + container: HTMLElement; + + @query('.content') + content: HTMLElement; + + /** + * Fires when the dialog is opened + */ + @event('bl-dialog-open') private onOpen: EventDispatcher; + + /** + * Fires when the dialog is closed + */ + @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 toggleDialogHandler() { + if (this.open) { + this.dialog.showModal?.(); + this.onOpen({ isOpen: true }); + document.body.style.overflow = 'hidden'; + } else { + this.dialog.close?.(); + this.onClose({ isOpen: false }); + document.body.style.overflow = 'auto'; + } + + this.resizeHandler(); + } + + clickOutsideHandler = (event: MouseEvent) => { + const eventPath = event.composedPath() as HTMLElement[]; + + if (!eventPath.includes(this.container)) { + this.closeDialog(); + } + }; + + private closeDialog() { + this.open = false; + } + + private onKeydown = (event: KeyboardEvent): void => { + if (event.code === 'Escape' && this.open) { + this.closeDialog(); + } + }; + + private toggleFooterSticky() { + if (this.content?.scrollHeight > this.content?.offsetHeight) { + this.footer?.classList?.add('sticky'); + } else { + this.footer?.classList?.remove('sticky'); + } + } + + 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`
+ + + +
` + : ''; + } + + render(): TemplateResult { + const title = this.caption ? html`

${this.caption}

` : ''; + + return html` + +
+
+ ${title} + +
+
+ ${this.renderFooter()} +
+
+ `; + } +} + +declare global { + interface HTMLElementTagNameMap { + 'bl-dialog': BlDialog; + } +}