diff --git a/apps/website/package.json b/apps/website/package.json index fdda816849..1b3ef1c51c 100644 --- a/apps/website/package.json +++ b/apps/website/package.json @@ -11,7 +11,6 @@ "@cloudscape-design/components": "^3.0.706", "@dxc-technology/halstack-react": "*", "@radix-ui/react-popover": "^1.0.7", - "@types/styled-components": "5.1.29", "cross-env": "^7.0.3", "next": "14.2.10", "raw-loader": "^4.0.2", @@ -30,6 +29,7 @@ "@types/react": "^18", "@types/react-color": "^3.0.6", "@types/react-dom": "^18", + "@types/styled-components": "5.1.29", "eslint": "^8", "eslint-config-next": "14.2.4", "typescript": "^5" diff --git a/apps/website/pages/components/toast/index.tsx b/apps/website/pages/components/toast/index.tsx new file mode 100644 index 0000000000..b91fc336eb --- /dev/null +++ b/apps/website/pages/components/toast/index.tsx @@ -0,0 +1,21 @@ +import Head from "next/head"; +import type { ReactElement } from "react"; +import ToastCodePage from "../../../screens/components/toast/code/ToastCodePage"; +import ToastPageLayout from "../../../screens/components/toast/ToastPageLayout"; + +const Index = () => { + return ( + <> + + Toast — Halstack Design System + + + + ); +}; + +Index.getLayout = function getLayout(page: ReactElement) { + return {page}; +}; + +export default Index; diff --git a/apps/website/pages/components/toast/specifications.tsx b/apps/website/pages/components/toast/specifications.tsx new file mode 100644 index 0000000000..afbc1934df --- /dev/null +++ b/apps/website/pages/components/toast/specifications.tsx @@ -0,0 +1,21 @@ +import Head from "next/head"; +import type { ReactElement } from "react"; +import ToastSpecsPage from "../../../screens/components/toast/specs/ToastSpecsPage"; +import ToastPageLayout from "../../../screens/components/toast/ToastPageLayout"; + +const Specifications = () => { + return ( + <> + + Toast Specs — Halstack Design System + + + + ); +}; + +Specifications.getLayout = function getLayout(page: ReactElement) { + return {page}; +}; + +export default Specifications; diff --git a/apps/website/pages/components/toast/usage.tsx b/apps/website/pages/components/toast/usage.tsx new file mode 100644 index 0000000000..f1e2bfbdc5 --- /dev/null +++ b/apps/website/pages/components/toast/usage.tsx @@ -0,0 +1,21 @@ +import Head from "next/head"; +import type { ReactElement } from "react"; +import ToastPageLayout from "../../../screens/components/toast/ToastPageLayout"; +import ToastUsagePage from "../../../screens/components/toast/usage/ToastUsagePage"; + +const Usage = () => { + return ( + <> + + Toast Usage — Halstack Design System + + + + ); +}; + +Usage.getLayout = function getLayout(page: ReactElement) { + return {page}; +}; + +export default Usage; diff --git a/apps/website/screens/common/TabsPageLayout.tsx b/apps/website/screens/common/TabsPageLayout.tsx index 386ec1486b..ad8cef410f 100644 --- a/apps/website/screens/common/TabsPageLayout.tsx +++ b/apps/website/screens/common/TabsPageLayout.tsx @@ -2,7 +2,6 @@ import { useRouter } from "next/router"; import { DxcNavTabs } from "@dxc-technology/halstack-react"; import React from "react"; import Link from "next/link"; -import styled from "styled-components"; type TabsPageLayoutProps = { tabs: { label: string; path: string }[]; diff --git a/apps/website/screens/common/componentsList.json b/apps/website/screens/common/componentsList.json index cddd7c5346..3d12e0c50f 100644 --- a/apps/website/screens/common/componentsList.json +++ b/apps/website/screens/common/componentsList.json @@ -115,6 +115,7 @@ "status": "stable" }, { "label": "Textarea", "path": "/components/textarea", "status": "stable" }, + { "label": "Toast", "path": "/components/toast", "status": "experimental" }, { "label": "Toggle Group", "path": "/components/toggle-group", diff --git a/apps/website/screens/components/accordion/specs/AccordionSpecsPage.tsx b/apps/website/screens/components/accordion/specs/AccordionSpecsPage.tsx index a6bc3768d7..85af32c2a8 100644 --- a/apps/website/screens/components/accordion/specs/AccordionSpecsPage.tsx +++ b/apps/website/screens/components/accordion/specs/AccordionSpecsPage.tsx @@ -33,31 +33,26 @@ const sections = [ ), }, { - title: "Formatting", - subSections: [ - { - title: "Anatomy", - content: ( - <> - Accordion anatomy - - Header - - Custom icon (Optional) - - Title - - Helper text (Optional) - - - Caret icon (Expand/collapse) - - Expanded panel - - - ), - }, - ], + title: "Anatomy", + content: ( + <> + Accordion anatomy + + Header + + Custom icon (Optional) + + Title + + Helper text (Optional) + + + Caret icon (Expand/collapse) + + Expanded panel + + + ), }, { title: "Design tokens", diff --git a/apps/website/screens/components/badge/usage/BadgeUsagePage.tsx b/apps/website/screens/components/badge/usage/BadgeUsagePage.tsx index 6892248df7..fde63180ae 100644 --- a/apps/website/screens/components/badge/usage/BadgeUsagePage.tsx +++ b/apps/website/screens/components/badge/usage/BadgeUsagePage.tsx @@ -29,7 +29,7 @@ const sections = [ the teams involved in the task, repositories, folders…). - Keep it concise: comprehensively use badges, only displaying essential information that adds value to the + Keep it concise. Comprehensively use badges, only displaying essential information that adds value to the user experience. @@ -119,12 +119,12 @@ const sections = [ - Non-semantic categorization: used in instances where the badge does not carry semantic meaning, it can - be employed in any color from the available palette. + Non-semantic categorization: used in instances where the badge does not carry semantic + meaning, it can be employed in any color from the available palette. - Semantic categorization: when the badge conveys semantic information, spacific semantic colors are - available: + Semantic categorization: when the badge conveys semantic information, specific semantic + colors are available: Green: positive actions, such as approved, completed, success… diff --git a/apps/website/screens/components/button/usage/ButtonUsagePage.tsx b/apps/website/screens/components/button/usage/ButtonUsagePage.tsx index dc640b6b33..1b8caf5085 100644 --- a/apps/website/screens/components/button/usage/ButtonUsagePage.tsx +++ b/apps/website/screens/components/button/usage/ButtonUsagePage.tsx @@ -14,6 +14,8 @@ import variants from "./examples/variants"; import icons from "./examples/iconUsage"; import HeaderDescriptionCell from "@/common/HeaderDescriptionCell"; import Code from "@/common/Code"; +import Image from "@/common/Image"; +import semanticButtons from "./images/semantic_buttons.png"; const sections = [ { @@ -141,7 +143,7 @@ const sections = [ Neutral action with no specific context. Typically used for general actions. Shown in the brand's - primary color. Use for neutral actions such as "Submit", "Save" or "Continue.” + primary color. Use for neutral actions such as "Submit", "Save" or "Continue.”. @@ -152,18 +154,18 @@ const sections = [ Indicates a destructive action or highlights a critical issue. Styled in red. Use for actions like - "Delete", "Remove" or "Cancel Subscription.” + "Delete", "Remove" or "Cancel Subscription.”. - - Warning + + Info - Alerts the user to potential issues or actions that need caution. Styled in orange. Use for actions like - "Warning" or "Attention Needed.” + Provides additional information or context. Shown in blue, the brand's secondary color. Use for actions + like "More Info", "Details" or "Learn More.”. @@ -174,22 +176,23 @@ const sections = [ Represents a positive action or confirms the completion of a task. Styled in green. Use for actions like - "Confirm", "Complete" or "Approve.” + "Confirm", "Complete" or "Approve.”. - - Info + + Warning - Provides additional information or context. Shown in blue, the brand's secondary color. Use for actions - like "More Info", "Details" or "Learn More.” + Alerts the user to potential issues or actions that need caution. Styled in orange. Use for actions like + "Warning" or "Attention Needed.”. + Semantic buttons based on their purpose ), }, diff --git a/apps/website/screens/components/button/usage/images/semantic_buttons.png b/apps/website/screens/components/button/usage/images/semantic_buttons.png new file mode 100644 index 0000000000..102018d9b6 Binary files /dev/null and b/apps/website/screens/components/button/usage/images/semantic_buttons.png differ diff --git a/apps/website/screens/components/checkbox/code/CheckboxCodePage.tsx b/apps/website/screens/components/checkbox/code/CheckboxCodePage.tsx index 192b45bec3..092be7e356 100644 --- a/apps/website/screens/components/checkbox/code/CheckboxCodePage.tsx +++ b/apps/website/screens/components/checkbox/code/CheckboxCodePage.tsx @@ -159,7 +159,7 @@ const sections = [ ref - {"React.Ref "} + {"React.Ref"} Reference to the component. - diff --git a/apps/website/screens/components/date-input/code/DateInputCodePage.tsx b/apps/website/screens/components/date-input/code/DateInputCodePage.tsx index 83889dc134..2d99122108 100644 --- a/apps/website/screens/components/date-input/code/DateInputCodePage.tsx +++ b/apps/website/screens/components/date-input/code/DateInputCodePage.tsx @@ -231,7 +231,7 @@ const sections = [ ref - {"React.Ref "} + {"React.Ref"} Reference to the component. - diff --git a/apps/website/screens/components/file-input/code/FileInputCodePage.tsx b/apps/website/screens/components/file-input/code/FileInputCodePage.tsx index 702450aaa7..1d8a5e857e 100644 --- a/apps/website/screens/components/file-input/code/FileInputCodePage.tsx +++ b/apps/website/screens/components/file-input/code/FileInputCodePage.tsx @@ -207,7 +207,7 @@ const sections = [ ref - {"React.Ref "} + {"React.Ref"} Reference to the component. - diff --git a/apps/website/screens/components/number-input/code/NumberInputCodePage.tsx b/apps/website/screens/components/number-input/code/NumberInputCodePage.tsx index 9c8fabf10c..3c2ab4bd7e 100644 --- a/apps/website/screens/components/number-input/code/NumberInputCodePage.tsx +++ b/apps/website/screens/components/number-input/code/NumberInputCodePage.tsx @@ -252,7 +252,7 @@ const sections = [ ref - {"React.Ref "} + {"React.Ref"} Reference to the component. - diff --git a/apps/website/screens/components/password-input/code/PasswordInputCodePage.tsx b/apps/website/screens/components/password-input/code/PasswordInputCodePage.tsx index ca0a1bfc71..7ff1ec01ad 100644 --- a/apps/website/screens/components/password-input/code/PasswordInputCodePage.tsx +++ b/apps/website/screens/components/password-input/code/PasswordInputCodePage.tsx @@ -199,7 +199,7 @@ const sections = [ ref - {"React.Ref "} + {"React.Ref"} Reference to the component. - diff --git a/apps/website/screens/components/radio-group/code/RadioGroupCodePage.tsx b/apps/website/screens/components/radio-group/code/RadioGroupCodePage.tsx index 17935dd19d..489b3f49db 100644 --- a/apps/website/screens/components/radio-group/code/RadioGroupCodePage.tsx +++ b/apps/website/screens/components/radio-group/code/RadioGroupCodePage.tsx @@ -201,7 +201,7 @@ const sections = [ ref - {"React.Ref "} + {"React.Ref"} Reference to the component. - diff --git a/apps/website/screens/components/select/code/SelectCodePage.tsx b/apps/website/screens/components/select/code/SelectCodePage.tsx index 1432e30b23..5d24722ee7 100644 --- a/apps/website/screens/components/select/code/SelectCodePage.tsx +++ b/apps/website/screens/components/select/code/SelectCodePage.tsx @@ -9,9 +9,15 @@ import uncontrolled from "./examples/uncontrolled"; import errorHandling from "./examples/errorHandling"; import groups from "./examples/groupedOptions"; import icons from "./examples/icons"; -import TableCode from "@/common/TableCode"; +import TableCode, { ExtendedTableCode } from "@/common/TableCode"; import StatusBadge from "@/common/StatusBadge"; +const optionsType = `{ + label: string; + value: string; + icon: string | Icon; +}`; + const sections = [ { title: "Props", @@ -74,11 +80,15 @@ const sections = [ - - { - "({ label: string, value: string, icon: (string | React.ReactNode & React.SVGProps ) })[] | ({ label: string, options: Option[] })[]" - } - + {"Option[] | ({ label: string, options: Option[] })[]"} +

+ being Option the following type: +

+ {optionsType} +

+ and Icon: +

+ {`React.ReactNode & React.SVGProps`} An array of objects representing the selectable options. Each object has the following properties @@ -253,7 +263,7 @@ const sections = [ ref - {"React.Ref "} + {"React.Ref"} Reference to the component. - diff --git a/apps/website/screens/components/slider/code/SliderCodePage.tsx b/apps/website/screens/components/slider/code/SliderCodePage.tsx index e161f7af3d..c832c7ee2b 100644 --- a/apps/website/screens/components/slider/code/SliderCodePage.tsx +++ b/apps/website/screens/components/slider/code/SliderCodePage.tsx @@ -208,7 +208,7 @@ const sections = [ ref - {"React.Ref "} + {"React.Ref"} Reference to the component. - diff --git a/apps/website/screens/components/switch/code/SwitchCodePage.tsx b/apps/website/screens/components/switch/code/SwitchCodePage.tsx index 7aa879615e..317584fb43 100644 --- a/apps/website/screens/components/switch/code/SwitchCodePage.tsx +++ b/apps/website/screens/components/switch/code/SwitchCodePage.tsx @@ -147,7 +147,7 @@ const sections = [ ref - {"React.Ref "} + {"React.Ref"} Reference to the component. - diff --git a/apps/website/screens/components/text-input/code/TextInputCodePage.tsx b/apps/website/screens/components/text-input/code/TextInputCodePage.tsx index abdc299711..81f377ddc5 100644 --- a/apps/website/screens/components/text-input/code/TextInputCodePage.tsx +++ b/apps/website/screens/components/text-input/code/TextInputCodePage.tsx @@ -325,7 +325,7 @@ const sections = [ ref - {"React.Ref "} + {"React.Ref"} Reference to the component. - diff --git a/apps/website/screens/components/textarea/code/TextareaCodePage.tsx b/apps/website/screens/components/textarea/code/TextareaCodePage.tsx index 782b46f432..9a95f7a0f9 100644 --- a/apps/website/screens/components/textarea/code/TextareaCodePage.tsx +++ b/apps/website/screens/components/textarea/code/TextareaCodePage.tsx @@ -272,7 +272,7 @@ const sections = [ ref - {"React.Ref "} + {"React.Ref"} Reference to the component. - diff --git a/apps/website/screens/components/toast/ToastPageLayout.tsx b/apps/website/screens/components/toast/ToastPageLayout.tsx new file mode 100644 index 0000000000..d55e1e9a78 --- /dev/null +++ b/apps/website/screens/components/toast/ToastPageLayout.tsx @@ -0,0 +1,31 @@ +import { DxcParagraph, DxcFlex } from "@dxc-technology/halstack-react"; +import PageHeading from "@/common/PageHeading"; +import TabsPageHeading from "@/common/TabsPageLayout"; +import ComponentHeading from "@/common/ComponentHeading"; + +const ToastPageHeading = ({ children }: { children: React.ReactNode }) => { + const tabs = [ + { label: "Code", path: "/components/toast" }, + { label: "Usage", path: "/components/toast/usage" }, + { label: "Specifications", path: "/components/toast/specifications" }, + ]; + + return ( + + + + + + The toast component is a lightweight notification element that appears temporarily to provide feedback or + updates to the user. It is commonly used to communicate non-critical information, such as success messages, + warning alerts, or brief updates. + + + + + {children} + + ); +}; + +export default ToastPageHeading; diff --git a/apps/website/screens/components/toast/code/ToastCodePage.tsx b/apps/website/screens/components/toast/code/ToastCodePage.tsx new file mode 100644 index 0000000000..fd6d07711e --- /dev/null +++ b/apps/website/screens/components/toast/code/ToastCodePage.tsx @@ -0,0 +1,304 @@ +import { DxcFlex, DxcLink, DxcParagraph, DxcTable, DxcToastsQueue } from "@dxc-technology/halstack-react"; +import QuickNavContainerLayout from "@/common/QuickNavContainerLayout"; +import QuickNavContainer from "@/common/QuickNavContainer"; +import DocFooter from "@/common/DocFooter"; +import TableCode, { ExtendedTableCode } from "@/common/TableCode"; +import StatusBadge from "@/common/StatusBadge"; +import Example from "@/common/example/Example"; +import basic from "./examples/basicUsage"; +import semantic from "./examples/semantic"; +import loading from "./examples/loading"; +import Code from "@/common/Code"; + +const actionTypeString = `{ + icon: string | + (React.ReactNode + & React.SVGProps); + label: string; + onClick: () => void; +}`; + +const sections = [ + { + title: "Toasts queue", + content: A component to be rendered at the top level of the application., + subSections: [ + { + title: "Props", + content: ( + + + + Name + Type + Description + Default + + + + + duration + + number + + + Duration in milliseconds before a toast automatically hides itself. The range goes from 3000ms to + 5000ms, any other value will not be taken into consideration. + + + 3000 + + + + + ), + }, + ], + }, + { + title: "useToast", + content: ( + <> + + A hook to queue toasts from any part of your application contained inside the Toast queue. It returns an + object with five methods, each explained below: + + + + + Method + Type + Description + + + + + default + + {`(toast: Default) => void`} + + Shows a toast with no implicit semantic meaning. + + + info + + {`(toast: Semantic) => void`} + + Shows a toast with an information semantic. + + + loading + + {`(toast: Loading) => (() => void)`} + + + Shows a loading status toast. Visually and semantically, it is the same as an information toast, but + with the difference that it never disappears from the screen. Its removal will always depend on the + user, thanks to the function returned by this method. + + + + success + + {`(toast: Semantic) => void`} + + Shows a toast with a success semantic. + + + warning + + {`(toast: Semantic) => void`} + + Shows a toast with a warning semantic. + + + + + Each method has a different argument type, which are detailed in the following sections. + + + ), + subSections: [ + { + title: "Default", + content: ( + + + + Name + Type + Description + Default + + + + + action + + {actionTypeString} + + Tertiary button which performs a custom action, specified by the user. + - + + + icon + + string | {"(React.ReactNode & React.SVGProps )"} + + + + Material Symbol + {" "} + name or SVG element as the icon that will be placed next to the panel label. When using Material + Symbols, replace spaces with underscores. By default they are outlined if you want it to be filled + prefix the symbol name with "filled_". + + - + + + + + + message + + + + string + + Message to be displayed as a toast. + - + + + + ), + }, + { + title: "Loading", + content: ( + + + + Name + Type + Description + Default + + + + + action + + {actionTypeString} + + Tertiary button which performs a custom action, specified by the user. + - + + + + + + message + + + + string + + Message to be displayed as a toast. + - + + + + ), + }, + { + title: "Semantic", + content: ( + + + + Name + Type + Description + Default + + + + + action + + {actionTypeString} + + Tertiary button which performs a custom action, specified by the user. + - + + + hideSemanticIcon + + boolean + + Flag that allows to hide the semantic icon of the toast. + + false + + + + + + + message + + + + string + + Message to be displayed as a toast. + - + + + + ), + }, + ], + }, + { + title: "Examples", + subSections: [ + { + title: "Basic usage", + content: , + }, + { + title: "Semantic toasts", + content: , + }, + { + title: "Loading toast", + content: ( + <> + + A loading toast is a toast that will never disappear from the screen. Its removal will always depend on + the user, thanks to the function returned by the loading method. This allows users to have + full control over the status of the process. + + + + ), + }, + ], + }, +]; + +const TextareaCodePage = () => { + return ( + + + + + + + + + ); +}; + +export default TextareaCodePage; diff --git a/apps/website/screens/components/toast/code/examples/basicUsage.ts b/apps/website/screens/components/toast/code/examples/basicUsage.ts new file mode 100644 index 0000000000..3f7475a915 --- /dev/null +++ b/apps/website/screens/components/toast/code/examples/basicUsage.ts @@ -0,0 +1,24 @@ +import { DxcButton, DxcInset, useToast } from "@dxc-technology/halstack-react"; + +const code = `() => { + const toast = useToast(); + + return ( + + { + toast.default({ message: "This is a basic message." }); + }} + /> + + ); +}`; + +const scope = { + DxcButton, + DxcInset, + useToast +}; + +export default { code, scope }; diff --git a/apps/website/screens/components/toast/code/examples/loading.ts b/apps/website/screens/components/toast/code/examples/loading.ts new file mode 100644 index 0000000000..09cf668688 --- /dev/null +++ b/apps/website/screens/components/toast/code/examples/loading.ts @@ -0,0 +1,31 @@ +import { DxcButton, DxcInset, useToast } from "@dxc-technology/halstack-react"; + +const code = `() => { + const toast = useToast(); + + const loadProcess = () => { + const loadingToast = toast.loading({ message: "Loading process..." }); + + setTimeout(() => { + loadingToast(); + toast.success({ message: "Process finished successfully." }); + }, 6000); + }; + + return ( + + + + ); +}`; + +const scope = { + DxcButton, + DxcInset, + useToast, +}; + +export default { code, scope }; diff --git a/apps/website/screens/components/toast/code/examples/semantic.ts b/apps/website/screens/components/toast/code/examples/semantic.ts new file mode 100644 index 0000000000..0eecef4f00 --- /dev/null +++ b/apps/website/screens/components/toast/code/examples/semantic.ts @@ -0,0 +1,44 @@ +import { DxcButton, DxcFlex, DxcInset, useToast } from "@dxc-technology/halstack-react"; + +const code = `() => { + const toast = useToast(); + + const action = { label: "Action", onClick: () => {} }; + + return ( + + + { + toast.info({ message: "This is a information message.", action }); + }} + /> + { + toast.success({ message: "This is a success message.", action }); + }} + /> + { + toast.warning({ message: "This is a warning message.", action }); + }} + /> + + + ); +}`; + +const scope = { + DxcButton, + DxcFlex, + DxcInset, + useToast, +}; + +export default { code, scope }; diff --git a/apps/website/screens/components/toast/specs/ToastSpecsPage.tsx b/apps/website/screens/components/toast/specs/ToastSpecsPage.tsx new file mode 100644 index 0000000000..93cda09b8d --- /dev/null +++ b/apps/website/screens/components/toast/specs/ToastSpecsPage.tsx @@ -0,0 +1,51 @@ +import { DxcBulletedList, DxcFlex, DxcParagraph } from "@dxc-technology/halstack-react"; +import QuickNavContainer from "@/common/QuickNavContainer"; +import QuickNavContainerLayout from "@/common/QuickNavContainerLayout"; +import DocFooter from "@/common/DocFooter"; +import Figure from "@/common/Figure"; +import Image from "@/common/Image"; +import specs from "./images/toast_specs.png"; +import anatomy from "./images/toast_anatomy.png"; + +const sections = [ + { + title: "Specifications", + content: ( +
+ Toast design specifications +
+ ), + }, + { + title: "Anatomy", + content: ( + <> + Toast anatomy + + Container + Icon + Text message + Action + Close action + + + ), + }, + { + title: "Design tokens", + content: This component currently has no design tokens., + }, +]; + +const TextareaSpecsPage = () => { + return ( + + + + + + + ); +}; + +export default TextareaSpecsPage; diff --git a/apps/website/screens/components/toast/specs/images/toast_anatomy.png b/apps/website/screens/components/toast/specs/images/toast_anatomy.png new file mode 100644 index 0000000000..5f6b66262d Binary files /dev/null and b/apps/website/screens/components/toast/specs/images/toast_anatomy.png differ diff --git a/apps/website/screens/components/toast/specs/images/toast_specs.png b/apps/website/screens/components/toast/specs/images/toast_specs.png new file mode 100644 index 0000000000..8ac5c2a006 Binary files /dev/null and b/apps/website/screens/components/toast/specs/images/toast_specs.png differ diff --git a/apps/website/screens/components/toast/usage/ToastUsagePage.tsx b/apps/website/screens/components/toast/usage/ToastUsagePage.tsx new file mode 100644 index 0000000000..5130034b2e --- /dev/null +++ b/apps/website/screens/components/toast/usage/ToastUsagePage.tsx @@ -0,0 +1,188 @@ +import { DxcBulletedList, DxcFlex, DxcParagraph, DxcTable, DxcTypography } from "@dxc-technology/halstack-react"; +import QuickNavContainer from "@/common/QuickNavContainer"; +import QuickNavContainerLayout from "@/common/QuickNavContainerLayout"; +import DocFooter from "@/common/DocFooter"; +import HeaderDescriptionCell from "@/common/HeaderDescriptionCell"; +import Figure from "@/common/Figure"; +import Image from "@/common/Image"; +import semanticToasts from "./images/semantic_toasts.png"; +import loadingToast from "./images/loading_toast.png"; +import toastsPositioning from "./images/toasts_positioning.png"; + +const sections = [ + { + title: "Usage", + content: ( + + Keep messages concise and clear to ensure quick readability. + + Position toasts in the bottom-right to avoid obstructing main content. + + + Display no more than 5 toasts simultaneously to avoid overwhelming users. + + + Maintain a consistent visual style and placement for all toasts across the application. + + + ), + }, + { + title: "Semantic toasts", + content: ( + <> + Toasts can be categorized based on their purpose: + + + + Semantic + Description + + + + + + + Default + + + Used for neutral messages or general notifications. (ie. Settings have been updated.) + + + + + Info + + + + Displays general information or updates. (ie. New message received. Check inbox. - New update available. + Download now.) + + + + + + Warning + + + + Indicates successful completion of an action. (ie. Operation successful. Changes saved. - Profile + updated successfully.) + + + + + + Success + + + + Provides cautionary advice without blocking actions. (ie. Unstable connection. Proceed with caution.) + + + + + Semantic toasts based on their purpose + + ), + }, + { + title: "Loading status toast", + content: ( + <> + + A loading toast provides users with real-time feedback during an ongoing process. Instead of a static icon, a + spinner is displayed to visually indicate that the process is still in progress. This toast remains visible + until the process is complete, ensuring users are aware that the system is working. Once the task is finished, + the loading toast will automatically disappear, and a follow-up toast will appear in the queue to confirm the + outcome of the process. + +
+ Common loading process represented with toasts +
+ + ), + }, + { + title: "Position and order on screen", + content: ( + <> + + Toasts should be positioned in a way that ensures they are easily noticeable without obstructing the main + content or interrupting the user's workflow. + + + + Bottom-Right: Toasts are aligned to the bottom-right corner of the screen. + + + Bottom-Center: On small devices, toasts are positioned in the bottom-center of the screen. + + + + This positions allows users to receive notifications without them interfering with primary tasks or content. + + Positioning of some example toasts on the screen + + Toasts should appear and disappear in a specific order to ensure clarity and consistency in user + notifications: + + + + Order of appearance: Toasts appear in the order they are triggered. This means the newest + toast will appear at the bottom of the stack. This ensures users see the most recent notification last, + making it easier to track the sequence of events. + + + Order of disappearance: Toasts disappear in the same order they appeared. This means the + oldest toast will disappear first, maintaining a First In, First Out (FIFO) system. This order helps + maintain a logical flow and ensures users have enough time to read each notification. + + + + ), + }, + { + title: "Managing multiple toasts", + content: ( + <> + + When multiple toasts appear on the screen simultaneously, it's important to manage their display to ensure + they don't overlap and that each one remains visible and readable. + + Key practices to ensure they remain effective and user-friendly: + + + Stacking: Toasts are displayed in a vertical stack. New toasts are added to the stack in a + consistent location (at the bottom). + + + Offset spacing: Small gap between toasts to visually separate them (8px) + + + Limit: Only 5 toast max. should be displayed at the same time. + + + Sequential display: Display toasts one after another rather than all at once. + + + Timing: Set a uniform duration for each toast to stay visible (3-5 seconds). + + + + ), + }, +]; + +const TextareaUsagePage = () => { + return ( + + + + + + + ); +}; + +export default TextareaUsagePage; diff --git a/apps/website/screens/components/toast/usage/images/loading_toast.png b/apps/website/screens/components/toast/usage/images/loading_toast.png new file mode 100644 index 0000000000..a99cdd72e6 Binary files /dev/null and b/apps/website/screens/components/toast/usage/images/loading_toast.png differ diff --git a/apps/website/screens/components/toast/usage/images/semantic_toasts.png b/apps/website/screens/components/toast/usage/images/semantic_toasts.png new file mode 100644 index 0000000000..1d6823278a Binary files /dev/null and b/apps/website/screens/components/toast/usage/images/semantic_toasts.png differ diff --git a/apps/website/screens/components/toast/usage/images/toasts_positioning.png b/apps/website/screens/components/toast/usage/images/toasts_positioning.png new file mode 100644 index 0000000000..3d34099ff3 Binary files /dev/null and b/apps/website/screens/components/toast/usage/images/toasts_positioning.png differ diff --git a/apps/website/screens/overview/introduction/IntroductionPage.tsx b/apps/website/screens/overview/introduction/IntroductionPage.tsx index b69e451d84..6023edd5d4 100644 --- a/apps/website/screens/overview/introduction/IntroductionPage.tsx +++ b/apps/website/screens/overview/introduction/IntroductionPage.tsx @@ -228,4 +228,4 @@ const Introduction = () => { ); }; -export default Introduction; +export default Introduction; \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 25f0880981..941ed2e993 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,7 +23,6 @@ "@cloudscape-design/components": "^3.0.706", "@dxc-technology/halstack-react": "*", "@radix-ui/react-popover": "^1.0.7", - "@types/styled-components": "5.1.29", "cross-env": "^7.0.3", "next": "14.2.10", "raw-loader": "^4.0.2", @@ -42,6 +41,7 @@ "@types/react": "^18", "@types/react-color": "^3.0.6", "@types/react-dom": "^18", + "@types/styled-components": "5.1.29", "eslint": "^8", "eslint-config-next": "14.2.4", "typescript": "^5" @@ -2148,9 +2148,9 @@ } }, "node_modules/@cloudscape-design/collection-hooks": { - "version": "1.0.52", - "resolved": "https://registry.npmjs.org/@cloudscape-design/collection-hooks/-/collection-hooks-1.0.52.tgz", - "integrity": "sha512-7HcMpyAaMBP4gnVnEAdShL6RqfKAs1b1KttAZml2sxI+tmrWY50DkbUH1gP4SuxRX/+b7eiA3kBvFm8HmlkWZA==", + "version": "1.0.53", + "resolved": "https://registry.npmjs.org/@cloudscape-design/collection-hooks/-/collection-hooks-1.0.53.tgz", + "integrity": "sha512-3EvwMN+t9AeoG4Lzu6LtTet/3OwM2hIegeYBA6QGRycMZ3VWKhI7y8GMj0xEGJY4+9Yx10jyjhPcC6ER1s0/UQ==", "license": "Apache-2.0", "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0" @@ -2167,9 +2167,9 @@ } }, "node_modules/@cloudscape-design/components": { - "version": "3.0.756", - "resolved": "https://registry.npmjs.org/@cloudscape-design/components/-/components-3.0.756.tgz", - "integrity": "sha512-EwG1cAXHtgyQ2/XEDvjDkxjZJR2cHJmyIDTZqUWls2ARHWa22+D8L1890yaxsn2Mb72qOKXh5rHZDDLllKKnHg==", + "version": "3.0.758", + "resolved": "https://registry.npmjs.org/@cloudscape-design/components/-/components-3.0.758.tgz", + "integrity": "sha512-XuQG5DW1ukeMdyckxWfqcUxcmUpwlcrpQbbu4GRsd2YPDi2pqzKSxofshbs8NWzJc4jK5U84CTF2qx2GjSdGCg==", "license": "Apache-2.0", "dependencies": { "@cloudscape-design/collection-hooks": "^1.0.0", @@ -2198,9 +2198,9 @@ } }, "node_modules/@cloudscape-design/test-utils-core": { - "version": "1.0.41", - "resolved": "https://registry.npmjs.org/@cloudscape-design/test-utils-core/-/test-utils-core-1.0.41.tgz", - "integrity": "sha512-CXvkRndyX+uCkuCNT/D4IUFvio7KhKK8Gfm2ISdk92H89ybku07ff4oK5R7elMfAQiX0DR/snb8g72b+e1XIYw==", + "version": "1.0.42", + "resolved": "https://registry.npmjs.org/@cloudscape-design/test-utils-core/-/test-utils-core-1.0.42.tgz", + "integrity": "sha512-vNSODT+6Kp8LSI56ddorC7hLkts2O+AH3v6DYfbwhx/giHs4rDtKPNGcQ6abyrY2DLIByWgFdbHpx2hNoQgCPw==", "license": "Apache-2.0", "dependencies": { "css-selector-tokenizer": "^0.8.0", @@ -5225,16 +5225,16 @@ } }, "node_modules/@rollup/pluginutils/node_modules/@types/estree": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", - "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", + "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", "dev": true, "license": "MIT" }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.21.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.21.3.tgz", - "integrity": "sha512-MmKSfaB9GX+zXl6E8z4koOr/xU63AMVleLEa64v7R0QF/ZloMs5vcD1sHgM64GXXS1csaJutG+ddtzcueI/BLg==", + "version": "4.22.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.22.0.tgz", + "integrity": "sha512-/IZQvg6ZR0tAkEi4tdXOraQoWeJy9gbQ/cx4I7k9dJaCk9qrXEcdouxRVz5kZXt5C2bQ9pILoAA+KB4C/d3pfw==", "cpu": [ "arm" ], @@ -5246,9 +5246,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.21.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.21.3.tgz", - "integrity": "sha512-zrt8ecH07PE3sB4jPOggweBjJMzI1JG5xI2DIsUbkA+7K+Gkjys6eV7i9pOenNSDJH3eOr/jLb/PzqtmdwDq5g==", + "version": "4.22.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.22.0.tgz", + "integrity": "sha512-ETHi4bxrYnvOtXeM7d4V4kZWixib2jddFacJjsOjwbgYSRsyXYtZHC4ht134OsslPIcnkqT+TKV4eU8rNBKyyQ==", "cpu": [ "arm64" ], @@ -5260,9 +5260,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.21.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.21.3.tgz", - "integrity": "sha512-P0UxIOrKNBFTQaXTxOH4RxuEBVCgEA5UTNV6Yz7z9QHnUJ7eLX9reOd/NYMO3+XZO2cco19mXTxDMXxit4R/eQ==", + "version": "4.22.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.22.0.tgz", + "integrity": "sha512-ZWgARzhSKE+gVUX7QWaECoRQsPwaD8ZR0Oxb3aUpzdErTvlEadfQpORPXkKSdKbFci9v8MJfkTtoEHnnW9Ulng==", "cpu": [ "arm64" ], @@ -5274,9 +5274,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.21.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.21.3.tgz", - "integrity": "sha512-L1M0vKGO5ASKntqtsFEjTq/fD91vAqnzeaF6sfNAy55aD+Hi2pBI5DKwCO+UNDQHWsDViJLqshxOahXyLSh3EA==", + "version": "4.22.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.22.0.tgz", + "integrity": "sha512-h0ZAtOfHyio8Az6cwIGS+nHUfRMWBDO5jXB8PQCARVF6Na/G6XS2SFxDl8Oem+S5ZsHQgtsI7RT4JQnI1qrlaw==", "cpu": [ "x64" ], @@ -5288,9 +5288,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.21.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.21.3.tgz", - "integrity": "sha512-btVgIsCjuYFKUjopPoWiDqmoUXQDiW2A4C3Mtmp5vACm7/GnyuprqIDPNczeyR5W8rTXEbkmrJux7cJmD99D2g==", + "version": "4.22.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.22.0.tgz", + "integrity": "sha512-9pxQJSPwFsVi0ttOmqLY4JJ9pg9t1gKhK0JDbV1yUEETSx55fdyCjt39eBQ54OQCzAF0nVGO6LfEH1KnCPvelA==", "cpu": [ "arm" ], @@ -5302,9 +5302,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.21.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.21.3.tgz", - "integrity": "sha512-zmjbSphplZlau6ZTkxd3+NMtE4UKVy7U4aVFMmHcgO5CUbw17ZP6QCgyxhzGaU/wFFdTfiojjbLG3/0p9HhAqA==", + "version": "4.22.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.22.0.tgz", + "integrity": "sha512-YJ5Ku5BmNJZb58A4qSEo3JlIG4d3G2lWyBi13ABlXzO41SsdnUKi3HQHe83VpwBVG4jHFTW65jOQb8qyoR+qzg==", "cpu": [ "arm" ], @@ -5316,9 +5316,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.21.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.21.3.tgz", - "integrity": "sha512-nSZfcZtAnQPRZmUkUQwZq2OjQciR6tEoJaZVFvLHsj0MF6QhNMg0fQ6mUOsiCUpTqxTx0/O6gX0V/nYc7LrgPw==", + "version": "4.22.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.22.0.tgz", + "integrity": "sha512-U4G4u7f+QCqHlVg1Nlx+qapZy+QoG+NV6ux+upo/T7arNGwKvKP2kmGM4W5QTbdewWFgudQxi3kDNST9GT1/mg==", "cpu": [ "arm64" ], @@ -5330,9 +5330,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.21.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.21.3.tgz", - "integrity": "sha512-MnvSPGO8KJXIMGlQDYfvYS3IosFN2rKsvxRpPO2l2cum+Z3exiExLwVU+GExL96pn8IP+GdH8Tz70EpBhO0sIQ==", + "version": "4.22.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.22.0.tgz", + "integrity": "sha512-aQpNlKmx3amwkA3a5J6nlXSahE1ijl0L9KuIjVOUhfOh7uw2S4piR3mtpxpRtbnK809SBtyPsM9q15CPTsY7HQ==", "cpu": [ "arm64" ], @@ -5344,9 +5344,9 @@ ] }, "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.21.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.21.3.tgz", - "integrity": "sha512-+W+p/9QNDr2vE2AXU0qIy0qQE75E8RTwTwgqS2G5CRQ11vzq0tbnfBd6brWhS9bCRjAjepJe2fvvkvS3dno+iw==", + "version": "4.22.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.22.0.tgz", + "integrity": "sha512-9fx6Zj/7vve/Fp4iexUFRKb5+RjLCff6YTRQl4CoDhdMfDoobWmhAxQWV3NfShMzQk1Q/iCnageFyGfqnsmeqQ==", "cpu": [ "ppc64" ], @@ -5358,9 +5358,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.21.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.21.3.tgz", - "integrity": "sha512-yXH6K6KfqGXaxHrtr+Uoy+JpNlUlI46BKVyonGiaD74ravdnF9BUNC+vV+SIuB96hUMGShhKV693rF9QDfO6nQ==", + "version": "4.22.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.22.0.tgz", + "integrity": "sha512-VWQiCcN7zBgZYLjndIEh5tamtnKg5TGxyZPWcN9zBtXBwfcGSZ5cHSdQZfQH/GB4uRxk0D3VYbOEe/chJhPGLQ==", "cpu": [ "riscv64" ], @@ -5372,9 +5372,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.21.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.21.3.tgz", - "integrity": "sha512-R8cwY9wcnApN/KDYWTH4gV/ypvy9yZUHlbJvfaiXSB48JO3KpwSpjOGqO4jnGkLDSk1hgjYkTbTt6Q7uvPf8eg==", + "version": "4.22.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.22.0.tgz", + "integrity": "sha512-EHmPnPWvyYqncObwqrosb/CpH3GOjE76vWVs0g4hWsDRUVhg61hBmlVg5TPXqF+g+PvIbqkC7i3h8wbn4Gp2Fg==", "cpu": [ "s390x" ], @@ -5386,9 +5386,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.21.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.21.3.tgz", - "integrity": "sha512-kZPbX/NOPh0vhS5sI+dR8L1bU2cSO9FgxwM8r7wHzGydzfSjLRCFAT87GR5U9scj2rhzN3JPYVC7NoBbl4FZ0g==", + "version": "4.22.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.22.0.tgz", + "integrity": "sha512-tsSWy3YQzmpjDKnQ1Vcpy3p9Z+kMFbSIesCdMNgLizDWFhrLZIoN21JSq01g+MZMDFF+Y1+4zxgrlqPjid5ohg==", "cpu": [ "x64" ], @@ -5400,9 +5400,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.21.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.21.3.tgz", - "integrity": "sha512-S0Yq+xA1VEH66uiMNhijsWAafffydd2X5b77eLHfRmfLsRSpbiAWiRHV6DEpz6aOToPsgid7TI9rGd6zB1rhbg==", + "version": "4.22.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.22.0.tgz", + "integrity": "sha512-anr1Y11uPOQrpuU8XOikY5lH4Qu94oS6j0xrulHk3NkLDq19MlX8Ng/pVipjxBJ9a2l3+F39REZYyWQFkZ4/fw==", "cpu": [ "x64" ], @@ -5414,9 +5414,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.21.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.21.3.tgz", - "integrity": "sha512-9isNzeL34yquCPyerog+IMCNxKR8XYmGd0tHSV+OVx0TmE0aJOo9uw4fZfUuk2qxobP5sug6vNdZR6u7Mw7Q+Q==", + "version": "4.22.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.22.0.tgz", + "integrity": "sha512-7LB+Bh+Ut7cfmO0m244/asvtIGQr5pG5Rvjz/l1Rnz1kDzM02pSX9jPaS0p+90H5I1x4d1FkCew+B7MOnoatNw==", "cpu": [ "arm64" ], @@ -5428,9 +5428,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.21.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.21.3.tgz", - "integrity": "sha512-nMIdKnfZfzn1Vsk+RuOvl43ONTZXoAPUUxgcU0tXooqg4YrAqzfKzVenqqk2g5efWh46/D28cKFrOzDSW28gTA==", + "version": "4.22.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.22.0.tgz", + "integrity": "sha512-+3qZ4rer7t/QsC5JwMpcvCVPRcJt1cJrYS/TMJZzXIJbxWFQEVhrIc26IhB+5Z9fT9umfVc+Es2mOZgl+7jdJQ==", "cpu": [ "ia32" ], @@ -5442,9 +5442,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.21.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.21.3.tgz", - "integrity": "sha512-fOvu7PCQjAj4eWDEuD8Xz5gpzFqXzGlxHZozHP4b9Jxv9APtdxL6STqztDzMLuRXEc4UpXGGhx029Xgm91QBeA==", + "version": "4.22.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.22.0.tgz", + "integrity": "sha512-YdicNOSJONVx/vuPkgPTyRoAPx3GbknBZRCOUkK84FJ/YTfs/F0vl/YsMscrB6Y177d+yDRcj+JWMPMCgshwrA==", "cpu": [ "x64" ], @@ -5978,9 +5978,9 @@ "license": "MIT" }, "node_modules/@storybook/icons": { - "version": "1.2.10", - "resolved": "https://registry.npmjs.org/@storybook/icons/-/icons-1.2.10.tgz", - "integrity": "sha512-310apKdDcjbbX2VSLWPwhEwAgjxTzVagrwucVZIdGPErwiAppX8KvBuWZgPo+rQLVrtH8S+pw1dbUwjcE6d7og==", + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@storybook/icons/-/icons-1.2.12.tgz", + "integrity": "sha512-UxgyK5W3/UV4VrI3dl6ajGfHM4aOqMAkFLWe2KibeQudLf6NJpDrDMSHwZj+3iKC4jFU7dkKbbtH2h/al4sW3Q==", "dev": true, "license": "MIT", "engines": { @@ -6660,108 +6660,31 @@ } }, "node_modules/@testing-library/react": { - "version": "13.4.0", - "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-13.4.0.tgz", - "integrity": "sha512-sXOGON+WNTh3MLE9rve97ftaZukN3oNf2KjDy7YTx6hcTO2uuLHuCGynMDhFwGw/jYf4OJ2Qk0i4i79qMNNkyw==", + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.0.1.tgz", + "integrity": "sha512-dSmwJVtJXmku+iocRhWOUFbrERC76TX2Mnf0ATODz8brzAZrMBbzLwQixlBSanZxR6LddK3eiwpSFZgDET1URg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/runtime": "^7.12.5", - "@testing-library/dom": "^8.5.0", - "@types/react-dom": "^18.0.0" + "@babel/runtime": "^7.12.5" }, "engines": { - "node": ">=12" + "node": ">=18" }, "peerDependencies": { + "@testing-library/dom": "^10.0.0", + "@types/react": "^18.0.0", + "@types/react-dom": "^18.0.0", "react": "^18.0.0", "react-dom": "^18.0.0" - } - }, - "node_modules/@testing-library/react/node_modules/@testing-library/dom": { - "version": "8.20.1", - "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-8.20.1.tgz", - "integrity": "sha512-/DiOQ5xBxgdYRC8LNk7U+RWat0S3qRLeIw3ZIkMQ9kkVlRmwD/Eg8k8CqIpD6GW7u20JIUOfMKbxtiLutpjQ4g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.10.4", - "@babel/runtime": "^7.12.5", - "@types/aria-query": "^5.0.1", - "aria-query": "5.1.3", - "chalk": "^4.1.0", - "dom-accessibility-api": "^0.5.9", - "lz-string": "^1.5.0", - "pretty-format": "^27.0.2" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@testing-library/react/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@testing-library/react/node_modules/aria-query": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.1.3.tgz", - "integrity": "sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "deep-equal": "^2.0.5" - } - }, - "node_modules/@testing-library/react/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/@testing-library/react/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/@testing-library/react/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } } }, "node_modules/@testing-library/user-event": { @@ -7135,6 +7058,7 @@ "version": "3.3.5", "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.5.tgz", "integrity": "sha512-SbcrWzkKBw2cdwRTwQAswfpB9g9LJWfjtUeW/jvNwbhC8cpmmNYVePa+ncbUe0rGTQ7G3Ff6mYUN2VMfLVr+Sg==", + "dev": true, "license": "MIT", "dependencies": { "@types/react": "*", @@ -7466,6 +7390,7 @@ "version": "5.1.29", "resolved": "https://registry.npmjs.org/@types/styled-components/-/styled-components-5.1.29.tgz", "integrity": "sha512-5h/ah9PAblggQ6Laa4peplT4iY5ddA8qM1LMD4HzwToUWs3hftfy0fayeRgbtH1JZUdw5CCaowmz7Lnb8SjIxQ==", + "dev": true, "license": "MIT", "dependencies": { "@types/hoist-non-react-statics": "*", @@ -8045,9 +7970,9 @@ } }, "node_modules/@vitest/expect/node_modules/@types/estree": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", - "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", + "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", "dev": true, "license": "MIT" }, @@ -9612,9 +9537,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001660", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001660.tgz", - "integrity": "sha512-GacvNTTuATm26qC74pt+ad1fW15mlQ/zuTzzY1ZoIzECTP8HURDfF43kNxPgf7H1jmelCBQTTbBNxdSXOA7Bqg==", + "version": "1.0.30001662", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001662.tgz", + "integrity": "sha512-sgMUVwLmGseH8ZIrm1d51UbrhqMCH3jvS7gF/M6byuHOnKyLOBL7W8yz5V02OHwgLGA36o/AFhWzzh4uc5aqTA==", "funding": [ { "type": "opencollective", @@ -22083,9 +22008,9 @@ } }, "node_modules/rollup": { - "version": "4.21.3", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.21.3.tgz", - "integrity": "sha512-7sqRtBNnEbcBtMeRVc6VRsJMmpI+JU1z9VTvW8D4gXIYQFz0aLcsE6rRkyghZkLfEgUZgVvOG7A5CVz/VW5GIA==", + "version": "4.22.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.22.0.tgz", + "integrity": "sha512-W21MUIFPZ4+O2Je/EU+GP3iz7PH4pVPUXSbEZdatQnxo29+3rsUjgrJmzuAZU24z7yRAnFN6ukxeAhZh/c7hzg==", "dev": true, "license": "MIT", "dependencies": { @@ -22099,22 +22024,22 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.21.3", - "@rollup/rollup-android-arm64": "4.21.3", - "@rollup/rollup-darwin-arm64": "4.21.3", - "@rollup/rollup-darwin-x64": "4.21.3", - "@rollup/rollup-linux-arm-gnueabihf": "4.21.3", - "@rollup/rollup-linux-arm-musleabihf": "4.21.3", - "@rollup/rollup-linux-arm64-gnu": "4.21.3", - "@rollup/rollup-linux-arm64-musl": "4.21.3", - "@rollup/rollup-linux-powerpc64le-gnu": "4.21.3", - "@rollup/rollup-linux-riscv64-gnu": "4.21.3", - "@rollup/rollup-linux-s390x-gnu": "4.21.3", - "@rollup/rollup-linux-x64-gnu": "4.21.3", - "@rollup/rollup-linux-x64-musl": "4.21.3", - "@rollup/rollup-win32-arm64-msvc": "4.21.3", - "@rollup/rollup-win32-ia32-msvc": "4.21.3", - "@rollup/rollup-win32-x64-msvc": "4.21.3", + "@rollup/rollup-android-arm-eabi": "4.22.0", + "@rollup/rollup-android-arm64": "4.22.0", + "@rollup/rollup-darwin-arm64": "4.22.0", + "@rollup/rollup-darwin-x64": "4.22.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.22.0", + "@rollup/rollup-linux-arm-musleabihf": "4.22.0", + "@rollup/rollup-linux-arm64-gnu": "4.22.0", + "@rollup/rollup-linux-arm64-musl": "4.22.0", + "@rollup/rollup-linux-powerpc64le-gnu": "4.22.0", + "@rollup/rollup-linux-riscv64-gnu": "4.22.0", + "@rollup/rollup-linux-s390x-gnu": "4.22.0", + "@rollup/rollup-linux-x64-gnu": "4.22.0", + "@rollup/rollup-linux-x64-musl": "4.22.0", + "@rollup/rollup-win32-arm64-msvc": "4.22.0", + "@rollup/rollup-win32-ia32-msvc": "4.22.0", + "@rollup/rollup-win32-x64-msvc": "4.22.0", "fsevents": "~2.3.2" } }, @@ -25672,9 +25597,9 @@ "license": "MIT" }, "node_modules/webpack/node_modules/@types/estree": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", - "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", + "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", "license": "MIT", "peer": true }, @@ -26160,12 +26085,13 @@ "@storybook/addon-essentials": "^8.1.10", "@storybook/addon-interactions": "^8.1.10", "@storybook/addon-links": "^8.1.10", + "@storybook/addon-viewport": "^8.2.9", "@storybook/blocks": "^8.1.10", "@storybook/react": "^8.1.10", "@storybook/react-vite": "^8.1.10", "@storybook/test": "^8.1.10", "@storybook/test-runner": "^0.18.2", - "@testing-library/react": "^13.0.0", + "@testing-library/react": "^16.0.1", "@testing-library/user-event": "^13.0.0", "@turbo/gen": "^1.12.4", "@types/eslint": "^8.56.5", @@ -26190,8 +26116,8 @@ "typescript": "^5.3.3" }, "peerDependencies": { - "react": "^18.x", - "react-dom": "^18.x", + "react": "^18.3.1", + "react-dom": "^18.3.1", "styled-components": "^5.0.1" } }, diff --git a/packages/lib/.storybook/main.ts b/packages/lib/.storybook/main.ts index ff5fe0b9cd..53feea3d29 100644 --- a/packages/lib/.storybook/main.ts +++ b/packages/lib/.storybook/main.ts @@ -6,6 +6,7 @@ const config: StorybookConfig = { "@storybook/addon-links", "@storybook/addon-essentials", "@storybook/addon-interactions", + '@storybook/addon-viewport', "storybook-addon-pseudo-states", "@storybook/addon-a11y", "@chromatic-com/storybook", diff --git a/packages/lib/package.json b/packages/lib/package.json index 4734de3d9d..b54f81a18e 100644 --- a/packages/lib/package.json +++ b/packages/lib/package.json @@ -29,8 +29,8 @@ "test:watch": "jest --env=jsdom --config=./jest.config.js --watch" }, "peerDependencies": { - "react": "^18.x", - "react-dom": "^18.x", + "react": "^18.3.1", + "react-dom": "^18.3.1", "styled-components": "^5.0.1" }, "dependencies": { @@ -57,12 +57,13 @@ "@storybook/addon-essentials": "^8.1.10", "@storybook/addon-interactions": "^8.1.10", "@storybook/addon-links": "^8.1.10", + "@storybook/addon-viewport": "^8.2.9", "@storybook/blocks": "^8.1.10", "@storybook/react": "^8.1.10", "@storybook/react-vite": "^8.1.10", "@storybook/test": "^8.1.10", "@storybook/test-runner": "^0.18.2", - "@testing-library/react": "^13.0.0", + "@testing-library/react": "^16.0.1", "@testing-library/user-event": "^13.0.0", "@turbo/gen": "^1.12.4", "@types/eslint": "^8.56.5", diff --git a/packages/lib/src/common/variables.ts b/packages/lib/src/common/variables.ts index cd81bf93f9..9ddb1e0a73 100644 --- a/packages/lib/src/common/variables.ts +++ b/packages/lib/src/common/variables.ts @@ -1500,14 +1500,6 @@ export const responsiveSizes = { }; export const defaultTranslatedComponentLabels = { - formFields: { - optionalLabel: "(Optional)", - requiredSelectionErrorMessage: "This field is required. Please, choose an option.", - requiredValueErrorMessage: "This field is required. Please, enter a value.", - formatRequestedErrorMessage: "Please match the format requested.", - lengthErrorMessage: (minLength?: number, maxLength?: number) => `Min length ${minLength}, max length ${maxLength}.`, - logoAlternativeText: "Logo", - }, applicationLayout: { visibilityToggleTitle: "Toggle sidenav visibility", }, @@ -1517,6 +1509,25 @@ export const defaultTranslatedComponentLabels = { warningTitleText: "warning", errorTitleText: "error", }, + calendar: { + daysShort: ["Mo", "Tu", "We", "Th", "Fr", "Sa", "Su"], + months: [ + "January", + "February", + "March", + "April", + "May", + "June", + "July", + "August", + "September", + "October", + "November", + "December", + ], + previousMonthTitle: "Previous month", + nextMonthTitle: "Next month", + }, dateInput: { invalidDateErrorMessage: "Invalid date.", }, @@ -1536,6 +1547,14 @@ export const defaultTranslatedComponentLabels = { footer: { copyrightText: (year: number) => `© DXC Technology ${year}. All rights reserved.`, }, + formFields: { + optionalLabel: "(Optional)", + requiredSelectionErrorMessage: "This field is required. Please, choose an option.", + requiredValueErrorMessage: "This field is required. Please, enter a value.", + formatRequestedErrorMessage: "Please match the format requested.", + lengthErrorMessage: (minLength?: number, maxLength?: number) => `Min length ${minLength}, max length ${maxLength}.`, + logoAlternativeText: "Logo", + }, header: { closeIcon: "Close menu", hamburguerTitle: "Menu", @@ -1577,24 +1596,8 @@ export const defaultTranslatedComponentLabels = { searchingMessage: "Searching...", fetchingDataErrorMessage: "Error fetching data", }, - calendar: { - daysShort: ["Mo", "Tu", "We", "Th", "Fr", "Sa", "Su"], - months: [ - "January", - "February", - "March", - "April", - "May", - "June", - "July", - "August", - "September", - "October", - "November", - "December", - ], - previousMonthTitle: "Previous month", - nextMonthTitle: "Next month", + toast: { + clearToastActionTitle: "Clear toast", }, }; diff --git a/packages/lib/src/index.ts b/packages/lib/src/index.ts index 9a720a5017..3c712db26a 100644 --- a/packages/lib/src/index.ts +++ b/packages/lib/src/index.ts @@ -44,11 +44,14 @@ import DxcTabs from "./tabs/Tabs"; import DxcTag from "./tag/Tag"; import DxcTextarea from "./textarea/Textarea"; import DxcTextInput from "./text-input/TextInput"; +import DxcToastsQueue from "./toast/ToastsQueue"; import DxcToggleGroup from "./toggle-group/ToggleGroup"; import DxcTooltip from "./tooltip/Tooltip"; import DxcTypography from "./typography/Typography"; import DxcWizard from "./wizard/Wizard"; +import useToast from "./toast/useToast"; + import HalstackContext, { HalstackProvider, HalstackLanguageContext } from "./HalstackContext"; export { @@ -98,6 +101,7 @@ export { DxcTag, DxcTextarea, DxcTextInput, + DxcToastsQueue, DxcToggleGroup, DxcTooltip, DxcTypography, @@ -105,4 +109,5 @@ export { HalstackContext, HalstackLanguageContext, HalstackProvider, + useToast as useToast, }; diff --git a/packages/lib/src/toast/Toast.accessibility.test.tsx b/packages/lib/src/toast/Toast.accessibility.test.tsx new file mode 100644 index 0000000000..a95bc3a4f1 --- /dev/null +++ b/packages/lib/src/toast/Toast.accessibility.test.tsx @@ -0,0 +1,70 @@ +import { render } from "@testing-library/react"; +import { axe } from "../../test/accessibility/axe-helper.js"; +import DxcToast from "./Toast"; +import DxcToastsQueue from "./ToastsQueue"; +import useToast from "./useToast"; +import DxcButton from "../button/Button"; +import userEvent from "@testing-library/user-event"; + +const actionIcon = { + label: "Action", + onClick: () => { + console.log("Action clicked"); + }, + icon: "restart_alt", +}; + +const ToastPage = () => { + const toast = useToast(); + return ( + { + toast.default({ message: "This is a simple placed toast." }); + }} + /> + ); +}; +const TestExample = () => ( + + + +); + +describe("Toast component accessibility tests", () => { + it("Toast queue should not have accessibility issues", async () => { + const { container } = render(); + const results = await axe(container); + const button = container.querySelector("button"); + userEvent.click(button); + expect(results).toHaveNoViolations(); + }); + it("Should not have basic accessibility issues", async () => { + const { container } = render( + {}} + icon="rocket" + action={actionIcon} + /> + ); + const results = await axe(container); + expect(results).toHaveNoViolations(); + }); + it("Should not have accessibility issues when loading", async () => { + const { container } = render( + {}} + action={actionIcon} + loading + /> + ); + const results = await axe(container); + expect(results).toHaveNoViolations(); + }); +}); diff --git a/packages/lib/src/toast/Toast.stories.tsx b/packages/lib/src/toast/Toast.stories.tsx new file mode 100644 index 0000000000..7e3d59d6c7 --- /dev/null +++ b/packages/lib/src/toast/Toast.stories.tsx @@ -0,0 +1,263 @@ +import { userEvent, within } from "@storybook/test"; +import ExampleContainer from "../../.storybook/components/ExampleContainer"; +import Title from "../../.storybook/components/Title"; +import DxcButton from "../button/Button"; +import DxcFlex from "../flex/Flex"; +import DxcToast from "./Toast"; +import DxcToastsQueue from "./ToastsQueue"; +import useToast from "./useToast"; +import { INITIAL_VIEWPORTS } from "@storybook/addon-viewport"; + +export default { + title: "Toast", + component: DxcToast, +}; + +const action = { + label: "Action", + onClick: () => { + console.log("Action clicked"); + }, +}; +const actionIcon = { + label: "Action", + onClick: () => { + console.log("Action clicked"); + }, + icon: "restart_alt", +}; +const onClear = () => {}; + +export const Chromatic = () => ( + <> + + <ExampleContainer> + <Title title="Simple" /> + <DxcToast semantic="default" duration={2147483647} message="This is a toast." onClear={onClear} /> + </ExampleContainer> + <ExampleContainer> + <Title title="With material icon" /> + <DxcToast semantic="default" duration={2147483647} message="This is a toast." onClear={onClear} icon="rocket" /> + </ExampleContainer> + <ExampleContainer> + <Title title="With custom icon" /> + <DxcToast + semantic="default" + duration={2147483647} + message="This is a toast." + onClear={onClear} + icon={ + <svg + xmlns="http://www.w3.org/2000/svg" + height="48px" + viewBox="0 -960 960 960" + width="48px" + fill="currentColor" + > + <path d="M120-566q0-90 40-165t107-125l36 48q-56 42-89.5 104.5T180-566h-60Zm660 0q0-75-33.5-137.5T657-808l36-48q67 50 107 125t40 165h-60ZM160-200v-60h80v-304q0-84 49.5-150.5T420-798v-22q0-25 17.5-42.5T480-880q25 0 42.5 17.5T540-820v22q81 17 130.5 83.5T720-564v304h80v60H160Zm320-302Zm0 422q-33 0-56.5-23.5T400-160h160q0 33-23.5 56.5T480-80ZM300-260h360v-304q0-75-52.5-127.5T480-744q-75 0-127.5 52.5T300-564v304Z" /> + </svg> + } + /> + </ExampleContainer> + <ExampleContainer> + <Title title="With action" /> + <DxcToast + semantic="default" + duration={2147483647} + message="This is a toast." + onClear={onClear} + icon="rocket" + action={action} + /> + </ExampleContainer> + <ExampleContainer> + <Title title="With action icon" /> + <DxcToast + semantic="default" + duration={2147483647} + message="This is a toast." + onClear={onClear} + icon="rocket" + action={actionIcon} + /> + </ExampleContainer> + <Title title="Info" level={2} /> + <ExampleContainer> + <Title title="Simple" /> + <DxcToast semantic="info" duration={2147483647} message="This is a toast." onClear={onClear} hideSemanticIcon /> + </ExampleContainer> + <ExampleContainer> + <Title title="With icon" /> + <DxcToast semantic="info" duration={2147483647} message="This is a toast." onClear={onClear} icon="rocket" /> + </ExampleContainer> + <ExampleContainer> + <Title title="With action" /> + <DxcToast + semantic="info" + duration={2147483647} + message="This is a toast." + onClear={onClear} + icon="rocket" + action={action} + /> + </ExampleContainer> + <ExampleContainer> + <Title title="With action icon" /> + <DxcToast + semantic="info" + duration={2147483647} + message="This is a toast." + onClear={onClear} + icon="rocket" + action={actionIcon} + /> + </ExampleContainer> + <ExampleContainer> + <Title title="With loading indicator" /> + <DxcToast + semantic="info" + duration={2147483647} + message="This is a toast." + onClear={onClear} + action={actionIcon} + loading + /> + </ExampleContainer> + <Title title="Success" level={2} /> + <ExampleContainer> + <Title title="Simple" /> + <DxcToast semantic="success" duration={2147483647} message="This is a toast." onClear={onClear} hideSemanticIcon /> + </ExampleContainer> + <ExampleContainer> + <Title title="With icon" /> + <DxcToast semantic="success" duration={2147483647} message="This is a toast." onClear={onClear} icon="rocket" /> + </ExampleContainer> + <ExampleContainer> + <Title title="With action" /> + <DxcToast + semantic="success" + duration={2147483647} + message="This is a toast." + onClear={onClear} + icon="rocket" + action={action} + /> + </ExampleContainer> + <ExampleContainer> + <Title title="With action icon" /> + <DxcToast + semantic="success" + duration={2147483647} + message="This is a toast." + onClear={onClear} + icon="rocket" + action={actionIcon} + /> + </ExampleContainer> + <Title title="Warning" level={2} /> + <ExampleContainer> + <Title title="Simple" /> + <DxcToast semantic="warning" duration={2147483647} message="This is a toast." onClear={onClear} hideSemanticIcon /> + </ExampleContainer> + <ExampleContainer> + <Title title="With icon" /> + <DxcToast semantic="warning" duration={2147483647} message="This is a toast." onClear={onClear} icon="rocket" /> + </ExampleContainer> + <ExampleContainer> + <Title title="With action" /> + <DxcToast + semantic="warning" + duration={2147483647} + message="This is a toast." + onClear={onClear} + icon="rocket" + action={action} + /> + </ExampleContainer> + <ExampleContainer> + <Title title="With action icon" /> + <DxcToast + semantic="warning" + duration={2147483647} + message="This is a toast." + onClear={onClear} + icon="rocket" + action={actionIcon} + /> + </ExampleContainer> + <Title title="Extra scenarios" level={2} /> + <ExampleContainer> + <Title title="Ellipsis" /> + <DxcToast + semantic="default" + duration={2147483647} + message="This is a very long label for a Toast. Please, always try to avoid this king of messages, be brief and concise." + onClear={onClear} + icon="rocket" + action={actionIcon} + /> + </ExampleContainer> + </> +); + +const Screens = () => { + const toast = useToast(); + + return ( + <ExampleContainer> + <Title title="Screen placement" /> + <DxcFlex gap="1rem" direction="column"> + <DxcButton + label="Show default toast" + onClick={() => { + toast.default({ message: "This is a simple placed toast." }); + }} + /> + <DxcButton + label="Show info toast" + onClick={() => { + toast.info({ + message: + "This is a very long label for a Toast. Please, always try to avoid this king of messages, be brief and concise.", + action: actionIcon, + }); + }} + /> + <DxcButton + label="Show success toast" + onClick={() => { + toast.success({ + message: + "This is another very long label for a Toast. Please, always try to avoid this king of messages, be brief and concise.", + action: action, + }); + }} + /> + </DxcFlex> + </ExampleContainer> + ); +}; +const ToastsQueue = () => ( + <DxcToastsQueue> + <Screens /> + </DxcToastsQueue> +); + +const playFunc = async ({ canvasElement }) => { + const canvas = within(canvasElement); + await userEvent.click(canvas.getByText("Show default toast")); + await userEvent.click(canvas.getByText("Show info toast")); + await userEvent.click(canvas.getByText("Show success toast")); +}; + +export const FullScreenToast = ToastsQueue.bind({}); +FullScreenToast.play = playFunc; + +export const MobileScreenToast = ToastsQueue.bind({}); +MobileScreenToast.parameters = { + viewport: { + viewports: INITIAL_VIEWPORTS, + defaultViewport: "iphonex", + }, +}; +MobileScreenToast.play = playFunc; diff --git a/packages/lib/src/toast/Toast.test.tsx b/packages/lib/src/toast/Toast.test.tsx new file mode 100644 index 0000000000..c483e471af --- /dev/null +++ b/packages/lib/src/toast/Toast.test.tsx @@ -0,0 +1,226 @@ +import userEvent from "@testing-library/user-event"; +import DxcButton from "../button/Button"; +import DxcToastsQueue from "./ToastsQueue"; +import useToast from "./useToast"; +import { render, waitFor } from "@testing-library/react"; +import { act } from "@testing-library/react"; + +const ToastPage = ({ onClick }: { onClick?: () => void }) => { + const toast = useToast(); + + const loadingFunc = () => { + const removeLoadingToast = toast.loading({ message: "Loading process..." }); + + setTimeout(() => { + removeLoadingToast(); + toast.success({ message: "The process ended successfully." }); + }, 5000); + }; + + return ( + <> + <DxcButton + label="Show info toast" + onClick={() => { + toast.info({ message: "This is an information toast." }); + }} + /> + <DxcButton + label="Show loading toast" + onClick={() => { + toast.loading({ message: "Loading..." }); + }} + /> + <DxcButton + label="Show toast" + onClick={() => { + onClick + ? toast.default({ message: "This is a simple toast.", action: { label: "Action", onClick } }) + : toast.default({ message: "This is a simple toast." }); + }} + /> + <DxcButton label="Load process" onClick={loadingFunc} /> + </> + ); +}; + +describe("Toast component tests", () => { + test("Renders the component", async () => { + const { getByText } = render( + <DxcToastsQueue> + <ToastPage /> + </DxcToastsQueue> + ); + const button = getByText("Show toast"); + userEvent.click(button); + await waitFor(() => { + expect(getByText("This is a simple toast.")).toBeTruthy(); + }); + }); + test("Toast disappears after the specified duration", async () => { + jest.useFakeTimers(); + const { getByText, queryByText } = render( + <DxcToastsQueue duration={4250}> + <ToastPage /> + </DxcToastsQueue> + ); + const button = getByText("Show toast"); + userEvent.click(button); + + act(() => { + jest.advanceTimersByTime(4249); + }); + expect(getByText("This is a simple toast.")).toBeTruthy(); + + act(() => { + jest.advanceTimersByTime(1); + }); + expect(queryByText("This is a simple toast.")).toBeFalsy(); + + jest.useRealTimers(); + }); + test("If duration > 5000, the toast disappears at 5000ms", async () => { + jest.useFakeTimers(); + const { getByText, queryByText } = render( + <DxcToastsQueue duration={1000000}> + <ToastPage /> + </DxcToastsQueue> + ); + const button = getByText("Show toast"); + userEvent.click(button); + + act(() => { + jest.advanceTimersByTime(5001); + }); + expect(queryByText("This is a simple toast.")).toBeFalsy(); + + jest.useRealTimers(); + }); + test("If duration < 3000, the toast disappears at 3000ms", async () => { + jest.useFakeTimers(); + const { getByText, queryByText } = render( + <DxcToastsQueue duration={100}> + <ToastPage /> + </DxcToastsQueue> + ); + const button = getByText("Show toast"); + userEvent.click(button); + + act(() => { + jest.advanceTimersByTime(3001); + }); + expect(queryByText("This is a simple toast.")).toBeFalsy(); + + jest.useRealTimers(); + }); + test("Clear action removes the toast", async () => { + const { getByText, getByLabelText, queryByText } = render( + <DxcToastsQueue> + <ToastPage /> + </DxcToastsQueue> + ); + const button = getByText("Show toast"); + userEvent.click(button); + const clearButton = getByLabelText("Clear toast"); + userEvent.click(clearButton); + await waitFor(() => { + expect(queryByText("This is a simple toast.")).toBeFalsy(); + }); + }); + test("Action button executes the onClick function", () => { + const onClick = jest.fn(); + const { getByText } = render( + <DxcToastsQueue> + <ToastPage onClick={onClick} /> + </DxcToastsQueue> + ); + const button = getByText("Show toast"); + userEvent.click(button); + const actionButton = getByText("Action"); + userEvent.click(actionButton); + expect(onClick).toHaveBeenCalled(); + }); + test("Toast queue can only accumulate 5 toasts at the same time", async () => { + const { getByText, getAllByText } = render( + <DxcToastsQueue> + <ToastPage /> + </DxcToastsQueue> + ); + const button = getByText("Show toast"); + for (let i = 0; i < 6; i++) { + userEvent.click(button); + } + await waitFor(() => { + expect(getAllByText("This is a simple toast.").length).toBe(5); + }); + }); + test("Toast queue removes the older toast when more than 5 toast accumulate", async () => { + const { getByText, getAllByText, queryByText } = render( + <DxcToastsQueue> + <ToastPage /> + </DxcToastsQueue> + ); + const infoBtn = getByText("Show info toast"); + const defaultBtn = getByText("Show toast"); + + userEvent.click(infoBtn); + waitFor(() => { + expect(getByText("This is an information toast.")).toBeTruthy(); + }); + for (let i = 0; i < 6; i++) { + userEvent.click(defaultBtn); + } + await waitFor(() => { + expect(queryByText("This is an information toast.")).toBeFalsy(); + expect(getAllByText("This is a simple toast.").length).toBe(5); + }); + }); + test("Loading toast is never removed automatically", async () => { + jest.useFakeTimers(); + const { getByText } = render( + <DxcToastsQueue> + <ToastPage /> + </DxcToastsQueue> + ); + const button = getByText("Show loading toast"); + userEvent.click(button); + act(() => { + jest.advanceTimersByTime(10000); // over 5000ms + }); + expect(getByText("Loading...")).toBeTruthy(); + jest.useRealTimers(); + }); + test("Loading toast can be cleared", async () => { + const { getByLabelText, getByText, queryByText } = render( + <DxcToastsQueue> + <ToastPage /> + </DxcToastsQueue> + ); + const button = getByText("Show loading toast"); + userEvent.click(button); + const clearButton = getByLabelText("Clear toast"); + userEvent.click(clearButton); + await waitFor(() => { + expect(queryByText("Loading...")).toBeFalsy(); + }); + }); + test("Loading toast can be removed programmatically", async () => { + jest.useFakeTimers(); + const { getByText, queryByText } = render( + <DxcToastsQueue> + <ToastPage /> + </DxcToastsQueue> + ); + const button = getByText("Load process"); + userEvent.click(button); + await waitFor(() => { + expect(getByText("Loading process...")).toBeTruthy(); + }); + act(() => { + jest.advanceTimersByTime(5000); + }); + expect(queryByText("Loading process...")).toBeFalsy(); + expect(getByText("The process ended successfully.")).toBeTruthy(); + jest.useRealTimers(); + }); +}); diff --git a/packages/lib/src/toast/Toast.tsx b/packages/lib/src/toast/Toast.tsx new file mode 100644 index 0000000000..bd56963947 --- /dev/null +++ b/packages/lib/src/toast/Toast.tsx @@ -0,0 +1,193 @@ +import { memo, useState } from "react"; +import styled, { keyframes } from "styled-components"; +import CoreTokens from "../common/coreTokens"; +import DxcActionIcon from "../action-icon/ActionIcon"; +import DxcButton from "../button/Button"; +import DxcFlex from "../flex/Flex"; +import DxcIcon from "../icon/Icon"; +import DxcSpinner from "../spinner/Spinner"; +import { HalstackProvider } from "../HalstackContext"; +import ToastPropsType from "./types"; +import useTimeout from "../utils/useTimeout"; +import useTranslatedLabels from "../useTranslatedLabels"; +import { responsiveSizes } from "../common/variables"; + +const getSemantic = (semantic: ToastPropsType["semantic"]) => { + switch (semantic) { + case "info": + return { + primaryColor: CoreTokens.color_blue_700, + secondaryColor: CoreTokens.color_blue_100, + icon: "filled_info", + }; + case "success": + return { + primaryColor: CoreTokens.color_green_700, + secondaryColor: CoreTokens.color_green_100, + icon: "filled_check_circle", + }; + case "warning": + return { + primaryColor: CoreTokens.color_orange_700, + secondaryColor: CoreTokens.color_orange_100, + icon: "filled_warning", + }; + default: + return { primaryColor: CoreTokens.color_purple_700, secondaryColor: CoreTokens.color_purple_100, icon: "" }; + } +}; + +const ContentContainer = styled.div<{ loading: ToastPropsType["loading"] }>` + display: flex; + align-items: center; + gap: ${CoreTokens.spacing_8}; + overflow: hidden; + + ${({ loading }) => !loading && `font-size: ${CoreTokens.type_scale_05}`}; + > svg { + width: 24px; + height: 24px; + } +`; + +const Message = styled.span` + color: ${CoreTokens.color_black}; + font-family: ${CoreTokens.type_sans}; + font-size: ${CoreTokens.type_scale_02}; + font-weight: ${CoreTokens.type_semibold}; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +`; + +const fadeInUp = keyframes` + 0% { + transform: translateY(100%); + opacity: 0; + } + 100% { + transform: translateY(0); + opacity: 1; + } +`; + +const fadeOutDown = keyframes` + 0% { + transform: translateY(0); + opacity: 1; + } + 100% { + transform: translateY(100%); + opacity: 0; + } +`; + +const Toast = styled.output<{ semantic: ToastPropsType["semantic"]; isClosing: boolean }>` + box-sizing: border-box; + min-width: 200px; + max-width: 600px; + width: fit-content; + border-radius: ${CoreTokens.border_radius_medium}; + border-left: ${CoreTokens.border_width_2} solid ${({ semantic }) => getSemantic(semantic).primaryColor}; + box-shadow: 0px 2px 2px 0px rgba(181, 181, 181, 0.4); + display: inline-flex; + justify-content: space-between; + gap: ${CoreTokens.spacing_24}; + padding: ${CoreTokens.spacing_8} ${CoreTokens.spacing_12}; + background-color: ${({ semantic }) => getSemantic(semantic).secondaryColor}; + color: ${({ semantic }) => getSemantic(semantic).primaryColor}; + animation: ${({ isClosing }) => (isClosing ? fadeOutDown : fadeInUp)} 0.3s ease forwards; + + @media (max-width: ${responsiveSizes.medium}rem) { + max-width: 100%; + } +`; + +const spinnerTheme = { + spinner: { + accentColor: getSemantic("info").primaryColor, + }, +}; + +const ToastIcon = memo( + ({ + icon, + hideSemanticIcon, + loading, + semantic, + }: Pick<ToastPropsType, "icon" | "hideSemanticIcon" | "loading" | "semantic">) => { + if (semantic === "default") return typeof icon === "string" ? <DxcIcon icon={icon} /> : icon; + else if (semantic === "info" && loading) + return ( + <HalstackProvider theme={spinnerTheme}> + <DxcSpinner mode="small" /> + </HalstackProvider> + ); + else return !hideSemanticIcon && <DxcIcon icon={getSemantic(semantic).icon} />; + } +); + +const DxcToast = ({ + action, + duration, + hideSemanticIcon, + icon, + loading, + message, + onClear, + semantic, +}: ToastPropsType) => { + const [isClosing, setIsClosing] = useState(false); + const translatedLabels = useTranslatedLabels(); + + const clearClosingAnimationTimer = useTimeout( + () => { + setIsClosing(true); + }, + loading ? null : duration - 300 + ); + + const clearTimer = useTimeout( + () => { + onClear(); + }, + loading ? null : duration + ); + + return ( + <Toast semantic={semantic} isClosing={isClosing} role="status"> + <ContentContainer loading={loading}> + <ToastIcon semantic={semantic} icon={icon} loading={loading} hideSemanticIcon={hideSemanticIcon} /> + <Message>{message}</Message> + </ContentContainer> + <DxcFlex alignItems="center" gap="0.25rem"> + {action && ( + <DxcButton + semantic={semantic} + mode="tertiary" + size={{ height: "small" }} + label={action.label} + icon={action.icon} + onClick={action.onClick} + /> + )} + <DxcActionIcon + icon="clear" + title={translatedLabels.toast.clearToastActionTitle} + onClick={() => { + if (!loading) { + clearClosingAnimationTimer(); + clearTimer(); + } + setIsClosing(true); + setTimeout(() => { + onClear(); + }, 300); + }} + /> + </DxcFlex> + </Toast> + ); +}; + +export default memo(DxcToast); diff --git a/packages/lib/src/toast/ToastsQueue.tsx b/packages/lib/src/toast/ToastsQueue.tsx new file mode 100644 index 0000000000..a117bc4e5b --- /dev/null +++ b/packages/lib/src/toast/ToastsQueue.tsx @@ -0,0 +1,84 @@ +import { createContext, useCallback, useEffect, useMemo, useState } from "react"; +import { createPortal } from "react-dom"; +import styled from "styled-components"; +import CoreTokens from "../common/coreTokens"; +import DxcToast from "./Toast"; +import { QueuedToast, Semantic, ToastContextType, ToastsQueuePropsType, ToastType } from "./types"; +import { responsiveSizes } from "../common/variables"; + +export const ToastContext = createContext<ToastContextType | null>(null); + +const generateUniqueToastId = (toasts: QueuedToast[]) => { + let id = ""; + let exists = true; + while (exists) { + id = `${performance.now()}-${Math.random().toString(36).slice(2, 9)}`; + exists = toasts.some((toast) => toast.id === id); + } + return id; +}; + +const ToastsQueue = styled.section` + box-sizing: border-box; + position: fixed; + bottom: 0; + right: 0; + z-index: 2147483647; + display: flex; + flex-direction: column; + align-items: flex-end; + gap: ${CoreTokens.spacing_8}; + padding: ${CoreTokens.spacing_24}; + + @media (max-width: ${responsiveSizes.medium}rem) { + align-items: center; + width: 100%; + } +`; + +const DxcToastsQueue = ({ children, duration = 3000 }: ToastsQueuePropsType) => { + const [toasts, setToasts] = useState<QueuedToast[]>([]); + const [isMounted, setIsMounted] = useState(false); // Next.js SSR mounting issue + const adjustedDuration = useMemo(() => (duration > 5000 ? 5000 : duration < 3000 ? 3000 : duration), [duration]); + + const add = useCallback( + (toast: ToastType, semantic: Semantic) => { + const id = generateUniqueToastId(toasts); + setToasts((prevToasts) => [...prevToasts, { id, semantic, ...toast }].slice(-5)); + return () => remove(id); + }, + [duration] + ); + + const remove = useCallback((id: string) => { + setToasts((prevToasts) => prevToasts.filter((toast) => toast.id !== id)); + }, []); + + useEffect(() => { + setIsMounted(true); + }, []); + + return ( + <ToastContext.Provider value={{ add }}> + {isMounted && + createPortal( + <ToastsQueue> + {toasts.map((t) => ( + <DxcToast + key={t.id} + duration={adjustedDuration} + onClear={() => { + remove(t.id); + }} + {...t} + /> + ))} + </ToastsQueue>, + document.body + )} + {children} + </ToastContext.Provider> + ); +}; + +export default DxcToastsQueue; diff --git a/packages/lib/src/toast/types.ts b/packages/lib/src/toast/types.ts new file mode 100644 index 0000000000..51d474d46d --- /dev/null +++ b/packages/lib/src/toast/types.ts @@ -0,0 +1,58 @@ +type SVG = React.ReactNode & React.SVGProps<SVGSVGElement>; +type Action = { + icon?: string | SVG; + label: string; + onClick: () => void; +}; + +type CommonProps = { + action?: Action; + message: string; +}; +type DefaultToast = CommonProps & { + icon?: string | SVG; +}; +type LoadingToast = CommonProps & { + loading: boolean; +}; +type SemanticToast = CommonProps & { + hideSemanticIcon?: boolean; +}; +type ToastType = DefaultToast | LoadingToast | SemanticToast; + +type Semantic = "default" | "info" | "success" | "warning"; + +type QueuedToast = ToastType & { + id: string; + semantic: Semantic; +}; + +type ToastContextType = { + add: (toast: ToastType, semantic: Semantic) => () => void; +}; + +type ToastPropsType = { + action?: Action; + duration: number; + icon?: string | SVG; + loading?: boolean; + message: string; + onClear: () => void; + semantic: Semantic; + hideSemanticIcon?: boolean; +}; + +type ToastsQueuePropsType = { duration?: number; children: React.ReactNode }; + +export default ToastPropsType; +export type { + CommonProps, + DefaultToast, + LoadingToast, + QueuedToast, + Semantic, + SemanticToast, + ToastContextType, + ToastsQueuePropsType, + ToastType, +}; diff --git a/packages/lib/src/toast/useToast.tsx b/packages/lib/src/toast/useToast.tsx new file mode 100644 index 0000000000..ff1b1febae --- /dev/null +++ b/packages/lib/src/toast/useToast.tsx @@ -0,0 +1,27 @@ +import { useContext } from "react"; +import { ToastContext } from "./ToastsQueue"; +import { ToastType, DefaultToast, Semantic, SemanticToast, LoadingToast } from "./types"; + +const useToast = () => { + const { add } = useContext(ToastContext); + + const show = <T extends ToastType>(toast: T, semantic: Semantic) => add(toast, semantic); + + return { + default: (toast: DefaultToast) => { + show(toast, "default"); + }, + info: (toast: SemanticToast) => { + show(toast, "info"); + }, + loading: (toast: Omit<LoadingToast, "loading">) => show({ ...toast, loading: true }, "info"), + success: (toast: SemanticToast) => { + show(toast, "success"); + }, + warning: (toast: SemanticToast) => { + show(toast, "warning"); + }, + }; +}; + +export default useToast; diff --git a/packages/lib/src/utils/useTimeout.tsx b/packages/lib/src/utils/useTimeout.tsx new file mode 100644 index 0000000000..da42f6c415 --- /dev/null +++ b/packages/lib/src/utils/useTimeout.tsx @@ -0,0 +1,32 @@ +import { useRef, useCallback, useEffect } from "react"; + +type UseTimeoutType = (callback: () => void, delay?: number) => () => void; + +/** + * Custom hook to handle setTimeout in a declarative way. + * Inspired by Dan Abramov's article: https://overreacted.io/making-setinterval-declarative-with-react-hooks/ + * + * @param callback Function to be executed after the delay + * @param delay Time in milliseconds to wait before executing the callback + * @returns Function to clear the timeout + */ +const useTimeout: UseTimeoutType = (callback, delay) => { + const savedCallback = useRef<() => void>(); + const timerRef = useRef<ReturnType<typeof setTimeout>>(); + const clearTimerCallback = useCallback(() => clearTimeout(timerRef.current), []); + + useEffect(() => { + savedCallback.current = callback; + }, [callback]); + + useEffect(() => { + if (delay != null) { + timerRef.current = setTimeout(savedCallback.current, delay); + return clearTimerCallback; + } + }, [delay, clearTimerCallback]); + + return clearTimerCallback; +}; + +export default useTimeout; diff --git a/packages/lib/tsconfig.json b/packages/lib/tsconfig.json index 00fd4ab5dc..ca7788ca2a 100644 --- a/packages/lib/tsconfig.json +++ b/packages/lib/tsconfig.json @@ -1,6 +1,7 @@ { "extends": "@dxc-technology/typescript-config/react-library.json", "compilerOptions": { + "jsx": "react-jsx", "outDir": "dist", "strict": false, "target": "es5",