diff --git a/apps/website/pages/components/toggle-group/code.tsx b/apps/website/pages/components/toggle-group/code.tsx new file mode 100644 index 0000000000..7056fd0a90 --- /dev/null +++ b/apps/website/pages/components/toggle-group/code.tsx @@ -0,0 +1,17 @@ +import Head from "next/head"; +import type { ReactElement } from "react"; +import ToggleGroupPageLayout from "screens/components/toggle-group/ToggleGroupPageLayout"; +import ToggleGroupCodePage from "screens/components/toggle-group/code/ToggleGroupCodePage"; + +const Code = () => ( + <> + + Toggle group code — Halstack Design System + + + +); + +Code.getLayout = (page: ReactElement) => {page}; + +export default Code; diff --git a/apps/website/pages/components/toggle-group/index.tsx b/apps/website/pages/components/toggle-group/index.tsx index 57fd486e43..e22f16103c 100644 --- a/apps/website/pages/components/toggle-group/index.tsx +++ b/apps/website/pages/components/toggle-group/index.tsx @@ -1,21 +1,17 @@ import Head from "next/head"; import type { ReactElement } from "react"; import ToggleGroupPageLayout from "screens/components/toggle-group/ToggleGroupPageLayout"; -import ToggleGroupCodePage from "screens/components/toggle-group/code/ToggleGroupCodePage"; +import ToggleGroupOverviewPage from "screens/components/toggle-group/overview/ToggleGroupOverviewPage"; -const Index = () => { - return ( - <> - - Toggle Group — Halstack Design System - - - - ); -}; +const Index = () => ( + <> + + Toggle Group — Halstack Design System + + + +); -Index.getLayout = function getLayout(page: ReactElement) { - return {page}; -}; +Index.getLayout = (page: ReactElement) => {page}; export default Index; diff --git a/apps/website/pages/components/toggle-group/specifications.tsx b/apps/website/pages/components/toggle-group/specifications.tsx deleted file mode 100644 index 07a9ecac8d..0000000000 --- a/apps/website/pages/components/toggle-group/specifications.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import Head from "next/head"; -import type { ReactElement } from "react"; -import ToggleGroupPageLayout from "screens/components/toggle-group/ToggleGroupPageLayout"; -import ToggleGroupSpecsPage from "screens/components/toggle-group/specs/ToggleGroupSpecsPage"; - -const Specifications = () => { - return ( - <> - - Toggle Group Specs — Halstack Design System - - - - ); -}; - -Specifications.getLayout = function getLayout(page: ReactElement) { - return {page}; -}; - -export default Specifications; diff --git a/apps/website/pages/components/toggle-group/usage.tsx b/apps/website/pages/components/toggle-group/usage.tsx deleted file mode 100644 index df03c22448..0000000000 --- a/apps/website/pages/components/toggle-group/usage.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import Head from "next/head"; -import type { ReactElement } from "react"; -import ToggleGroupPageLayout from "screens/components/toggle-group/ToggleGroupPageLayout"; -import ToggleGroupUsagePage from "screens/components/toggle-group/usage/ToggleGroupUsagePage"; - -const Usage = () => { - return ( - <> - - ToggleGroup Usage — Halstack Design System - - - - ); -}; - -Usage.getLayout = function getLayout(page: ReactElement) { - return {page}; -}; - -export default Usage; diff --git a/apps/website/screens/components/container/code/examples/listbox.ts b/apps/website/screens/components/container/code/examples/listbox.ts index 00953231f0..19d00ad1bc 100644 --- a/apps/website/screens/components/container/code/examples/listbox.ts +++ b/apps/website/screens/components/container/code/examples/listbox.ts @@ -9,8 +9,8 @@ const code = `() => { background={{ color: "var(--border-color-neutral-brighter)" }} border={{ color: "var(--border-color-neutral-medium)", - width: "var(--border-width-s)", - style: "var(--border-style-default)" + style: "var(--border-style-default)", + width: "var(--border-width-s)" }} borderRadius="var(--border-radius-s)" boxShadow="var(--shadow-mid-x-position) var(--shadow-mid-y-position) var(--shadow-mid-blur) var(--shadow-mid-spread) var(--shadow-light)" diff --git a/apps/website/screens/components/contextual-menu/overview/ContextualMenuOverviewPage.tsx b/apps/website/screens/components/contextual-menu/overview/ContextualMenuOverviewPage.tsx index b2e4195fbf..dafe0021a7 100644 --- a/apps/website/screens/components/contextual-menu/overview/ContextualMenuOverviewPage.tsx +++ b/apps/website/screens/components/contextual-menu/overview/ContextualMenuOverviewPage.tsx @@ -3,7 +3,7 @@ import DocFooter from "@/common/DocFooter"; import QuickNavContainer from "@/common/QuickNavContainer"; import QuickNavContainerLayout from "@/common/QuickNavContainerLayout"; import Image from "@/common/Image"; -import anatomy from "./images/contextual-menu_anatomy.png"; +import anatomy from "./images/contextual_menu_anatomy.png"; const sections = [ { diff --git a/apps/website/screens/components/contextual-menu/overview/images/contextual-menu_anatomy.png b/apps/website/screens/components/contextual-menu/overview/images/contextual_menu_anatomy.png similarity index 100% rename from apps/website/screens/components/contextual-menu/overview/images/contextual-menu_anatomy.png rename to apps/website/screens/components/contextual-menu/overview/images/contextual_menu_anatomy.png diff --git a/apps/website/screens/components/toggle-group/ToggleGroupPageLayout.tsx b/apps/website/screens/components/toggle-group/ToggleGroupPageLayout.tsx index e38d6e79d9..7a6858bdbe 100644 --- a/apps/website/screens/components/toggle-group/ToggleGroupPageLayout.tsx +++ b/apps/website/screens/components/toggle-group/ToggleGroupPageLayout.tsx @@ -6,26 +6,21 @@ import { ReactNode } from "react"; const ToggleGroupPageHeading = ({ children }: { children: ReactNode }) => { const tabs = [ - { label: "Code", path: "/components/toggle-group" }, - { label: "Usage", path: "/components/toggle-group/usage" }, - { - label: "Specifications", - path: "/components/toggle-group/specifications", - }, + { label: "Overview", path: "/components/toggle-group" }, + { label: "Code", path: "/components/toggle-group/code" }, ]; return ( - + - Toggle buttons can be used to put together related options that share a common attribute modification. It - allows the user to switch from one selected option to another in the same control, having one option - selected at a time. Also, there can be another variation that allows selecting multiple options from the - current toggle group. + The toggle group component is a set of toggle buttons that function as a unified control, allowing users to + make either single or multiple selections. It is ideal for grouping related actions or options within a + compact and interactive interface. - + {children} diff --git a/apps/website/screens/components/toggle-group/code/ToggleGroupCodePage.tsx b/apps/website/screens/components/toggle-group/code/ToggleGroupCodePage.tsx index 86c4ef9c8c..b9ea54743a 100644 --- a/apps/website/screens/components/toggle-group/code/ToggleGroupCodePage.tsx +++ b/apps/website/screens/components/toggle-group/code/ToggleGroupCodePage.tsx @@ -1,4 +1,4 @@ -import { DxcFlex, DxcLink, DxcTable } from "@dxc-technology/halstack-react"; +import { DxcFlex, DxcTable } from "@dxc-technology/halstack-react"; import QuickNavContainerLayout from "@/common/QuickNavContainerLayout"; import QuickNavContainer from "@/common/QuickNavContainer"; import Code from "@/common/Code"; @@ -7,7 +7,16 @@ import Example from "@/common/example/Example"; import controlled from "./examples/controlled"; import uncontrolled from "./examples/uncontrolled"; import StatusBadge from "@/common/StatusBadge"; -import TableCode from "@/common/TableCode"; +import TableCode, { ExtendedTableCode } from "@/common/TableCode"; + +const optionTypeString = `{ + disabled?: boolean; + icon?: string | + (React.ReactNode + & React.SVGProps); + label?: string; + value: string; +}`; const sections = [ { @@ -32,81 +41,16 @@ const sections = [ - - value - - number | number[] - - - The key(s) of the selected value(s). If the toggle group component doesn't allow multiple selection, it - must be one unique value. If the component allows multiple selection, value must be an array. If - undefined, the component will be uncontrolled and the value will be managed internally by the component. - - - - - - label - - string - - Text to be placed above the component. - - - - - helperText - - string - - Helper text to be placed above the component. - - - - + margin - - - options - + 'xxsmall' | 'xsmall' | 'small' | 'medium' | 'large' | 'xlarge' | 'xxlarge' | Margin - - { - "{ label?: string; icon: string | (React.ReactNode & React.SVGProps ); value: string; title?: string; }[]" - } - - - - An array of objects representing the selectable options. Each object has the following properties: -
    -
  • - label: String with the option display value. -
  • -
  • - icon:{" "} - - Material Symbol - {" "} - name or SVG element used as the icon of an option. -
  • -
  • - value: Number with the option inner value. -
  • -
  • - title: Text representing advisory information related to an option. Under the hood, it also - serves as an accessible label for the icon. -
  • -
+ Size of the margin to be applied to the component. You can pass an object with 'top', 'bottom', 'left' and + 'right' properties in order to specify different margin sizes. - - - disabled - - boolean - - If true, the component will be disabled. - - false - - multiple @@ -133,16 +77,32 @@ const sections = [ - - margin - 'xxsmall' | 'xsmall' | 'small' | 'medium' | 'large' | 'xlarge' | 'xxlarge' | Margin + + + options + - Size of the margin to be applied to the component. You can pass an object with 'top', 'bottom', 'left' and - 'right' properties in order to specify different margin sizes. + Option[] +

+ being Option an object with the following properties: +

+ {optionTypeString} + An array of objects representing the selectable options. - + + orientation + + 'horizontal' | 'vertical' + + The orientation of the toggle group. + + 'horizontal' + + tabIndex @@ -155,6 +115,18 @@ const sections = [ 0 + + value + + number | number[] + + + The key(s) of the selected value(s). If the toggle group component doesn't allow multiple selection, it + must be one unique value. If the component allows multiple selection, value must be an array. If + undefined, the component will be uncontrolled and the value will be managed internally by the component. + + - + ), @@ -174,15 +146,13 @@ const sections = [ }, ]; -const ToggleGroupCodePage = () => { - return ( - - - - - - - ); -}; +const ToggleGroupCodePage = () => ( + + + + + + +); export default ToggleGroupCodePage; diff --git a/apps/website/screens/components/toggle-group/code/examples/controlled.ts b/apps/website/screens/components/toggle-group/code/examples/controlled.ts index 744b0ba885..eff86e6d85 100644 --- a/apps/website/screens/components/toggle-group/code/examples/controlled.ts +++ b/apps/website/screens/components/toggle-group/code/examples/controlled.ts @@ -9,24 +9,23 @@ const code = `() => { const options = [ { value: 1, - label: "Facebook", + label: "Web", }, { value: 2, - label: "X", + label: "Android", }, { value: 3, - label: "Linkedin", + label: "iOS", }, ]; return ( diff --git a/apps/website/screens/components/toggle-group/code/examples/uncontrolled.ts b/apps/website/screens/components/toggle-group/code/examples/uncontrolled.ts index dfab6fd540..f0a5e8619d 100644 --- a/apps/website/screens/components/toggle-group/code/examples/uncontrolled.ts +++ b/apps/website/screens/components/toggle-group/code/examples/uncontrolled.ts @@ -1,42 +1,110 @@ -import { DxcToggleGroup, DxcInset } from "@dxc-technology/halstack-react"; +import { DxcContainer, DxcFlex, DxcInset, DxcToggleGroup } from "@dxc-technology/halstack-react"; +import { useRef } from "react"; const code = `() => { - const onChange = (newValue) => { - console.log(newValue); - }; + const refText = useRef(null); + + const onChange = (selectedValue) => { + if (refText.current) { + refText.current.style.fontWeight = "normal"; + refText.current.style.fontStyle = "normal"; + refText.current.style.textDecoration = "none"; + refText.current.style.textAlign = "left"; + selectedValue.forEach((textOption) => { + switch (textOption) { + case 1: + refText.current.style.fontWeight = "bold"; + break; + case 2: + refText.current.style.fontStyle = "italic"; + break; + case 3: + refText.current.style.textDecoration = "underline"; + break; + case 4: + refText.current.style.textAlign = "left"; + break; + case 5: + refText.current.style.textAlign = "center"; + break; + case 6: + refText.current.style.textAlign = "right"; + break; + default: + break; + } + }); + } + } + const options = [ { value: 1, - label: "Facebook", - icon: "filled_thumb_up" + icon: "format_bold", + title: "Bold", }, { value: 2, - label: "X", - icon: "filled_raven" + icon: "format_italic", + title: "Italic", }, { value: 3, - label: "Linkedin", - icon: "filled_work" + icon: "format_underlined", + title: "Underlined", + }, + { + value: 4, + icon: "format_align_left", + title: "Align left", + }, + { + value: 5, + icon: "format_align_center", + title: "Align center", + }, + { + value: 6, + icon: "format_align_right", + title: "Align right", }, ]; return ( - + + + +

+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. +

+
+
); }`; const scope = { - DxcToggleGroup, + DxcContainer, + DxcFlex, DxcInset, + DxcToggleGroup, + useRef }; export default { code, scope }; diff --git a/apps/website/screens/components/toggle-group/overview/ToggleGroupOverviewPage.tsx b/apps/website/screens/components/toggle-group/overview/ToggleGroupOverviewPage.tsx new file mode 100644 index 0000000000..ee6c7994db --- /dev/null +++ b/apps/website/screens/components/toggle-group/overview/ToggleGroupOverviewPage.tsx @@ -0,0 +1,194 @@ +import { DxcParagraph, DxcBulletedList, DxcFlex } from "@dxc-technology/halstack-react"; +import QuickNavContainer from "@/common/QuickNavContainer"; +import DocFooter from "@/common/DocFooter"; +import QuickNavContainerLayout from "@/common/QuickNavContainerLayout"; +import Example from "@/common/example/Example"; +import Code from "@/common/Code"; +import Image from "@/common/Image"; +import anatomy from "./images/toggle_group_anatomy.png"; +import singleSelection from "./examples/singleSelection"; +import multipleSelection from "./examples/multipleSelection"; +import orientation from "./examples/orientation"; + +const sections = [ + { + title: "Introduction", + content: ( + + The toggle group component provides a flexible way to present related options or actions within + a single interface. It consists of multiple toggle buttons, allowing users to make either single or multiple + selections depending on the configuration. This component is particularly useful for settings, filtering + options, or mode switching, where users need to quickly toggle between states. By grouping + these actions together, it enhances usability and keeps the interface organized, ensuring a seamless interaction + experience. + + ), + }, + { + title: "Anatomy", + content: ( + <> + Toggle group anatomy + + + Container: the structural wrapper that holds all toggle buttons together. + + + Icon: an optional visual element within a toggle button that helps users quickly identify + its function or meaning. + + + Label: the textual representation inside each toggle button, describing its function or + selection state. + + + Toggle button (selected): a button in an active state, indicating that the user + has chosen this option. + + + Toggle button (unselected): a button in its default or inactive state. It remains + visually subdued compared to the selected button but is still clearly visible and interactive, allowing + users to switch selections easily. + + + + ), + }, + { + title: "Variants", + content: ( + + Depending on the number of actions or options the user can select on a toggle group, there are two different + variants of the component. + + ), + subSections: [ + { + title: "Single selection", + content: ( + + The single selection variant allows users to select{" "} + only one option at a time. When a new option is selected, the previous one is automatically + deselected. This variant is ideal for scenarios where users need to toggle between mutually exclusive + options, ensuring clarity and preventing conflicting selections. + + ), + subSections: [ + { + title: "Use cases", + content: ( + <> + + + View selection: allowing users to switch between different data presentation + formats, such as grid view vs. list view in a product catalog. + + + Mode switching: Enabling users to toggle between modes like{" "} + light mode vs. dark mode in an interface. + + + Filter selection: Helping users refine content by choosing a{" "} + single category filter in dashboards or reports. + + + + + ), + }, + ], + }, + { + title: "Multiple selection", + content: ( + + The multiple selection variant of the toggle group component allows users to select + multiple options at the same time. Unlike the single selection variant, this version enables users to + activate or deactivate multiple toggles independently, making it useful for scenarios where multiple choices + can be applied simultaneously. + + ), + subSections: [ + { + title: "Use cases", + content: ( + <> + + + Formatting options: enabling users to apply bold,{" "} + italic, and underline text styles simultaneously in a text editor. + + + Risk assessment options: underwriters can enable multiple risk factors when + evaluating a client, such as pre-existing conditions, vehicle age, + and past claim history. + + + Filtering: allowing users to refine searches by toggling filters like "in + progress", "ready to review" or "done". + + + + + ), + }, + ], + }, + { + title: "Orientation", + content: ( + <> + + Although not technically a variant, the toggle group can also be{" "} + stacked in two different ways: horizontally and vertically. Users can choose the option + that better fits their needs according to layout constraints. + + + + ), + }, + ], + }, + { + title: "Best practices", + content: ( + + + Choose the right selection mode: use single selection when only one option + can be active at a time (e.g., selecting a payment method). Use multiple selection when users + can activate several options simultaneously (e.g., selecting policy add-ons). + + + Group related actions logically: ensure that the toggle buttons represent actions or choices + that are related to each other, helping users make clear and informed selections. + + + Use appropriate labels and icons: labels should be concise and self-explanatory. If using + icons, ensure they clearly represent their respective actions or choices. + + + Stack options based on content needs: use a horizontal layout when there are + only a few options and space allows, making comparisons easier. Use a vertical layout when + dealing with multiple choices or longer text labels to enhance readability. + + + Be mindful of where you place the toggle group: this component is not meant to replace{" "} + radio buttons, checkboxes, or switches, as it does not + register selections as form inputs. If the user needs to provide structured answers, use the appropriate form + elements instead. + + + ), + }, +]; + +const ToggleGroupOverviewPage = () => ( + + + + + + +); + +export default ToggleGroupOverviewPage; diff --git a/apps/website/screens/components/toggle-group/overview/examples/multipleSelection.ts b/apps/website/screens/components/toggle-group/overview/examples/multipleSelection.ts new file mode 100644 index 0000000000..6503e67e65 --- /dev/null +++ b/apps/website/screens/components/toggle-group/overview/examples/multipleSelection.ts @@ -0,0 +1,53 @@ +import { DxcInset, DxcToggleGroup } from "@dxc-technology/halstack-react"; + +const code = `() => { + const options = [ + { + value: 1, + icon: "format_bold", + title: "Bold", + }, + { + value: 2, + icon: "format_italic", + title: "Italic", + }, + { + value: 3, + icon: "format_underlined", + title: "Underlined", + }, + { + value: 4, + icon: "format_align_left", + title: "Align left", + }, + { + value: 5, + icon: "format_align_center", + title: "Align center", + }, + { + value: 6, + icon: "format_align_right", + title: "Align right", + }, + ]; + + return ( + + + + ); +}`; + +const scope = { + DxcInset, + DxcToggleGroup, +}; + +export default { code, scope }; diff --git a/apps/website/screens/components/toggle-group/overview/examples/orientation.ts b/apps/website/screens/components/toggle-group/overview/examples/orientation.ts new file mode 100644 index 0000000000..f7b04854d5 --- /dev/null +++ b/apps/website/screens/components/toggle-group/overview/examples/orientation.ts @@ -0,0 +1,45 @@ +import { DxcFlex, DxcInset, DxcToggleGroup } from "@dxc-technology/halstack-react"; + +const code = `() => { + const options = [ + { + value: 1, + icon: "wifi", + label: "Wifi", + }, + { + value: 2, + label: "Ethernet", + }, + { + value: 3, + icon: "5g", + label: "5G", + }, + ]; + + return ( + + + + + + + ); +}`; + +const scope = { + DxcFlex, + DxcInset, + DxcToggleGroup, +}; + +export default { code, scope }; diff --git a/apps/website/screens/components/toggle-group/overview/examples/singleSelection.ts b/apps/website/screens/components/toggle-group/overview/examples/singleSelection.ts new file mode 100644 index 0000000000..c427c0e1f3 --- /dev/null +++ b/apps/website/screens/components/toggle-group/overview/examples/singleSelection.ts @@ -0,0 +1,42 @@ +import { DxcInset, DxcToggleGroup } from "@dxc-technology/halstack-react"; + +const code = `() => { + const options = [ + { + value: 1, + label: "Not started", + }, + { + value: 2, + label: "In progress", + }, + { + value: 3, + label: "Ready to review", + }, + { + value: 4, + label: "Completed", + }, + { + value: 5, + label: "Awaiting approval", + }, + ]; + + return ( + + + + ); +}`; + +const scope = { + DxcInset, + DxcToggleGroup, +}; + +export default { code, scope }; diff --git a/apps/website/screens/components/toggle-group/overview/images/toggle_group_anatomy.png b/apps/website/screens/components/toggle-group/overview/images/toggle_group_anatomy.png new file mode 100644 index 0000000000..a5411c91dc Binary files /dev/null and b/apps/website/screens/components/toggle-group/overview/images/toggle_group_anatomy.png differ diff --git a/apps/website/screens/components/toggle-group/specs/ToggleGroupSpecsPage.tsx b/apps/website/screens/components/toggle-group/specs/ToggleGroupSpecsPage.tsx deleted file mode 100644 index c6f672a87c..0000000000 --- a/apps/website/screens/components/toggle-group/specs/ToggleGroupSpecsPage.tsx +++ /dev/null @@ -1,680 +0,0 @@ -import { DxcTable, DxcParagraph, DxcBulletedList, DxcFlex } from "@dxc-technology/halstack-react"; -import Image from "@/common/Image"; -import QuickNavContainer from "@/common/QuickNavContainer"; -import QuickNavContainerLayout from "@/common/QuickNavContainerLayout"; -import Figure from "@/common/Figure"; -import Code from "@/common/Code"; -import DocFooter from "@/common/DocFooter"; -import toggleGroupAnatomy from "./images/toggle_group_anatomy.png"; -import toggleGroupStates from "./images/toggle_group_states.png"; -import toggleGroupSpecs from "./images/toggle_group_specs.png"; - -const sections = [ - { - title: "Specifications", - content: ( -
- Toggle design specifications -
- ), - }, - { - title: "States", - content: ( - <> - - Different states are defined in the life cycle of the component: unselected enabled,{" "} - unselected hover, unselected focus, unselected active,{" "} - unselected disabled, selected enabled, selected hover,{" "} - selected focus, selected active and selected disabled - -
- Toggle button states -
- - ), - }, - { - title: "Anatomy", - content: ( - <> - Toggle group anatomy - - Label - Helper text - Container - Button - Button icon - Button label - - - ), - }, - { - title: "Design tokens", - subSections: [ - { - title: "Color", - content: ( - - - - Component token - Element - Core token - Value - - - - - - labelFontColor - - Label - - color-black - - #000000 - - - - disabledLabelFontColor - - Label:disabled - - color-grey-500 - - #999999 - - - - helperTextFontColor - - Helper text - - color-black - - #000000 - - - - disabledHelperTextFontColor - - Helper text:disabled - - color-grey-500 - - #999999 - - - - containerBackgroundColor - - Container - - color-grey-50 - - #fafafa - - - - containerBorderColor - - Container - - color-grey-500 - - #999999 - - - - unselectedBackgroundColor - - Button fill:enabled - - color-grey-200 - - #e6e6e6 - - - - unselectedHoverBackgroundColor - - Button fill:hover - - color-grey-300 - - #cccccc - - - - unselectedActiveBackgroundColor - - Button fill:active - - color-purple-700 - - #5f249f - - - - unselectedDisabledBackgroundColor - - Button fill:disabled - - color-grey-100 - - #f2f2f2 - - - - unselectedFontColor - - Button label - - color-black - - #000000 - - - - unselectedDisabledFontColor - - Button label:disabled - - color-grey-500 - - #999999 - - - - selectedBackgroundColor - - Button fill:enabled - - color-purple-700 - - #5f249f - - - - selectedHoverBackgroundColor - - Button fill:hover - - color-purple-800 - - #4b1c7d - - - - selectedActiveBackgroundColor - - Button fill:active - - color-purple-900 - - #321353 - - - - selectedDisabledBackgroundColor - - Button fill:disabled - - color-purple-100 - - #f2eafa - - - - selectedFontColor - - Button label - - color-white - - #ffffff - - - - selectedDisabledFontColor - - Button label:disabled - - color-purple-300 - - #cbacec - - - - focusColor - - Focus indicator - - color-blue-600 - - #0095ff - - - - ), - }, - { - title: "Typography", - content: ( - - - - Component token - Element - Core token - Value - - - - - - labelFontFamily - - Label - - font-family - - 'Open Sans', sans-serif - - - - labelFontSize - - Label - - font-scale-02 - - 0.875rem / 14px - - - - labelFontStyle - - Label - - font-style-normal - - normal - - - - labelFontWeight - - Label - - font-weight-semibold - - 600 - - - - labelLineHeight - - Label - - font-leading-loose-01 - - 1.715em - - - - helperTextFontFamily - - Helper text - - font-family - - 'Open Sans', sans-serif - - - - helperTextFontSize - - Helper text - - font-scale-01 - - 0.75rem / 12px - - - - helperTextFontStyle - - Helper text - - font-style-normal - - normal - - - - helperTextFontWeight - - Helper text - - font-weight-regular - - 400 - - - - helperTextLineHeight - - Helper text - - font-leading-normal - - 1.5em - - - - optionLabelFontFamily - - Button label - - font-family - - 'Open Sans', sans-serif - - - - optionLabelFontSize - - Button label - - font-scale-03 - - 1rem / 16px - - - - optionLabelFontStyle - - Button label - - font-style-normal - - normal - - - - optionLabelFontWeight - - Button label - - font-weight-regular - - 400 - - - - ), - }, - { - title: "Spacing", - content: ( - - - - Component token - Element - Core token - Value - - - - - - iconPaddingRight - - Icon - - spacing-8 - - 0.5rem / 8px - - - - iconPaddingLeft - - Icon - - spacing-8 - - 0.5rem / 8px - - - - labelPaddingLeft - - Label (Label + icon) - - spacing-24 - - 1.5rem / 24px - - - - labelPaddingRight - - Label (Label + icon) - - spacing-24 - - 1.5rem / 24px - - - - iconMarginRight - - Icon (Label + icon) - - spacing-8 - - 0.5rem / 8px - - - - containerMarginTop - - Container - - spacing-4 - - 0.25rem / 4px - - - - ), - }, - { - title: "Border", - content: ( - - - - Property - Element - Core token - Value - - - - - - border-width - - Button - - border-width-0 - - 0rem / 0px - - - - border-style - - Button - - border-style-none - - none - - - - border-radius - - Button - - border-radius-medium - - 0.25rem / 4px - - - - border-width - - Container - - border-width-1 - - 1px - - - - border-style - - Container - - border-style-solid - - solid - - - - border-radius - - Container - - - 0.375rem / 6px - - - - border-width - - Focus border - - border-width-2 - - 2 - - - - border-style - - Focus border - - border-style-solid - - solid - - - - border-radius - - Focus border - - border-radius-medium - - 0.25rem / 4px - - - - ), - }, - { - title: "Margin", - content: ( - <> - - - - Margin - Value - - - - - - xxsmall - - 6px - - - - xsmall - - 16px - - - - small - - 24px - - - - medium - - 36px - - - - large - - 48px - - - - xlarge - - 64px - - - - xxlarge - - 100px - - - - - And also apply different values to each side of the component: - top, bottom, left and right - - - ), - }, - ], - }, -]; - -const ToggleGroupSpecsPage = () => { - return ( - - - - - - - ); -}; - -export default ToggleGroupSpecsPage; diff --git a/apps/website/screens/components/toggle-group/specs/images/toggle_group_anatomy.png b/apps/website/screens/components/toggle-group/specs/images/toggle_group_anatomy.png deleted file mode 100644 index 3d8123ea2f..0000000000 Binary files a/apps/website/screens/components/toggle-group/specs/images/toggle_group_anatomy.png and /dev/null differ diff --git a/apps/website/screens/components/toggle-group/specs/images/toggle_group_specs.png b/apps/website/screens/components/toggle-group/specs/images/toggle_group_specs.png deleted file mode 100644 index 050d3d44d8..0000000000 Binary files a/apps/website/screens/components/toggle-group/specs/images/toggle_group_specs.png and /dev/null differ diff --git a/apps/website/screens/components/toggle-group/specs/images/toggle_group_states.png b/apps/website/screens/components/toggle-group/specs/images/toggle_group_states.png deleted file mode 100644 index 788d80398d..0000000000 Binary files a/apps/website/screens/components/toggle-group/specs/images/toggle_group_states.png and /dev/null differ diff --git a/apps/website/screens/components/toggle-group/usage/ToggleGroupUsagePage.tsx b/apps/website/screens/components/toggle-group/usage/ToggleGroupUsagePage.tsx deleted file mode 100644 index ceb684e0e3..0000000000 --- a/apps/website/screens/components/toggle-group/usage/ToggleGroupUsagePage.tsx +++ /dev/null @@ -1,82 +0,0 @@ -import { DxcParagraph, DxcBulletedList, DxcFlex } from "@dxc-technology/halstack-react"; -import QuickNavContainer from "@/common/QuickNavContainer"; -import DocFooter from "@/common/DocFooter"; -import QuickNavContainerLayout from "@/common/QuickNavContainerLayout"; -import Example from "@/common/example/Example"; -import variants from "./examples/variants"; -import icons from "./examples/icons"; -import Code from "@/common/Code"; - -const sections = [ - { - title: "Usage", - content: ( - <> - Toggles should be used in place of radio buttons whenever the options are: - - - Minimal in number, i.e. 3 or 4 maximum choices where only one selection is required. - - Opposites of each other. - - - ), - }, - { - title: "Variants", - content: ( - <> - - - The selection of the toggle group can be mutually exclusive (single variant) or mutually inclusive (multiple - variant). - - - ), - }, - { - title: "Icon usage", - content: ( - <> - - Icons can be used to add information and clarify the action performed by each button in the toggle group. Do - not use icons primarily for visual interest. - - - - The size of the icons is 24 by 24 pixels. They must be aligned vertically and horizontally with respect to - the corresponding toggle button box. - - - A group of icon-only toggle buttons is a valid use case and allowed in the design system. In such a - situation and in order to preserve the accessibility of the component, the use of the title prop is - mandatory. - - - The title prop offers a contextual bubble with missing information necessary to clarify the - action performed by each toggle button. It also provides an accessible gateway when no regular label can be - specified. - - - Try to limit the use of icon-only toggle groups. Whenever possible, the icon should be accompanied by a - label. - - - - - ), - }, -]; - -const ToggleGroupUsagePage = () => { - return ( - - - - - - - ); -}; - -export default ToggleGroupUsagePage; diff --git a/apps/website/screens/components/toggle-group/usage/examples/icons.tsx b/apps/website/screens/components/toggle-group/usage/examples/icons.tsx deleted file mode 100644 index f4d76756f6..0000000000 --- a/apps/website/screens/components/toggle-group/usage/examples/icons.tsx +++ /dev/null @@ -1,94 +0,0 @@ -import { DxcToggleGroup, DxcInset, DxcFlex } from "@dxc-technology/halstack-react"; -import { useState } from "react"; - -const icons = { - ethernet: ( - - - - - ), - gMobile: ( - - - - - - - - - - - ), - wifi: ( - - - - - ), -}; - -const code = `() => { - const options1 = [ - { - value: 1, - label: "Wi-fi", - icon: icons.wifi, - }, - { - value: 2, - label: "Ethernet", - icon: icons.ethernet, - }, - { - value: 3, - label: "3G Mobile", - icon: icons.gMobile, - }, - ]; - - const options2 = [ - { - value: 1, - icon: icons.wifi, - title: "Wi-fi connection", - }, - { - value: 2, - icon: icons.ethernet, - title: "Ethernet connection" - }, - { - value: 3, - icon: icons.gMobile, - title: "3G Mobile data connection" - }, - ]; - - return ( - - - - - - - ); -}`; - -const scope = { - DxcToggleGroup, - DxcInset, - DxcFlex, - useState, - icons, -}; - -export default { code, scope }; diff --git a/apps/website/screens/components/toggle-group/usage/examples/variants.ts b/apps/website/screens/components/toggle-group/usage/examples/variants.ts deleted file mode 100644 index 1002e31900..0000000000 --- a/apps/website/screens/components/toggle-group/usage/examples/variants.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { DxcToggleGroup, DxcInset, DxcFlex } from "@dxc-technology/halstack-react"; - -const code = `() => { - const options1 = [ - { - value: 1, - label: "Option 01", - }, - { - value: 2, - label: "Option 02", - }, - ]; - - const options2 = [ - { - value: 1, - label: "Option 01", - }, - { - value: 2, - label: "Option 02", - }, - { - value: 3, - label: "Option 02", - }, - ]; - - return ( - - - - - - - ); -}`; - -const scope = { - DxcToggleGroup, - DxcInset, - DxcFlex, -}; - -export default { code, scope }; diff --git a/apps/website/screens/theme-generator/components/previews/ToggleGroup.tsx b/apps/website/screens/theme-generator/components/previews/ToggleGroup.tsx index 90b4d74bec..f2a20aa047 100644 --- a/apps/website/screens/theme-generator/components/previews/ToggleGroup.tsx +++ b/apps/website/screens/theme-generator/components/previews/ToggleGroup.tsx @@ -1,8 +1,5 @@ import { DxcToggleGroup } from "@dxc-technology/halstack-react"; import Mode from "../Mode"; -import facebookIcon from "../../images/FacebookIcon"; -import linkedinIcon from "../../images/LinkedinIcon"; -import xIcon from "../../images/XIcon"; import PreviewContainer from "./PreviewContainer"; const options = [ @@ -20,21 +17,37 @@ const options = [ }, ]; +const disabledOptions = [ + { + value: 1, + label: "Wi-fi", + }, + { + value: 2, + label: "Ethernet", + disabled: true, + }, + { + value: 3, + label: "5G", + }, +]; + const optionsWithIcons = [ { value: 1, - label: "Facebook", - icon: facebookIcon, + icon: "format_bold", + title: "Bold", }, { value: 2, - label: "Linkedin", - icon: linkedinIcon, + icon: "format_italic", + title: "Italic", }, { value: 3, - label: "X", - icon: xIcon, + icon: "format_underlined", + title: "Underlined", }, ]; @@ -43,11 +56,11 @@ const ToggleGroup = () => ( - - + + - - + + ); diff --git a/packages/lib/src/button/utils.ts b/packages/lib/src/button/utils.ts index c0896e970c..13a3b1ec0c 100644 --- a/packages/lib/src/button/utils.ts +++ b/packages/lib/src/button/utils.ts @@ -3,7 +3,7 @@ import ButtonPropsType, { Mode, Semantic, Size } from "./types"; export const getButtonStyles = ( mode: Mode, - semantic: Semantic, + semantic: Semantic | "unselected" | "selected", size: Size, ) => { let enabled = ""; @@ -26,6 +26,16 @@ export const getButtonStyles = ( switch (mode) { case "primary": switch (semantic) { + case "unselected": + enabled = `background-color: var(--color-bg-neutral-medium); + color: var(--color-fg-neutral-dark);`; + hover = `background-color: var(--color-bg-neutral-strong);`; + active = `background-color: var(--color-bg-primary-strong); + color: var(--color-fg-neutral-bright);`; + disabled = `background-color: var(--color-bg-neutral-light); + color: var(--color-fg-neutral-medium);`; + break; + case "selected": case "default": enabled = `background-color: var(--color-bg-primary-strong);`; hover = `background-color: var(--color-bg-primary-stronger);`; diff --git a/packages/lib/src/toggle-group/ToggleGroup.accessibility.test.tsx b/packages/lib/src/toggle-group/ToggleGroup.accessibility.test.tsx index 19ad30c202..08a6bdb686 100644 --- a/packages/lib/src/toggle-group/ToggleGroup.accessibility.test.tsx +++ b/packages/lib/src/toggle-group/ToggleGroup.accessibility.test.tsx @@ -45,31 +45,33 @@ const options = [ }, ]; +const disabledOption = [ + { + value: 1, + icon: wifiSVG, + title: "WiFi connection", + disabled: true, + }, + { + value: 2, + icon: ethernetSVG, + title: "Ethernet connection", + }, + { + value: 3, + icon: gMobileSVG, + title: "3G Mobile data connection", + }, +]; + describe("Toggle group component accessibility tests", () => { it("Should not have basic accessibility issues", async () => { - const { container } = render( - - ); + const { container } = render(); const results = await axe(container); expect(results).toHaveNoViolations(); }); it("Should not have basic accessibility issues for disabled mode", async () => { - const { container } = render( - - ); + const { container } = render(); const results = await axe(container); expect(results).toHaveNoViolations(); }); diff --git a/packages/lib/src/toggle-group/ToggleGroup.stories.tsx b/packages/lib/src/toggle-group/ToggleGroup.stories.tsx index 4099966f40..c0916452ee 100644 --- a/packages/lib/src/toggle-group/ToggleGroup.stories.tsx +++ b/packages/lib/src/toggle-group/ToggleGroup.stories.tsx @@ -1,7 +1,5 @@ -import { userEvent, within } from "@storybook/test"; import ExampleContainer from "../../.storybook/components/ExampleContainer"; import Title from "../../.storybook/components/Title"; -import { HalstackProvider } from "../HalstackContext"; import DxcToggleGroup from "./ToggleGroup"; import { Meta, StoryObj } from "@storybook/react"; @@ -49,23 +47,55 @@ const options = [ label: "Linkedin", }, ]; +const disabledOptions = [ + { + value: 1, + label: "Facebook", + }, + { + value: 2, + label: "X", + icon: "raven", + disabled: true, + }, + { + value: 3, + label: "Linkedin", + }, +]; const optionsWithIcon = [ { value: 1, - icon: "wifi", - title: "WiFi connection", + icon: "format_bold", + title: "Bold", }, { value: 2, - icon: "filled_lan", - title: "Ethernet connection", + icon: "format_italic", + title: "Italic", }, { value: 3, - icon: "5g", - title: "3G Mobile data connection", + icon: "format_underlined", + title: "Underlined", + }, + { + value: 4, + icon: "format_align_left", + title: "Align left", + }, + { + value: 5, + icon: "format_align_center", + title: "Align center", + }, + { + value: 6, + icon: "format_align_right", + title: "Align right", }, ]; + const optionsWithIconAndLabel = [ { value: 1, @@ -83,148 +113,100 @@ const optionsWithIconAndLabel = [ icon: gMobileSVG, }, ]; -const twoOptions = [ + +const oneOption = [ { value: 1, label: "Facebook", }, - { - value: 2, - label: "X", - }, ]; -const opinionatedTheme = { - toggleGroup: { - selectedBaseColor: "#5f249f", - selectedFontColor: "#ffffff", - unselectedBaseColor: "#e6e6e6", - unselectedFontColor: "#000000", - }, -}; - const ToggleGroup = () => ( <> - - - <DxcToggleGroup label="Toggle group" helperText="HelperText" options={options} /> + <Title title="Unselected" theme="light" level={3} /> + <ExampleContainer pseudoState="pseudo-hover"> + <Title title="Hover" theme="light" level={4} /> + <DxcToggleGroup options={oneOption} /> + </ExampleContainer> + <ExampleContainer pseudoState="pseudo-active"> + <Title title="Active" theme="light" level={4} /> + <DxcToggleGroup options={oneOption} /> + </ExampleContainer> + <ExampleContainer pseudoState="pseudo-focus"> + <Title title="Focus" theme="light" level={4} /> + <DxcToggleGroup options={oneOption} /> + </ExampleContainer> + <Title title="Selected" theme="light" level={3} /> + <ExampleContainer pseudoState="pseudo-hover"> + <Title title="Hover" theme="light" level={4} /> + <DxcToggleGroup options={oneOption} defaultValue={1} /> + </ExampleContainer> + <ExampleContainer pseudoState="pseudo-active"> + <Title title="Active" theme="light" level={4} /> + <DxcToggleGroup options={oneOption} defaultValue={1} /> + </ExampleContainer> + <ExampleContainer pseudoState="pseudo-focus"> + <Title title="Focus" theme="light" level={4} /> + <DxcToggleGroup options={oneOption} defaultValue={1} /> </ExampleContainer> <ExampleContainer> - <Title title="Selected" theme="light" level={4} /> - <DxcToggleGroup label="Selected" helperText="HelperText" defaultValue={2} options={options} /> + <Title title="Label only" theme="light" level={4} /> + <DxcToggleGroup options={options} /> </ExampleContainer> <ExampleContainer> - <Title title="Icons toggle group" theme="light" level={4} /> - <DxcToggleGroup label="Icons group" options={optionsWithIcon} /> + <Title title="Icons only" theme="light" level={4} /> + <DxcToggleGroup options={optionsWithIcon} /> </ExampleContainer> <ExampleContainer> - <Title title="Icons & label toggle group" theme="light" level={4} /> - <DxcToggleGroup label="Icons & label" options={optionsWithIconAndLabel} /> + <Title title="Icons & label" theme="light" level={4} /> + <DxcToggleGroup options={optionsWithIconAndLabel} /> </ExampleContainer> <ExampleContainer> - <Title title="Disabled" theme="light" level={4} /> - <DxcToggleGroup label="Disabled" defaultValue={2} options={options} disabled /> + <Title title="Disabled option" theme="light" level={4} /> + <DxcToggleGroup defaultValue={2} options={disabledOptions} /> </ExampleContainer> - <ExampleContainer pseudoState="pseudo-hover"> - <Title title="Hovered" theme="light" level={4} /> - <DxcToggleGroup label="Hovered" options={twoOptions} defaultValue={2} /> + <ExampleContainer> + <Title title="Multiple options selected" theme="light" level={4} /> + <DxcToggleGroup options={optionsWithIcon} defaultValue={[1, 3]} multiple /> </ExampleContainer> <ExampleContainer> - <Title title="Multiple toggleGroup" theme="light" level={4} /> - <DxcToggleGroup - label="Toggle group" - helperText="Please select one or more" - options={options} - defaultValue={[1, 3]} - multiple - ></DxcToggleGroup> + <Title title="Vertically stacked" theme="light" level={4} /> + <DxcToggleGroup defaultValue={3} options={optionsWithIcon} orientation="vertical" /> </ExampleContainer> <Title title="Margins" theme="light" level={2} /> <ExampleContainer> <Title title="xxSmall" theme="light" level={4} /> - <DxcToggleGroup label="xxSmall margin" options={options} margin="xxsmall" /> + <DxcToggleGroup options={options} margin="xxsmall" /> </ExampleContainer> <ExampleContainer> <Title title="xSmall" theme="light" level={4} /> - <DxcToggleGroup label="xSmall margin" options={options} margin="xsmall" /> + <DxcToggleGroup options={options} margin="xsmall" /> </ExampleContainer> <ExampleContainer> <Title title="Small" theme="light" level={4} /> - <DxcToggleGroup label="Small margin" options={options} margin="small" /> + <DxcToggleGroup options={options} margin="small" /> </ExampleContainer> <ExampleContainer> <Title title="Medium" theme="light" level={4} /> - <DxcToggleGroup label="Medium margin" options={options} margin="medium" /> + <DxcToggleGroup options={options} margin="medium" /> </ExampleContainer> <ExampleContainer> <Title title="Large" theme="light" level={4} /> - <DxcToggleGroup label="Large margin" options={options} margin="large" /> + <DxcToggleGroup options={options} margin="large" /> </ExampleContainer> <ExampleContainer> <Title title="xLarge" theme="light" level={4} /> - <DxcToggleGroup label="xLarge margin" options={options} margin="xlarge" /> + <DxcToggleGroup options={options} margin="xlarge" /> </ExampleContainer> <ExampleContainer> <Title title="xxLarge" theme="light" level={4} /> - <DxcToggleGroup label="xxLarge margin" options={options} margin="xxlarge" /> - </ExampleContainer> - <Title title="Opinionated theme" theme="light" level={2} /> - <ExampleContainer> - <HalstackProvider theme={opinionatedTheme}> - <Title title="Selected" theme="light" level={4} /> - <DxcToggleGroup label="Selected" helperText="HelperText" defaultValue={2} options={options} /> - </HalstackProvider> - </ExampleContainer> - <ExampleContainer> - <HalstackProvider theme={opinionatedTheme}> - <Title title="Icons & label toggle group" theme="light" level={4} /> - <DxcToggleGroup label="Icons & label" options={optionsWithIconAndLabel} /> - </HalstackProvider> - </ExampleContainer> - <ExampleContainer> - <HalstackProvider theme={opinionatedTheme}> - <Title title="Disabled" theme="light" level={4} /> - <DxcToggleGroup label="Disabled" defaultValue={2} options={options} disabled /> - </HalstackProvider> - </ExampleContainer> - <ExampleContainer pseudoState="pseudo-hover"> - <Title title="Hovered" theme="light" level={4} /> - <HalstackProvider theme={opinionatedTheme}> - <DxcToggleGroup label="Hovered" options={twoOptions} defaultValue={2} /> - </HalstackProvider> - </ExampleContainer> - <ExampleContainer pseudoState="pseudo-active"> - <Title title="Actived" theme="light" level={4} /> - <HalstackProvider theme={opinionatedTheme}> - <DxcToggleGroup label="Actived" options={twoOptions} defaultValue={2} /> - </HalstackProvider> + <DxcToggleGroup options={options} margin="xxlarge" /> </ExampleContainer> </> ); -const OptionSelected = () => <DxcToggleGroup label="Toggle group" helperText="HelperText" options={options} />; - type Story = StoryObj<typeof DxcToggleGroup>; export const Chromatic: Story = { render: ToggleGroup, }; - -export const ToggleGroupSelectedActived: Story = { - render: OptionSelected, - play: async ({ canvasElement }) => { - const canvas = within(canvasElement); - const option = canvas.getByText("Linkedin"); - await userEvent.click(option); - }, -}; - -export const ToggleGroupUnselectedActived: Story = { - render: OptionSelected, - play: async ({ canvasElement }) => { - const canvas = within(canvasElement); - const option = canvas.getByText("X"); - await userEvent.click(option); - userEvent.tab(); - }, -}; diff --git a/packages/lib/src/toggle-group/ToggleGroup.test.tsx b/packages/lib/src/toggle-group/ToggleGroup.test.tsx index eb8c3be84d..1c95547373 100644 --- a/packages/lib/src/toggle-group/ToggleGroup.test.tsx +++ b/packages/lib/src/toggle-group/ToggleGroup.test.tsx @@ -22,22 +22,17 @@ const options = [ describe("Toggle group component tests", () => { test("Toggle group renders with correct labels", () => { - const { getByText } = render( - <DxcToggleGroup label="Toggle group label" helperText="Toggle group helper text" options={options} /> - ); - expect(getByText("Toggle group label")).toBeTruthy(); - expect(getByText("Toggle group helper text")).toBeTruthy(); + const { getByText, getByRole } = render(<DxcToggleGroup options={options} />); + const toggleGroup = getByRole("toolbar"); expect(getByText("Amazon")).toBeTruthy(); expect(getByText("Ebay")).toBeTruthy(); expect(getByText("Apple")).toBeTruthy(); expect(getByText("Google")).toBeTruthy(); + expect(toggleGroup.getAttribute("aria-orientation")).toBe("horizontal"); }); - test("Toggle group renders with correct aria-label in only-icon scenario", () => { const { getByRole } = render( <DxcToggleGroup - label="Toggle group label" - helperText="Toggle group helper text" options={[ { value: 1, icon: "https://cdn.icon-icons.com/icons2/2645/PNG/512/mic_mute_icon_159965.png", title: "Mute" }, ]} @@ -45,7 +40,6 @@ describe("Toggle group component tests", () => { ); expect(getByRole("button").getAttribute("aria-label")).toBe("Mute"); }); - test("Uncontrolled toggle group calls correct function on change with value", () => { const onChange = jest.fn(); const { getByText } = render(<DxcToggleGroup options={options} onChange={onChange} />); @@ -53,7 +47,6 @@ describe("Toggle group component tests", () => { fireEvent.click(option); expect(onChange).toHaveBeenCalledWith(2); }); - test("Controlled toggle group calls correct function on change with value", () => { const onChange = jest.fn(); const { getByText } = render(<DxcToggleGroup options={options} onChange={onChange} value={1} />); @@ -61,15 +54,6 @@ describe("Toggle group component tests", () => { fireEvent.click(option); expect(onChange).toHaveBeenCalledWith(2); }); - - test("Function on change is not called when disable", () => { - const onChange = jest.fn(); - const { getByText } = render(<DxcToggleGroup options={options} onChange={onChange} disabled />); - const option = getByText("Ebay"); - fireEvent.click(option); - expect(onChange).toHaveBeenCalledTimes(0); - }); - test("Uncontrolled multiple toggle group calls correct function on change with value when is multiple", () => { const onChange = jest.fn(); const { getAllByRole } = render(<DxcToggleGroup options={options} onChange={onChange} multiple />); @@ -83,7 +67,6 @@ describe("Toggle group component tests", () => { expect(toggleOptions[1]?.getAttribute("aria-pressed")).toBe("true"); expect(toggleOptions[3]?.getAttribute("aria-pressed")).toBe("true"); }); - test("Controlled multiple toggle returns always same values", () => { const onChange = jest.fn(); const { getByText } = render(<DxcToggleGroup options={options} onChange={onChange} value={[1]} multiple />); @@ -94,17 +77,20 @@ describe("Toggle group component tests", () => { fireEvent.click(option2); expect(onChange).toHaveBeenNthCalledWith(2, [1, 4]); }); - test("Single selection: Renders with correct default value", () => { const { getAllByRole } = render(<DxcToggleGroup options={options} defaultValue={2} />); const toggleOptions = getAllByRole("button"); expect(toggleOptions[1]?.getAttribute("aria-pressed")).toBe("true"); }); - test("Multiple selection: Renders with correct default value", () => { const { getAllByRole } = render(<DxcToggleGroup options={options} defaultValue={[2, 4]} multiple />); const toggleOptions = getAllByRole("button"); expect(toggleOptions[1]?.getAttribute("aria-pressed")).toBe("true"); expect(toggleOptions[3]?.getAttribute("aria-pressed")).toBe("true"); }); + test("Aria orientation is set correctly", () => { + const { getByRole } = render(<DxcToggleGroup options={options} orientation="vertical" />); + const toggleGroup = getByRole("toolbar"); + expect(toggleGroup.getAttribute("aria-orientation")).toBe("vertical"); + }); }); diff --git a/packages/lib/src/toggle-group/ToggleGroup.tsx b/packages/lib/src/toggle-group/ToggleGroup.tsx index 6ef06f8b4d..e4cb3623c4 100644 --- a/packages/lib/src/toggle-group/ToggleGroup.tsx +++ b/packages/lib/src/toggle-group/ToggleGroup.tsx @@ -1,55 +1,91 @@ -import { KeyboardEvent, useContext, useId, useState } from "react"; -import styled, { ThemeProvider } from "styled-components"; +import { KeyboardEvent, useState } from "react"; +import styled from "styled-components"; import { spaces } from "../common/variables"; -import DxcFlex from "../flex/Flex"; import DxcIcon from "../icon/Icon"; import { Tooltip } from "../tooltip/Tooltip"; -import HalstackContext from "../HalstackContext"; -import ToggleGroupPropsType, { OptionLabel } from "./types"; +import ToggleGroupPropsType from "./types"; +import { getButtonStyles, getHeight } from "../button/utils"; -const DxcToggleGroup = ({ - label, - helperText, +const ToggleGroup = styled.div<{ margin: ToggleGroupPropsType["margin"] }>` + display: flex; + &[aria-orientation="vertical"] { + flex-direction: column; + } + gap: var(--spacing-gap-xs); + padding: var(--spacing-padding-xxs); + height: fit-content; + width: fit-content; + border: var(--border-width-s) var(--border-style-default) var(--border-color-neutral-strong); + border-radius: var(--border-radius-m); + margin: ${({ margin }) => (margin && typeof margin !== "object" ? spaces[margin] : "0px")}; + margin-top: ${({ margin }) => (margin && typeof margin === "object" && margin.top ? spaces[margin.top] : "")}; + margin-right: ${({ margin }) => (margin && typeof margin === "object" && margin.right ? spaces[margin.right] : "")}; + margin-bottom: ${({ margin }) => + margin && typeof margin === "object" && margin.bottom ? spaces[margin.bottom] : ""}; + margin-left: ${({ margin }) => (margin && typeof margin === "object" && margin.left ? spaces[margin.left] : "")}; +`; + +const ToggleButton = styled.button<{ + onlyIcon: boolean; + selected: boolean; +}>` + display: inline-flex; + align-items: center; + justify-content: center; + gap: var(--spacing-gap-s); + height: ${getHeight("large")}; + padding: var(--spacing-padding-none) + ${({ onlyIcon }) => (onlyIcon ? "var(--spacing-padding-xs)" : "var(--spacing-padding-m)")}; + cursor: pointer; + ${({ selected }) => + getButtonStyles("primary", selected ? "selected" : "unselected", { height: "large", width: "fitContent" })}; +`; + +const IconContainer = styled.div` + display: flex; + font-size: var(--height-s); + svg { + width: 24px; + height: var(--height-s); + } +`; + +const isToggleButtonSelected = ( + multiple: ToggleGroupPropsType["multiple"], + optionValue: number, + value: ToggleGroupPropsType["value"] +) => (multiple ? Array.isArray(value) && value.includes(optionValue) : optionValue === value); + +export default function DxcToggleGroup({ defaultValue, - value, + margin, + multiple, onChange, - disabled = false, options, - margin, - multiple = false, + orientation = "horizontal", tabIndex = 0, -}: ToggleGroupPropsType): JSX.Element => { - const toggleGroupLabelId = `label-toggle-group-${useId()}`; + value, +}: ToggleGroupPropsType) { const [selectedValue, setSelectedValue] = useState(defaultValue ?? (multiple ? [] : -1)); - const colorsTheme = useContext(HalstackContext); - - const handleToggleChange = (selectedOption: number) => { + const handleOnChange = (selectedOption: number) => { let newSelectedOptions: number[] = []; - if (value == null) { if (multiple && Array.isArray(selectedValue)) { newSelectedOptions = selectedValue.map((singleValue) => singleValue); if (newSelectedOptions.includes(selectedOption)) { const index = newSelectedOptions.indexOf(selectedOption); newSelectedOptions.splice(index, 1); - } else { - newSelectedOptions.push(selectedOption); - } + } else newSelectedOptions.push(selectedOption); setSelectedValue(newSelectedOptions); - } else { - setSelectedValue(selectedOption === selectedValue ? -1 : selectedOption); - } + } else setSelectedValue(selectedOption === selectedValue ? -1 : selectedOption); } else if (multiple) { newSelectedOptions = Array.isArray(value) ? value.map((v) => v) : [value]; if (newSelectedOptions.includes(selectedOption)) { const index = newSelectedOptions.indexOf(selectedOption); newSelectedOptions.splice(index, 1); - } else { - newSelectedOptions.push(selectedOption); - } + } else newSelectedOptions.push(selectedOption); } - onChange?.((multiple ? newSelectedOptions : selectedOption) as number & number[]); }; @@ -58,7 +94,7 @@ const DxcToggleGroup = ({ case "Enter": case " ": event.preventDefault(); - handleToggleChange(optionValue); + handleOnChange(optionValue); break; default: break; @@ -66,171 +102,35 @@ const DxcToggleGroup = ({ }; return ( - <ThemeProvider theme={colorsTheme.toggleGroup}> - <ToggleGroup margin={margin}> - <Label id={toggleGroupLabelId} disabled={disabled}> - {label} - </Label> - <HelperText disabled={disabled}>{helperText}</HelperText> - <OptionsContainer aria-labelledby={toggleGroupLabelId}> - {options.map((option, i) => ( - <Tooltip label={option.title} key={`toggle-${i}-${option.label}`}> - <ToggleButton - aria-label={option.title} - aria-pressed={ - multiple - ? value - ? Array.isArray(value) && value.includes(option.value) - : Array.isArray(selectedValue) && selectedValue.includes(option.value) - : value - ? option.value === value - : option.value === selectedValue - } - disabled={disabled} - onClick={() => { - handleToggleChange(option.value); - }} - onKeyDown={(event) => { - handleOnKeyDown(event, option.value); - }} - tabIndex={!disabled ? tabIndex : -1} - hasIcon={option.icon} - optionLabel={option.label ?? ""} - selected={ - multiple - ? value - ? Array.isArray(value) && value.includes(option.value) - : Array.isArray(selectedValue) && selectedValue.includes(option.value) - : value - ? option.value === value - : option.value === selectedValue - } - > - <DxcFlex alignItems="center"> - {option.icon && ( - <IconContainer optionLabel={option.label ?? ""}> - {typeof option.icon === "string" ? <DxcIcon icon={option.icon} /> : option.icon} - </IconContainer> - )} - {option.label && <LabelContainer>{option.label}</LabelContainer>} - </DxcFlex> - </ToggleButton> - </Tooltip> - ))} - </OptionsContainer> - </ToggleGroup> - </ThemeProvider> + <ToggleGroup aria-orientation={orientation} margin={margin} role="toolbar"> + {options.map((option, i) => { + const selected = !option.disabled && isToggleButtonSelected(multiple, option.value, value ?? selectedValue); + return ( + <Tooltip label={option.title} key={`toggle-${i}-${option.label}`}> + <ToggleButton + aria-label={option.title} + aria-pressed={selected} + disabled={option.disabled} + onClick={() => { + handleOnChange(option.value); + }} + onKeyDown={(event) => { + handleOnKeyDown(event, option.value); + }} + onlyIcon={!option.label && !!option.icon} + selected={selected} + tabIndex={!option.disabled ? tabIndex : -1} + > + {option.icon && ( + <IconContainer> + {typeof option.icon === "string" ? <DxcIcon icon={option.icon} /> : option.icon} + </IconContainer> + )} + {option.label && <span>{option.label}</span>} + </ToggleButton> + </Tooltip> + ); + })} + </ToggleGroup> ); -}; - -const ToggleGroup = styled.div<{ margin: ToggleGroupPropsType["margin"] }>` - display: inline-flex; - flex-direction: column; - margin: ${(props) => (props.margin && typeof props.margin !== "object" ? spaces[props.margin] : "0px")}; - margin-top: ${(props) => - props.margin && typeof props.margin === "object" && props.margin.top ? spaces[props.margin.top] : ""}; - margin-right: ${(props) => - props.margin && typeof props.margin === "object" && props.margin.right ? spaces[props.margin.right] : ""}; - margin-bottom: ${(props) => - props.margin && typeof props.margin === "object" && props.margin.bottom ? spaces[props.margin.bottom] : ""}; - margin-left: ${(props) => - props.margin && typeof props.margin === "object" && props.margin.left ? spaces[props.margin.left] : ""}; -`; - -const Label = styled.label<{ disabled: ToggleGroupPropsType["disabled"] }>` - color: ${(props) => (props.disabled ? props.theme.disabledLabelFontColor : props.theme.labelFontColor)}; - font-family: ${(props) => props.theme.labelFontFamily}; - font-size: ${(props) => props.theme.labelFontSize}; - font-style: ${(props) => props.theme.labelFontStyle}; - font-weight: ${(props) => props.theme.labelFontWeight}; - line-height: ${(props) => props.theme.labelLineHeight}; -`; - -const HelperText = styled.span<{ disabled: ToggleGroupPropsType["disabled"] }>` - color: ${(props) => (props.disabled ? props.theme.disabledHelperTextFontColor : props.theme.helperTextFontColor)}; - font-family: ${(props) => props.theme.helperTextFontFamily}; - font-size: ${(props) => props.theme.helperTextFontSize}; - font-style: ${(props) => props.theme.helperTextFontStyle}; - font-weight: ${(props) => props.theme.helperTextFontWeight}; - line-height: ${(props) => props.theme.helperTextLineHeight}; -`; - -const OptionsContainer = styled.div` - display: flex; - gap: 0.25rem; - width: max-content; - height: calc(48px - 4px - 4px); - padding: 0.25rem; - border-width: ${(props) => props.theme.containerBorderThickness}; - border-style: ${(props) => props.theme.containerBorderStyle}; - border-radius: ${(props) => props.theme.containerBorderRadius}; - border-color: ${(props) => props.theme.containerBorderColor}; - margin-top: ${(props) => props.theme.containerMarginTop}; - background-color: ${(props) => props.theme.containerBackgroundColor}; -`; - -const ToggleButton = styled.button<{ - selected: boolean; - hasIcon: OptionLabel["icon"]; - optionLabel: OptionLabel["label"]; -}>` - display: flex; - flex-direction: column; - justify-content: center; - padding-left: ${(props) => - (props.optionLabel && props.hasIcon) || (props.optionLabel && !props.hasIcon) - ? props.theme.labelPaddingLeft - : props.theme.iconPaddingLeft}; - padding-right: ${(props) => - (props.optionLabel && props.hasIcon) || (props.optionLabel && !props.hasIcon) - ? props.theme.labelPaddingRight - : props.theme.iconPaddingRight}; - border-width: ${(props) => props.theme.optionBorderThickness}; - border-style: ${(props) => props.theme.optionBorderStyle}; - border-radius: ${(props) => props.theme.optionBorderRadius}; - background-color: ${(props) => - props.selected ? props.theme.selectedBackgroundColor : props.theme.unselectedBackgroundColor}; - color: ${(props) => (props.selected ? props.theme.selectedFontColor : props.theme.unselectedFontColor)}; - cursor: pointer; - - &:hover { - background-color: ${(props) => - props.selected ? props.theme.selectedHoverBackgroundColor : props.theme.unselectedHoverBackgroundColor}; - } - &:active { - background-color: ${(props) => - props.selected ? props.theme.selectedActiveBackgroundColor : props.theme.unselectedActiveBackgroundColor}; - color: #ffffff; - } - &:focus { - outline: none; - box-shadow: ${(props) => `0 0 0 ${props.theme.optionFocusBorderThickness} ${props.theme.focusColor}`}; - } - &:disabled { - background-color: ${(props) => - props.selected ? props.theme.selectedDisabledBackgroundColor : props.theme.unselectedDisabledBackgroundColor}; - color: ${(props) => - props.selected ? props.theme.selectedDisabledFontColor : props.theme.unselectedDisabledFontColor}; - cursor: not-allowed; - } -`; - -const LabelContainer = styled.span` - font-family: ${(props) => props.theme.optionLabelFontFamily}; - font-size: ${(props) => props.theme.optionLabelFontSize}; - font-style: ${(props) => props.theme.optionLabelFontStyle}; - font-weight: ${(props) => props.theme.optionLabelFontWeight}; -`; - -const IconContainer = styled.div<{ optionLabel: OptionLabel["label"] }>` - display: flex; - margin-right: ${(props) => props.optionLabel && props.theme.iconMarginRight}; - overflow: hidden; - font-size: 24px; - svg { - height: 24px; - width: 24px; - } -`; - -export default DxcToggleGroup; +} diff --git a/packages/lib/src/toggle-group/types.ts b/packages/lib/src/toggle-group/types.ts index 1b713bda6a..fe055c35f4 100644 --- a/packages/lib/src/toggle-group/types.ts +++ b/packages/lib/src/toggle-group/types.ts @@ -1,36 +1,42 @@ import { Margin, SVG, Space } from "../common/utils"; type OptionIcon = { - /** - * String with the option display value. - */ - label?: never; /** * Material Symbols icon or SVG element. Icon and label can't be used at same time. */ icon: string | SVG; + /** + * String with the option display value. + */ + label?: never; /** * Value for the HTML properties title and aria-label. * When a label is defined, this prop can not be use. */ title: string; }; -export type OptionLabel = { - /** - * String with the option display value. - */ - label: string; + +type OptionLabel = { /** * Material Symbols icon or SVG element. Icon and label can't be used at same time. */ icon?: string | SVG; + /** + * String with the option display value. + */ + label: string; /** * Value for the HTML properties title and aria-label. * When a label is defined, this prop can not be use. */ title?: never; }; + type Option = { + /** + * If true, the option will be disabled. + */ + disabled?: boolean; /** * Number with the option inner value. */ @@ -39,72 +45,66 @@ type Option = { type CommonProps = { /** - * Text to be placed above the component. - */ - label?: string; - /** - * Helper text to be placed above the component. - */ - helperText?: string; - /** - * If true, the component will be disabled. + * Size of the margin to be applied to the component ('xxsmall' | 'xsmall' | 'small' | 'medium' | 'large' | 'xlarge' | 'xxlarge'). + * You can pass an object with 'top', 'bottom', 'left' and 'right' properties in order to specify different margin sizes. */ - disabled?: boolean; + margin?: Space | Margin; /** * An array of objects representing the selectable options. */ options: Option[]; /** - * Size of the margin to be applied to the component ('xxsmall' | 'xsmall' | 'small' | 'medium' | 'large' | 'xlarge' | 'xxlarge'). - * You can pass an object with 'top', 'bottom', 'left' and 'right' properties in order to specify different margin sizes. + * The orientation of the toggle group. */ - margin?: Space | Margin; + orientation?: "horizontal" | "vertical"; /** * Value of the tabindex. */ tabIndex?: number; }; -type SingleSelectionToggleGroup = CommonProps & { +type MultipleSelectionToggleGroup = { + /** + * The array of keys with the initially selected values. + */ + defaultValue?: number[]; /** * If true, the toggle group will support multiple selection. In that case, value must be an array of numbers with the keys of the selected values. */ - multiple?: false; + multiple: true; /** - * The key of the initially selected value. + * This function will be called every time the selection changes. An array with the key of + * the selected values will be passed as a parameter to this function. */ - defaultValue?: number; + onChange?: (optionIndex: number[]) => void; /** - * The key of the selected value. If the component allows multiple selection, value must be an array. + * An array with the keys of the selected values. * If undefined, the component will be uncontrolled and the value will be managed internally by the component. */ - value?: number; + value?: number[]; +}; + +type SingleSelectionToggleGroup = { /** - * This function will be called every time the selection changes. The number with the key of the selected - * value will be passed as a parameter to this function. + * The key of the initially selected value. */ - onChange?: (optionIndex: number) => void; -}; -type MultipleSelectionToggleGroup = CommonProps & { + defaultValue?: number; /** * If true, the toggle group will support multiple selection. In that case, value must be an array of numbers with the keys of the selected values. */ - multiple: true; + multiple?: false; /** - * The array of keys with the initially selected values. + * This function will be called every time the selection changes. The number with the key of the selected + * value will be passed as a parameter to this function. */ - defaultValue?: number[]; + onChange?: (optionIndex: number) => void; /** - * An array with the keys of the selected values. + * The key of the selected value. If the component allows multiple selection, value must be an array. * If undefined, the component will be uncontrolled and the value will be managed internally by the component. */ - value?: number[]; - /** - * This function will be called every time the selection changes. An array with the key of - * the selected values will be passed as a parameter to this function. - */ - onChange?: (optionIndex: number[]) => void; + value?: number; }; -type Props = SingleSelectionToggleGroup | MultipleSelectionToggleGroup; + +type Props = CommonProps & (MultipleSelectionToggleGroup | SingleSelectionToggleGroup); export default Props;