diff --git a/apps/website/pages/components/checkbox/code.tsx b/apps/website/pages/components/checkbox/code.tsx new file mode 100644 index 0000000000..5041cb1fde --- /dev/null +++ b/apps/website/pages/components/checkbox/code.tsx @@ -0,0 +1,17 @@ +import Head from "next/head"; +import type { ReactElement } from "react"; +import CheckboxPageLayout from "screens/components/checkbox/CheckboxPageLayout"; +import CheckboxCodePage from "screens/components/checkbox/code/CheckboxCodePage"; + +const Code = () => ( + <> + + Checkbox code — Halstack Design System + + + +); + +Code.getLayout = (page: ReactElement) => {page}; + +export default Code; diff --git a/apps/website/pages/components/checkbox/index.tsx b/apps/website/pages/components/checkbox/index.tsx index 0d1de85a5f..60b254d22c 100644 --- a/apps/website/pages/components/checkbox/index.tsx +++ b/apps/website/pages/components/checkbox/index.tsx @@ -1,21 +1,17 @@ import Head from "next/head"; import type { ReactElement } from "react"; -import CheckboxCodePage from "screens/components/checkbox/code/CheckboxCodePage"; +import CheckboxOverviewPage from "screens/components/checkbox/overview/CheckboxOverviewPage"; import CheckboxPageLayout from "screens/components/checkbox/CheckboxPageLayout"; -const Usage = () => { - return ( - <> - - Checkbox — Halstack Design System - - - - ); -}; +const Index = () => ( + <> + + Checkbox — Halstack Design System + + + +); -Usage.getLayout = function getLayout(page: ReactElement) { - return {page}; -}; +Index.getLayout = (page: ReactElement) => {page}; -export default Usage; +export default Index; diff --git a/apps/website/pages/components/checkbox/specifications.tsx b/apps/website/pages/components/checkbox/specifications.tsx deleted file mode 100644 index b131a0af96..0000000000 --- a/apps/website/pages/components/checkbox/specifications.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import Head from "next/head"; -import type { ReactElement } from "react"; -import CheckboxSpecsPage from "screens/components/checkbox/specs/CheckboxSpecsPage"; -import CheckboxPageLayout from "screens/components/checkbox/CheckboxPageLayout"; - -const Specifications = () => { - return ( - <> - - Checkbox Specs — Halstack Design System - - - - ); -}; - -Specifications.getLayout = function getLayout(page: ReactElement) { - return {page}; -}; - -export default Specifications; diff --git a/apps/website/pages/components/checkbox/usage.tsx b/apps/website/pages/components/checkbox/usage.tsx deleted file mode 100644 index 1052a89dd2..0000000000 --- a/apps/website/pages/components/checkbox/usage.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import Head from "next/head"; -import type { ReactElement } from "react"; -import CheckboxPageLayout from "screens/components/checkbox/CheckboxPageLayout"; -import CheckboxUsagePage from "screens/components/checkbox/usage/CheckboxUsagePage"; - -const Usage = () => { - return ( - <> - - Checkbox Usage — Halstack Design System - - - - ); -}; - -Usage.getLayout = function getLayout(page: ReactElement) { - return {page}; -}; - -export default Usage; diff --git a/apps/website/screens/components/checkbox/CheckboxPageLayout.tsx b/apps/website/screens/components/checkbox/CheckboxPageLayout.tsx index 7044bc1192..af9d9ad8c5 100644 --- a/apps/website/screens/components/checkbox/CheckboxPageLayout.tsx +++ b/apps/website/screens/components/checkbox/CheckboxPageLayout.tsx @@ -6,9 +6,8 @@ import { ReactNode } from "react"; const CheckboxPageHeading = ({ children }: { children: ReactNode }) => { const tabs = [ - { label: "Code", path: "/components/checkbox" }, - { label: "Usage", path: "/components/checkbox/usage" }, - { label: "Specifications", path: "/components/checkbox/specifications" }, + { label: "Overview", path: "/components/checkbox" }, + { label: "Code", path: "/components/checkbox/code" }, ]; return ( @@ -17,10 +16,9 @@ const CheckboxPageHeading = ({ children }: { children: ReactNode }) => { - Checkboxes are inputs that offer to the user the possibility to select one or more options from a range of - attributes. + Checkboxes are inputs that allow the user to select one or more options from a range of attributes. - + {children} diff --git a/apps/website/screens/components/checkbox/code/CheckboxCodePage.tsx b/apps/website/screens/components/checkbox/code/CheckboxCodePage.tsx index 71d37db836..ce33c37237 100644 --- a/apps/website/screens/components/checkbox/code/CheckboxCodePage.tsx +++ b/apps/website/screens/components/checkbox/code/CheckboxCodePage.tsx @@ -23,13 +23,15 @@ const sections = [ - defaultChecked + ariaLabel - boolean + string - Initial state of the checkbox, only when it is uncontrolled. - false + Specifies a string to be used as the name for the checkbox element when no label is provided. + + + 'Checkbox' @@ -41,20 +43,27 @@ const sections = [ If true, the component is checked. If undefined the component will be uncontrolled and the value will be managed internally by the component. + - + + + defaultChecked + + boolean + + Initial state of the checkbox, only when it is uncontrolled. - - + false - value + disabled - string + boolean + If true, the component will be disabled. - Will be passed to the value attribute of the HTML input element. When inside a form, this - value will be only submitted if the checkbox is checked. + false - - label @@ -74,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 @@ -83,14 +103,15 @@ const sections = [ - - disabled + onChange - boolean + {"(value: boolean) => void"} - If true, the component will be disabled. - false + This function will be called when the user clicks the checkbox. The new value will be passed as a + parameter. + - optional @@ -113,25 +134,11 @@ const sections = [ - onChange - - {"(value: boolean) => void"} - - - This function will be called when the user clicks the checkbox. The new value 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. - @@ -157,22 +164,15 @@ const sections = [ - ref - - {"React.Ref"} - - Reference to the component. - - - - - ariaLabel + value string - Specifies a string to be used as the name for the checkbox 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 checkbox is checked. - 'Checkbox' + - @@ -193,15 +193,13 @@ const sections = [ }, ]; -const CheckboxCodePage = () => { - return ( - - - - - - - ); -}; +const CheckboxCodePage = () => ( + + + + + + +); export default CheckboxCodePage; diff --git a/apps/website/screens/components/checkbox/overview/CheckboxOverviewPage.tsx b/apps/website/screens/components/checkbox/overview/CheckboxOverviewPage.tsx new file mode 100644 index 0000000000..b525ce0e1a --- /dev/null +++ b/apps/website/screens/components/checkbox/overview/CheckboxOverviewPage.tsx @@ -0,0 +1,126 @@ +import { DxcParagraph, DxcBulletedList, DxcFlex, DxcTable } from "@dxc-technology/halstack-react"; +import QuickNavContainerLayout from "@/common/QuickNavContainerLayout"; +import QuickNavContainer from "@/common/QuickNavContainer"; +import DocFooter from "@/common/DocFooter"; +import Example from "@/common/example/Example"; +import stacking from "./examples/stacking"; +import Image from "@/common/Image"; +import anatomy from "./images/checkbox_anatomy.png"; + +const sections = [ + { + title: "Introduction", + content: ( + <> + + Checkboxes support different states, including checked, unchecked, and indeterminate, providing clear visual + feedback. Checkboxes should be used when multiple selections are needed, unlike radio + buttons, which are for single-choice scenarios. Proper spacing and alignment help maintain clarity, and labels + should be concise and descriptive to enhance usability. + + + ), + }, + { + title: "Anatomy", + content: ( + <> + Checkbox's anatomy + + + Checkbox input: the interactive element that allows users to toggle between checked, + unchecked, and indeterminate states. It provides visual feedback based on user selection and supports + accessibility attributes for better usability. + + + Label: descriptive text associated with the checkbox, helping users understand the option + they select. It should be concise and placed close to the checkbox for clear association. + + + + ), + }, + { + title: "Stacking checkboxes", + content: ( + <> + Checkboxes can be stacked vertically or horizontally, depending on the use case. + + + ), + subSections: [ + { + 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. + + ), + }, + { + 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. + + ), + }, + ], + }, + { + title: "Best practices", + content: ( + <> + + + Use for multiple selections: checkboxes should be used when users can select multiple + options independently. If only one selection is allowed, use radio buttons instead. + + + Ensure clear labels: each checkbox should have a clear, concise label that accurately + describes the option. Avoid ambiguous wording that might confuse users. + + + Group related options: when checkboxes are part of a related set, use a group label to + provide context. This improves readability and helps users understand the available choices. + + + Prioritize vertical stacking: for better readability and usability, stack checkboxes + vertically, especially when dealing with multiple options. Horizontal stacking should be reserved for short + lists with clear, non-wrapping labels. + + + Use the indeterminate state properly: the indeterminate state should only be used when a + parent checkbox controls multiple sub-options. This visually indicates that some but not all child options + are selected. + + + Maintain sufficient spacing: provide adequate spacing between checkboxes to prevent + misclicks and ensure a clean, organized layout. + + + Ensure accessibility: make checkboxes large enough to be easily clickable and ensure they + are keyboard-navigable. Labels should be linked correctly for screen readers to interpret them properly. + + + + ), + }, +]; + +const CheckboxInputOverviewPage = () => ( + + + + + + +); + +export default CheckboxInputOverviewPage; diff --git a/apps/website/screens/components/checkbox/usage/examples/stacking.ts b/apps/website/screens/components/checkbox/overview/examples/stacking.ts similarity index 100% rename from apps/website/screens/components/checkbox/usage/examples/stacking.ts rename to apps/website/screens/components/checkbox/overview/examples/stacking.ts diff --git a/apps/website/screens/components/checkbox/overview/images/checkbox_anatomy.png b/apps/website/screens/components/checkbox/overview/images/checkbox_anatomy.png new file mode 100644 index 0000000000..ff4051b3d0 Binary files /dev/null and b/apps/website/screens/components/checkbox/overview/images/checkbox_anatomy.png differ diff --git a/apps/website/screens/components/checkbox/specs/CheckboxSpecsPage.tsx b/apps/website/screens/components/checkbox/specs/CheckboxSpecsPage.tsx deleted file mode 100644 index 120d96e76f..0000000000 --- a/apps/website/screens/components/checkbox/specs/CheckboxSpecsPage.tsx +++ /dev/null @@ -1,497 +0,0 @@ -import { DxcParagraph, DxcBulletedList, DxcFlex, DxcTable, DxcLink } 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 DocFooter from "@/common/DocFooter"; -import Code from "@/common/Code"; -import checkboxAnatomy from "./images/checkbox_anatomy.png"; -import checkboxSpecs from "./images/checkbox_specs.png"; -import checkboxStates from "./images/checkbox_states.png"; - -const sections = [ - { - title: "Specifications", - content: ( -
- Checkbox design specifications -
- ), - }, - { - title: "States", - content: ( - <> - - The following states are defined in the life cycle of the component: unselected enabled,{" "} - unselected hover, unselected focus, unselected disabled,{" "} - selected enabled, selected hover, selected focus and{" "} - selected disabled. - -
- Checkbox states -
- - ), - }, - { - title: "Anatomy", - content: ( - <> - Checkbox anatomy - - Checkbox input - Label - - - ), - }, - { - title: "Design tokens", - subSections: [ - { - title: "Color", - content: ( - - - - Component token - Element - Core token - Value - - - - - - backgroundColorChecked - - Fill - - color-blue-800 - - #0067b3 - - - - hoverBackgroundColorChecked - - Fill:hover - - color-blue-900 - - #003c66 - - - - disabledBackgroundColorChecked - - Fill:disabled - - color-grey-500 - - #999999 - - - - readOnlyBackgroundColorChecked - - Fill:readonly - - color-grey-500 - - #999999 - - - - hoverReadOnlyBackgroundColorChecked - - Fill:readonly:hover - - color-grey-600 - - #808080 - - - - borderColor - - Border - - color-blue-800 - - #0067b3 - - - - hoverBorderColor - - Border:hover - - color-blue-900 - - #003c66 - - - - disabledBorderColor - - Border:disabled - - color-grey-500 - - #999999 - - - - readOnlyBorderColor - - Border:readonly - - color-grey-500 - - #999999 - - - - hoverReadOnlyBorderColor - - Border:readonly:hover - - color-grey-600 - - #808080 - - - - checkColor - - Check mark - - color-white - - #ffffff - - - - disabledCheckColor - - Check mark:disabled - - color-white - - #ffffff - - - - readOnlyCheckColor - - Check mark:readonly - - color-white - - #ffffff - - - - fontColor - - Label - - color-black - - #000000 - - - - disabledFontColor - - Label:disabled - - color-grey-500 - - #999999 - - - - focusColor - - Outline:focus - - color-blue-600 - - #0095ff - - - - ), - }, - { - title: "Spacing", - content: ( - - - - Component token - Element - Core token - Value - - - - - - inputMargin - - Checkbox input - - spacing-8 - - 0.5rem / 8px - - - - ), - }, - { - title: "Typography", - content: ( - - - - Component token - Element - Core token - Value - - - - - - fontFamily - - Label - - font-family-sans - - 'Open Sans', sans-serif - - - - fontSize - - Label - - font-scale-02 - - 0.875rem / 14px - - - - fontWeight - - Label - - font-weight-regular - - 400 - - - - ), - }, - { - title: "Border", - content: ( - - - - Property - Element - Core token - Value - - - - - - border-width - - Checkbox input - - border-width-2 - - 2px - - - - border-style - - Checkbox input - - border-style-solid - - solid - - - - border-radius - - Checkbox input - - border-radius-small - - 0.125rem / 2px - - - - border-width - - Focus border - - border-width-2 - - 2px - - - - border-style - - Focus border - - border-style-solid - - solid - - - - border-radius - - Focus border - - border-radius-medium - - 0.25rem / 4px - - - - ), - }, - { - title: "Margin", - content: ( - <> - - Margin can be set independently for top, right, bottom,{" "} - left. - - - - - Margin - Value - - - - - - xxsmall - - 6px - - - - xsmall - - 16px - - - - small - - 24px - - - - medium - - 36px - - - - large - - 48px - - - - xlarge - - 64px - - - - xxlarge - - 100px - - - - - ), - }, - ], - }, - { - title: "Accessibility", - subSections: [ - { - title: "WCAG 2.2", - content: ( - <> - - - Understanding WCAG 2.2 -{" "} - - SC 1.3.1: Info and Relationships - - - - Understanding WCAG 2.2 -{" "} - - SC 4.1.2: Name, Role, Value - - - - - ), - }, - { - title: "WAI-ARIA 1.2", - content: ( - - - WAI-ARIA Authoring Practices 1.2 -{" "} - - 3.7 Checkbox - - - - ), - }, - ], - }, -]; - -const CheckboxSpecsPage = () => { - return ( - - - - - - - ); -}; - -export default CheckboxSpecsPage; diff --git a/apps/website/screens/components/checkbox/specs/images/checkbox_anatomy.png b/apps/website/screens/components/checkbox/specs/images/checkbox_anatomy.png deleted file mode 100644 index 40c875b6fe..0000000000 Binary files a/apps/website/screens/components/checkbox/specs/images/checkbox_anatomy.png and /dev/null differ diff --git a/apps/website/screens/components/checkbox/specs/images/checkbox_specs.png b/apps/website/screens/components/checkbox/specs/images/checkbox_specs.png deleted file mode 100644 index b4872a6c99..0000000000 Binary files a/apps/website/screens/components/checkbox/specs/images/checkbox_specs.png and /dev/null differ diff --git a/apps/website/screens/components/checkbox/specs/images/checkbox_states.png b/apps/website/screens/components/checkbox/specs/images/checkbox_states.png deleted file mode 100644 index 505cc4e15b..0000000000 Binary files a/apps/website/screens/components/checkbox/specs/images/checkbox_states.png and /dev/null differ diff --git a/apps/website/screens/components/checkbox/usage/CheckboxUsagePage.tsx b/apps/website/screens/components/checkbox/usage/CheckboxUsagePage.tsx deleted file mode 100644 index e79be0fe46..0000000000 --- a/apps/website/screens/components/checkbox/usage/CheckboxUsagePage.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import { DxcParagraph, DxcBulletedList, DxcFlex, DxcTable } from "@dxc-technology/halstack-react"; -import QuickNavContainerLayout from "@/common/QuickNavContainerLayout"; -import QuickNavContainer from "@/common/QuickNavContainer"; -import DocFooter from "@/common/DocFooter"; -import Example from "@/common/example/Example"; -import stacking from "./examples/stacking"; - -const sections = [ - { - title: "Usage", - content: ( - <> - Use the checkbox when: - - Multiple choices offered. - Binary response are requested (yes/no). - Accepting conditions and additional features. - - - ), - }, - { - title: "Stacking", - content: ( - <> - Checkbox may be either vertically or horizontally stacked. - - - - - Type - Usage - - - - - - Vertical - - - Related checkboxes that belong to the same category. The horizontal spacing between horizontally stacked - checkboxes should be 8px. - - - - - Horizontal - - - Checkboxes are independent of a category*. The vertical spacing between stacked checkboxes should be - 32px. Don't stack more than 3 options - - - - - - ), - }, -]; - -const CheckboxInputUsagePage = () => { - return ( - - - - - - - ); -}; - -export default CheckboxInputUsagePage; diff --git a/packages/lib/src/checkbox/Checkbox.stories.tsx b/packages/lib/src/checkbox/Checkbox.stories.tsx index 3214db89de..307126e281 100644 --- a/packages/lib/src/checkbox/Checkbox.stories.tsx +++ b/packages/lib/src/checkbox/Checkbox.stories.tsx @@ -1,7 +1,6 @@ import styled from "styled-components"; import ExampleContainer from "../../.storybook/components/ExampleContainer"; import Title from "../../.storybook/components/Title"; -import { HalstackProvider } from "../HalstackContext"; import DxcCheckbox from "./Checkbox"; import { Meta, StoryObj } from "@storybook/react"; @@ -10,14 +9,6 @@ export default { component: DxcCheckbox, } as Meta; -const opinionatedTheme = { - checkbox: { - baseColor: "#0067b3", - checkColor: "#ffffff", - fontColor: "#000000", - }, -}; - const ScrollableContainer = styled.div` display: flex; flex-direction: column; @@ -39,6 +30,7 @@ const SmallContainer = styled.div` const Checkbox = () => ( <> + <ExampleContainer> <Title title="Default" theme="light" level={4} /> <DxcCheckbox label="Checkbox" /> @@ -47,6 +39,31 @@ const Checkbox = () => ( <Title title="Checked" theme="light" level={4} /> <DxcCheckbox label="Checkbox" defaultChecked /> </ExampleContainer> + <ExampleContainer pseudoState="pseudo-focus"> + <Title title="Focused" theme="light" level={4} /> + <DxcCheckbox label="Focused" /> + </ExampleContainer> + <ExampleContainer pseudoState="pseudo-hover"> + <Title title="Hovered" theme="light" level={4} /> + <DxcCheckbox label="Hovered" /> + </ExampleContainer> + <ExampleContainer pseudoState="pseudo-hover"> + <Title title="Hovered and checked" theme="light" level={4} /> + <DxcCheckbox label="Hovered" defaultChecked /> + </ExampleContainer> + <ExampleContainer pseudoState={["pseudo-focus", "pseudo-active"]}> + <Title title="Active" theme="light" level={4} /> + <DxcCheckbox label="Active" /> + </ExampleContainer> + <ExampleContainer pseudoState={["pseudo-focus", "pseudo-active"]}> + <Title title="Active and checked" theme="light" level={4} /> + <DxcCheckbox label="Active" defaultChecked /> + </ExampleContainer> + <ExampleContainer> + <Title title="Optional" theme="light" level={4} /> + <DxcCheckbox label="Checkbox" optional /> + </ExampleContainer> + <Title title="Disabled" theme="light" level={2} /> <ExampleContainer> <Title title="Disabled" theme="light" level={4} /> <DxcCheckbox label="Checkbox" disabled /> @@ -55,37 +72,38 @@ const Checkbox = () => ( <Title title="Disabled, checked and optional" theme="light" level={4} /> <DxcCheckbox label="Checkbox" disabled defaultChecked optional /> </ExampleContainer> + <Title title="Read only" theme="light" level={2} /> <ExampleContainer> <Title title="Read-only" theme="light" level={4} /> <DxcCheckbox label="Checkbox" readOnly /> </ExampleContainer> - <ExampleContainer pseudoState="pseudo-hover"> - <Title title="Hovered read-only" theme="light" level={4} /> - <DxcCheckbox label="Checkbox" readOnly /> - </ExampleContainer> <ExampleContainer> - <Title title="Read-only, checked and optional" theme="light" level={4} /> - <DxcCheckbox label="Checkbox" readOnly defaultChecked optional /> - </ExampleContainer> - <ExampleContainer pseudoState="pseudo-hover"> - <Title title="Hovered read-only and checked" theme="light" level={4} /> - <DxcCheckbox label="Checkbox" readOnly defaultChecked optional /> + <Title title="Read-only and optional" theme="light" level={4} /> + <DxcCheckbox label="Checkbox" readOnly optional /> </ExampleContainer> <ExampleContainer pseudoState="pseudo-focus"> - <Title title="Focused" theme="light" level={4} /> - <DxcCheckbox label="Focused" /> + <Title title="Read-only and focused" theme="light" level={4} /> + <DxcCheckbox label="Checkbox" readOnly optional /> + </ExampleContainer> + <ExampleContainer> + <Title title="Read-only and checked" theme="light" level={4} /> + <DxcCheckbox label="Checkbox" readOnly defaultChecked /> </ExampleContainer> <ExampleContainer pseudoState="pseudo-hover"> - <Title title="Hovered" theme="light" level={4} /> - <DxcCheckbox label="Hovered" /> + <Title title="Hovered and read-only" theme="light" level={4} /> + <DxcCheckbox label="Checkbox" readOnly /> + </ExampleContainer> + <ExampleContainer pseudoState={["pseudo-active", "pseudo-focus"]}> + <Title title="Active and read-only" theme="light" level={4} /> + <DxcCheckbox label="Checkbox" readOnly /> </ExampleContainer> <ExampleContainer pseudoState="pseudo-hover"> - <Title title="Hovered and checked" theme="light" level={4} /> - <DxcCheckbox label="Hovered" defaultChecked /> + <Title title="Hovered, read-only and checked" theme="light" level={4} /> + <DxcCheckbox label="Checkbox" readOnly defaultChecked /> </ExampleContainer> - <ExampleContainer> - <Title title="Optional" theme="light" level={4} /> - <DxcCheckbox label="Checkbox" optional /> + <ExampleContainer pseudoState={["pseudo-active", "pseudo-focus"]}> + <Title title="Active, read-only and checked" theme="light" level={4} /> + <DxcCheckbox label="Checkbox" readOnly defaultChecked /> </ExampleContainer> <ExampleContainer> <Title title="Label after" theme="light" level={4} /> @@ -168,49 +186,6 @@ const Checkbox = () => ( <DxcCheckbox label="Very long label to check its overflowing" labelPosition="after" /> </SmallContainer> </ExampleContainer> - <Title title="Opinionated theme" theme="light" level={2} /> - <ExampleContainer> - <Title title="Default" theme="light" level={4} /> - <HalstackProvider theme={opinionatedTheme}> - <DxcCheckbox label="Checkbox" /> - </HalstackProvider> - </ExampleContainer> - <ExampleContainer> - <Title title="Checked" theme="light" level={4} /> - <HalstackProvider theme={opinionatedTheme}> - <DxcCheckbox label="Checkbox" defaultChecked /> - </HalstackProvider> - </ExampleContainer> - <ExampleContainer> - <Title title="Disabled" theme="light" level={4} /> - <HalstackProvider theme={opinionatedTheme}> - <DxcCheckbox label="Checkbox" disabled /> - </HalstackProvider> - </ExampleContainer> - <ExampleContainer> - <Title title="Disabled checked" theme="light" level={4} /> - <HalstackProvider theme={opinionatedTheme}> - <DxcCheckbox label="Checkbox" defaultChecked disabled /> - </HalstackProvider> - </ExampleContainer> - <ExampleContainer pseudoState="pseudo-focus"> - <Title title="Focused" theme="light" level={4} /> - <HalstackProvider theme={opinionatedTheme}> - <DxcCheckbox label="Focused" /> - </HalstackProvider> - </ExampleContainer> - <ExampleContainer pseudoState="pseudo-hover"> - <Title title="Hovered" theme="light" level={4} /> - <HalstackProvider theme={opinionatedTheme}> - <DxcCheckbox label="Hovered" /> - </HalstackProvider> - </ExampleContainer> - <ExampleContainer pseudoState="pseudo-hover"> - <Title title="Hovered and checked" theme="light" level={4} /> - <HalstackProvider theme={opinionatedTheme}> - <DxcCheckbox label="Hovered" defaultChecked /> - </HalstackProvider> - </ExampleContainer> </> ); diff --git a/packages/lib/src/checkbox/Checkbox.tsx b/packages/lib/src/checkbox/Checkbox.tsx index a7c6a730a5..471992656d 100644 --- a/packages/lib/src/checkbox/Checkbox.tsx +++ b/packages/lib/src/checkbox/Checkbox.tsx @@ -1,59 +1,112 @@ import { useContext, useState, useRef, useId, forwardRef, KeyboardEvent } from "react"; -import styled, { ThemeProvider } from "styled-components"; -import { AdvancedTheme, spaces } from "../common/variables"; -import { getMargin } from "../common/utils"; -import HalstackContext, { HalstackLanguageContext } from "../HalstackContext"; +import styled from "styled-components"; +import { HalstackLanguageContext } from "../HalstackContext"; import CheckboxPropsType, { RefType } from "./types"; +import { calculateWidth, icons, spaces } from "./utils"; -const checkedIcon = ( - <svg fill="currentColor" focusable="false" viewBox="0 0 24 24"> - <path d="M19 3H5c-1.11 0-2 .9-2 2v14c0 1.1.89 2 2 2h14c1.11 0 2-.9 2-2V5c0-1.1-.89-2-2-2zm-9 14-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"></path> - </svg> -); +const Label = styled.span<{ + disabled: CheckboxPropsType["disabled"]; +}>` + 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); + span { + color: ${({ disabled }) => (disabled ? "var(--color-fg-neutral-medium)" : "var(--color-fg-neutral-stronger)")}; + } +`; + +const Checkbox = styled.span<{ + disabled: CheckboxPropsType["disabled"]; + readOnly: CheckboxPropsType["readOnly"]; +}>` + display: flex; + border-radius: var(--border-radius-s); + color: ${({ disabled, readOnly }) => + disabled || readOnly ? "var(--color-fg-neutral-medium)" : "var(--color-fg-secondary-medium)"}; + ${({ disabled }) => disabled && "pointer-events: none;"} + + &:focus { + outline: var(--border-width-m) var(--border-style-default) var(--border-color-secondary-medium); + outline-offset: -2px; + } + svg { + width: 24px; + height: var(--height-s); + } +`; + +const CheckboxContainer = styled.div<{ + disabled: CheckboxPropsType["disabled"]; + labelPosition: CheckboxPropsType["labelPosition"]; + margin: CheckboxPropsType["margin"]; + readOnly: CheckboxPropsType["readOnly"]; + size: CheckboxPropsType["size"]; +}>` + display: flex; + align-items: center; + flex-direction: ${({ labelPosition }) => (labelPosition === "before" ? "row" : "row-reverse")}; + gap: var(--spacing-gap-s); + width: ${(props) => calculateWidth(props.margin, props.size)}; + 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] : ""}; + cursor: ${({ disabled, readOnly }) => (disabled ? "not-allowed" : readOnly ? "default" : "pointer")}; + + &:hover ${Checkbox} { + ${({ disabled, readOnly }) => + !disabled && `color: ${readOnly ? "var(--color-fg-neutral-strong)" : "var(--color-fg-secondary-strong)"}`}; + } + &:active ${Checkbox} { + ${({ disabled, readOnly }) => + !disabled && `color: ${readOnly ? "var(--color-fg-neutral-strong)" : "var(--color-fg-secondary-strong)"}`}; + } +`; const DxcCheckbox = forwardRef<RefType, CheckboxPropsType>( ( { + ariaLabel = "Checkbox", checked, defaultChecked = false, - value, + disabled = false, label = "", labelPosition = "before", + margin, name = "", - disabled = false, + onChange, optional = false, readOnly = false, - onChange, - margin, size = "fitContent", tabIndex = 0, - ariaLabel = "Checkbox", + value, }, ref - ): JSX.Element => { + ) => { const labelId = `label-checkbox-${useId()}`; const [innerChecked, setInnerChecked] = useState(defaultChecked); const checkboxRef = useRef<HTMLSpanElement | null>(null); - const colorsTheme = useContext(HalstackContext); const translatedLabels = useContext(HalstackLanguageContext); - const handleCheckboxChange = () => { + const handleOnChange = () => { if (!disabled && !readOnly) { - if (document.activeElement !== checkboxRef.current) { - checkboxRef.current?.focus(); - } - if (checked == null) { - setInnerChecked((innerCurrentlyChecked) => !innerCurrentlyChecked); - } + if (document.activeElement !== checkboxRef.current) checkboxRef.current?.focus(); + if (checked == null) setInnerChecked((innerCurrentlyChecked) => !innerCurrentlyChecked); onChange?.(!(checked ?? innerChecked)); } }; - const handleKeyboard = (event: KeyboardEvent<HTMLSpanElement>) => { + const handleOnKeyDown = (event: KeyboardEvent<HTMLSpanElement>) => { switch (event.key) { case " ": event.preventDefault(); - handleCheckboxChange(); + handleOnChange(); break; default: break; @@ -61,227 +114,48 @@ const DxcCheckbox = forwardRef<RefType, CheckboxPropsType>( }; return ( - <ThemeProvider theme={colorsTheme.checkbox}> - <MainContainer + <CheckboxContainer + disabled={disabled} + labelPosition={labelPosition} + margin={margin} + onClick={handleOnChange} + readOnly={readOnly} + ref={ref} + size={size} + > + {label && ( + <Label aria-label={label} disabled={disabled} id={labelId}> + {label} {optional && <span>{translatedLabels.formFields.optionalLabel}</span>} + </Label> + )} + <Checkbox + aria-checked={checked ?? innerChecked} + aria-disabled={disabled} + aria-label={label ? undefined : ariaLabel} + aria-labelledby={label ? labelId : undefined} + aria-readonly={readOnly} + aria-required={!disabled && !optional} disabled={disabled} + onKeyDown={handleOnKeyDown} readOnly={readOnly} - onClick={handleCheckboxChange} - margin={margin} - size={size} - checked={checked ?? innerChecked} - ref={ref} + role="checkbox" + ref={checkboxRef} + tabIndex={disabled ? -1 : tabIndex} > - {label && ( - <LabelContainer id={labelId} disabled={disabled} labelPosition={labelPosition} aria-label={label}> - {label} - {optional && ` ${translatedLabels.formFields.optionalLabel}`} - </LabelContainer> - )} - <ValueInput - type="checkbox" - checked={checked ?? innerChecked} - name={name} - value={value} - disabled={disabled} - readOnly - /> - <CheckboxContainer> - <Checkbox - onKeyDown={handleKeyboard} - role="checkbox" - tabIndex={disabled ? -1 : tabIndex} - aria-checked={checked ?? innerChecked} - aria-disabled={disabled} - aria-readonly={readOnly} - aria-required={!disabled && !optional} - aria-labelledby={label ? labelId : undefined} - aria-label={label ? undefined : ariaLabel} - checked={checked ?? innerChecked} - disabled={disabled} - readOnly={readOnly} - ref={checkboxRef} - > - {(checked ?? innerChecked) && checkedIcon} - </Checkbox> - </CheckboxContainer> - </MainContainer> - </ThemeProvider> + {(checked ?? innerChecked) ? icons.checked : icons.unchecked} + </Checkbox> + <input + checked={checked ?? innerChecked} + disabled={disabled} + name={name} + readOnly + style={{ display: "none" }} + type="checkbox" + value={value} + /> + </CheckboxContainer> ); } ); -const sizes = { - small: "120px", - medium: "240px", - large: "480px", - fillParent: "100%", - fitContent: "fit-content", -}; - -const calculateWidth = (margin: CheckboxPropsType["margin"], size: CheckboxPropsType["size"]) => - size === "fillParent" - ? `calc(${sizes[size]} - ${getMargin(margin, "left")} - ${getMargin(margin, "right")})` - : size && sizes[size]; - -const getDisabledColor = (theme: AdvancedTheme["checkbox"], element: string) => { - switch (element) { - case "check": - return theme.disabledCheckColor; - case "background": - return theme.disabledBackgroundColorChecked; - case "border": - return theme.disabledBorderColor; - case "label": - return theme.disabledFontColor; - default: - return undefined; - } -}; - -const getReadOnlyColor = (theme: AdvancedTheme["checkbox"], element: string) => { - switch (element) { - case "check": - return theme.readOnlyCheckColor; - case "background": - return theme.readOnlyBackgroundColorChecked; - case "hoverBackground": - return theme.hoverReadOnlyBackgroundColorChecked; - case "border": - return theme.readOnlyBorderColor; - case "hoverBorder": - return theme.hoverReadOnlyBorderColor; - default: - return undefined; - } -}; - -const getEnabledColor = (theme: AdvancedTheme["checkbox"], element: string) => { - switch (element) { - case "check": - return theme.checkColor; - case "background": - return theme.backgroundColorChecked; - case "hoverBackground": - return theme.hoverBackgroundColorChecked; - case "border": - return theme.borderColor; - case "hoverBorder": - return theme.hoverBorderColor; - case "label": - return theme.fontColor; - default: - return undefined; - } -}; - -const LabelContainer = styled.span<{ - disabled: CheckboxPropsType["disabled"]; - labelPosition: CheckboxPropsType["labelPosition"]; -}>` - order: ${(props) => (props.labelPosition === "before" ? 0 : 1)}; - color: ${(props) => - props.disabled ? getDisabledColor(props.theme, "label") : getEnabledColor(props.theme, "label")}; - font-family: ${(props) => props.theme.fontFamily}; - font-size: ${(props) => props.theme.fontSize}; - font-weight: ${(props) => props.theme.fontWeight}; -`; - -const ValueInput = styled.input` - display: none; -`; - -const CheckboxContainer = styled.span` - display: flex; - align-items: center; - justify-content: center; - height: 24px; - width: 24px; -`; - -const Checkbox = styled.span<{ - checked: CheckboxPropsType["checked"]; - disabled: CheckboxPropsType["disabled"]; - readOnly: CheckboxPropsType["readOnly"]; -}>` - position: relative; - box-sizing: border-box; - display: flex; - align-items: center; - justify-content: center; - height: 18px; - width: 18px; - border: 2px solid - ${(props) => - props.disabled - ? getDisabledColor(props.theme, "border") - : props.readOnly - ? getReadOnlyColor(props.theme, "border") - : getEnabledColor(props.theme, "border")}; - border-radius: 2px; - background-color: ${(props) => - props.checked - ? props.disabled - ? getDisabledColor(props.theme, "check") - : props.readOnly - ? getReadOnlyColor(props.theme, "check") - : getEnabledColor(props.theme, "check") - : "transparent"}; - color: ${(props) => - props.disabled - ? getDisabledColor(props.theme, "background") - : props.readOnly - ? getReadOnlyColor(props.theme, "background") - : getEnabledColor(props.theme, "background")}; - - &:focus { - outline: 2px solid ${(props) => props.theme.focusColor}; - outline-offset: 2px; - } - svg { - position: absolute; - width: 22px; - height: 22px; - } - ${(props) => props.disabled && "pointer-events: none;"} -`; - -const MainContainer = styled.div<{ - margin: CheckboxPropsType["margin"]; - size: CheckboxPropsType["size"]; - disabled: CheckboxPropsType["disabled"]; - readOnly: CheckboxPropsType["readOnly"]; - checked: CheckboxPropsType["checked"]; -}>` - display: inline-flex; - align-items: center; - gap: ${(props) => props.theme.checkLabelSpacing}; - width: ${(props) => calculateWidth(props.margin, props.size)}; - 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] : ""}; - cursor: ${(props) => (props.disabled ? "not-allowed" : props.readOnly ? "default" : "pointer")}; - - &:hover ${Checkbox} { - border: 2px solid - ${(props) => { - if (!props.disabled) - return props.readOnly - ? getReadOnlyColor(props.theme, "hoverBorder") - : getEnabledColor(props.theme, "hoverBorder"); - }}; - color: ${(props) => { - if (!props.disabled) - return props.readOnly - ? getReadOnlyColor(props.theme, "hoverBackground") - : getEnabledColor(props.theme, "hoverBackground"); - }}; - } -`; - export default DxcCheckbox; diff --git a/packages/lib/src/checkbox/types.ts b/packages/lib/src/checkbox/types.ts index 1559591832..2558af5cbc 100644 --- a/packages/lib/src/checkbox/types.ts +++ b/packages/lib/src/checkbox/types.ts @@ -2,19 +2,22 @@ import { Margin, Space } from "../common/utils"; type Props = { /** - * Initial state of the checkbox, only when it is uncontrolled. + * Specifies a string to be used as the name for the checkbox 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 value 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 checkbox is checked. + * Initial state of the checkbox, 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 checkbox. */ @@ -23,14 +26,22 @@ type Props = { * Whether the label should appear after or before the checkbox. */ labelPosition?: "before" | "after"; + /** + * 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; /** * Name attribute of the input element. */ name?: string; /** - * If true, the component will be disabled. + * This function will be called when the user clicks the checkbox. + * The new value will be passed as a parameter. */ - disabled?: boolean; + onChange?: (value: boolean) => void; /** * If true, the component will display '(Optional)' next to the label. */ @@ -39,18 +50,6 @@ type Props = { * If true, the component will not be mutable, meaning the user can not edit the control. */ readOnly?: boolean; - /** - * This function will be called when the user clicks the checkbox. - * The new value will be passed as a parameter. - */ - onChange?: (value: boolean) => void; - /** - * 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. */ @@ -60,9 +59,10 @@ type Props = { */ tabIndex?: number; /** - * Specifies a string to be used as the name for the checkbox 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 checkbox is checked. */ - ariaLabel?: string; + value?: string; }; /** diff --git a/packages/lib/src/checkbox/utils.tsx b/packages/lib/src/checkbox/utils.tsx new file mode 100644 index 0000000000..a112847d1c --- /dev/null +++ b/packages/lib/src/checkbox/utils.tsx @@ -0,0 +1,44 @@ +import { getMargin } from "../common/utils"; +import CheckboxPropsType from "./types"; + +const sizes = { + small: "120px", + medium: "240px", + large: "480px", + fillParent: "100%", + fitContent: "fit-content", +}; + +export const spaces = { + xxsmall: "var(--spacing-padding-xxs)", + xsmall: "var(--spacing-padding-xs)", + small: "var(--spacing-padding-s)", + medium: "var(--spacing-padding-m)", + large: "var(--spacing-padding-l)", + xlarge: "var(--spacing-padding-xl)", + xxlarge: "var(--spacing-padding-xxl)", +}; + +export const calculateWidth = (margin: CheckboxPropsType["margin"], size: CheckboxPropsType["size"]) => + size === "fillParent" + ? `calc(${sizes[size]} - ${getMargin(margin, "left")} - ${getMargin(margin, "right")})` + : size && sizes[size]; + +export const icons = { + checked: ( + <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"> + <path + d="M19 3H5C3.89 3 3 3.9 3 5V19C3 20.1 3.89 21 5 21H19C20.11 21 21 20.1 21 19V5C21 3.9 20.11 3 19 3ZM10 17L5 12L6.41 10.59L10 14.17L17.59 6.58L19 8L10 17Z" + fill="currentColor" + /> + </svg> + ), + unchecked: ( + <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"> + <path + d="M19 5V19H5V5H19ZM19 3H5C3.9 3 3 3.9 3 5V19C3 20.1 3.9 21 5 21H19C20.1 21 21 20.1 21 19V5C21 3.9 20.1 3 19 3Z" + fill="currentColor" + /> + </svg> + ), +};