From 0afc349177d051e799d3b06dc56176419256b16f Mon Sep 17 00:00:00 2001 From: BogdanDanilescu <153850559+BogdanDanilescu@users.noreply.github.com> Date: Fri, 22 Nov 2024 16:32:16 +0200 Subject: [PATCH] feat: html toast component (#335) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: “BogdanDanilescu” --- .../3-components/2-library/2-alert.mdx | 2 +- .../3-components/2-library/45-toast.mdx | 172 ++++++++++++++++++ .../{45-tooltip.mdx => 46-tooltip.mdx} | 0 ...6-warning-text.mdx => 47-warning-text.mdx} | 0 .../{47-spinner.mdx => 48-spinner.mdx} | 0 .../src/components/document/common/mdx.tsx | 2 + apps/docs/src/lib/components.ts | 20 ++ examples/wagtail/core/jinja/home_page.jinja | 17 ++ packages/design/tailwind/css/components.css | 2 +- packages/html/ds/package.json | 2 +- packages/html/ds/src/common/instances.ts | 2 + packages/html/ds/src/index.ts | 2 + .../html/ds/src/toast/assets/notyf.min.css | 1 + .../html/ds/src/toast/assets/notyf.min.js | 1 + packages/html/ds/src/toast/ds-toast.html | 55 ++++++ packages/html/ds/src/toast/helpers.html | 31 ++++ packages/html/ds/src/toast/toast.html | 22 +++ packages/html/ds/src/toast/toast.schema.ts | 53 ++++++ packages/html/ds/src/toast/toast.stories.ts | 116 ++++++++++++ packages/html/ds/src/toast/toast.test.ts | 70 +++++++ packages/html/ds/src/toast/toast.ts | 94 ++++++++++ packages/html/ds/styles.css | 2 + packages/react/ds/src/toast/toast.test.tsx | 4 +- pnpm-lock.yaml | 41 +---- 24 files changed, 672 insertions(+), 39 deletions(-) create mode 100644 apps/docs/content/3-components/2-library/45-toast.mdx rename apps/docs/content/3-components/2-library/{45-tooltip.mdx => 46-tooltip.mdx} (100%) rename apps/docs/content/3-components/2-library/{46-warning-text.mdx => 47-warning-text.mdx} (100%) rename apps/docs/content/3-components/2-library/{47-spinner.mdx => 48-spinner.mdx} (100%) create mode 100644 packages/html/ds/src/toast/assets/notyf.min.css create mode 100644 packages/html/ds/src/toast/assets/notyf.min.js create mode 100644 packages/html/ds/src/toast/ds-toast.html create mode 100644 packages/html/ds/src/toast/helpers.html create mode 100644 packages/html/ds/src/toast/toast.html create mode 100644 packages/html/ds/src/toast/toast.schema.ts create mode 100644 packages/html/ds/src/toast/toast.stories.ts create mode 100644 packages/html/ds/src/toast/toast.test.ts create mode 100644 packages/html/ds/src/toast/toast.ts diff --git a/apps/docs/content/3-components/2-library/2-alert.mdx b/apps/docs/content/3-components/2-library/2-alert.mdx index 419c5ac0f..acf6251f5 100644 --- a/apps/docs/content/3-components/2-library/2-alert.mdx +++ b/apps/docs/content/3-components/2-library/2-alert.mdx @@ -5,7 +5,7 @@ draft: false status: stable --- -# Actions +# Alert diff --git a/apps/docs/content/3-components/2-library/45-toast.mdx b/apps/docs/content/3-components/2-library/45-toast.mdx new file mode 100644 index 000000000..04afb561d --- /dev/null +++ b/apps/docs/content/3-components/2-library/45-toast.mdx @@ -0,0 +1,172 @@ +--- +title: Toast +description: Toast HTML Component +draft: false +status: stable +--- + +# Toast + + + +## Toast without trigger + +This toast will automatically appear when the page loads. \ +*Refresh the current to see the toast being rendered* + + + + + + Macro + React + + + ```html + {{ govieToast({ + "title": "Toast", + "description": "This toast has rendered on page load", + }) }} + ``` + + + ```react + import { Toast } from '@govie-ds/react'; + + + ``` + + +## Info Toast with trigger +Trigger Toast}/> + + + + Macro + React + + + ```html + {{ govieToast({ + "title": "Toast Triggered", + "description": "This is an info toast", + "trigger": { + "content": "Trigger Toast" + } + }) }} + ``` + + + ```react + import { Toast } from '@govie-ds/react'; + + Trigger Toast} + /> + ``` + + +## Success Toast with trigger +Trigger Toast}/> + + + + Macro + React + + + ```html + {{ govieToast({ + "title": "Toast Triggered", + "description": "This is a success toast", + "variant": "success", + "trigger": { + "content": "Trigger Toast" + } + }) }} + ``` + + + ```react + import { Toast } from '@govie-ds/react'; + + Trigger Toast} + /> + ``` + + +## Error Toast with trigger +Trigger Toast}/> + + + + Macro + React + + + ```html + {{ govieToast({ + "title": "Toast Triggered", + "description": "This is an error toast", + "variant": "danger", + "trigger": { + "content": "Trigger Toast" + } + }) }} + ``` + + + ```react + import { Toast } from '@govie-ds/react'; + + Trigger Toast} + /> + ``` + + +## Warning Toast with trigger +Trigger Toast}/> + + + + Macro + React + + + ```html + {{ govieToast({ + "title": "Toast Triggered", + "description": "This is a warning toast", + "variant": "warning", + "trigger": { + "content": "Trigger Toast" + } + }) }} + ``` + + + ```react + import { Toast } from '@govie-ds/react'; + + Trigger Toast} + /> + ``` + + \ No newline at end of file diff --git a/apps/docs/content/3-components/2-library/45-tooltip.mdx b/apps/docs/content/3-components/2-library/46-tooltip.mdx similarity index 100% rename from apps/docs/content/3-components/2-library/45-tooltip.mdx rename to apps/docs/content/3-components/2-library/46-tooltip.mdx diff --git a/apps/docs/content/3-components/2-library/46-warning-text.mdx b/apps/docs/content/3-components/2-library/47-warning-text.mdx similarity index 100% rename from apps/docs/content/3-components/2-library/46-warning-text.mdx rename to apps/docs/content/3-components/2-library/47-warning-text.mdx diff --git a/apps/docs/content/3-components/2-library/47-spinner.mdx b/apps/docs/content/3-components/2-library/48-spinner.mdx similarity index 100% rename from apps/docs/content/3-components/2-library/47-spinner.mdx rename to apps/docs/content/3-components/2-library/48-spinner.mdx diff --git a/apps/docs/src/components/document/common/mdx.tsx b/apps/docs/src/components/document/common/mdx.tsx index 4f9bd4d63..c3c1f511b 100644 --- a/apps/docs/src/components/document/common/mdx.tsx +++ b/apps/docs/src/components/document/common/mdx.tsx @@ -29,6 +29,7 @@ import { Stack, Pagination, Alert, + Toast, } from '@govie-ds/react'; import { MDXComponents } from 'mdx/types'; import { useMDXComponent } from 'next-contentlayer/hooks'; @@ -194,6 +195,7 @@ const documentComponents: MDXComponents = { Stack: (props) => , Pagination: (props) => , Alert: (props) => , + Toast: (props) => , }; export function Mdx({ code }: MdxProps) { diff --git a/apps/docs/src/lib/components.ts b/apps/docs/src/lib/components.ts index 94bc07ca0..bc69ba694 100644 --- a/apps/docs/src/lib/components.ts +++ b/apps/docs/src/lib/components.ts @@ -41,6 +41,26 @@ const localHtmlStorybookBaseUrl = export function getComponents(): ComponentDetail[] { let components: ComponentDetail[] = [ + { + id: 'toast', + name: 'Toast', + statuses: [ + { + platform: { + id: 'global', + href: '?path=/docs/application-toast--docs', + }, + status: 'beta', + }, + { + platform: { + id: 'react', + href: '?path=//docs/application-toast--docs', + }, + status: 'beta', + }, + ], + }, { id: 'alert', name: 'Alert', diff --git a/examples/wagtail/core/jinja/home_page.jinja b/examples/wagtail/core/jinja/home_page.jinja index d044e71f2..96e0b8ea6 100644 --- a/examples/wagtail/core/jinja/home_page.jinja +++ b/examples/wagtail/core/jinja/home_page.jinja @@ -20,6 +20,7 @@ {% from 'list/list.html' import govieList %} {% from 'combo-box/combo-box.html' import govieComboBox %} {% from 'alert/alert.html' import govieAlert %} +{% from 'toast/toast.html' import govieToast %} {% from 'vars.jinja' import comboBoxProps, cookieBannerProps, modalProps, footerProps, HeaderProps, SelectProps, tabsProps, radioProps, listProps %} @@ -195,6 +196,22 @@ {{ govieModal(modalProps) }} +

Toast with Trigger

+ + {{ govieToast({ + "title": "Toast Triggered", + "description": "This is some content", + "trigger": { + "content": "Trigger Toast" + } + }) }} + + {{ govieToast({ + "title": "Success", + "description": "This is a success toast", + "variant": "success" + }) }} +

Icon Button

{{govieIconButton({ diff --git a/packages/design/tailwind/css/components.css b/packages/design/tailwind/css/components.css index 96516485b..2db2ef38d 100644 --- a/packages/design/tailwind/css/components.css +++ b/packages/design/tailwind/css/components.css @@ -836,7 +836,7 @@ input[type='file' i] { @apply !gi-gap-6 } .notyf__toast { - @apply !gi-m-0 !gi-p-0 !gi-min-w-[340px] !gi-max-w-[460px] !gi-overflow-auto; + @apply !gi-m-0 !gi-p-0 !gi-min-w-[340px] !gi-max-w-[460px]; animation-delay: 0s !important; } .notyf__message { diff --git a/packages/html/ds/package.json b/packages/html/ds/package.json index b8dd6d29a..4febb2518 100644 --- a/packages/html/ds/package.json +++ b/packages/html/ds/package.json @@ -38,9 +38,9 @@ "license": "MIT", "devDependencies": { "@govie-ds/eslint-config": "workspace:*", + "@govie-ds/macro": "workspace:*", "@govie-ds/prettier-config": "workspace:*", "@govie-ds/tailwind": "workspace:*", - "@govie-ds/macro": "workspace:*", "@testing-library/dom": "^10.4.0", "@testing-library/jest-dom": "^6.4.8", "@testing-library/user-event": "^14.5.2", diff --git a/packages/html/ds/src/common/instances.ts b/packages/html/ds/src/common/instances.ts index 0e8a27e28..a73c2911e 100644 --- a/packages/html/ds/src/common/instances.ts +++ b/packages/html/ds/src/common/instances.ts @@ -7,6 +7,7 @@ import { Modal } from '../modal/modal'; import { Radio } from '../radio/radio'; import { Tabs } from '../tabs/tabs'; import { Textarea } from '../textarea/textarea'; +import { Toast } from '../toast/toast'; import { BaseComponent, BaseComponentOptions } from './component'; function generateRandomId() { @@ -23,6 +24,7 @@ const componentRegistry = { Tabs, ComboBox, Alert, + Toast, // TODO: additional component classes } as const; diff --git a/packages/html/ds/src/index.ts b/packages/html/ds/src/index.ts index 890fae6fa..fd037c975 100644 --- a/packages/html/ds/src/index.ts +++ b/packages/html/ds/src/index.ts @@ -9,6 +9,7 @@ import { initModal } from './modal/modal.js'; import { initRadios } from './radio/radio.js'; import { initTabs } from './tabs/tabs.js'; import { initTextarea } from './textarea/textarea.js'; +import { initToast } from './toast/toast.js'; export * as properties from './dist/properties.js'; @@ -36,6 +37,7 @@ export function initGovIe() { initTabs(); initComboBox(); initAlert(); + initToast(); } export function destroyGovIe() { diff --git a/packages/html/ds/src/toast/assets/notyf.min.css b/packages/html/ds/src/toast/assets/notyf.min.css new file mode 100644 index 000000000..dbb5a1648 --- /dev/null +++ b/packages/html/ds/src/toast/assets/notyf.min.css @@ -0,0 +1 @@ +@-webkit-keyframes notyf-fadeinup{0%{opacity:0;transform:translateY(25%)}to{opacity:1;transform:translateY(0)}}@keyframes notyf-fadeinup{0%{opacity:0;transform:translateY(25%)}to{opacity:1;transform:translateY(0)}}@-webkit-keyframes notyf-fadeinleft{0%{opacity:0;transform:translateX(25%)}to{opacity:1;transform:translateX(0)}}@keyframes notyf-fadeinleft{0%{opacity:0;transform:translateX(25%)}to{opacity:1;transform:translateX(0)}}@-webkit-keyframes notyf-fadeoutright{0%{opacity:1;transform:translateX(0)}to{opacity:0;transform:translateX(25%)}}@keyframes notyf-fadeoutright{0%{opacity:1;transform:translateX(0)}to{opacity:0;transform:translateX(25%)}}@-webkit-keyframes notyf-fadeoutdown{0%{opacity:1;transform:translateY(0)}to{opacity:0;transform:translateY(25%)}}@keyframes notyf-fadeoutdown{0%{opacity:1;transform:translateY(0)}to{opacity:0;transform:translateY(25%)}}@-webkit-keyframes ripple{0%{transform:scale(0) translateY(-45%) translateX(13%)}to{transform:scale(1) translateY(-45%) translateX(13%)}}@keyframes ripple{0%{transform:scale(0) translateY(-45%) translateX(13%)}to{transform:scale(1) translateY(-45%) translateX(13%)}}.notyf{position:fixed;top:0;left:0;height:100%;width:100%;color:#fff;z-index:9999;display:flex;flex-direction:column;align-items:flex-end;justify-content:flex-end;pointer-events:none;box-sizing:border-box;padding:20px}.notyf__icon--error,.notyf__icon--success{height:21px;width:21px;background:#fff;border-radius:50%;display:block;margin:0 auto;position:relative}.notyf__icon--error:after,.notyf__icon--error:before{content:"";background:currentColor;display:block;position:absolute;width:3px;border-radius:3px;left:9px;height:12px;top:5px}.notyf__icon--error:after{transform:rotate(-45deg)}.notyf__icon--error:before{transform:rotate(45deg)}.notyf__icon--success:after,.notyf__icon--success:before{content:"";background:currentColor;display:block;position:absolute;width:3px;border-radius:3px}.notyf__icon--success:after{height:6px;transform:rotate(-45deg);top:9px;left:6px}.notyf__icon--success:before{height:11px;transform:rotate(45deg);top:5px;left:10px}.notyf__toast{display:block;overflow:hidden;pointer-events:auto;-webkit-animation:notyf-fadeinup .3s ease-in forwards;animation:notyf-fadeinup .3s ease-in forwards;box-shadow:0 3px 7px 0 rgba(0,0,0,.25);position:relative;padding:0 15px;border-radius:2px;max-width:300px;transform:translateY(25%);box-sizing:border-box;flex-shrink:0}.notyf__toast--disappear{transform:translateY(0);-webkit-animation:notyf-fadeoutdown .3s forwards;animation:notyf-fadeoutdown .3s forwards;-webkit-animation-delay:.25s;animation-delay:.25s}.notyf__toast--disappear .notyf__icon,.notyf__toast--disappear .notyf__message{-webkit-animation:notyf-fadeoutdown .3s forwards;animation:notyf-fadeoutdown .3s forwards;opacity:1;transform:translateY(0)}.notyf__toast--disappear .notyf__dismiss{-webkit-animation:notyf-fadeoutright .3s forwards;animation:notyf-fadeoutright .3s forwards;opacity:1;transform:translateX(0)}.notyf__toast--disappear .notyf__message{-webkit-animation-delay:.05s;animation-delay:.05s}.notyf__toast--upper{margin-bottom:20px}.notyf__toast--lower{margin-top:20px}.notyf__toast--dismissible .notyf__wrapper{padding-right:30px}.notyf__ripple{height:400px;width:400px;position:absolute;transform-origin:bottom right;right:0;top:0;border-radius:50%;transform:scale(0) translateY(-51%) translateX(13%);z-index:5;-webkit-animation:ripple .4s ease-out forwards;animation:ripple .4s ease-out forwards}.notyf__wrapper{display:flex;align-items:center;padding-top:17px;padding-bottom:17px;padding-right:15px;border-radius:3px;position:relative;z-index:10}.notyf__icon{width:22px;text-align:center;font-size:1.3em;opacity:0;-webkit-animation:notyf-fadeinup .3s forwards;animation:notyf-fadeinup .3s forwards;-webkit-animation-delay:.3s;animation-delay:.3s;margin-right:13px}.notyf__dismiss{position:absolute;top:0;right:0;height:100%;width:26px;margin-right:-15px;-webkit-animation:notyf-fadeinleft .3s forwards;animation:notyf-fadeinleft .3s forwards;-webkit-animation-delay:.35s;animation-delay:.35s;opacity:0}.notyf__dismiss-btn{background-color:rgba(0,0,0,.25);border:none;cursor:pointer;transition:opacity .2s ease,background-color .2s ease;outline:none;opacity:.35;height:100%;width:100%}.notyf__dismiss-btn:after,.notyf__dismiss-btn:before{content:"";background:#fff;height:12px;width:2px;border-radius:3px;position:absolute;left:calc(50% - 1px);top:calc(50% - 5px)}.notyf__dismiss-btn:after{transform:rotate(-45deg)}.notyf__dismiss-btn:before{transform:rotate(45deg)}.notyf__dismiss-btn:hover{opacity:.7;background-color:rgba(0,0,0,.15)}.notyf__dismiss-btn:active{opacity:.8}.notyf__message{vertical-align:middle;position:relative;opacity:0;-webkit-animation:notyf-fadeinup .3s forwards;animation:notyf-fadeinup .3s forwards;-webkit-animation-delay:.25s;animation-delay:.25s;line-height:1.5em}@media only screen and (max-width:480px){.notyf{padding:0}.notyf__ripple{height:600px;width:600px;-webkit-animation-duration:.5s;animation-duration:.5s}.notyf__toast{max-width:none;border-radius:0;box-shadow:0 -2px 7px 0 rgba(0,0,0,.13);width:100%}.notyf__dismiss{width:56px}} \ No newline at end of file diff --git a/packages/html/ds/src/toast/assets/notyf.min.js b/packages/html/ds/src/toast/assets/notyf.min.js new file mode 100644 index 000000000..ae8c35c91 --- /dev/null +++ b/packages/html/ds/src/toast/assets/notyf.min.js @@ -0,0 +1 @@ +export var Notyf=function(){"use strict";var e,o=function(){return(o=Object.assign||function(t){for(var i,e=1,n=arguments.length;e + {{ + govieIcon({ + "size": "lg", + "icon": icon + }) + }} +
+

{{ props.title }}

+ {{ + govieParagraph({ + "content": props.description, + "className": "!gi-mb-0" + }) + }} + {% if props.action %} +
+ {{ + govieLink({ + "noColor": true, + "size": "md", + "href": props.action.href, + "label": props.action.label + }) + }} +
+ {% endif %} +
+ {% if props.dismissible %} + {{ + govieIconButton({ + "className": "gi-toast-dismiss", + "size": "small", + "appearance": "dark", + "variant": "flat", + "icon": { + "icon": "close" + } + }) + }} + {% endif %} + +{% endmacro %} diff --git a/packages/html/ds/src/toast/helpers.html b/packages/html/ds/src/toast/helpers.html new file mode 100644 index 000000000..32f119130 --- /dev/null +++ b/packages/html/ds/src/toast/helpers.html @@ -0,0 +1,31 @@ +{% macro getIcon(variant) %} + {% if variant == 'warning' %} + warning + {% elif variant == 'success' %} + check_circle + {% elif variant == 'danger' %} + error + {% else %} + info + {% endif %} +{% endmacro %} + +{% macro getVariant(variant) %} + {% if variant == 'danger' %} + gi-toast-danger + {% elif variant == 'success' %} + gi-toast-success + {% elif variant == 'warning' %} + gi-toast-warning + {% else %} + gi-toast-info + {% endif %} +{% endmacro %} + +{% macro getBaseToastClass(dismissible) %} + {% if dismissible %} + gi-toast-base-dismissible + {% else %} + gi-toast-base + {% endif %} +{% endmacro %} diff --git a/packages/html/ds/src/toast/toast.html b/packages/html/ds/src/toast/toast.html new file mode 100644 index 000000000..1819d198a --- /dev/null +++ b/packages/html/ds/src/toast/toast.html @@ -0,0 +1,22 @@ +{% from 'toast/ds-toast.html' import govieDsToast %} +{% from 'button/button.html' import govieButton %} + +{% macro govieToast(props) %} + {% set duration = props.duration %} + {% set positionX = props.position.x if props.position %} + {% set positionY = props.position.y if props.position %} + +
+ {% if props.trigger %} + {{ govieButton(props.trigger) }} + {% endif %} +
+
{{ govieDsToast(props) }}
+{% endmacro %} diff --git a/packages/html/ds/src/toast/toast.schema.ts b/packages/html/ds/src/toast/toast.schema.ts new file mode 100644 index 000000000..a06467a91 --- /dev/null +++ b/packages/html/ds/src/toast/toast.schema.ts @@ -0,0 +1,53 @@ +import * as zod from 'zod'; +import { buttonSchema } from '../button/button-schema'; +import { linkSchema } from '../link/link.schema'; + +export enum ToastVariant { + INFO = 'info', + DANGER = 'danger', + SUCCESS = 'success', + WARNING = 'warning', +} + +export const toastSchema = zod.object({ + duration: zod + .number({ description: 'Specify the content in the toast component' }) + .optional(), + position: zod + .object({ + x: zod.union([ + zod.literal('center'), + zod.literal('left'), + zod.literal('right'), + ]), + y: zod.union([ + zod.literal('center'), + zod.literal('top'), + zod.literal('bottom'), + ]), + }) + .describe('Set the duration of the toast appearing on screen') + .optional(), + trigger: buttonSchema + .describe( + 'If specified the toast will be triggered by the click event of this React Button Component', + ) + .optional(), + variant: zod + .nativeEnum(ToastVariant, { description: 'Toast variant' }) + .optional(), + title: zod.string({ + description: 'Specify the title of the toast component', + }), + description: zod + .string({ description: 'Specify the content in the toast component' }) + .optional(), + action: linkSchema + .describe('Specify a link for the toast component') + .optional(), + dismissible: zod + .boolean({ description: 'Specify if the toast is dismissible' }) + .optional(), +}); + +export type ToastProps = zod.infer; diff --git a/packages/html/ds/src/toast/toast.stories.ts b/packages/html/ds/src/toast/toast.stories.ts new file mode 100644 index 000000000..cdce2c81d --- /dev/null +++ b/packages/html/ds/src/toast/toast.stories.ts @@ -0,0 +1,116 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { renderComponent } from '../storybook/storybook'; +import html from './toast.html?raw'; +import { ToastProps } from './toast.schema'; + +// Name of the folder the macro resides +const path = import.meta.url.split('/toast')[0]; + +const macro = { name: 'govieToast', html, path }; + +const Toast = renderComponent(macro); + +const meta = { + component: Toast, + title: 'Application/Toast', + argTypes: { + variant: { + control: 'radio', + description: 'Specify the variant of the toast component', + options: ['info', 'danger', 'success', 'warning'], + }, + title: { + control: 'text', + description: 'Specify the title of the toast component', + }, + dismissible: { + control: 'boolean', + description: 'Specify if the toast is dismissible', + }, + description: { + control: 'text', + description: 'Specify the content in the toast component', + }, + action: { + control: 'object', + description: 'Specify a link for the toast component', + }, + duration: { + control: 'number', + description: 'Set the duration of the toast appearing on screen', + }, + position: { + control: 'object', + table: { + type: { + summary: `x: ['left', 'center', 'right'] y: ['top', 'cented', 'bottom']`, + }, + }, + description: 'Specify the position of the toast', + }, + trigger: { + control: 'object', + description: + 'If specified the toast will be triggered by the click event of this React Button Component', + }, + }, + parameters: { + macro, + docs: { + description: { + component: 'Toast component', + }, + }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const WithTrigger: Story = { + args: { + title: 'Toast Triggered', + description: 'This is some content', + trigger: { + content: 'Trigger Toast', + }, + }, +}; + +export const WithAction: Story = { + args: { + title: 'Dismissible', + description: 'This is some content', + action: { + href: '#', + label: 'Go to Link', + }, + }, +}; + +export const Dismissible: Story = { + args: { + title: 'Dismissible', + description: 'This is some content', + dismissible: true, + }, +}; + +export const withLongerDuration: Story = { + args: { + title: 'WithDuration', + description: 'This is some content', + duration: 8000, + }, +}; + +export const withPositionChange: Story = { + args: { + title: 'withPositionChange', + description: 'This is some content', + position: { + x: 'left', + y: 'bottom', + }, + }, +}; diff --git a/packages/html/ds/src/toast/toast.test.ts b/packages/html/ds/src/toast/toast.test.ts new file mode 100644 index 000000000..591913f27 --- /dev/null +++ b/packages/html/ds/src/toast/toast.test.ts @@ -0,0 +1,70 @@ +import { userEvent } from '@testing-library/user-event'; +import { render } from '../common/render'; +import html from '../toast/toast.html?raw'; +import { ToastProps, ToastVariant } from './toast.schema'; + +const standardProps = { + title: 'Toast Title', + description: 'This is the toast content', +}; + +describe('toast', () => { + const renderToast = render({ + componentName: 'toast', + macroName: 'govieToast', + html, + }); + + it('should render toast with title and message', () => { + const screen = renderToast(standardProps); + + expect(screen.getByText('Toast Title')).toBeInTheDocument(); + expect(screen.getByText('This is the toast content')).toBeInTheDocument(); + }); + + it('should render all different variants', () => { + const variants: ToastProps['variant'][] = [ + ToastVariant.INFO, + ToastVariant.SUCCESS, + ToastVariant.WARNING, + ToastVariant.DANGER, + ]; + + for (const variant of variants) { + const screen = renderToast({ + variant, + title: `${variant} Toast`, + description: `This is a ${variant} toast`, + }); + + const toastElement = screen.getByText(`${variant} Toast`); + + expect(toastElement.parentElement?.parentElement).toHaveClass( + `gi-toast-${variant}`, + ); + } + }); + it('should pass axe accessibility tests', async () => { + const screen = renderToast({ + variant: ToastVariant.SUCCESS, + title: 'Accessible Toast', + description: 'This toast should be accessible', + }); + await screen.axe(); + }); + it('should render toast on button trigger', async () => { + const screen = renderToast({ + title: 'Toast with Trigger', + description: 'Toast has been triggered', + trigger: { + content: 'Click me', + }, + }); + + const buttonElement = screen.container.querySelector('button'); + buttonElement && userEvent.click(buttonElement); + + expect(screen.getByText('Toast with Trigger')).toBeInTheDocument(); + expect(screen.getByText('Toast has been triggered')).toBeInTheDocument(); + }); +}); diff --git a/packages/html/ds/src/toast/toast.ts b/packages/html/ds/src/toast/toast.ts new file mode 100644 index 000000000..efde15b73 --- /dev/null +++ b/packages/html/ds/src/toast/toast.ts @@ -0,0 +1,94 @@ +import { + BaseComponent, + type BaseComponentOptions, + initialiseModule, +} from '../common/component'; +// @ts-expect-error The TS error is necessary as we are integrating the notyf library within our repo and thus no longer the libraries declarations +import { Notyf } from './assets/notyf.min.js'; + +export type ToastOptions = BaseComponentOptions; +type NotyfVerticalPosition = 'center' | 'top' | 'bottom'; +type NotyfHorizontalPosition = 'left' | 'center' | 'right'; + +const notyf = new Notyf(); + +export class Toast extends BaseComponent { + container: HTMLElement; + dsToastContainer: Element | null; + clonedNode: HTMLElement | undefined; + triggerButton: HTMLButtonElement | null; + notyf: Notyf; + + renderNotyf: () => void; + + constructor(options: ToastOptions) { + super(options); + this.container = options.element as HTMLElement; + this.dsToastContainer = this.container.nextElementSibling; + this.triggerButton = this.container.querySelector(':scope > button'); + this.notyf = notyf; + + setTimeout(() => { + const notyfContainer = document.querySelectorAll('.notyf .notyf__toast'); + + for (const toast of notyfContainer) { + toast + .querySelector('.gi-toast-dismiss') + ?.addEventListener('click', () => { + toast.classList.add('!gi-hidden'); + }); + } + }); + + this.renderNotyf = () => { + const duration = this.container.dataset.duration + ? Number(this.container.dataset.duration) + : undefined; + const x = this.container.dataset.positionX as + | NotyfHorizontalPosition + | undefined; + const y = this.container.dataset.positionY as + | NotyfVerticalPosition + | undefined; + const hasPos = x && y; + const position = hasPos + ? { + x, + y, + } + : undefined; + + this.clonedNode = this.dsToastContainer?.cloneNode(true) as HTMLElement; + this.clonedNode?.classList.remove('gi-hidden'); + if (this.triggerButton) { + this.triggerButton.addEventListener('click', () => { + this.notyf && + this.notyf.open({ + type: 'open', + message: this.clonedNode?.outerHTML, + duration, + position, + }); + }); + } else { + this.notyf && + this.notyf.open({ + type: 'open', + message: this.clonedNode?.outerHTML, + duration, + position, + }); + } + }; + } + + initComponent() { + this.renderNotyf(); + } + destroyComponent() {} +} + +export const initToast = initialiseModule({ + name: 'toast', + component: 'Toast', +}); diff --git a/packages/html/ds/styles.css b/packages/html/ds/styles.css index 2dae1ae89..bea528a7a 100644 --- a/packages/html/ds/styles.css +++ b/packages/html/ds/styles.css @@ -10,6 +10,8 @@ @import 'node_modules/@govie-ds/tailwind/css/components.css'; @import 'node_modules/@govie-ds/tailwind/css/typography.css'; +@import './src/toast/assets/notyf.min.css'; + .gieds-js .js\:gi-hidden { display: none; } diff --git a/packages/react/ds/src/toast/toast.test.tsx b/packages/react/ds/src/toast/toast.test.tsx index e9466bc66..1e6bed269 100644 --- a/packages/react/ds/src/toast/toast.test.tsx +++ b/packages/react/ds/src/toast/toast.test.tsx @@ -62,9 +62,9 @@ describe('Toast', () => { const buttonElement = screen.container.querySelector('button'); buttonElement && userEvent.click(buttonElement); - expect(await screen.findByText('Toast Title')).toBeInTheDocument(); + expect(await screen.findByText('Toast with Trigger')).toBeInTheDocument(); expect( - await screen.findByText('This is the toast content'), + await screen.findByText('Toast has been triggered'), ).toBeInTheDocument(); }); }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index de30d3f66..2ef6b5274 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -16097,8 +16097,8 @@ snapshots: '@typescript-eslint/parser': 7.18.0(eslint@8.57.0)(typescript@5.6.3) eslint: 8.57.0 eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.57.0) - eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.6.3))(eslint@8.57.0) + eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.6.3))(eslint@8.57.0))(eslint@8.57.0) + eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.6.3))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0) eslint-plugin-jsx-a11y: 6.9.0(eslint@8.57.0) eslint-plugin-react: 7.34.3(eslint@8.57.0) eslint-plugin-react-hooks: 4.6.2(eslint@8.57.0) @@ -16116,13 +16116,13 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.57.0): + eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.6.3))(eslint@8.57.0))(eslint@8.57.0): dependencies: debug: 4.3.5 enhanced-resolve: 5.17.0 eslint: 8.57.0 - eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.57.0))(eslint@8.57.0) - eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.6.3))(eslint@8.57.0) + eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.6.3))(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0) + eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.6.3))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0) fast-glob: 3.3.2 get-tsconfig: 4.7.5 is-core-module: 2.13.1 @@ -16150,14 +16150,14 @@ snapshots: - eslint-import-resolver-webpack - supports-color - eslint-module-utils@2.8.1(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.57.0))(eslint@8.57.0): + eslint-module-utils@2.8.1(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.6.3))(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0): dependencies: debug: 3.2.7 optionalDependencies: '@typescript-eslint/parser': 7.18.0(eslint@8.57.0)(typescript@5.6.3) eslint: 8.57.0 eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.57.0) + eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.6.3))(eslint@8.57.0))(eslint@8.57.0) transitivePeerDependencies: - supports-color @@ -16199,33 +16199,6 @@ snapshots: - eslint-import-resolver-webpack - supports-color - eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.6.3))(eslint@8.57.0): - dependencies: - array-includes: 3.1.8 - array.prototype.findlastindex: 1.2.5 - array.prototype.flat: 1.3.2 - array.prototype.flatmap: 1.3.2 - debug: 3.2.7 - doctrine: 2.1.0 - eslint: 8.57.0 - eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.57.0))(eslint@8.57.0) - hasown: 2.0.2 - is-core-module: 2.13.1 - is-glob: 4.0.3 - minimatch: 3.1.2 - object.fromentries: 2.0.8 - object.groupby: 1.0.3 - object.values: 1.2.0 - semver: 6.3.1 - tsconfig-paths: 3.15.0 - optionalDependencies: - '@typescript-eslint/parser': 7.18.0(eslint@8.57.0)(typescript@5.6.3) - transitivePeerDependencies: - - eslint-import-resolver-typescript - - eslint-import-resolver-webpack - - supports-color - eslint-plugin-jsx-a11y@6.9.0(eslint@8.57.0): dependencies: aria-query: 5.1.3