From f3fc1bbe47362a88a0754324160f1e25f2fccc65 Mon Sep 17 00:00:00 2001 From: Matheus Rosa <106702647+matheus-rosa-jsm@users.noreply.github.com> Date: Fri, 16 Aug 2024 13:37:36 -0300 Subject: [PATCH 1/8] feat(modal): add modal component (#534) --- apps/docs-react/.storybook/preview.ts | 2 +- apps/docs-vue/.storybook/preview.ts | 2 +- apps/docs/.storybook/preview.ts | 2 +- packages/core/src/components.d.ts | 63 ++++++- .../core/src/components/button/button.spec.ts | 10 +- .../core/src/components/button/button.tsx | 7 +- packages/core/src/components/modal/modal.scss | 93 +++++++++++ .../core/src/components/modal/modal.spec.ts | 132 +++++++++++++++ packages/core/src/components/modal/modal.tsx | 154 ++++++++++++++++++ .../components/modal/stories/modal.args.ts | 146 +++++++++++++++++ .../modal/stories/modal.core.stories.tsx | 77 +++++++++ .../modal/stories/modal.react.stories.tsx | 75 +++++++++ .../modal/stories/modal.vue.stories.tsx | 78 +++++++++ .../{ => templates}/DocumentationTemplate.mdx | 0 .../templates/DocumentationWithoutStories.mdx | 20 +++ 15 files changed, 850 insertions(+), 11 deletions(-) create mode 100644 packages/core/src/components/modal/modal.scss create mode 100644 packages/core/src/components/modal/modal.spec.ts create mode 100644 packages/core/src/components/modal/modal.tsx create mode 100644 packages/core/src/components/modal/stories/modal.args.ts create mode 100644 packages/core/src/components/modal/stories/modal.core.stories.tsx create mode 100644 packages/core/src/components/modal/stories/modal.react.stories.tsx create mode 100644 packages/core/src/components/modal/stories/modal.vue.stories.tsx rename utils/storybook/{ => templates}/DocumentationTemplate.mdx (100%) create mode 100644 utils/storybook/templates/DocumentationWithoutStories.mdx diff --git a/apps/docs-react/.storybook/preview.ts b/apps/docs-react/.storybook/preview.ts index 6689d19e9..50f2931c6 100644 --- a/apps/docs-react/.storybook/preview.ts +++ b/apps/docs-react/.storybook/preview.ts @@ -1,5 +1,5 @@ import { CustomViewports } from '@atomium/storybook-utils/custom-viewports' -import DocumentationTemplate from '@atomium/storybook-utils/DocumentationTemplate.mdx' +import DocumentationTemplate from '@atomium/storybook-utils/templates/DocumentationTemplate.mdx' import '@atomium/storybook-utils/preview.css' diff --git a/apps/docs-vue/.storybook/preview.ts b/apps/docs-vue/.storybook/preview.ts index 2eef9c7f6..d33577b66 100644 --- a/apps/docs-vue/.storybook/preview.ts +++ b/apps/docs-vue/.storybook/preview.ts @@ -1,5 +1,5 @@ import { CustomViewports } from '@atomium/storybook-utils/custom-viewports' -import DocumentationTemplate from '@atomium/storybook-utils/DocumentationTemplate.mdx' +import DocumentationTemplate from '@atomium/storybook-utils/templates/DocumentationTemplate.mdx' import '@atomium/storybook-utils/preview.css' diff --git a/apps/docs/.storybook/preview.ts b/apps/docs/.storybook/preview.ts index 0c521b403..1c8473a60 100644 --- a/apps/docs/.storybook/preview.ts +++ b/apps/docs/.storybook/preview.ts @@ -1,7 +1,7 @@ import { defineCustomElements } from '@juntossomosmais/atomium/loader' import { CustomViewports } from '@atomium/storybook-utils/custom-viewports' -import DocumentationTemplate from '@atomium/storybook-utils/DocumentationTemplate.mdx' +import DocumentationTemplate from '@atomium/storybook-utils/templates/DocumentationTemplate.mdx' import '@atomium/storybook-utils/preview.css' diff --git a/packages/core/src/components.d.ts b/packages/core/src/components.d.ts index a838b79e1..b2af27881 100644 --- a/packages/core/src/components.d.ts +++ b/packages/core/src/components.d.ts @@ -38,7 +38,10 @@ export namespace Components { "disabled"?: boolean; "download"?: string; "expand"?: 'block'; - "fill": 'clear' | 'outline' | 'outline-filled' | 'solid'; + "fill": | 'clear' + | 'outline' + | 'outline-filled' + | 'solid'; "href"?: string; "loading"?: boolean; "mode": Mode; @@ -154,6 +157,16 @@ export namespace Components { } interface AtomListSliderItem { } + interface AtomModal { + "alertType"?: 'alert' | 'error'; + "hasDivider"?: boolean; + "hasFooter"?: boolean; + "headerTitle"?: string; + "primaryText"?: string; + "progress"?: number; + "secondaryText"?: string; + "trigger"?: string; + } interface AtomSelect { "color"?: 'primary' | 'secondary' | 'danger'; "disabled"?: boolean; @@ -260,6 +273,10 @@ export interface AtomListSliderCustomEvent extends CustomEvent { detail: T; target: HTMLAtomListSliderElement; } +export interface AtomModalCustomEvent extends CustomEvent { + detail: T; + target: HTMLAtomModalElement; +} export interface AtomSelectCustomEvent extends CustomEvent { detail: T; target: HTMLAtomSelectElement; @@ -430,6 +447,27 @@ declare global { prototype: HTMLAtomListSliderItemElement; new (): HTMLAtomListSliderItemElement; }; + interface HTMLAtomModalElementEventMap { + "atomCloseClick": any; + "atomDidDismiss": any; + "atomDidPresent": any; + "atomPrimaryClick": any; + "atomSecondaryClick": any; + } + interface HTMLAtomModalElement extends Components.AtomModal, HTMLStencilElement { + addEventListener(type: K, listener: (this: HTMLAtomModalElement, ev: AtomModalCustomEvent) => any, options?: boolean | AddEventListenerOptions): void; + addEventListener(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void; + addEventListener(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void; + addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void; + removeEventListener(type: K, listener: (this: HTMLAtomModalElement, ev: AtomModalCustomEvent) => any, options?: boolean | EventListenerOptions): void; + removeEventListener(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | EventListenerOptions): void; + removeEventListener(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | EventListenerOptions): void; + removeEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void; + } + var HTMLAtomModalElement: { + prototype: HTMLAtomModalElement; + new (): HTMLAtomModalElement; + }; interface HTMLAtomSelectElementEventMap { "atomBlur": void; "atomCancel": void; @@ -493,6 +531,7 @@ declare global { "atom-link": HTMLAtomLinkElement; "atom-list-slider": HTMLAtomListSliderElement; "atom-list-slider-item": HTMLAtomListSliderItemElement; + "atom-modal": HTMLAtomModalElement; "atom-select": HTMLAtomSelectElement; "atom-tag": HTMLAtomTagElement; "atom-textarea": HTMLAtomTextareaElement; @@ -527,7 +566,10 @@ declare namespace LocalJSX { "disabled"?: boolean; "download"?: string; "expand"?: 'block'; - "fill"?: 'clear' | 'outline' | 'outline-filled' | 'solid'; + "fill"?: | 'clear' + | 'outline' + | 'outline-filled' + | 'solid'; "href"?: string; "loading"?: boolean; "mode"?: Mode; @@ -649,6 +691,21 @@ declare namespace LocalJSX { } interface AtomListSliderItem { } + interface AtomModal { + "alertType"?: 'alert' | 'error'; + "hasDivider"?: boolean; + "hasFooter"?: boolean; + "headerTitle"?: string; + "onAtomCloseClick"?: (event: AtomModalCustomEvent) => void; + "onAtomDidDismiss"?: (event: AtomModalCustomEvent) => void; + "onAtomDidPresent"?: (event: AtomModalCustomEvent) => void; + "onAtomPrimaryClick"?: (event: AtomModalCustomEvent) => void; + "onAtomSecondaryClick"?: (event: AtomModalCustomEvent) => void; + "primaryText"?: string; + "progress"?: number; + "secondaryText"?: string; + "trigger"?: string; + } interface AtomSelect { "color"?: 'primary' | 'secondary' | 'danger'; "disabled"?: boolean; @@ -752,6 +809,7 @@ declare namespace LocalJSX { "atom-link": AtomLink; "atom-list-slider": AtomListSlider; "atom-list-slider-item": AtomListSliderItem; + "atom-modal": AtomModal; "atom-select": AtomSelect; "atom-tag": AtomTag; "atom-textarea": AtomTextarea; @@ -776,6 +834,7 @@ declare module "@stencil/core" { "atom-link": LocalJSX.AtomLink & JSXBase.HTMLAttributes; "atom-list-slider": LocalJSX.AtomListSlider & JSXBase.HTMLAttributes; "atom-list-slider-item": LocalJSX.AtomListSliderItem & JSXBase.HTMLAttributes; + "atom-modal": LocalJSX.AtomModal & JSXBase.HTMLAttributes; "atom-select": LocalJSX.AtomSelect & JSXBase.HTMLAttributes; "atom-tag": LocalJSX.AtomTag & JSXBase.HTMLAttributes; "atom-textarea": LocalJSX.AtomTextarea & JSXBase.HTMLAttributes; diff --git a/packages/core/src/components/button/button.spec.ts b/packages/core/src/components/button/button.spec.ts index 1e7fc3d8b..2c0027d2f 100644 --- a/packages/core/src/components/button/button.spec.ts +++ b/packages/core/src/components/button/button.spec.ts @@ -25,7 +25,7 @@ describe('AtomButton', () => { expect(page.root).toEqualHtml(` - + @@ -47,7 +47,7 @@ describe('AtomButton', () => { expect(page.root).toEqualHtml(` - + @@ -69,7 +69,7 @@ describe('AtomButton', () => { await page.waitForChanges() expect(page.root?.shadowRoot).toEqualHtml(` - + @@ -89,7 +89,7 @@ describe('AtomButton', () => { await page.waitForChanges() expect(page.root?.shadowRoot).toEqualHtml(` - + @@ -106,7 +106,7 @@ describe('AtomButton', () => { await page.waitForChanges() expect(page.root?.shadowRoot).toEqualHtml(` - + diff --git a/packages/core/src/components/button/button.tsx b/packages/core/src/components/button/button.tsx index 276047f7a..60cfff316 100644 --- a/packages/core/src/components/button/button.tsx +++ b/packages/core/src/components/button/button.tsx @@ -19,7 +19,11 @@ export class AtomButton { @Prop({ mutable: true }) disabled?: boolean @Prop({ mutable: true }) download?: string @Prop({ mutable: true }) expand?: 'block' - @Prop({ mutable: true }) fill: 'clear' | 'outline' | 'outline-filled' | 'solid' = 'solid' + @Prop({ mutable: true }) fill: + | 'clear' + | 'outline' + | 'outline-filled' + | 'solid' = 'solid' @Prop({ mutable: true }) shape?: 'round' | 'circle' = 'round' @Prop({ mutable: true }) href?: string @Prop({ mutable: true }) loading?: boolean @@ -89,6 +93,7 @@ export class AtomButton { target={this.target} download={this.download} onClick={this.handleClick.bind(this)} + part='button' > {this.loading && ( diff --git a/packages/core/src/components/modal/modal.scss b/packages/core/src/components/modal/modal.scss new file mode 100644 index 000000000..34a1dbe51 --- /dev/null +++ b/packages/core/src/components/modal/modal.scss @@ -0,0 +1,93 @@ +@import '~@atomium/scss-utils/index'; + +.atom-modal { + &__close { + position: absolute; + right: var(--spacing-small); + top: var(--spacing-small); + } + + &__close-icon { + color: var(--color-neutral-regular); + font-size: var(--spacing-large); + } + + &__content { + color: var(--color-neutral-light-1); + font: var(--text-body-medium); + height: 100%; + letter-spacing: var(--text-body-medium-letter); + padding: var(--spacing-large); + padding-top: 0; + + &--divided { + border-bottom: 1px solid var(--color-neutral-light-3); + border-top: 1px solid var(--color-neutral-light-3); + padding-top: var(--spacing-large); + } + } + + &__footer { + display: flex; + flex-wrap: wrap; + gap: var(--spacing-medium); + justify-content: flex-end; + padding: var(--spacing-large); + } + + &__header { + color: var(--color-neutral-light-1); + padding: var(--spacing-large); + position: relative; + } + + &__icon-type { + font-size: var(--spacing-xxxxlarge); + margin-bottom: var(--spacing-large); + + &--warning { + color: var(--color-contextual-warning-dark-1); + } + + &--error { + color: var(--color-contextual-error-regular); + } + } + + &__title, + &__header { + font: var(--title-headline-xsmall); + letter-spacing: var(--title-headline-xsmall-letter); + margin: 0; + } + + &--progress { + .atom-modal__content--divided { + border-top: 0; + } + } + + ion-progress-bar::part(track) { + background-color: var(--color-brand-primary-light-2); + } + + @include below(small) { + &__btn-action { + width: 100%; + + &::part(button) { + width: 100%; + } + } + } +} + +ion-modal::part(content) { + --border-radius: 8px; + height: auto; + + @include below(small) { + --border-radius: 0; + height: 100%; + } +} diff --git a/packages/core/src/components/modal/modal.spec.ts b/packages/core/src/components/modal/modal.spec.ts new file mode 100644 index 000000000..303b93887 --- /dev/null +++ b/packages/core/src/components/modal/modal.spec.ts @@ -0,0 +1,132 @@ +import { newSpecPage, SpecPage } from '@stencil/core/testing' + +import { AtomModal } from './modal' + +describe('atom-modal', () => { + let page: SpecPage + + beforeEach(async () => { + page = await newSpecPage({ + components: [AtomModal], + html: ` + + Modal content + `, + }) + }) + + it('should render modal with default values', async () => { + expect(page.root).toEqualHtml(` + + +
+
+ + + +
+
+ Modal content +
+
+ + Secondary + + + Primary + +
+
+
+ `) + }) + + it('should render header slot when headerTitle is not passed', async () => { + await page.setContent(` + + Modal content +
Custom Header
+
+ `) + + expect(page.root?.textContent).toContain('Custom Header') + }) + + it('should render header from prop when headerTitle is passed', async () => { + await page.setContent(` + + Modal content +
Custom Header
+
+ `) + + expect(page.root?.innerHTML).not.toContain('