diff --git a/apps/website/pages/components/switch/code.tsx b/apps/website/pages/components/switch/code.tsx new file mode 100644 index 0000000000..a8170451dd --- /dev/null +++ b/apps/website/pages/components/switch/code.tsx @@ -0,0 +1,17 @@ +import Head from "next/head"; +import type { ReactElement } from "react"; +import SwitchPageLayout from "screens/components/switch/SwitchPageLayout"; +import SwitchCodePage from "screens/components/switch/code/SwitchCodePage"; + +const Code = () => ( + <> + + Switch code — Halstack Design System + + + +); + +Code.getLayout = (page: ReactElement) => {page}; + +export default Code; diff --git a/apps/website/pages/components/switch/index.tsx b/apps/website/pages/components/switch/index.tsx index d6b877b8be..7fec11d115 100644 --- a/apps/website/pages/components/switch/index.tsx +++ b/apps/website/pages/components/switch/index.tsx @@ -1,21 +1,17 @@ import Head from "next/head"; import type { ReactElement } from "react"; import SwitchPageLayout from "screens/components/switch/SwitchPageLayout"; -import SwitchCodePage from "screens/components/switch/code/SwitchCodePage"; +import SwitchOverviewPage from "screens/components/switch/overview/SwitchOverviewPage"; -const Index = () => { - return ( - <> - - Switch — Halstack Design System - - - - ); -}; +const Index = () => ( + <> + + Switch — 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/switch/specifications.tsx b/apps/website/pages/components/switch/specifications.tsx deleted file mode 100644 index 76eec4eecf..0000000000 --- a/apps/website/pages/components/switch/specifications.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import Head from "next/head"; -import type { ReactElement } from "react"; -import SwitchPageLayout from "screens/components/switch/SwitchPageLayout"; -import SwitchSpecsPage from "screens/components/switch/specs/SwitchSpecsPage"; - -const Specifications = () => { - return ( - <> - - Switch Specs — Halstack Design System - - - - ); -}; - -Specifications.getLayout = function getLayout(page: ReactElement) { - return {page}; -}; - -export default Specifications; diff --git a/apps/website/pages/components/switch/usage.tsx b/apps/website/pages/components/switch/usage.tsx deleted file mode 100644 index 088499b613..0000000000 --- a/apps/website/pages/components/switch/usage.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import Head from "next/head"; -import type { ReactElement } from "react"; -import SwitchPageLayout from "screens/components/switch/SwitchPageLayout"; -import SwitchUsagePage from "screens/components/switch/usage/SwitchUsagePage"; - -const Usage = () => { - return ( - <> - - Switch Usage — Halstack Design System - - - - ); -}; - -Usage.getLayout = function getLayout(page: ReactElement) { - return {page}; -}; - -export default Usage; diff --git a/apps/website/screens/components/checkbox/overview/CheckboxOverviewPage.tsx b/apps/website/screens/components/checkbox/overview/CheckboxOverviewPage.tsx index b525ce0e1a..ea9ae4b6e6 100644 --- a/apps/website/screens/components/checkbox/overview/CheckboxOverviewPage.tsx +++ b/apps/website/screens/components/checkbox/overview/CheckboxOverviewPage.tsx @@ -1,4 +1,4 @@ -import { DxcParagraph, DxcBulletedList, DxcFlex, DxcTable } from "@dxc-technology/halstack-react"; +import { DxcParagraph, DxcBulletedList, DxcFlex, DxcTable, DxcLink } from "@dxc-technology/halstack-react"; import QuickNavContainerLayout from "@/common/QuickNavContainerLayout"; import QuickNavContainer from "@/common/QuickNavContainer"; import DocFooter from "@/common/DocFooter"; @@ -6,6 +6,8 @@ import Example from "@/common/example/Example"; import stacking from "./examples/stacking"; import Image from "@/common/Image"; import anatomy from "./images/checkbox_anatomy.png"; +import Link from "next/link"; +import labelPosition from "./examples/labelPosition"; const sections = [ { @@ -52,27 +54,106 @@ const sections = [ { title: "Vertical stacking", content: ( - - To improve readability and scalability, checkboxes can be stacked vertically, especially in forms or - settings panels, allowing users to process options more efficiently without excessive eye movement. Leave - 8px of spacing between vertically stacked checkboxes. - + <> + + To improve readability and scalability, checkboxes can be stacked vertically, especially + in forms or settings panels, allowing users to process options more efficiently without excessive eye + movement. + + Leave minimum 4px of spacing between vertically stacked checkboxes. + ), }, { title: "Horizontal stacking", content: ( - - Used in scenarios with limited vertical space, checkboxes can be stacked horizontally, along with a - consistent spacing and alignment, to maintain a structured and organized layout. If a set of checkboxes is - related to a single category, consider using a group label to provide context as this will enhance usability - and help users make informed selections. Horizontally stacked checkboxes maintain a separation of, minimum, - 32px. - + <> + + Used in scenarios with limited vertical space, checkboxes can be stacked horizontally, along with a + consistent spacing and alignment, to maintain a structured and organized layout. If a set of checkboxes is + related to a single category, consider using a group label to provide context as this will enhance + usability and help users make informed selections. + + Horizontally stacked checkboxes maintain a separation of, minimum, 24px. + ), }, ], }, + { + title: "Label position", + content: ( + <> + + By default, the label is placed to the left of the checkbox, but it can also be positioned{" "} + to the right when needed. Choose the label position that best fits your layout and ensures + clear readability and alignment with surrounding elements. + + + We recommend selecting a label placement that maintains a strong visual connection between the checkbox and + its description, without disrupting the overall flow of the interface. + + + + ), + }, + { + title: "Checkbox vs. radio group vs. switch", + content: ( + <> + + Although checkboxes,{" "} + + radio groups + + , and{" "} + + switches + {" "} + may appear as selection controls, they serve distinct purposes in a user interface: + + + + + Component + Use case + + + + + + Checkbox + + + Use when users can select multiple options independently. Each checkbox represents an + on/off decision, making them suitable for filters, preference settings, or multi-select tasks. A group + may allow none, some, or all options to be selected. + + + + + Radio group + + + Use when the user must select only one option from a list of predefined, mutually + exclusive choices. Ideal for short, static lists where all options should be visible at once to support + decision-making. + + + + + Switch + + + Use for a single, immediate toggle between two states, like on/off or enabled/disabled. + Switches should act instantly and are best for system or UI-level settings. + + + + + + ), + }, { title: "Best practices", content: ( diff --git a/apps/website/screens/components/checkbox/overview/examples/labelPosition.ts b/apps/website/screens/components/checkbox/overview/examples/labelPosition.ts new file mode 100644 index 0000000000..060382b0ad --- /dev/null +++ b/apps/website/screens/components/checkbox/overview/examples/labelPosition.ts @@ -0,0 +1,20 @@ +import { DxcCheckbox, DxcInset, DxcFlex } from "@dxc-technology/halstack-react"; + +const code = `() => { + return ( + + + + + + + ); +}`; + +const scope = { + DxcCheckbox, + DxcInset, + DxcFlex, +}; + +export default { code, scope }; diff --git a/apps/website/screens/components/checkbox/overview/examples/stacking.ts b/apps/website/screens/components/checkbox/overview/examples/stacking.ts index 435d7358ef..e63ce15200 100644 --- a/apps/website/screens/components/checkbox/overview/examples/stacking.ts +++ b/apps/website/screens/components/checkbox/overview/examples/stacking.ts @@ -1,21 +1,33 @@ -import { DxcCheckbox, DxcInset, DxcFlex, DxcHeading } from "@dxc-technology/halstack-react"; +import { DxcCheckbox, DxcInset, DxcFlex, DxcTypography } from "@dxc-technology/halstack-react"; const code = `() => { return ( - - - - - - + + + + Vertical + + + + - - - - - - + + + Horizontal + + + + + @@ -27,7 +39,7 @@ const scope = { DxcCheckbox, DxcInset, DxcFlex, - DxcHeading, + DxcTypography, }; export default { code, scope }; diff --git a/apps/website/screens/components/switch/SwitchPageLayout.tsx b/apps/website/screens/components/switch/SwitchPageLayout.tsx index df66b64c7a..b12acec0cd 100644 --- a/apps/website/screens/components/switch/SwitchPageLayout.tsx +++ b/apps/website/screens/components/switch/SwitchPageLayout.tsx @@ -6,9 +6,8 @@ import { ReactNode } from "react"; const SwitchPageHeading = ({ children }: { children: ReactNode }) => { const tabs = [ - { label: "Code", path: "/components/switch" }, - { label: "Usage", path: "/components/switch/usage" }, - { label: "Specifications", path: "/components/switch/specifications" }, + { label: "Overview", path: "/components/switch" }, + { label: "Code", path: "/components/switch/code" }, ]; return ( @@ -17,11 +16,10 @@ const SwitchPageHeading = ({ children }: { children: ReactNode }) => { - Switch toggles are elements that can get two simple states, each of them has an impact on the system and it - can be switched on or off, there are no more options. If the switch toggle is on one state, the action to - change it will modify to value of the element to the contrary. + A switch allows users to toggle a single setting between two opposing states, such as on/off or + enabled/disabled. It represents a binary choice that takes effect immediately. - + {children} diff --git a/apps/website/screens/components/switch/code/SwitchCodePage.tsx b/apps/website/screens/components/switch/code/SwitchCodePage.tsx index 44040bdd3b..a4fdf930be 100644 --- a/apps/website/screens/components/switch/code/SwitchCodePage.tsx +++ b/apps/website/screens/components/switch/code/SwitchCodePage.tsx @@ -23,13 +23,15 @@ const sections = [ - defaultChecked + ariaLabel - boolean + {"string"} - Initial state of the switch, only when it is uncontrolled. - false + Specifies a string to be used as the name for the switch element when no label is provided. + + + 'Switch' @@ -44,15 +46,24 @@ const sections = [ - - value + defaultChecked - string + boolean + Initial state of the switch, only when it is uncontrolled. - Will be passed to the value attribute of the HTML input element. When inside a form, this - value will be only submitted if the switch is checked. + false + + + + disabled + + boolean + + If true, the component will be disabled. + + false - - label @@ -72,6 +83,17 @@ const sections = [ 'before' + + margin + + 'xxsmall' | 'xsmall' | 'small' | 'medium' | 'large' | 'xlarge' | 'xxlarge' | Margin + + + 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. + + - + name @@ -81,14 +103,15 @@ const sections = [ - - disabled + onChange - boolean + {"(checked: boolean) => void"} - If true, the component will be disabled. - false + This function will be called when the user clicks the switch. The new value of the checked attribute will + be passed as a parameter. + - optional @@ -101,25 +124,11 @@ const sections = [ - onChange - - {"(checked: boolean) => void"} - - - This function will be called when the user clicks the switch. The new value of the checked attribute will - be passed as a parameter. - - - - - - margin - - 'xxsmall' | 'xsmall' | 'small' | 'medium' | 'large' | 'xlarge' | 'xxlarge' | Margin - + ref - 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. + {"React.Ref"} + Reference to the component. - @@ -145,20 +154,15 @@ const sections = [ - ref + value - {"React.Ref"} + string - Reference to the component. - - - - - ariaLabel - {"string"} + Will be passed to the value attribute of the HTML input element. When inside a form, this + value will be only submitted if the switch is checked. - Specifies a string to be used as the name for the switch element when no `label` is provided. - 'Switch' + - @@ -179,15 +183,13 @@ const sections = [ }, ]; -const SwitchCodePage = () => { - return ( - - - - - - - ); -}; +const SwitchCodePage = () => ( + + + + + + +); export default SwitchCodePage; diff --git a/apps/website/screens/components/switch/overview/SwitchOverviewPage.tsx b/apps/website/screens/components/switch/overview/SwitchOverviewPage.tsx new file mode 100644 index 0000000000..ec49dbb0e9 --- /dev/null +++ b/apps/website/screens/components/switch/overview/SwitchOverviewPage.tsx @@ -0,0 +1,221 @@ +import { DxcParagraph, DxcBulletedList, DxcFlex, DxcTable, DxcLink } from "@dxc-technology/halstack-react"; +import QuickNavContainer from "@/common/QuickNavContainer"; +import QuickNavContainerLayout from "@/common/QuickNavContainerLayout"; +import DocFooter from "@/common/DocFooter"; +import Example from "@/common/example/Example"; +import labelPosition from "./examples/labelPosition"; +import stacking from "./examples/stacking"; +import Image from "@/common/Image"; +import anatomy from "./images/switch_anatomy.png"; +import Link from "next/link"; + +const sections = [ + { + title: "Introduction", + content: ( + + The switch component enables users to toggle a specific setting on or off. It is designed for + binary decisions that directly affect system behavior or user preferences. Unlike checkboxes or radio buttons, + switches represent immediate state changes — they do not require form submission. They are especially effective + in settings panels, preferences, and mobile interfaces where quick, inline control is essential. + + ), + }, + { + title: "Anatomy", + content: ( + <> + Switch's anatomy + + + Label: describes the setting or feature being toggled. Helps users understand the + consequence of switching it on or off. + + + Thumb: the sliding circle that moves to indicate the current state. + + + Track: the background container of the switch that reflects the active/inactive state with + color. + + + + ), + }, + { + title: "Stacking switches", + content: ( + <> + + Switches can be arranged vertically or horizontally depending on context, user needs, and available screen + space. + + + + ), + subSections: [ + { + title: "Vertical stacking", + content: ( + <> + + The preferred layout for lists of switches. It enhances readability and ensures each + toggle is clearly associated with its label. Ideal for settings or control panels with multiple toggles. + + Leave minimum 4px of spacing between vertically stacked switches. + + ), + }, + { + title: "Horizontal stacking", + content: ( + <> + + Used when space is constrained or when only a few switches are present. This layout can{" "} + save space but should be avoided for long labels or more than two options. + + Horizontally stacked switches maintain a separation of, minimum, 24px. + + ), + }, + ], + }, + { + title: "Label position", + content: ( + <> + + By default, the label is placed before the switch, as this position clearly communicates what + the control is for and improves accessibility. However, it's also possible to position the label{" "} + after the switch in specific cases — particularly when you want to emphasize the{" "} + current state of the control (e.g., "On", "Off"). + + + We recommend changing the default label position{" "} + only when the component's use case justifies it, and as long as the meaning and state of the + switch remain clear to the user. + + + + + + Position + Description + + + + + + Label before + + Improves clarity by describing the setting being controlled. This is the default and recommended position. + + + + Label after + + Useful in minimalist interfaces or when the switch’s current state needs to be highlighted. Recommended only for specific use cases. + + + + + ), + }, + { + title: "Switch vs. radio group vs. checkbox", + content: ( + <> + + Although switches,{" "} + + radio groups + + , and{" "} + + checkboxes + {" "} + may appear as selection controls, they serve distinct purposes in a user interface: + + + + + Component + Use case + + + + + + Switch + + + Use for a single, immediate toggle between two states, like on/off or enabled/disabled. + Switches should act instantly and are best for system or UI-level settings. + + + + + Radio group + + + Use when the user must select only one option from a list of predefined, mutually + exclusive choices. Ideal for short, static lists where all options should be visible at once to support + decision-making. + + + + + Checkbox + + + Use when users can select multiple options independently. Each checkbox represents an + on/off decision, making them suitable for filters, preference settings, or multi-select tasks. A group + may allow none, some, or all options to be selected. + + + + + + ), + }, + { + title: "Best practices", + content: ( + + + Use for binary, opposing states: switches are ideal when users need to turn a setting{" "} + on or off, such as enabling notifications or dark mode. Avoid using switches + for choices that are not immediately clear opposites (use radio buttons instead). + + + Trigger immediate changes: switches should take effect immediately without + requiring form submission. Do not pair switches with a submit button or use them for decisions that need + confirmation. + + + Use clear, descriptive labels: labels should clarify the effect of toggling the switch. Use + positive, action-oriented phrasing when possible (e.g., “Enable sound”). + + + Stack vertically for better scannability: when multiple switches are used together, stack + them vertically to maintain clarity and reduce visual clutter. + + + Don't overuse switches: too many toggles on one screen can overwhelm users. Group related + settings and consider alternatives like grouped checkboxes or forms when appropriate. + + + ), + }, +]; + +const SwitchOverviewPage = () => ( + + + + + + +); + +export default SwitchOverviewPage; diff --git a/apps/website/screens/components/switch/overview/examples/labelPosition.ts b/apps/website/screens/components/switch/overview/examples/labelPosition.ts new file mode 100644 index 0000000000..ee61f300bd --- /dev/null +++ b/apps/website/screens/components/switch/overview/examples/labelPosition.ts @@ -0,0 +1,20 @@ +import { DxcSwitch, DxcInset, DxcFlex } from "@dxc-technology/halstack-react"; + +const code = `() => { + return ( + + + + + + + ); +}`; + +const scope = { + DxcSwitch, + DxcInset, + DxcFlex, +}; + +export default { code, scope }; diff --git a/apps/website/screens/components/switch/overview/examples/stacking.ts b/apps/website/screens/components/switch/overview/examples/stacking.ts new file mode 100644 index 0000000000..493a67c7cb --- /dev/null +++ b/apps/website/screens/components/switch/overview/examples/stacking.ts @@ -0,0 +1,45 @@ +import { DxcSwitch, DxcInset, DxcFlex, DxcTypography } from "@dxc-technology/halstack-react"; + +const code = `() => { + return ( + + + + + Vertical + + + + + + + + Horizontal + + + + + + + + + + ); +}`; + +const scope = { + DxcSwitch, + DxcInset, + DxcFlex, + DxcTypography, +}; + +export default { code, scope }; diff --git a/apps/website/screens/components/switch/overview/images/switch_anatomy.png b/apps/website/screens/components/switch/overview/images/switch_anatomy.png new file mode 100644 index 0000000000..698fd431bf Binary files /dev/null and b/apps/website/screens/components/switch/overview/images/switch_anatomy.png differ diff --git a/apps/website/screens/components/switch/specs/SwitchSpecsPage.tsx b/apps/website/screens/components/switch/specs/SwitchSpecsPage.tsx deleted file mode 100644 index 8cb4e615b9..0000000000 --- a/apps/website/screens/components/switch/specs/SwitchSpecsPage.tsx +++ /dev/null @@ -1,554 +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 specsImage from "./images/switch_specs.png"; -import stackingImage from "./images/switch_stacking.png"; -import statesImage from "./images/switch_states.png"; -import anatomyImage from "./images/switch_anatomy.png"; - -const sections = [ - { - title: "Specifications", - content: ( - <> -
- Switch design specifications -
-
- Switch stacking design specifications -
- - ), - }, - { - title: "States", - content: ( - <> - - Five different states are defined in the life cycle of the component: unselected enabled,{" "} - unselected focus, unselected disabled, selected enabled,{" "} - selected focus and selected disabled. - -
- Switch states -
- - ), - }, - { - title: "Anatomy", - content: ( - <> - Switch anatomy - - Label - Thumb - Track - - - ), - }, - { - 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 - - - - checkedThumbBackgroundColor - - Thumb checked - - color-white - - #ffffff - - - - checkedTrackBackgroundColor - - Track checked - - color-purple-700 - - #5f249f - - - - uncheckedTrackBackgroundColor - - Track unchecked - - color-grey-400 - - #bfbfbf - - - - uncheckedThumbBackgroundColor - - Thumb unchecked - - color-white - - #ffffff - - - - disabledCheckedTrackBackgroundColor - - Track:disabled checked - - color-purple-100 - - #f2eafa - - - - disabledUncheckedTrackBackgroundColor - - Track:disabled unchecked - - color-grey-100 - - #f2f2f2 - - - - disabledCheckedThumbBackgroundColor - - Thumb:disabled checked - - color-white - - #ffffff - - - - disabledUncheckedTrackBackgroundColor - - Track:disabled unchecked - - color-grey-100 - - #f2f2f2 - - - - disabledUncheckedThumbBackgroundColor - - Thumb:disabled unchecked - - color-white - - #ffffff - - - - thumbFocusColor - - Thumb:focus - - color-blue-600 - - #0095ff - - - - ), - }, - { - title: "Typography", - content: ( - - - - Component token - Element - Core token - Value - - - - - - labelFontFamily - - Label - - font-family-sans - - Open sans - - - - labelFontSize - - Label - - font-scale-02 - - 1rem / 16px - - - - labelFontWeight - - Label - - font-weight-regular - - 400 - - - - labelFontStyle - - Label - - font-style-normal - - normal - - - - ), - }, - { - title: "Size", - content: ( - - - - Component token - Element - Core token - Value - - - - - - thumbHeight - - Thumb - - - 24px - - - - thumbWidth - - Thumb - - - 24px - - - - trackHeight - - Track - - - 12px - - - - trackWidth - - Track - - - 36px - - - - focusHeight - - Focus indicator - - - 40px - - - - focusWidth - - Focus indicator - - - 40px - - - - ), - }, - { - title: "Border", - content: ( - - - - Property - Element - Core token - Value - - - - - - border-width - - Track - - border-width-0 - - 0rem / 0px - - - - border-style - - Track - - border-style-none - - none - - - - border-radius - - Track - - border-radius-full - - 9999px - - - - border-width - - Thumb - - border-width-0 - - 0rem / 0px - - - - border-style - - Thumb - - border-style-none - - none - - - - border-radius - - Thumb - - border-radius-full - - 9999px - - - - border-width - - Focus border - - border-width-2 - - 2px - - - - border-style - - Focus border - - border-style-solid - - solid - - - - border-radius - - Focus border - - border-radius-full - - 9999px - - - - ), - }, - { - 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. - - - ), - }, - { - title: "Spacing", - content: ( - <> - - - - Property - Element - Core token - Value - - - - - - padding - - Thumb - - spacing-8 - - 0.5rem / 8px - - - - margin-left/right* - - Switch - - spacing-8 - - 0.5rem / 8px - - - - padding - - Track - - spacing-12 - - 0.75rem / 12px - - - - - *Depending on the position of the label - - - ), - }, - ], - }, -]; - -const SwitchSpecsPage = () => { - return ( - - - - - - - ); -}; - -export default SwitchSpecsPage; diff --git a/apps/website/screens/components/switch/specs/images/switch_anatomy.png b/apps/website/screens/components/switch/specs/images/switch_anatomy.png deleted file mode 100644 index bbfef210c9..0000000000 Binary files a/apps/website/screens/components/switch/specs/images/switch_anatomy.png and /dev/null differ diff --git a/apps/website/screens/components/switch/specs/images/switch_specs.png b/apps/website/screens/components/switch/specs/images/switch_specs.png deleted file mode 100644 index 2ce871a165..0000000000 Binary files a/apps/website/screens/components/switch/specs/images/switch_specs.png and /dev/null differ diff --git a/apps/website/screens/components/switch/specs/images/switch_stacking.png b/apps/website/screens/components/switch/specs/images/switch_stacking.png deleted file mode 100644 index 492ab08d74..0000000000 Binary files a/apps/website/screens/components/switch/specs/images/switch_stacking.png and /dev/null differ diff --git a/apps/website/screens/components/switch/specs/images/switch_states.png b/apps/website/screens/components/switch/specs/images/switch_states.png deleted file mode 100644 index 1bec13c5c3..0000000000 Binary files a/apps/website/screens/components/switch/specs/images/switch_states.png and /dev/null differ diff --git a/apps/website/screens/components/switch/usage/SwitchUsagePage.tsx b/apps/website/screens/components/switch/usage/SwitchUsagePage.tsx deleted file mode 100644 index c264647936..0000000000 --- a/apps/website/screens/components/switch/usage/SwitchUsagePage.tsx +++ /dev/null @@ -1,82 +0,0 @@ -import { DxcParagraph, DxcBulletedList, DxcFlex, DxcTable } from "@dxc-technology/halstack-react"; -import QuickNavContainer from "@/common/QuickNavContainer"; -import QuickNavContainerLayout from "@/common/QuickNavContainerLayout"; -import DocFooter from "@/common/DocFooter"; -import Example from "@/common/example/Example"; -import labelPosition from "./examples/labelPosition"; -import stacking from "./examples/stacking"; -import HeaderDescriptionCell from "@/common/HeaderDescriptionCell"; - -const sections = [ - { - title: "Usage", - content: ( - - - Switch toggles should be used in place of radio buttons whenever the options are opposites of each other (i.e. - yes/no, on/off, etc). - - Whenever is possible stack the switch component vertically. - - Switches have immediate effect over the application, changing preferences and configuration settings. - Don't use a submit button. - - - ), - }, - { - title: "Stacking", - content: ( - <> - - - In some application the use of several switches based on the requirements could appear, that why we provide - some indications in the case that the user needs to use stacked switches. - - - ), - }, - { - title: "Label position", - content: ( - <> - - - - - Position - Description - - - - - - Label before - - Labels before the switch indicate what the switch is for - - - - Label after - - Labels after the switch indicate the state of the switch - - - - - ), - }, -]; - -const SwitchUsagePage = () => { - return ( - - - - - - - ); -}; - -export default SwitchUsagePage; diff --git a/apps/website/screens/components/switch/usage/examples/labelPosition.ts b/apps/website/screens/components/switch/usage/examples/labelPosition.ts deleted file mode 100644 index 531405b760..0000000000 --- a/apps/website/screens/components/switch/usage/examples/labelPosition.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { DxcSwitch, DxcInset, DxcFlex, DxcHeading } from "@dxc-technology/halstack-react"; - -const code = `() => { - return ( - - - - - - - - - - - - - ); -}`; - -const scope = { - DxcSwitch, - DxcInset, - DxcFlex, - DxcHeading, -}; - -export default { code, scope }; diff --git a/apps/website/screens/components/switch/usage/examples/stacking.ts b/apps/website/screens/components/switch/usage/examples/stacking.ts deleted file mode 100644 index 20ef982e66..0000000000 --- a/apps/website/screens/components/switch/usage/examples/stacking.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { DxcSwitch, DxcInset, DxcFlex, DxcHeading } from "@dxc-technology/halstack-react"; - -const code = `() => { - return ( - - - - - - - - - - - - - - - - - - - ); -}`; - -const scope = { - DxcSwitch, - DxcInset, - DxcFlex, - DxcHeading, -}; - -export default { code, scope }; diff --git a/packages/lib/src/switch/Switch.stories.tsx b/packages/lib/src/switch/Switch.stories.tsx index 097a01c378..7eaa4cca8e 100644 --- a/packages/lib/src/switch/Switch.stories.tsx +++ b/packages/lib/src/switch/Switch.stories.tsx @@ -3,7 +3,6 @@ import ExampleContainer from "../../.storybook/components/ExampleContainer"; import Title from "../../.storybook/components/Title"; import preview from "../../.storybook/preview"; import { disabledRules } from "../../test/accessibility/rules/specific/switch/disabledRules"; -import { HalstackProvider } from "../HalstackContext"; import DxcSwitch from "./Switch"; export default { @@ -21,13 +20,6 @@ export default { }, } as Meta; -const opinionatedTheme = { - switch: { - checkedBaseColor: "#5f249f", - fontColor: "#000000", - }, -}; - const Switch = () => ( <> @@ -38,7 +30,7 @@ const Switch = () => ( <DxcSwitch /> </ExampleContainer> - <ExampleContainer pseudoState="pseudo-focus-visible"> + <ExampleContainer pseudoState="pseudo-focus"> <Title title="Focused" theme="light" level={4} /> <DxcSwitch label="Switch" labelPosition="after" /> </ExampleContainer> @@ -97,15 +89,16 @@ const Switch = () => ( <DxcSwitch label="Small" size="small" /> </ExampleContainer> <ExampleContainer> - <Title title="Medium size (with large label)" theme="light" level={4} /> + <Title title="Medium size (with long label)" theme="light" level={4} /> <DxcSwitch label="Very very very large label or even huge" size="medium" /> </ExampleContainer> <ExampleContainer> - <Title title="Medium size (with long label)" theme="light" level={4} /> + <Title title="Medium size (with long label + optional label)" theme="light" level={4} /> <DxcSwitch label="Large texttttttttttttttttttttttttttttttttttttttttttttttttttttttttttttt" labelPosition="after" size="medium" + optional /> </ExampleContainer> <ExampleContainer> @@ -120,31 +113,6 @@ const Switch = () => ( <Title title="FitContent size" theme="light" level={4} /> <DxcSwitch label="FitContent" size="fitContent" /> </ExampleContainer> - <Title title="Opinionated theme" theme="light" level={2} /> - <ExampleContainer> - <Title title="Checked" theme="light" level={4} /> - <HalstackProvider theme={opinionatedTheme}> - <DxcSwitch label="Switch" defaultChecked /> - </HalstackProvider> - </ExampleContainer> - <ExampleContainer> - <Title title="Default" theme="light" level={4} /> - <HalstackProvider theme={opinionatedTheme}> - <DxcSwitch label="Switch" /> - </HalstackProvider> - </ExampleContainer> - <ExampleContainer> - <Title title="Disabled" theme="light" level={4} /> - <HalstackProvider theme={opinionatedTheme}> - <DxcSwitch label="Switch" disabled /> - </HalstackProvider> - </ExampleContainer> - <ExampleContainer> - <Title title="Disabled checked" theme="light" level={4} /> - <HalstackProvider theme={opinionatedTheme}> - <DxcSwitch label="Switch" disabled defaultChecked /> - </HalstackProvider> - </ExampleContainer> </> ); diff --git a/packages/lib/src/switch/Switch.test.tsx b/packages/lib/src/switch/Switch.test.tsx index 16079f5f24..23fbfd0c34 100644 --- a/packages/lib/src/switch/Switch.test.tsx +++ b/packages/lib/src/switch/Switch.test.tsx @@ -9,14 +9,12 @@ describe("Switch component tests", () => { const { getByText } = render(<DxcSwitch label="SwitchComponent" checked={false} onChange={onChange} />); expect(getByText("SwitchComponent")).toBeTruthy(); }); - test("Calls correct function on click", () => { const onChange = jest.fn(); const { getByText } = render(<DxcSwitch label="SwitchComponent" checked={false} onChange={onChange} />); fireEvent.click(getByText("SwitchComponent")); expect(onChange).toHaveBeenCalled(); }); - test("Calls correct function on key down", () => { const onChange = jest.fn(); const { getByText } = render(<DxcSwitch label="SwitchComponent" checked={false} onChange={onChange} />); @@ -26,8 +24,7 @@ describe("Switch component tests", () => { fireEvent.keyDown(getByText("SwitchComponent"), { key: " " }); expect(onChange).toHaveBeenCalled(); }); - - test("Everytime the user clicks the component the onchange function is called with the correct value CONTROLLED COMPONENT", () => { + test("Every time the user clicks the component the onchange function is called with the correct value CONTROLLED COMPONENT", () => { const onChange = jest.fn(); const { getByText } = render(<DxcSwitch label="SwitchComponent" checked={false} onChange={onChange} />); fireEvent.click(getByText("SwitchComponent")); @@ -35,8 +32,7 @@ describe("Switch component tests", () => { expect(onChange.mock.calls[0][0]).toBe(true); expect(onChange.mock.calls[1][0]).toBe(true); }); - - test("Everytime the user use enter in the component, the onchange function is called with the correct value CONTROLLED COMPONENT", () => { + test("Every time the user use enter in the component, the onchange function is called with the correct value CONTROLLED COMPONENT", () => { const onChange = jest.fn(); const { getByText } = render(<DxcSwitch label="SwitchComponent" checked={false} onChange={onChange} />); fireEvent.focus(getByText("SwitchComponent")); @@ -45,8 +41,7 @@ describe("Switch component tests", () => { expect(onChange.mock.calls[0][0]).toBe(true); expect(onChange.mock.calls[1][0]).toBe(true); }); - - test("Everytime the user use space in the component, the onchange function is called with the correct value CONTROLLED COMPONENT", () => { + test("Every time the user use space in the component, the onchange function is called with the correct value CONTROLLED COMPONENT", () => { const onChange = jest.fn(); const { getByText } = render(<DxcSwitch label="SwitchComponent" checked={false} onChange={onChange} />); fireEvent.focus(getByText("SwitchComponent")); @@ -55,8 +50,7 @@ describe("Switch component tests", () => { expect(onChange.mock.calls[0][0]).toBe(true); expect(onChange.mock.calls[1][0]).toBe(true); }); - - test("Everytime the user clicks the component the onchange function is called with the correct value UNCONTROLLED COMPONENT", () => { + test("Every time the user clicks the component the onchange function is called with the correct value UNCONTROLLED COMPONENT", () => { const onChange = jest.fn(); const { getByText } = render(<DxcSwitch label="SwitchComponent" onChange={onChange} />); fireEvent.click(getByText("SwitchComponent")); @@ -64,8 +58,7 @@ describe("Switch component tests", () => { expect(onChange.mock.calls[0][0]).toBe(true); expect(onChange.mock.calls[1][0]).toBe(false); }); - - test("Everytime the user use enter in the component, the onchange function is called with the correct value UNCONTROLLED COMPONENT", () => { + test("Every time the user use enter in the component, the onchange function is called with the correct value UNCONTROLLED COMPONENT", () => { const onChange = jest.fn(); const { getByText } = render(<DxcSwitch label="SwitchComponent" onChange={onChange} />); fireEvent.focus(getByText("SwitchComponent")); @@ -74,8 +67,7 @@ describe("Switch component tests", () => { expect(onChange.mock.calls[0][0]).toBe(true); expect(onChange.mock.calls[1][0]).toBe(false); }); - - test("Everytime the user use space in the component, the onchange function is called with the correct value UNCONTROLLED COMPONENT", () => { + test("Every time the user use space in the component, the onchange function is called with the correct value UNCONTROLLED COMPONENT", () => { const onChange = jest.fn(); const { getByText } = render(<DxcSwitch label="SwitchComponent" onChange={onChange} />); fireEvent.focus(getByText("SwitchComponent")); @@ -84,7 +76,6 @@ describe("Switch component tests", () => { expect(onChange.mock.calls[0][0]).toBe(true); expect(onChange.mock.calls[1][0]).toBe(false); }); - test("Renders with correct initial value and initial state when it is uncontrolled", () => { const component = render( <DxcSwitch label="Default label" defaultChecked value="test-defaultChecked" name="test" /> @@ -94,28 +85,23 @@ describe("Switch component tests", () => { expect(inputEl?.value).toBe("test-defaultChecked"); expect(switchEl.getAttribute("aria-checked")).toBe("true"); }); - test("Renders with correct aria attributes", () => { - const { getByText, getByRole } = render(<DxcSwitch label="Default label" />); + const { getByRole } = render(<DxcSwitch label="Default label" />); const switchEl = getByRole("switch"); - const label = getByText("Default label"); - expect(switchEl.getAttribute("aria-labelledby")).toBe(label.id); expect(switchEl.getAttribute("aria-checked")).toBe("false"); expect(switchEl.getAttribute("aria-label")).toBeNull(); + expect(switchEl.getAttribute("aria-disabled")).toBeNull(); }); - test("Renders with correct aria-label", () => { const { getByRole } = render(<DxcSwitch ariaLabel="Example aria label" />); const switchEl = getByRole("switch"); expect(switchEl.getAttribute("aria-label")).toBe("Example aria label"); }); - test("Renders disabled switch correctly", () => { - const { getByText, getByRole } = render(<DxcSwitch label="Default label" disabled />); + const { getByRole } = render(<DxcSwitch label="Default label" disabled />); const switchEl = getByRole("switch"); - const label = getByText("Default label"); - expect(switchEl.getAttribute("aria-labelledby")).toBe(label.id); expect(switchEl.getAttribute("aria-checked")).toBe("false"); + expect(switchEl.getAttribute("aria-label")).toBeNull(); expect(switchEl.getAttribute("aria-disabled")).toBe("true"); }); }); diff --git a/packages/lib/src/switch/Switch.tsx b/packages/lib/src/switch/Switch.tsx index 67464536c4..148cd6b581 100644 --- a/packages/lib/src/switch/Switch.tsx +++ b/packages/lib/src/switch/Switch.tsx @@ -1,105 +1,10 @@ -import { forwardRef, KeyboardEvent, useContext, useId, useRef, useState } from "react"; -import styled, { ThemeProvider } from "styled-components"; -import { AdvancedTheme, spaces } from "../common/variables"; +import { forwardRef, KeyboardEvent, useContext, useState } from "react"; +import styled from "styled-components"; +import { spaces } from "../common/variables"; import { getMargin } from "../common/utils"; -import HalstackContext, { HalstackLanguageContext } from "../HalstackContext"; +import { HalstackLanguageContext } from "../HalstackContext"; import SwitchPropsType, { RefType } from "./types"; -const DxcSwitch = forwardRef<RefType, SwitchPropsType>( - ( - { - defaultChecked = false, - checked, - value, - label = "", - labelPosition = "before", - name = "", - disabled = false, - optional = false, - onChange, - margin, - size = "fitContent", - tabIndex = 0, - ariaLabel = "Switch", - }, - ref - ): JSX.Element => { - const switchId = `switch-${useId()}`; - const labelId = `label-${switchId}`; - const [innerChecked, setInnerChecked] = useState(defaultChecked); - - const colorsTheme = useContext(HalstackContext); - const translatedLabels = useContext(HalstackLanguageContext); - const refTrack = useRef<HTMLSpanElement | null>(null); - - const handleOnKeyDown = (event: KeyboardEvent<HTMLDivElement>) => { - switch (event.key) { - case "Enter": - case " ": - event.preventDefault(); - refTrack.current?.focus(); - setInnerChecked(!(checked ?? innerChecked)); - onChange?.(!(checked ?? innerChecked)); - break; - default: - break; - } - }; - - const handlerSwitchChange = () => { - if (checked == null) { - setInnerChecked((currentInnerChecked) => !currentInnerChecked); - } - onChange?.(checked ? !checked : !innerChecked); - }; - - return ( - <ThemeProvider theme={colorsTheme.switch}> - <SwitchContainer - margin={margin} - size={size} - onKeyDown={handleOnKeyDown} - disabled={disabled} - onClick={!disabled ? handlerSwitchChange : undefined} - ref={ref} - > - {labelPosition === "before" && label && ( - <LabelContainer id={labelId} labelPosition={labelPosition} disabled={disabled} label={label}> - {label} {optional && <>{translatedLabels.formFields.optionalLabel}</>} - </LabelContainer> - )} - <ValueInput - type="checkbox" - name={name} - aria-hidden - value={value} - disabled={disabled} - checked={checked ?? innerChecked} - readOnly - /> - <SwitchBase> - <SwitchTrack - role="switch" - aria-checked={checked ?? innerChecked} - aria-disabled={disabled} - disabled={disabled} - aria-labelledby={labelId} - aria-label={label ? undefined : ariaLabel} - tabIndex={!disabled ? tabIndex : -1} - ref={refTrack} - /> - </SwitchBase> - {labelPosition === "after" && label && ( - <LabelContainer id={labelId} labelPosition={labelPosition} disabled={disabled} label={label}> - {optional && <>{translatedLabels.formFields.optionalLabel}</>} {label} - </LabelContainer> - )} - </SwitchContainer> - </ThemeProvider> - ); - } -); - const sizes = { small: "60px", medium: "240px", @@ -112,187 +17,176 @@ const calculateWidth = (margin: SwitchPropsType["margin"], size: SwitchPropsType size === "fillParent" ? `calc(${sizes[size]} - ${getMargin(margin, "left")} - ${getMargin(margin, "right")})` : size && sizes[size]; - -const getDisabledColor = ( - theme: AdvancedTheme["switch"], - element: "track" | "thumb" | "label", - subElement?: "check" | "uncheck" -) => { - switch (element) { - case "track": - switch (subElement) { - case "check": - return theme.disabledCheckedTrackBackgroundColor; - case "uncheck": - return theme.disabledUncheckedTrackBackgroundColor; - default: - return undefined; - } - case "thumb": - switch (subElement) { - case "check": - return theme.disabledCheckedThumbBackgroundColor; - case "uncheck": - return theme.disabledUncheckedThumbBackgroundColor; - default: - return undefined; - } - case "label": - return theme.disabledLabelFontColor; - default: - return undefined; - } -}; - -const getNotDisabledColor = ( - theme: AdvancedTheme["switch"], - element: "track" | "thumb" | "label", - subElement?: "check" | "uncheck" -) => { - switch (element) { - case "track": - switch (subElement) { - case "check": - return theme.checkedTrackBackgroundColor; - case "uncheck": - return theme.uncheckedTrackBackgroundColor; - default: - return undefined; - } - break; - case "thumb": - switch (subElement) { - case "check": - return theme.checkedThumbBackgroundColor; - case "uncheck": - return theme.uncheckedThumbBackgroundColor; - default: - return undefined; - } - break; - case "label": - return theme.labelFontColor; - default: - return undefined; - } -}; + +const getTrackColor = (checked: SwitchPropsType["checked"], disabled: SwitchPropsType["disabled"]) => + disabled + ? checked + ? "var(--color-bg-primary-lighter)" + : "var(--color-bg-neutral-light)" + : checked + ? "var(--color-bg-primary-strong)" + : "var(--color-bg-neutral-strong)"; const SwitchContainer = styled.div<{ + disabled: SwitchPropsType["disabled"]; + labelPosition: SwitchPropsType["labelPosition"]; margin: SwitchPropsType["margin"]; size: SwitchPropsType["size"]; - disabled: SwitchPropsType["disabled"]; }>` - display: inline-flex; - align-items: center; - width: ${(props) => calculateWidth(props.margin, props.size)}; - height: 40px; - cursor: ${(props) => (props.disabled === true ? "not-allowed" : "pointer")}; - - 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] : ""}; + display: inline-grid; + grid-template-columns: ${({ labelPosition }) => + labelPosition === "after" ? "52px minmax(0, max-content)" : "minmax(0, max-content) 52px"}; + place-items: center; + gap: var(--spacing-gap-m); + width: ${({ margin, size }) => calculateWidth(margin, size)}; + height: var(--height-m); + margin: ${({ margin }) => (margin && typeof margin !== "object" ? spaces[margin] : "")}; + 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] : "")}; + cursor: ${({ disabled }) => (disabled ? "not-allowed" : "pointer")}; + + &:focus { + outline: none; + /* Thumb focus */ + &:not([aria-disabled="true"]) { + > span::before { + outline: var(--border-width-m) var(--border-style-default) var(--border-color-secondary-medium); + outline-offset: var(--spacing-padding-xxxs); + } + } + } `; const LabelContainer = styled.span<{ - labelPosition: SwitchPropsType["labelPosition"]; disabled: SwitchPropsType["disabled"]; - label: SwitchPropsType["label"]; + labelPosition: SwitchPropsType["labelPosition"]; }>` + display: flex; + gap: var(--spacing-gap-xs); + color: ${({ disabled }) => (disabled ? "var(--color-fg-neutral-medium)" : "var(--color-fg-neutral-dark)")}; + font-family: var(--typography-font-family); + font-size: var(--typography-label-m); + font-weight: var(--typography-label-regular); + max-width: 100%; + order: ${({ labelPosition }) => (labelPosition === "before" ? 0 : 1)}; +`; + +const Label = styled.span` overflow: hidden; text-overflow: ellipsis; white-space: nowrap; - color: ${(props) => - props.disabled ? getDisabledColor(props.theme, "label") : getNotDisabledColor(props.theme, "label")}; - opacity: 1; - font-family: ${(props) => props.theme.labelFontFamily}; - font-size: ${(props) => props.theme.labelFontSize}; - font-style: ${(props) => (props.disabled ? props.theme.disabledLabelFontStyle : props.theme.labelFontStyle)}; - font-weight: ${(props) => props.theme.labelFontWeight}; - - ${(props) => - !props.label - ? "margin: 0px;" - : props.labelPosition === "after" - ? `margin-left: ${props.theme.spaceBetweenLabelSwitch};` - : `margin-right: ${props.theme.spaceBetweenLabelSwitch};`}; - - ${(props) => props.labelPosition === "before" && "order: -1"} `; -const SwitchBase = styled.label` - display: flex; - align-items: center; - justify-content: space-between; - cursor: pointer; - margin: 0px 12px; -`; - -const ValueInput = styled.input` - display: none; +const OptionalLabel = styled.span<{ + disabled: SwitchPropsType["disabled"]; +}>` + ${({ disabled }) => !disabled && "color: var(--color-fg-neutral-stronger);"} `; -const SwitchTrack = styled.span<{ disabled: SwitchPropsType["disabled"] }>` - border-radius: 15px; - width: ${(props) => props.theme.trackWidth}; - height: ${(props) => props.theme.trackHeight}; +const Switch = styled.span<{ checked: SwitchPropsType["checked"]; disabled: SwitchPropsType["disabled"] }>` position: relative; - cursor: ${(props) => (props.disabled ? "not-allowed" : "pointer")}; + width: 36px; + height: var(--height-xxxs); + border-radius: var(--border-radius-xl); + background-color: ${({ checked, disabled }) => getTrackColor(checked, disabled)}; + transition: background-color 0.2s ease-in-out; /* Background color transition */ - &:focus-visible { - outline: none; - ::before { - outline: ${(props) => `${props.theme.thumbFocusColor} solid 2px`}; - outline-offset: 6px; - } - } - - /* Thumb element */ + /* Thumb */ ::before { content: ""; - transform: initial; position: absolute; - width: ${(props) => props.theme.thumbWidth}; - height: ${(props) => props.theme.thumbHeight}; - border-radius: 50%; - box-shadow: - 0px 2px 1px -1px rgb(0 0 0 / 20%), - 0px 1px 1px 0px rgb(0 0 0 / 14%), - 0px 1px 3px 0px rgb(0 0 0 / 12%); bottom: -6px; left: -4px; - transform: translateX(0px); - background-color: ${(props) => - props.disabled - ? getDisabledColor(props.theme, "thumb", "uncheck") - : getNotDisabledColor(props.theme, "thumb", "uncheck")}; + width: 24px; + height: var(--height-s); + background-color: var(--color-fg-neutral-bright); + border-radius: 50%; + box-shadow: var(--shadow-low-x-position) var(--shadow-low-y-position) var(--shadow-low-blur) + var(--shadow-low-spread) var(--shadow-dark); + transform: ${({ checked }) => checked && "translateX(20px)"}; + transition: transform 0.2s ease-in-out; /* Thumb transform transition */ } +`; - /* Unchecked */ - background-color: ${(props) => - props.disabled - ? getDisabledColor(props.theme, "track", "uncheck") - : getNotDisabledColor(props.theme, "track", "uncheck")}; +const DxcSwitch = forwardRef<RefType, SwitchPropsType>( + ( + { + ariaLabel = "Switch", + checked, + defaultChecked = false, + disabled, + label, + labelPosition = "before", + margin, + name, + onChange, + optional, + size = "fitContent", + tabIndex = 0, + value, + }, + ref + ) => { + const [innerChecked, setInnerChecked] = useState(defaultChecked); + const translatedLabels = useContext(HalstackLanguageContext); - /* Checked */ - &[aria-checked="true"] { - background-color: ${(props) => - props.disabled - ? getDisabledColor(props.theme, "track", "check") - : getNotDisabledColor(props.theme, "track", "check")}; - ::before { - transform: translateX(${(props) => props.theme.thumbShift}); - background-color: ${(props) => - props.disabled - ? getDisabledColor(props.theme, "thumb", "check") - : getNotDisabledColor(props.theme, "thumb", "check")}; - } + const handleOnChange = () => { + if (checked == null) setInnerChecked((currentInnerChecked) => !currentInnerChecked); + onChange?.(!(checked ?? innerChecked)); + }; + + const handleOnKeyDown = (event: KeyboardEvent<HTMLDivElement>) => { + switch (event.key) { + case "Enter": + case " ": + event.preventDefault(); + setInnerChecked(!(checked ?? innerChecked)); + onChange?.(!(checked ?? innerChecked)); + break; + default: + break; + } + }; + + return ( + <SwitchContainer + aria-checked={checked ?? innerChecked} + aria-disabled={disabled} + aria-label={label ? undefined : ariaLabel} + disabled={disabled} + labelPosition={labelPosition} + margin={margin} + onClick={!disabled ? handleOnChange : undefined} + onKeyDown={!disabled ? handleOnKeyDown : undefined} + ref={ref} + role="switch" + size={size} + tabIndex={disabled ? -1 : tabIndex} + > + {label && ( + <LabelContainer disabled={disabled} labelPosition={labelPosition}> + <Label>{label}</Label> + {optional && <OptionalLabel disabled={disabled}>{translatedLabels.formFields.optionalLabel}</OptionalLabel>} + </LabelContainer> + )} + <Switch checked={checked ?? innerChecked} disabled={disabled} /> + <input + aria-hidden + checked={checked ?? innerChecked} + disabled={disabled} + name={name} + readOnly + role="switch" + style={{ display: "none" }} + type="checkbox" + value={value} + /> + </SwitchContainer> + ); } -`; +); export default DxcSwitch; diff --git a/packages/lib/src/switch/types.ts b/packages/lib/src/switch/types.ts index 1253a0f988..e4ceed7339 100644 --- a/packages/lib/src/switch/types.ts +++ b/packages/lib/src/switch/types.ts @@ -2,19 +2,22 @@ import { Margin, Space } from "../common/utils"; type Props = { /** - * Initial state of the switch, only when it is uncontrolled. + * Specifies a string to be used as the name for the switch element when no `label` is provided. */ - defaultChecked?: boolean; + ariaLabel?: string; /** * If true, the component is checked. If undefined, the component will be uncontrolled * and the checked attribute will be managed internally by the component. */ checked?: boolean; /** - * Will be passed to the value attribute of the html input element. When inside a form, - * this value will be only submitted if the switch is checked. + * Initial state of the switch, only when it is uncontrolled. */ - value?: string; + defaultChecked?: boolean; + /** + * If true, the component will be disabled. + */ + disabled?: boolean; /** * Text to be placed next to the switch. */ @@ -24,13 +27,14 @@ type Props = { */ labelPosition?: "before" | "after"; /** - * Name attribute of the input element. + * 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. */ - name?: string; + margin?: Space | Margin; /** - * If true, the component will be disabled. + * Name attribute of the input element. */ - disabled?: boolean; + name?: string; /** * This function will be called when the user changes the state of the switch. * The new value of the checked attribute will be passed as a parameter. @@ -40,11 +44,6 @@ type Props = { * If true, the component will display '(Optional)' next to the label. */ optional?: boolean; - /** - * 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. - */ - margin?: Space | Margin; /** * Size of the component. */ @@ -54,9 +53,10 @@ type Props = { */ tabIndex?: number; /** - * Specifies a string to be used as the name for the switch element when no `label` is provided. + * Will be passed to the value attribute of the html input element. When inside a form, + * this value will be only submitted if the switch is checked. */ - ariaLabel?: string; + value?: string; }; /**