diff --git a/apps/website/screens/common/themes/advanced-theme.json b/apps/website/screens/common/themes/advanced-theme.json index 0a2cbdc4aa..2ec0b6c056 100644 --- a/apps/website/screens/common/themes/advanced-theme.json +++ b/apps/website/screens/common/themes/advanced-theme.json @@ -403,15 +403,8 @@ "dialog": { "overlayColor": "#000000b3", "backgroundColor": "#ffffff", - "closeIconSize": "24px", - "closeIconTopPosition": "20px", - "closeIconRightPosition": "20px", "closeIconBackgroundColor": "transparent", - "closeIconBorderColor": "none", "closeIconColor": "#000000", - "closeIconBorderThickness": "0px", - "closeIconBorderStyle": "solid", - "closeIconBorderRadius": "2px", "boxShadowOffsetX": "0px", "boxShadowOffsetY": "1px", "boxShadowBlur": "3px", @@ -546,15 +539,15 @@ }, "header": { "backgroundColor": "#ffffff", - "hamburguerFocusColor": "#0095ff", - "hamburguerFontFamily": "Open Sans, sans-serif", - "hamburguerFontStyle": "normal", - "hamburguerFontColor": "#000000", - "hamburguerFontSize": "10px", - "hamburguerFontWeight": "600", - "hamburguerTextTransform": "uppercase", - "hamburguerIconColor": "#000000", - "hamburguerHoverColor": "#e6e6e6", + "hamburgerFocusColor": "#0095ff", + "hamburgerFontFamily": "Open Sans, sans-serif", + "hamburgerFontStyle": "normal", + "hamburgerFontColor": "#000000", + "hamburgerFontSize": "10px", + "hamburgerFontWeight": "600", + "hamburgerTextTransform": "uppercase", + "hamburgerIconColor": "#000000", + "hamburgerHoverColor": "#e6e6e6", "logo": "", "logoResponsive": "", "logoHeight": "40px", diff --git a/apps/website/screens/common/themes/opinionated-theme.json b/apps/website/screens/common/themes/opinionated-theme.json index 092f4ebb58..050215d6ad 100644 --- a/apps/website/screens/common/themes/opinionated-theme.json +++ b/apps/website/screens/common/themes/opinionated-theme.json @@ -58,7 +58,7 @@ "accentColor": "#000000", "fontColor": "#000000", "menuBaseColor": "#ffffff", - "hamburguerColor": "#000000", + "hamburgerColor": "#000000", "logo": "/dxc_header_logo.svg", "logoResponsive": "/dxc_header_logo.svg", "contentColor": "#000000", diff --git a/apps/website/screens/components/accordion/code/AccordionCodePage.tsx b/apps/website/screens/components/accordion/code/AccordionCodePage.tsx index 44e31be748..38a210a296 100644 --- a/apps/website/screens/components/accordion/code/AccordionCodePage.tsx +++ b/apps/website/screens/components/accordion/code/AccordionCodePage.tsx @@ -45,7 +45,9 @@ const sections = [ boolean Initial state of the panel, only when it is uncontrolled. - - + + false + isExpanded diff --git a/apps/website/screens/components/checkbox/code/CheckboxCodePage.tsx b/apps/website/screens/components/checkbox/code/CheckboxCodePage.tsx index 1aa21a0573..71d37db836 100644 --- a/apps/website/screens/components/checkbox/code/CheckboxCodePage.tsx +++ b/apps/website/screens/components/checkbox/code/CheckboxCodePage.tsx @@ -42,7 +42,7 @@ const sections = [ managed internally by the component. - false + - diff --git a/apps/website/screens/components/dialog/specs/DialogSpecsPage.tsx b/apps/website/screens/components/dialog/specs/DialogSpecsPage.tsx index 07d9e50616..0ea5c60f56 100644 --- a/apps/website/screens/components/dialog/specs/DialogSpecsPage.tsx +++ b/apps/website/screens/components/dialog/specs/DialogSpecsPage.tsx @@ -98,14 +98,6 @@ const sections = [ transparent - - - closeIconBorderColor - - Icon close - - - - - ), @@ -161,48 +153,6 @@ const sections = [ title: "Border", content: ( <> - - - - Component token - Element - Core token - Value - - - - - - closeIconBorderThickness - - Icon close - - border-width-0 - - 0px - - - - closeIconBorderStyle - - Icon close - - border-style-solid - - solid - - - - closeIconBorderRadius - - Icon close - - border-radius-small - - 0.125rem / 2px - - - diff --git a/apps/website/screens/components/header/specs/HeaderSpecsPage.tsx b/apps/website/screens/components/header/specs/HeaderSpecsPage.tsx index 9a9e5b8047..9414807ee6 100644 --- a/apps/website/screens/components/header/specs/HeaderSpecsPage.tsx +++ b/apps/website/screens/components/header/specs/HeaderSpecsPage.tsx @@ -80,7 +80,7 @@ const sections = [ - hamburguerHoverColor + hamburgerHoverColor Menu:hover @@ -90,7 +90,7 @@ const sections = [ - hamburguerFocusColor + hamburgerFocusColor Menu:focus @@ -100,7 +100,7 @@ const sections = [ - hamburguerFontColor + hamburgerFontColor Menu label @@ -110,7 +110,7 @@ const sections = [ - hamburguerIconColor + hamburgerIconColor Menu icon @@ -157,7 +157,7 @@ const sections = [ - hamburguerFontFamily + hamburgerFontFamily Menu label @@ -167,7 +167,7 @@ const sections = [ - hamburguerFontStyle + hamburgerFontStyle Menu label @@ -177,7 +177,7 @@ const sections = [ - hamburguerFontSize + hamburgerFontSize Menu label @@ -187,7 +187,7 @@ const sections = [ - hamburguerFontWeight + hamburgerFontWeight Menu label diff --git a/apps/website/screens/components/progress-bar/code/ProgressBarCodePage.tsx b/apps/website/screens/components/progress-bar/code/ProgressBarCodePage.tsx index 6d2350de2e..4cc180b298 100644 --- a/apps/website/screens/components/progress-bar/code/ProgressBarCodePage.tsx +++ b/apps/website/screens/components/progress-bar/code/ProgressBarCodePage.tsx @@ -63,7 +63,7 @@ const sections = [ boolean - If true, the value is displayed above the progress bar. + If true, the determined value is displayed above the progress bar. false diff --git a/apps/website/screens/components/progress-bar/code/examples/overlay.ts b/apps/website/screens/components/progress-bar/code/examples/overlay.ts index 29c7108e69..0cdd0be6f2 100644 --- a/apps/website/screens/components/progress-bar/code/examples/overlay.ts +++ b/apps/website/screens/components/progress-bar/code/examples/overlay.ts @@ -25,11 +25,10 @@ const code = `() => { label="Show Progress Bar for 3 seconds" onClick={showModal} /> - {isVisible && ( )} diff --git a/apps/website/screens/components/switch/code/SwitchCodePage.tsx b/apps/website/screens/components/switch/code/SwitchCodePage.tsx index bcb068d27c..170c598243 100644 --- a/apps/website/screens/components/switch/code/SwitchCodePage.tsx +++ b/apps/website/screens/components/switch/code/SwitchCodePage.tsx @@ -28,7 +28,9 @@ const sections = [ boolean Initial state of the switch, only when it is uncontrolled. - - + + false + checked @@ -39,9 +41,7 @@ const sections = [ If true, the component is checked. If undefined, the component will be uncontrolled and the checked attribute will be managed internally by the component. - - false - + - value diff --git a/apps/website/screens/components/toast/code/ToastCodePage.tsx b/apps/website/screens/components/toast/code/ToastCodePage.tsx index b9c45f2a70..3ac8256f14 100644 --- a/apps/website/screens/components/toast/code/ToastCodePage.tsx +++ b/apps/website/screens/components/toast/code/ToastCodePage.tsx @@ -49,6 +49,14 @@ const sections = [ 3000 + + children + + ReactNode + + Tree of components from which the useToast hook can be triggered. + - + ), diff --git a/apps/website/screens/components/wizard/code/WizardCodePage.tsx b/apps/website/screens/components/wizard/code/WizardCodePage.tsx index 86885472a4..acb4d9f9bb 100644 --- a/apps/website/screens/components/wizard/code/WizardCodePage.tsx +++ b/apps/website/screens/components/wizard/code/WizardCodePage.tsx @@ -30,7 +30,9 @@ const sections = [ number Initially selected step, only when it is uncontrolled. - - + + 0 + currentStep @@ -41,9 +43,7 @@ const sections = [ Defines which step is marked as the current. The numeration starts at 0. If undefined, the component will be uncontrolled and the step will be managed internally by the component. - - 0 - + - mode diff --git a/apps/website/screens/principles/localization/LocalizationPage.tsx b/apps/website/screens/principles/localization/LocalizationPage.tsx index 29b25c2b08..20c37536e1 100644 --- a/apps/website/screens/principles/localization/LocalizationPage.tsx +++ b/apps/website/screens/principles/localization/LocalizationPage.tsx @@ -353,7 +353,7 @@ const sections = [ - hamburguerTitle + hamburgerTitle Menu diff --git a/apps/website/screens/principles/themes/ThemesPage.tsx b/apps/website/screens/principles/themes/ThemesPage.tsx index 760f6ee9fb..ddeba37ef5 100644 --- a/apps/website/screens/principles/themes/ThemesPage.tsx +++ b/apps/website/screens/principles/themes/ThemesPage.tsx @@ -671,7 +671,7 @@ const sections = [ Font color - hamburguerFontColor + hamburgerFontColor @@ -681,12 +681,12 @@ const sections = [ - Hamburguer color + Hamburger color - hamburguerIconColor + hamburgerIconColor

- hamburguerHoverColor (+90% of lightness) + hamburgerHoverColor (+90% of lightness) diff --git a/apps/website/screens/principles/themes/examples/bloomTheme.ts b/apps/website/screens/principles/themes/examples/bloomTheme.ts index 970f2046dd..461419993b 100644 --- a/apps/website/screens/principles/themes/examples/bloomTheme.ts +++ b/apps/website/screens/principles/themes/examples/bloomTheme.ts @@ -40,7 +40,7 @@ export default { accentColor: "#000000", fontColor: "#000000", menuBaseColor: "#ffffff", - hamburguerColor: "#000000", + hamburgerColor: "#000000", logo: "https://assure.proxy.lambda/image/image_1674206652560.jpg", logoResponsive: "https://assure.proxy.lambda/image/image_1674206660896.jpg", contentColor: "#000000", diff --git a/apps/website/screens/theme-generator/themes/schemas/advanced.schema.json b/apps/website/screens/theme-generator/themes/schemas/advanced.schema.json index cbc0944068..58b7ee07bb 100644 --- a/apps/website/screens/theme-generator/themes/schemas/advanced.schema.json +++ b/apps/website/screens/theme-generator/themes/schemas/advanced.schema.json @@ -403,15 +403,8 @@ "dialog": { "overlayColor": "color", "backgroundColor": "color", - "closeIconSize": "length", - "closeIconTopPosition": "length", - "closeIconRightPosition": "length", "closeIconBackgroundColor": "color", "closeIconColor": "color", - "closeIconBorderColor": "color", - "closeIconBorderThickness": "bWidth", - "closeIconBorderStyle": "bStyle", - "closeIconBorderRadius": "length", "boxShadowOffsetX": "length", "boxShadowOffsetY": "length", "boxShadowBlur": "length", @@ -548,13 +541,13 @@ "backgroundColor": "color", "underlinedColor": "color", "menuBackgroundColor": "color", - "hamburguerIconColor": "color", - "hamburguerHoverColor": "color", + "hamburgerIconColor": "color", + "hamburgerHoverColor": "color", "overlayColor": "color", - "hamburguerFocusColor": "color", + "hamburgerFocusColor": "color", "logo": "image", "logoResponsive": "image", - "hamburguerTextTransform": "fTextTransform", + "hamburgerTextTransform": "fTextTransform", "underlinedThickness": "bWidth", "underlinedStyle": "bStyle", "minHeight": "length", @@ -569,11 +562,11 @@ "menuMobileWidth": "length", "overlayOpacity": "alphaValue", "overlayZindex": "integer", - "hamburguerFontFamily": "fFamily", - "hamburguerFontStyle": "fStyle", - "hamburguerFontColor": "color", - "hamburguerFontSize": "length", - "hamburguerFontWeight": "fWeight", + "hamburgerFontFamily": "fFamily", + "hamburgerFontStyle": "fStyle", + "hamburgerFontColor": "color", + "hamburgerFontSize": "length", + "hamburgerFontWeight": "fWeight", "contentColor": "color" }, "heading": { diff --git a/apps/website/screens/theme-generator/themes/schemas/opinionated.schema.json b/apps/website/screens/theme-generator/themes/schemas/opinionated.schema.json index f766b055d2..f61d8dd52f 100644 --- a/apps/website/screens/theme-generator/themes/schemas/opinionated.schema.json +++ b/apps/website/screens/theme-generator/themes/schemas/opinionated.schema.json @@ -58,7 +58,7 @@ "accentColor": "color", "fontColor": "color", "menuBaseColor": "color", - "hamburguerColor": "color", + "hamburgerColor": "color", "logo": "image", "logoResponsive": "image", "contentColor": "color", diff --git a/packages/lib/src/HalstackContext.tsx b/packages/lib/src/HalstackContext.tsx index 69693bf199..500f2e7a86 100644 --- a/packages/lib/src/HalstackContext.tsx +++ b/packages/lib/src/HalstackContext.tsx @@ -190,10 +190,10 @@ const parseTheme = (theme: DeepPartial): AdvancedTheme => { headerTokens.backgroundColor = theme.header?.baseColor ?? headerTokens.backgroundColor; headerTokens.underlinedColor = theme.header?.accentColor ?? headerTokens.underlinedColor; headerTokens.menuBackgroundColor = theme.header?.menuBaseColor ?? headerTokens.menuBackgroundColor; - headerTokens.hamburguerFontColor = theme.header?.fontColor ?? headerTokens.hamburguerFontColor; - headerTokens.hamburguerIconColor = theme.header?.hamburguerColor ?? headerTokens.hamburguerIconColor; - headerTokens.hamburguerHoverColor = - addLightness(90, theme.header?.hamburguerColor) ?? headerTokens.hamburguerHoverColor; + headerTokens.hamburgerFontColor = theme.header?.fontColor ?? headerTokens.hamburgerFontColor; + headerTokens.hamburgerIconColor = theme.header?.hamburgerColor ?? headerTokens.hamburgerIconColor; + headerTokens.hamburgerHoverColor = + addLightness(90, theme.header?.hamburgerColor) ?? headerTokens.hamburgerHoverColor; headerTokens.logo = theme.header?.logo ?? headerTokens.logo; headerTokens.logoResponsive = theme.header?.logoResponsive ?? headerTokens.logoResponsive; headerTokens.contentColor = theme.header?.contentColor ?? headerTokens.contentColor; diff --git a/packages/lib/src/accordion/Accordion.tsx b/packages/lib/src/accordion/Accordion.tsx index 5d576f6b94..cbdea4f20b 100644 --- a/packages/lib/src/accordion/Accordion.tsx +++ b/packages/lib/src/accordion/Accordion.tsx @@ -6,68 +6,6 @@ import HalstackContext from "../HalstackContext"; import AccordionPropsType from "./types"; import DxcIcon from "../icon/Icon"; -const DxcAccordion = ({ - label = "", - defaultIsExpanded, - isExpanded, - icon, - assistiveText = "", - disabled = false, - onChange, - children, - margin, - tabIndex = 0, -}: AccordionPropsType): JSX.Element => { - const id = useId(); - const [innerIsExpanded, setInnerIsExpanded] = useState(defaultIsExpanded ?? false); - const colorsTheme = useContext(HalstackContext); - - const handleAccordionState = () => { - if (isExpanded == null) { - setInnerIsExpanded((innerIsCurrentlyExpanded) => !innerIsCurrentlyExpanded); - } - onChange?.(isExpanded != null ? !isExpanded : !innerIsExpanded); - }; - - return ( - - - - - - - {icon && ( - - {typeof icon === "string" ? : icon} - - )} - {label} - - {assistiveText && {assistiveText}} - - - - - - - {(isExpanded ?? innerIsExpanded) && ( - - {children} - - )} - - - ); -}; - const calculateWidth = (margin: AccordionPropsType["margin"]) => `calc(100% - ${getMargin(margin, "left")} - ${getMargin(margin, "right")})`; @@ -191,4 +129,66 @@ const AccordionPanel = styled.div` border-bottom-right-radius: ${(props) => props.theme.borderRadius}; `; +const DxcAccordion = ({ + label = "", + defaultIsExpanded = false, + isExpanded, + icon, + assistiveText = "", + disabled = false, + onChange, + children, + margin, + tabIndex = 0, +}: AccordionPropsType): JSX.Element => { + const id = useId(); + const [innerIsExpanded, setInnerIsExpanded] = useState(defaultIsExpanded); + const colorsTheme = useContext(HalstackContext); + + const handleAccordionState = () => { + if (isExpanded == null) { + setInnerIsExpanded((innerIsCurrentlyExpanded) => !innerIsCurrentlyExpanded); + } + onChange?.(isExpanded != null ? !isExpanded : !innerIsExpanded); + }; + + return ( + + + + + + + {icon && ( + + {typeof icon === "string" ? : icon} + + )} + {label} + + {assistiveText && {assistiveText}} + + + + + + + {(isExpanded ?? innerIsExpanded) && ( + + {children} + + )} + + + ); +}; + export default DxcAccordion; diff --git a/packages/lib/src/alert/types.ts b/packages/lib/src/alert/types.ts index 71c6643784..5985269862 100644 --- a/packages/lib/src/alert/types.ts +++ b/packages/lib/src/alert/types.ts @@ -2,31 +2,87 @@ import { ReactNode } from "react"; import { SVG } from "../common/utils"; type Action = { + /** + * The icon of the action. It can be a string with the name of the icon or an SVG component. + */ icon?: string | SVG; + /** + * The label of the action. + */ label: string; + /** + * The function that will be executed when the user clicks on the action button. + */ onClick: () => void; }; type Message = { + /** + * The function that will be executed when the user clicks on the close action button. + */ onClose?: () => void; + /** + * The content of the message. The only Halstack component allowed within the text of an alert is the Link component, + * and it should be used exclusively to direct users to additional resources or relevant pages. + * No other components are permitted within the content of an alert. + */ text: ReactNode; }; type CommonProps = { + /** + * If true, the alert will have a close button that will remove the message from the alert, + * only in banner and inline modes. The rest of the functionality will depend + * on the onClose event of each message (e.g. closing the modal alert). + */ closable?: boolean; + /** + * Primary action of the alert. + */ primaryAction?: Action; + /** + * Secondary action of the alert. + */ secondaryAction?: Action; + /** + * Specifies the semantic meaning of the alert. + */ semantic?: "error" | "info" | "success" | "warning"; + /** + * Title of the alert. + */ title: string; }; type ModeSpecificProps = | { + /** + * List of messages to be displayed. Each message has a close action that will, + * apart from remove from the alert the current message, call the onClose if it is defined. + * If the message is an array, the alert will show a navigation bar to move between the messages. + * The modal mode only allows one message per alert. In this case, the message must be an object + * and the presence of the onClose attribute is mandatory, since the management of the closing + * of the alert relies completely on the user. + */ message?: Message | Message[]; + /** + * The mode of the alert. + */ mode?: "inline" | "banner"; } | { + /** + * List of messages to be displayed. Each message has a close action that will, + * apart from remove from the alert the current message, call the onClose if it is defined. + * If the message is an array, the alert will show a navigation bar to move between the messages. + * The modal mode only allows one message per alert. In this case, the message must be an object + * and the presence of the onClose attribute is mandatory, since the management of the closing + * of the alert relies completely on the user. + */ message: Required; + /** + * The mode of the alert. + */ mode: "modal"; }; diff --git a/packages/lib/src/common/variables.ts b/packages/lib/src/common/variables.ts index 6990e4dbe3..a7349d1833 100644 --- a/packages/lib/src/common/variables.ts +++ b/packages/lib/src/common/variables.ts @@ -388,15 +388,8 @@ export const componentTokens = { dialog: { overlayColor: CoreTokens.color_grey_800_a, backgroundColor: CoreTokens.color_white, - closeIconSize: "24px", - closeIconTopPosition: "20px", - closeIconRightPosition: "20px", closeIconBackgroundColor: CoreTokens.color_transparent, - closeIconBorderColor: CoreTokens.border_none, closeIconColor: CoreTokens.color_black, - closeIconBorderThickness: CoreTokens.border_width_0, - closeIconBorderStyle: CoreTokens.border_solid, - closeIconBorderRadius: "2px", boxShadowOffsetX: "0px", boxShadowOffsetY: "1px", boxShadowBlur: "3px", @@ -531,15 +524,15 @@ export const componentTokens = { }, header: { backgroundColor: CoreTokens.color_white, - hamburguerFocusColor: CoreTokens.color_blue_600, - hamburguerFontFamily: CoreTokens.type_sans, - hamburguerFontStyle: CoreTokens.type_normal, - hamburguerFontColor: CoreTokens.color_black, - hamburguerFontSize: "10px", - hamburguerFontWeight: CoreTokens.type_semibold, - hamburguerTextTransform: CoreTokens.type_uppercase, - hamburguerIconColor: CoreTokens.color_black, - hamburguerHoverColor: CoreTokens.color_grey_200, + hamburgerFocusColor: CoreTokens.color_blue_600, + hamburgerFontFamily: CoreTokens.type_sans, + hamburgerFontStyle: CoreTokens.type_normal, + hamburgerFontColor: CoreTokens.color_black, + hamburgerFontSize: "10px", + hamburgerFontWeight: CoreTokens.type_semibold, + hamburgerTextTransform: CoreTokens.type_uppercase, + hamburgerIconColor: CoreTokens.color_black, + hamburgerHoverColor: CoreTokens.color_grey_200, logo: "", logoResponsive: "", logoHeight: "40px", @@ -1353,7 +1346,7 @@ export type OpinionatedTheme = { accentColor: string; fontColor: string; menuBaseColor: string; - hamburguerColor: string; + hamburgerColor: string; logo: string; logoResponsive: string; contentColor: string; @@ -1519,7 +1512,7 @@ export const defaultTranslatedComponentLabels = { }, header: { closeIcon: "Close menu", - hamburguerTitle: "Menu", + hamburgerTitle: "Menu", }, numberInput: { valueGreaterThanOrEqualToErrorMessage: (value: number) => `Value must be greater than or equal to ${value}.`, diff --git a/packages/lib/src/dialog/Dialog.accessibility.test.tsx b/packages/lib/src/dialog/Dialog.accessibility.test.tsx index 1f4001bbc8..e61471afdd 100644 --- a/packages/lib/src/dialog/Dialog.accessibility.test.tsx +++ b/packages/lib/src/dialog/Dialog.accessibility.test.tsx @@ -2,6 +2,16 @@ import { render } from "@testing-library/react"; import { axe } from "../../test/accessibility/axe-helper"; import DxcDialog from "./Dialog"; +(global as any).globalThis = global; +(global as any).DOMRect = { + fromRect: () => ({ top: 0, left: 0, bottom: 0, right: 0, width: 0, height: 0 }), +}; +(global as any).ResizeObserver = class ResizeObserver { + observe() {} + unobserve() {} + disconnect() {} +}; + describe("Dialog component accessibility tests", () => { it("Should not have basic accessibility issues", async () => { // baseElement is needed when using React Portals @@ -10,13 +20,11 @@ describe("Dialog component accessibility tests", () => { expect(results).toHaveNoViolations(); }); it("Should not have basic accessibility issues for close button not visible", async () => { - // baseElement is needed when using React Portals const { baseElement } = render(Dialog text); const results = await axe(baseElement); expect(results).toHaveNoViolations(); }); it("Should not have basic accessibility issues for overlay not visible", async () => { - // baseElement is needed when using React Portals const { baseElement } = render(Dialog text); const results = await axe(baseElement); expect(results).toHaveNoViolations(); diff --git a/packages/lib/src/dialog/Dialog.tsx b/packages/lib/src/dialog/Dialog.tsx index d86d7f6412..b5e4af27e2 100644 --- a/packages/lib/src/dialog/Dialog.tsx +++ b/packages/lib/src/dialog/Dialog.tsx @@ -2,7 +2,7 @@ import { useContext, useEffect } from "react"; import { createPortal } from "react-dom"; import styled, { createGlobalStyle, ThemeProvider } from "styled-components"; import { responsiveSizes } from "../common/variables"; -import DxcIcon from "../icon/Icon"; +import DxcActionIcon from "../action-icon/ActionIcon"; import HalstackContext, { HalstackLanguageContext } from "../HalstackContext"; import FocusLock from "../utils/FocusLock"; import DialogPropsType from "./types"; @@ -48,45 +48,18 @@ const Dialog = styled.div<{ closable: DialogPropsType["closable"] }>` } `; -const CloseIconAction = styled.button` - all: unset; +const CloseIconActionContainer = styled.div` position: absolute; top: 24px; right: 24px; - display: flex; - align-items: center; - justify-content: center; - background-color: ${(props) => props.theme.closeIconBackgroundColor}; - box-shadow: 0 0 0 2px transparent; - color: ${(props) => props.theme.closeIconColor}; - border-radius: ${(props) => props.theme.closeIconBorderRadius}; - border-width: ${(props) => props.theme.closeIconBorderThickness}; - border-style: ${(props) => props.theme.closeIconBorderStyle}; - border-color: ${(props) => props.theme.closeIconBorderColor}; - cursor: pointer; - z-index: 1; - - &:focus { - outline: none; - box-shadow: 0 0 0 2px #0095ff; - } - &:hover { - background-color: #f2f2f2; - } - &:active { - background-color: #cccccc; - } - span { - font-size: ${(props) => props.theme.closeIconSize}; - } `; const DxcDialog = ({ + children, closable = true, + onBackgroundClick, onCloseClick, - children, overlay = true, - onBackgroundClick, tabIndex = 0, }: DialogPropsType): JSX.Element => { const colorsTheme = useContext(HalstackContext); @@ -111,26 +84,26 @@ const DxcDialog = ({ {createPortal( - {overlay && ( - { - onBackgroundClick?.(); - }} - /> - )} - + {overlay && } + {children} {closable && ( - { - onCloseClick?.(); + - - + + + + )} diff --git a/packages/lib/src/file-input/FileInput.stories.tsx b/packages/lib/src/file-input/FileInput.stories.tsx index 9adc64af92..be4fbdbb9f 100644 --- a/packages/lib/src/file-input/FileInput.stories.tsx +++ b/packages/lib/src/file-input/FileInput.stories.tsx @@ -3,7 +3,6 @@ import ExampleContainer from "../../.storybook/components/ExampleContainer"; import Title from "../../.storybook/components/Title"; import { HalstackProvider } from "../HalstackContext"; import DxcFileInput from "./FileInput"; -import FileItem from "./FileItem"; export default { title: "File Input", @@ -79,46 +78,6 @@ const opinionatedTheme = { const FileInput = () => ( <> - - <ExampleContainer pseudoState="pseudo-hover"> - <Title title="Hovered" theme="light" level={4} /> - <FileItem - fileName="file" - error="" - singleFileMode={false} - showPreview={false} - preview={picPreview} - type="image/png" - onDelete={() => {}} - tabIndex={0} - /> - </ExampleContainer> - <ExampleContainer pseudoState="pseudo-focus"> - <Title title="Focused" theme="light" level={4} /> - <FileItem - fileName="file" - error="" - singleFileMode={false} - showPreview={false} - preview={picPreview} - type="image/png" - onDelete={() => {}} - tabIndex={0} - /> - </ExampleContainer> - <ExampleContainer pseudoState="pseudo-active"> - <Title title="Actived" theme="light" level={4} /> - <FileItem - fileName="file" - error="" - singleFileMode={false} - showPreview={false} - preview={picPreview} - type="image/png" - onDelete={() => {}} - tabIndex={0} - /> - </ExampleContainer> <Title title="File" theme="light" level={2} /> <ExampleContainer> <Title title="Without label" theme="light" level={4} /> diff --git a/packages/lib/src/file-input/FileInput.tsx b/packages/lib/src/file-input/FileInput.tsx index 0a8d1a64ec..1b1301536a 100644 --- a/packages/lib/src/file-input/FileInput.tsx +++ b/packages/lib/src/file-input/FileInput.tsx @@ -5,32 +5,127 @@ import { spaces } from "../common/variables"; import FileItem from "./FileItem"; import FileInputPropsType, { FileData, RefType } from "./types"; import HalstackContext, { HalstackLanguageContext } from "../HalstackContext"; +import { getFilePreview, isFileIncluded } from "./utils"; -const getFilePreview = async (file: File): Promise<string> => { - if (file.type.includes("video")) return "filled_movie"; - else if (file.type.includes("audio")) return "music_video"; - else if (file.type.includes("image")) { - return new Promise<string>((resolve) => { - const reader = new FileReader(); - reader.readAsDataURL(file); - reader.onload = (e) => { - resolve(e.target?.result as string); - }; - }); - } else return "draft"; -}; +const FileInputContainer = styled.div<{ margin: FileInputPropsType["margin"] }>` + display: flex; + flex-direction: column; + margin: ${(props) => (props.margin && typeof props.margin !== "object" ? spaces[props.margin] : "0px")}; + margin-top: ${(props) => + props.margin && typeof props.margin === "object" && props.margin.top ? spaces[props.margin.top] : ""}; + margin-right: ${(props) => + props.margin && typeof props.margin === "object" && props.margin.right ? spaces[props.margin.right] : ""}; + margin-bottom: ${(props) => + props.margin && typeof props.margin === "object" && props.margin.bottom ? spaces[props.margin.bottom] : ""}; + margin-left: ${(props) => + props.margin && typeof props.margin === "object" && props.margin.left ? spaces[props.margin.left] : ""}; + width: fit-content; +`; + +const Label = styled.label<{ disabled: FileInputPropsType["disabled"] }>` + color: ${(props) => (props.disabled ? props.theme.disabledLabelFontColor : props.theme.labelFontColor)}; + font-family: ${(props) => props.theme.labelFontFamily}; + font-size: ${(props) => props.theme.labelFontSize}; + font-weight: ${(props) => props.theme.labelFontWeight}; + line-height: ${(props) => props.theme.labelLineHeight}; +`; + +const HelperText = styled.span<{ disabled: FileInputPropsType["disabled"] }>` + color: ${(props) => (props.disabled ? props.theme.disabledHelperTextFontColor : props.theme.helperTextFontColor)}; + font-family: ${(props) => props.theme.helperTextFontFamily}; + font-size: ${(props) => props.theme.helperTextFontSize}; + font-weight: ${(props) => props.theme.helperTextFontWeight}; + line-height: ${(props) => props.theme.helperTextLineHeight}; +`; + +const FileContainer = styled.div<{ singleFileMode: boolean }>` + display: flex; + ${(props) => + props.singleFileMode ? "flex-direction: row; column-gap: 0.25rem;" : "flex-direction: column; row-gap: 0.25rem;"} + margin-top: 0.25rem; +`; + +const ValueInput = styled.input` + display: none; +`; + +const FileItemListContainer = styled.div` + display: flex; + flex-direction: column; + row-gap: 0.25rem; +`; + +const Container = styled.div` + display: flex; + flex-direction: column; + row-gap: 0.25rem; + margin-top: 0.25rem; +`; + +const DragDropArea = styled.div<{ + mode: FileInputPropsType["mode"]; + disabled: FileInputPropsType["disabled"]; + isDragging: boolean; +}>` + box-sizing: border-box; + display: flex; + ${(props) => + props.mode === "filedrop" + ? "flex-direction: row; column-gap: 0.75rem; height: 48px;" + : "justify-content: center; flex-direction: column; row-gap: 0.5rem; height: 160px;"} + align-items: center; + width: 320px; + padding: ${(props) => + props.mode === "filedrop" + ? `calc(4px - ${props.theme.dropBorderThickness}) 1rem calc(4px - ${props.theme.dropBorderThickness}) calc(4px - ${props.theme.dropBorderThickness})` + : "1rem"}; + overflow: hidden; + box-shadow: 0 0 0 2px transparent; + border-radius: ${(props) => props.theme.dropBorderRadius}; + border-width: ${(props) => props.theme.dropBorderThickness}; + border-style: ${(props) => props.theme.dropBorderStyle}; + border-color: ${(props) => (props.disabled ? props.theme.disabledDropBorderColor : props.theme.dropBorderColor)}; + ${(props) => + props.isDragging && + ` + background-color: ${props.theme.dragoverDropBackgroundColor}; + border-color: transparent; + box-shadow: 0 0 0 2px ${props.theme.focusDropBorderColor}; + `} + cursor: ${(props) => props.disabled && "not-allowed"}; +`; + +const DropzoneLabel = styled.div<{ disabled: FileInputPropsType["disabled"] }>` + display: -webkit-box; + -webkit-box-orient: vertical; + overflow: hidden; + text-overflow: ellipsis; + -webkit-line-clamp: 3; + text-align: center; + color: ${(props) => (props.disabled ? props.theme.disabledDropLabelFontColor : props.theme.dropLabelFontColor)}; + font-family: ${(props) => props.theme.dropLabelFontFamily}; + font-size: ${(props) => props.theme.dropLabelFontSize}; + font-weight: ${(props) => props.theme.dropLabelFontWeight}; +`; + +const FiledropLabel = styled.span<{ disabled: FileInputPropsType["disabled"] }>` + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + color: ${(props) => (props.disabled ? props.theme.disabledDropLabelFontColor : props.theme.dropLabelFontColor)}; + font-family: ${(props) => props.theme.dropLabelFontFamily}; + font-size: ${(props) => props.theme.dropLabelFontSize}; + font-weight: ${(props) => props.theme.dropLabelFontWeight}; +`; -const isFileIncluded = (file: FileData, fileList: FileData[]) => { - const fileListInfo = fileList.map((existingFile) => existingFile.file); - return fileListInfo.some( - ({ name, size, type, lastModified, webkitRelativePath }) => - name === file.file.name && - size === file.file.size && - type === file.file.type && - lastModified === file.file.lastModified && - webkitRelativePath === file.file.webkitRelativePath - ); -}; +const ErrorMessage = styled.div` + color: ${(props) => props.theme.errorMessageFontColor}; + font-family: ${(props) => props.theme.errorMessageFontFamily}; + font-size: ${(props) => props.theme.errorMessageFontSize}; + font-weight: ${(props) => props.theme.errorMessageFontWeight}; + line-height: ${(props) => props.theme.errorMessageLineHeight}; + margin-top: 0.25rem; +`; const DxcFileInput = forwardRef<RefType, FileInputPropsType>( ( @@ -188,7 +283,7 @@ const DxcFileInput = forwardRef<RefType, FileInputPropsType>( tabIndex={tabIndex} /> {files.length > 0 && ( - <FileItemListContainer> + <FileItemListContainer role="list"> {files.map((file, index) => ( <FileItem fileName={file.file.name} @@ -249,7 +344,7 @@ const DxcFileInput = forwardRef<RefType, FileInputPropsType>( )} </DragDropArea> {files.length > 0 && ( - <FileItemListContainer> + <FileItemListContainer role="list"> {files.map((file, index) => ( <FileItem fileName={file.file.name} @@ -276,124 +371,4 @@ const DxcFileInput = forwardRef<RefType, FileInputPropsType>( } ); -const FileInputContainer = styled.div<{ margin: FileInputPropsType["margin"] }>` - display: flex; - flex-direction: column; - margin: ${(props) => (props.margin && typeof props.margin !== "object" ? spaces[props.margin] : "0px")}; - margin-top: ${(props) => - props.margin && typeof props.margin === "object" && props.margin.top ? spaces[props.margin.top] : ""}; - margin-right: ${(props) => - props.margin && typeof props.margin === "object" && props.margin.right ? spaces[props.margin.right] : ""}; - margin-bottom: ${(props) => - props.margin && typeof props.margin === "object" && props.margin.bottom ? spaces[props.margin.bottom] : ""}; - margin-left: ${(props) => - props.margin && typeof props.margin === "object" && props.margin.left ? spaces[props.margin.left] : ""}; - width: fit-content; -`; - -const Label = styled.label<{ disabled: FileInputPropsType["disabled"] }>` - color: ${(props) => (props.disabled ? props.theme.disabledLabelFontColor : props.theme.labelFontColor)}; - font-family: ${(props) => props.theme.labelFontFamily}; - font-size: ${(props) => props.theme.labelFontSize}; - font-weight: ${(props) => props.theme.labelFontWeight}; - line-height: ${(props) => props.theme.labelLineHeight}; -`; - -const HelperText = styled.span<{ disabled: FileInputPropsType["disabled"] }>` - color: ${(props) => (props.disabled ? props.theme.disabledHelperTextFontColor : props.theme.helperTextFontColor)}; - font-family: ${(props) => props.theme.helperTextFontFamily}; - font-size: ${(props) => props.theme.helperTextFontSize}; - font-weight: ${(props) => props.theme.helperTextFontWeight}; - line-height: ${(props) => props.theme.helperTextLineHeight}; -`; - -const FileContainer = styled.div<{ singleFileMode: boolean }>` - display: flex; - ${(props) => - props.singleFileMode ? "flex-direction: row; column-gap: 0.25rem;" : "flex-direction: column; row-gap: 0.25rem;"} - margin-top: 0.25rem; -`; - -const ValueInput = styled.input` - display: none; -`; - -const FileItemListContainer = styled.div` - display: flex; - flex-direction: column; - row-gap: 0.25rem; -`; - -const Container = styled.div` - display: flex; - flex-direction: column; - row-gap: 0.25rem; - margin-top: 0.25rem; -`; - -const DragDropArea = styled.div<{ - mode: FileInputPropsType["mode"]; - disabled: FileInputPropsType["disabled"]; - isDragging: boolean; -}>` - box-sizing: border-box; - display: flex; - ${(props) => - props.mode === "filedrop" - ? "flex-direction: row; column-gap: 0.75rem; height: 48px;" - : "justify-content: center; flex-direction: column; row-gap: 0.5rem; height: 160px;"} - align-items: center; - width: 320px; - padding: ${(props) => - props.mode === "filedrop" - ? `calc(4px - ${props.theme.dropBorderThickness}) 1rem calc(4px - ${props.theme.dropBorderThickness}) calc(4px - ${props.theme.dropBorderThickness})` - : "1rem"}; - overflow: hidden; - box-shadow: 0 0 0 2px transparent; - border-radius: ${(props) => props.theme.dropBorderRadius}; - border-width: ${(props) => props.theme.dropBorderThickness}; - border-style: ${(props) => props.theme.dropBorderStyle}; - border-color: ${(props) => (props.disabled ? props.theme.disabledDropBorderColor : props.theme.dropBorderColor)}; - ${(props) => - props.isDragging && - ` - background-color: ${props.theme.dragoverDropBackgroundColor}; - border-color: transparent; - box-shadow: 0 0 0 2px ${props.theme.focusDropBorderColor}; - `} - cursor: ${(props) => props.disabled && "not-allowed"}; -`; - -const DropzoneLabel = styled.div<{ disabled: FileInputPropsType["disabled"] }>` - display: -webkit-box; - -webkit-box-orient: vertical; - overflow: hidden; - text-overflow: ellipsis; - -webkit-line-clamp: 3; - text-align: center; - color: ${(props) => (props.disabled ? props.theme.disabledDropLabelFontColor : props.theme.dropLabelFontColor)}; - font-family: ${(props) => props.theme.dropLabelFontFamily}; - font-size: ${(props) => props.theme.dropLabelFontSize}; - font-weight: ${(props) => props.theme.dropLabelFontWeight}; -`; - -const FiledropLabel = styled.span<{ disabled: FileInputPropsType["disabled"] }>` - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; - color: ${(props) => (props.disabled ? props.theme.disabledDropLabelFontColor : props.theme.dropLabelFontColor)}; - font-family: ${(props) => props.theme.dropLabelFontFamily}; - font-size: ${(props) => props.theme.dropLabelFontSize}; - font-weight: ${(props) => props.theme.dropLabelFontWeight}; -`; - -const ErrorMessage = styled.div` - color: ${(props) => props.theme.errorMessageFontColor}; - font-family: ${(props) => props.theme.errorMessageFontFamily}; - font-size: ${(props) => props.theme.errorMessageFontSize}; - font-weight: ${(props) => props.theme.errorMessageFontWeight}; - line-height: ${(props) => props.theme.errorMessageLineHeight}; - margin-top: 0.25rem; -`; - export default DxcFileInput; diff --git a/packages/lib/src/file-input/FileItem.tsx b/packages/lib/src/file-input/FileItem.tsx index 1175f483a9..02737b5006 100644 --- a/packages/lib/src/file-input/FileItem.tsx +++ b/packages/lib/src/file-input/FileItem.tsx @@ -1,62 +1,10 @@ -import { memo, useContext } from "react"; -import styled, { ThemeProvider } from "styled-components"; +import { memo, useContext, useId } from "react"; +import styled from "styled-components"; import DxcFlex from "../flex/Flex"; import { FileItemProps } from "./types"; import DxcIcon from "../icon/Icon"; import DxcActionIcon from "../action-icon/ActionIcon"; -import HalstackContext, { HalstackLanguageContext } from "../HalstackContext"; - -const FileItem = ({ - fileName = "", - error = "", - singleFileMode, - showPreview, - preview, - type, - onDelete, - tabIndex, -}: FileItemProps): JSX.Element => { - const colorsTheme = useContext(HalstackContext); - const translatedLabels = useContext(HalstackLanguageContext); - - const getIconAriaLabel = () => (type.includes("video") ? "video" : type.includes("audio") ? "audio" : "file"); - - return ( - <ThemeProvider theme={colorsTheme.fileInput}> - <MainContainer error={error} singleFileMode={singleFileMode} showPreview={showPreview}> - {showPreview && - (type.includes("image") ? ( - <ImagePreview src={preview} alt={fileName} /> - ) : ( - <IconPreview error={error} role="document" aria-label={getIconAriaLabel()}> - <DxcIcon icon={preview} /> - </IconPreview> - ))} - <FileItemContent> - <FileName>{fileName}</FileName> - <DxcFlex gap="0.25rem"> - {error && ( - <ErrorIcon> - <DxcIcon icon="filled_error" /> - </ErrorIcon> - )} - <DxcActionIcon - onClick={() => onDelete(fileName)} - icon="close" - tabIndex={tabIndex} - title={translatedLabels.fileInput.deleteFileActionTitle} - /> - </DxcFlex> - {error && !singleFileMode && ( - <ErrorMessage role="alert" aria-live="assertive"> - {error} - </ErrorMessage> - )} - </FileItemContent> - </MainContainer> - </ThemeProvider> - ); -}; +import { HalstackLanguageContext } from "../HalstackContext"; const MainContainer = styled.div<{ error: FileItemProps["error"]; @@ -144,4 +92,52 @@ const ErrorMessage = styled.span` line-height: ${(props) => props.theme.errorMessageLineHeight}; `; +const FileItem = ({ + fileName = "", + error = "", + singleFileMode, + showPreview, + preview, + type, + onDelete, + tabIndex, +}: FileItemProps): JSX.Element => { + const translatedLabels = useContext(HalstackLanguageContext); + const fileNameId = useId(); + + return ( + <MainContainer error={error} role="listitem" singleFileMode={singleFileMode} showPreview={showPreview}> + {showPreview && + (type.includes("image") ? ( + <ImagePreview src={preview} alt={fileName} /> + ) : ( + <IconPreview aria-labelledby={fileNameId} error={error} role="img"> + <DxcIcon icon={preview} /> + </IconPreview> + ))} + <FileItemContent> + <FileName id={fileNameId}>{fileName}</FileName> + <DxcFlex gap="0.25rem"> + {error && ( + <ErrorIcon> + <DxcIcon icon="filled_error" /> + </ErrorIcon> + )} + <DxcActionIcon + onClick={() => onDelete(fileName)} + icon="close" + tabIndex={tabIndex} + title={translatedLabels.fileInput.deleteFileActionTitle} + /> + </DxcFlex> + {error && !singleFileMode && ( + <ErrorMessage role="alert" aria-live="assertive"> + {error} + </ErrorMessage> + )} + </FileItemContent> + </MainContainer> + ); +}; + export default memo(FileItem); diff --git a/packages/lib/src/file-input/utils.ts b/packages/lib/src/file-input/utils.ts new file mode 100644 index 0000000000..7b76761e73 --- /dev/null +++ b/packages/lib/src/file-input/utils.ts @@ -0,0 +1,27 @@ +import { FileData } from "./types"; + +export const getFilePreview = async (file: File): Promise<string> => { + if (file.type.includes("video")) return "filled_movie"; + else if (file.type.includes("audio")) return "music_video"; + else if (file.type.includes("image")) { + return new Promise<string>((resolve) => { + const reader = new FileReader(); + reader.readAsDataURL(file); + reader.onload = (e) => { + resolve(e.target?.result as string); + }; + }); + } else return "draft"; +}; + +export const isFileIncluded = (file: FileData, fileList: FileData[]) => { + const fileListInfo = fileList.map((existingFile) => existingFile.file); + return fileListInfo.some( + ({ name, size, type, lastModified, webkitRelativePath }) => + name === file.file.name && + size === file.file.size && + type === file.file.type && + lastModified === file.file.lastModified && + webkitRelativePath === file.file.webkitRelativePath + ); +}; \ No newline at end of file diff --git a/packages/lib/src/header/Header.stories.tsx b/packages/lib/src/header/Header.stories.tsx index e7b0ccf14a..4c76a3b6d7 100644 --- a/packages/lib/src/header/Header.stories.tsx +++ b/packages/lib/src/header/Header.stories.tsx @@ -57,7 +57,7 @@ const opinionatedTheme = { accentColor: "#000000", fontColor: "#000000", menuBaseColor: "#ffffff", - hamburguerColor: "#000000", + hamburgerColor: "#000000", logo: "https://upload.wikimedia.org/wikipedia/commons/thumb/b/b8/2021_Facebook_icon.svg/2048px-2021_Facebook_icon.svg.png", logoResponsive: "https://upload.wikimedia.org/wikipedia/commons/thumb/b/b8/2021_Facebook_icon.svg/2048px-2021_Facebook_icon.svg.png", diff --git a/packages/lib/src/header/Header.tsx b/packages/lib/src/header/Header.tsx index ba23eec1de..2dd9784283 100644 --- a/packages/lib/src/header/Header.tsx +++ b/packages/lib/src/header/Header.tsx @@ -115,7 +115,7 @@ const DxcHeader = ({ <ChildContainer> <HamburgerTrigger tabIndex={tabIndex} onClick={handleMenu} aria-label="Show options"> <DxcIcon icon="menu" /> - {translatedLabels.header.hamburguerTitle} + {translatedLabels.header.hamburgerTitle} </HamburgerTrigger> </ChildContainer> <ResponsiveMenu hasVisibility={isMenuVisible}> @@ -224,23 +224,23 @@ const HamburgerTrigger = styled.button` border-radius: 2px; background-color: transparent; :hover { - background-color: ${(props) => props.theme.hamburguerHoverColor}; + background-color: ${(props) => props.theme.hamburgerHoverColor}; } &:focus { - outline: ${(props) => props.theme.hamburguerFocusColor} auto 1px; + outline: ${(props) => props.theme.hamburgerFocusColor} auto 1px; } & > svg { - fill: ${(props) => props.theme.hamburguerIconColor}; + fill: ${(props) => props.theme.hamburgerIconColor}; } & > span { font-size: 24px; } - font-family: ${(props) => props.theme.hamburguerFontFamily}; - font-style: ${(props) => props.theme.hamburguerFontStyle}; - font-size: ${(props) => props.theme.hamburguerFontSize}; - text-transform: ${(props) => props.theme.hamburguerTextTransform}; - font-weight: ${(props) => props.theme.hamburguerFontWeight}; - color: ${(props) => props.theme.hamburguerFontColor}; + font-family: ${(props) => props.theme.hamburgerFontFamily}; + font-style: ${(props) => props.theme.hamburgerFontStyle}; + font-size: ${(props) => props.theme.hamburgerFontSize}; + text-transform: ${(props) => props.theme.hamburgerTextTransform}; + font-weight: ${(props) => props.theme.hamburgerFontWeight}; + color: ${(props) => props.theme.hamburgerFontColor}; `; const ResponsiveMenu = styled.div<{ hasVisibility: boolean }>` @@ -288,7 +288,7 @@ const CloseAction = styled.button` :focus, :focus-visible { - outline: ${(props) => props.theme.hamburguerFocusColor} auto 1px; + outline: ${(props) => props.theme.hamburgerFocusColor} auto 1px; } font-size: 24px; svg { diff --git a/packages/lib/src/icon/Icon.tsx b/packages/lib/src/icon/Icon.tsx index d49b99db7a..cc2b977e6f 100644 --- a/packages/lib/src/icon/Icon.tsx +++ b/packages/lib/src/icon/Icon.tsx @@ -3,7 +3,6 @@ import styled from "styled-components"; const DxcIcon = ({ icon }: { icon: string }): JSX.Element => ( <IconContainer role="img" - aria-label={icon} filled={icon.startsWith("filled_")} icon={icon.startsWith("filled_") ? icon.replace(/filled_/g, "") : icon} aria-hidden="true" diff --git a/packages/lib/src/progress-bar/ProgressBar.stories.tsx b/packages/lib/src/progress-bar/ProgressBar.stories.tsx index 7e078f4cdd..60e9f7feec 100644 --- a/packages/lib/src/progress-bar/ProgressBar.stories.tsx +++ b/packages/lib/src/progress-bar/ProgressBar.stories.tsx @@ -22,14 +22,16 @@ const opinionatedTheme = { const ProgressBar = () => ( <> <ExampleContainer> - <Title title="Without labels" theme="light" level={4} /> + <Title title="Default" theme="light" level={4} /> + <DxcProgressBar /> + <Title title="Value only" theme="light" level={4} /> <DxcProgressBar value={50} showValue /> <Title title="With helperText" theme="light" level={4} /> <DxcProgressBar helperText="Helper text" value={24} showValue /> - <Title title="Without default value" theme="light" level={4} /> - <DxcProgressBar label="Loading..." showValue /> - <Title title="With full value" theme="light" level={4} /> - <DxcProgressBar label="Loading..." value={100} showValue /> + <Title title="Label only" theme="light" level={4} /> + <DxcProgressBar label="Loading..." /> + <Title title="Complete progress bar" theme="light" level={4} /> + <DxcProgressBar label="Loading..." value={100} showValue helperText="Helper text" /> </ExampleContainer> <Title title="Margins" theme="light" level={2} /> <ExampleContainer> diff --git a/packages/lib/src/progress-bar/ProgressBar.test.tsx b/packages/lib/src/progress-bar/ProgressBar.test.tsx index c62c6302bb..e470277bcc 100644 --- a/packages/lib/src/progress-bar/ProgressBar.test.tsx +++ b/packages/lib/src/progress-bar/ProgressBar.test.tsx @@ -2,59 +2,38 @@ import { render } from "@testing-library/react"; import DxcProgressBar from "./ProgressBar"; describe("ProgressBar component tests", () => { - test("ProgressBar renders with label and helperText", () => { - const { getByText } = render(<DxcProgressBar label="test-label" helperText="helper-text"></DxcProgressBar>); - expect(getByText("test-label")).toBeTruthy(); - expect(getByText("helper-text")).toBeTruthy(); - }); - - test("ProgressBar renders with default value", () => { - const value = 0; - const { getByText, getByRole } = render(<DxcProgressBar showValue></DxcProgressBar>); + test("ProgressBar renders no value when it is indeterminate", () => { + const { queryByText, getByRole } = render(<DxcProgressBar showValue />); const progressBar = getByRole("progressbar"); - expect(getByText("0 %")).toBeTruthy(); - expect(progressBar.getAttribute("aria-valuenow")).toEqual(value.toString()); + expect(queryByText("0 %")).toBeFalsy(); + expect(progressBar.getAttribute("aria-valuenow")).toBeNull(); }); - - test("ProgressBar renders with value", () => { - const value = 25; - const { getByText, getByRole } = render(<DxcProgressBar showValue value={value}></DxcProgressBar>); + test("ProgressBar renders with value when it is determinate and has the flag showValue set to true", () => { + const { getByText, getByRole } = render(<DxcProgressBar showValue value={25} />); const progressBar = getByRole("progressbar"); expect(getByText("25 %")).toBeTruthy(); - expect(progressBar.getAttribute("aria-valuenow")).toEqual(value.toString()); + expect(progressBar.getAttribute("aria-valuenow")).toEqual("25"); + }); + test("ProgressBar doesn't render with value when it is determinate but has the flag showValue set to false", () => { + const { queryByText, getByRole } = render(<DxcProgressBar value={25} />); + const progressBar = getByRole("progressbar"); + expect(queryByText("25 %")).toBeFalsy(); + expect(progressBar.getAttribute("aria-valuenow")).toEqual("25"); }); - test("ProgressBar renders with negative value", () => { - const value = 0; - const { getByText, getByRole } = render(<DxcProgressBar showValue value={-20}></DxcProgressBar>); + const { getByText, getByRole } = render(<DxcProgressBar showValue value={-20} />); const progressBar = getByRole("progressbar"); expect(getByText("0 %")).toBeTruthy(); - expect(progressBar.getAttribute("aria-valuenow")).toEqual(value.toString()); + expect(progressBar.getAttribute("aria-valuenow")).toEqual("0"); }); - test("ProgressBar renders as indeterminate", () => { - const { getByRole } = render(<DxcProgressBar></DxcProgressBar>); - const progressBar = getByRole("progressbar"); - expect(progressBar.getAttribute("aria-valuenow")).toBe(null); - }); - - test("Overlay progressBar renders with label and helperText", () => { - const { getByText } = render(<DxcProgressBar label="test-label" helperText="helper-text" overlay></DxcProgressBar>); - expect(getByText("test-label")).toBeTruthy(); - expect(getByText("helper-text")).toBeTruthy(); - }); - - test("Overlay progressBar renders with default value", () => { - const value = 0; - const { getByText, getByRole } = render(<DxcProgressBar showValue overlay></DxcProgressBar>); + const { getByRole } = render(<DxcProgressBar />); const progressBar = getByRole("progressbar"); - expect(getByText("0 %")).toBeTruthy(); - expect(progressBar.getAttribute("aria-valuenow")).toEqual(value.toString()); + expect(progressBar.getAttribute("aria-valuenow")).toBeNull(); }); - test("Overlay progressBar renders with value", () => { const value = 25; - const { getByText, getByRole } = render(<DxcProgressBar showValue value={value} overlay></DxcProgressBar>); + const { getByText, getByRole } = render(<DxcProgressBar showValue value={value} overlay />); const progressBar = getByRole("progressbar"); expect(getByText("25 %")).toBeTruthy(); expect(progressBar.getAttribute("aria-valuenow")).toEqual(value.toString()); diff --git a/packages/lib/src/progress-bar/ProgressBar.tsx b/packages/lib/src/progress-bar/ProgressBar.tsx index 5b0bb23f25..2c6170ac19 100644 --- a/packages/lib/src/progress-bar/ProgressBar.tsx +++ b/packages/lib/src/progress-bar/ProgressBar.tsx @@ -1,58 +1,11 @@ -import { useContext, useEffect, useState } from "react"; +import { useContext, useEffect, useId, useState } from "react"; import styled, { ThemeProvider } from "styled-components"; import { spaces } from "../common/variables"; import HalstackContext from "../HalstackContext"; import ProgressBarPropsType from "./types"; +import DxcFlex from "../flex/Flex"; -const DxcProgressBar = ({ - label = "", - helperText = "", - overlay = false, - value, - showValue = false, - margin, -}: ProgressBarPropsType): JSX.Element => { - const colorsTheme = useContext(HalstackContext); - const [valueProgressBar, setValueProgressBar] = useState(0); - - useEffect(() => { - setValueProgressBar( - value === null || value === undefined || value < 0 ? 0 : value >= 0 && value <= 100 ? value : 100 - ); - }, [value]); - - return ( - <ThemeProvider theme={colorsTheme.progressBar}> - <BackgroundProgressBar overlay={overlay}> - <ProgressBarContainer overlay={overlay} margin={margin}> - <InfoProgressBar> - <ProgressBarLabel overlay={overlay}>{label}</ProgressBarLabel> - <ProgressBarProgress overlay={overlay} showValue={showValue} value={valueProgressBar}> - {valueProgressBar} % - </ProgressBarProgress> - </InfoProgressBar> - <LinearProgress - role="progressbar" - helperText={helperText} - aria-valuenow={showValue ? valueProgressBar : undefined} - aria-valuemin={0} - aria-valuemax={100} - aria-label={label || "Progress Bar"} - > - <LinearProgressBar - variant={value === null || value === undefined ? "indeterminate" : "determinate"} - container="first" - value={valueProgressBar} - ></LinearProgressBar> - </LinearProgress> - {helperText && <HelperText overlay={overlay}>{helperText}</HelperText>} - </ProgressBarContainer> - </BackgroundProgressBar> - </ThemeProvider> - ); -}; - -const BackgroundProgressBar = styled.div<{ +const Overlay = styled.div<{ overlay: ProgressBarPropsType["overlay"]; }>` ${({ overlay, theme }) => @@ -69,19 +22,18 @@ const BackgroundProgressBar = styled.div<{ left: 0; right: 0; z-index: 1300;` - : `background-color: "transparent";`} + : `background-color: transparent;`} display: flex; flex-wrap: wrap; min-width: 100px; width: 100%; `; -const ProgressBarContainer = styled.div<{ +const MainContainer = styled.div<{ overlay: ProgressBarPropsType["overlay"]; margin: ProgressBarPropsType["margin"]; }>` - z-index: ${(props) => (props.overlay === true && "100") || "0"}; - width: ${(props) => (props.overlay === true ? "80%" : "100%")}; + width: ${(props) => (props.overlay ? "80%" : "100%")}; 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] : ""}; @@ -91,17 +43,10 @@ const ProgressBarContainer = styled.div<{ props.margin && typeof props.margin === "object" && props.margin.bottom ? spaces[props.margin.bottom] : ""}; margin-left: ${(props) => props.margin && typeof props.margin === "object" && props.margin.left ? spaces[props.margin.left] : ""}; -`; - -const InfoProgressBar = styled.div` display: flex; - flex-direction: row; - width: 685px; - flex-wrap: wrap; - width: 100%; - margin-bottom: 8px; - align-items: baseline; - justify-content: space-between; + flex-direction: column; + gap: 0.5rem; + z-index: ${(props) => props.overlay ? "100" : "0"}; `; const ProgressBarLabel = styled.div<{ @@ -112,7 +57,7 @@ const ProgressBarLabel = styled.div<{ font-size: ${(props) => props.theme.labelFontSize}; font-weight: ${(props) => props.theme.labelFontWeight}; text-transform: ${(props) => props.theme.labelFontTextTransform}; - color: ${(props) => (props.overlay === true ? props.theme.overlayFontColor : props.theme.labelFontColor)}; + color: ${(props) => (props.overlay ? props.theme.overlayFontColor : props.theme.labelFontColor)}; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; @@ -121,22 +66,18 @@ const ProgressBarLabel = styled.div<{ const ProgressBarProgress = styled.div<{ overlay: ProgressBarPropsType["overlay"]; - showValue: ProgressBarPropsType["showValue"]; - value: ProgressBarPropsType["value"]; }>` + flex-shrink: 0; + color: ${(props) => (props.overlay ? props.theme.overlayFontColor : props.theme.valueFontColor)}; font-family: ${(props) => props.theme.valueFontFamily}; font-style: ${(props) => props.theme.valueFontStyle}; font-size: ${(props) => props.theme.valueFontSize}; font-weight: ${(props) => props.theme.valueFontWeight}; text-transform: ${(props) => props.theme.valueFontTextTransform}; - color: ${(props) => (props.overlay === true ? props.theme.overlayFontColor : props.theme.valueFontColor)}; - display: ${(props) => - (props.value !== undefined && props.value !== null && props.showValue === true && "block") || "none"}; - flex-shrink: 0; `; const HelperText = styled.span<{ overlay: ProgressBarPropsType["overlay"] }>` - color: ${(props) => (props.overlay === true ? props.theme.overlayFontColor : props.theme.helperTextFontColor)}; + color: ${(props) => (props.overlay ? props.theme.overlayFontColor : props.theme.helperTextFontColor)}; font-family: ${(props) => props.theme.helperTextFontFamily}; font-size: ${(props) => props.theme.helperTextFontSize}; font-style: ${(props) => props.theme.helperTextFontStyle}; @@ -147,34 +88,29 @@ const HelperText = styled.span<{ overlay: ProgressBarPropsType["overlay"] }>` const LinearProgress = styled.div<{ helperText: ProgressBarPropsType["helperText"]; }>` + position: relative; + border-radius: ${(props) => props.theme.borderRadius}; height: ${(props) => props.theme.thickness}; background-color: ${(props) => props.theme.totalLineColor}; - border-radius: ${(props) => props.theme.borderRadius}; - margin-bottom: ${(props) => props.helperText !== "" && "8px"}; overflow: hidden; - position: relative; `; const LinearProgressBar = styled.span<{ variant: "determinate" | "indeterminate"; value: ProgressBarPropsType["value"]; - container: string; }>` - background-color: ${(props) => props.theme.trackLineColor}; - transform: ${(props) => `translateX(-${props.variant === "determinate" ? 100 - (props.value ?? 0) : 0}%)`}; + position: absolute; top: 0; - left: 0; - width: 100%; bottom: 0; - position: absolute; + left: 0; + width: ${(props) => props.variant === "indeterminate" ? "auto" : "100%"}; + transform: ${(props) => `translateX(-${props.variant === "determinate" ? 100 - (props.value ?? 0) : 0}%)`}; transition: ${(props) => (props.variant === "determinate" ? "transform .4s linear" : "transform 0.2s linear")}; transform-origin: left; - ${(props) => props.variant === "indeterminate" && "width: auto;"}; + background-color: ${(props) => props.theme.trackLineColor}; ${(props) => props.variant === "indeterminate" - ? props.container === "first" - ? "animation: keyframes-indeterminate-first 2.1s cubic-bezier(0.65, 0.815, 0.735, 0.395) infinite;" - : "animation: keyframes-indeterminate-second 2.1s cubic-bezier(0.165, 0.84, 0.44, 1) 1.15s infinite;" + ? "animation: keyframes-indeterminate-first 2.1s cubic-bezier(0.65, 0.815, 0.735, 0.395) infinite;" : ""}; @keyframes keyframes-indeterminate-first { @@ -208,4 +144,52 @@ const LinearProgressBar = styled.span<{ } `; +const DxcProgressBar = ({ + label, + helperText, + overlay, + value, + showValue, + margin, +}: ProgressBarPropsType): JSX.Element => { + const colorsTheme = useContext(HalstackContext); + const labelId = `label-${useId()}`; + const [innerValue, setInnerValue] = useState<number | undefined>(); + + useEffect(() => { + if (value != null) setInnerValue(value < 0 ? 0 : value > 100 ? 100 : value); + }, [value]); + + return ( + <ThemeProvider theme={colorsTheme.progressBar}> + <Overlay overlay={overlay}> + <MainContainer overlay={overlay} margin={margin}> + <DxcFlex justifyContent="space-between" gap="0.5rem"> + {label && ( + <ProgressBarLabel id={labelId} overlay={overlay}> + {label} + </ProgressBarLabel> + )} + {innerValue != null && showValue && ( + <ProgressBarProgress overlay={overlay}>{innerValue} %</ProgressBarProgress> + )} + </DxcFlex> + <LinearProgress + role="progressbar" + helperText={helperText} + aria-label="Progress bar" + aria-labelledby={labelId} + aria-valuenow={innerValue} + aria-valuemin={0} + aria-valuemax={100} + > + <LinearProgressBar variant={innerValue == null ? "indeterminate" : "determinate"} value={innerValue} /> + </LinearProgress> + {helperText && <HelperText overlay={overlay}>{helperText}</HelperText>} + </MainContainer> + </Overlay> + </ThemeProvider> + ); +}; + export default DxcProgressBar; diff --git a/packages/lib/src/slider/Slider.tsx b/packages/lib/src/slider/Slider.tsx index 58abe0bf63..f0e868b79d 100644 --- a/packages/lib/src/slider/Slider.tsx +++ b/packages/lib/src/slider/Slider.tsx @@ -6,150 +6,6 @@ import { getMargin } from "../common/utils"; import HalstackContext from "../HalstackContext"; import SliderPropsType, { RefType } from "./types"; -const DxcSlider = forwardRef<RefType, SliderPropsType>( - ( - { - label = "", - name = "", - defaultValue, - value, - helperText = "", - minValue = 0, - maxValue = 100, - step = 1, - showLimitsValues = false, - showInput = false, - disabled = false, - marks = false, - onChange, - onDragEnd, - labelFormatCallback, - margin, - size = "fillParent", - }, - ref - ): JSX.Element => { - const labelId = `label-${useId()}`; - const [innerValue, setInnerValue] = useState(defaultValue ?? 0); - const [dragging, setDragging] = useState(false); - const colorsTheme = useContext(HalstackContext); - const isFirefox = navigator.userAgent.indexOf("Firefox") !== -1; - - const minLabel = useMemo( - () => (labelFormatCallback ? labelFormatCallback(minValue) : minValue), - [labelFormatCallback, minValue] - ); - - const maxLabel = useMemo( - () => (labelFormatCallback ? labelFormatCallback(maxValue) : maxValue), - [labelFormatCallback, maxValue] - ); - - const tickMarks = useMemo(() => { - const numberOfMarks = Math.floor(maxValue / step - minValue / step); - const range = maxValue - minValue; - const ticks = []; - - if (marks) { - for (let index = 0; index <= numberOfMarks; index++) { - ticks.push( - <TickMark - disabled={disabled} - stepPosition={(step * index) / range} - stepValue={(value ?? innerValue) / maxValue} - key={`tickmark-${index}-${labelId}`} - /> - ); - } - return ticks; - } - return null; - }, [minValue, maxValue, step, value, innerValue]); - - const handleSliderChange = (event: ChangeEvent<HTMLInputElement>) => { - const intValue = parseInt(event.target.value, 10); - if (intValue !== value || intValue !== innerValue) { - setInnerValue(intValue); - } - onChange?.(intValue); - }; - - const handleSliderDragging = () => { - setDragging(true); - }; - - const handleSliderOnChangeCommitted = (event: MouseEvent<HTMLInputElement>) => { - const intValue = parseInt((event.target as HTMLInputElement).value, 10); - if (dragging) { - setDragging(false); - onDragEnd?.(intValue); - } - }; - - const handlerInputChange = (event: { value: string; error?: string }) => { - const intValue = parseInt(event.value, 10); - if (value == null) { - if (!Number.isNaN(intValue)) { - setInnerValue(intValue > maxValue ? maxValue : intValue); - } - } - if (!Number.isNaN(intValue)) { - onChange?.(intValue > maxValue ? maxValue : intValue); - } - }; - - return ( - <ThemeProvider theme={colorsTheme.slider}> - <Container margin={margin} size={size} ref={ref}> - <Label id={labelId} disabled={disabled}> - {label} - </Label> - <HelperText disabled={disabled}>{helperText}</HelperText> - <SliderContainer> - {showLimitsValues && <MinLabelContainer disabled={disabled}>{minLabel}</MinLabelContainer>} - <SliderInputContainer> - <SliderInput - role="slider" - type="range" - value={value != null && value >= 0 ? value : innerValue} - min={minValue} - max={maxValue} - step={step} - disabled={disabled} - aria-labelledby={labelId} - aria-orientation="horizontal" - aria-valuemax={maxValue} - aria-valuemin={minValue} - aria-valuenow={value != null && value >= 0 ? value : innerValue} - onChange={handleSliderChange} - onMouseUp={handleSliderOnChangeCommitted} - onMouseDown={handleSliderDragging} - /> - {marks && <MarksContainer isFirefox={isFirefox}>{tickMarks}</MarksContainer>} - </SliderInputContainer> - {showLimitsValues && ( - <MaxLabelContainer disabled={disabled} step={step}> - {maxLabel} - </MaxLabelContainer> - )} - {showInput && ( - <StyledTextInput> - <DxcTextInput - name={name} - value={value != null && value >= 0 ? value.toString() : innerValue.toString()} - disabled={disabled} - onChange={handlerInputChange} - size="fillParent" - /> - </StyledTextInput> - )} - </SliderContainer> - </Container> - </ThemeProvider> - ); - } -); - const sizes = { medium: "360px", large: "480px", @@ -165,7 +21,7 @@ const getChromeStyles = () => ` width: 100%; margin-right: 4px;`; -const getFireFoxStyles = () => ` +const getFirefoxStyles = () => ` width: calc(100% - 16px); margin-right: 3px;`; @@ -315,7 +171,6 @@ const LimitLabelContainer = styled.span<{ disabled: SliderPropsType["disabled"]; }>` color: ${(props) => (props.disabled ? props.theme.disabledLimitValuesFontColor : props.theme.limitValuesFontColor)}; - font-family: ${(props) => props.theme.fontFamily}; font-size: ${(props) => props.theme.limitValuesFontSize}; font-style: ${(props) => props.theme.limitValuesFontStyle}; @@ -345,7 +200,7 @@ const SliderInputContainer = styled.div` `; const MarksContainer = styled.div<{ isFirefox: boolean }>` - ${(props) => (props.isFirefox ? getFireFoxStyles() : getChromeStyles())} + ${(props) => (props.isFirefox ? getFirefoxStyles() : getChromeStyles())} position: absolute; pointer-events: none; height: 100%; @@ -368,9 +223,149 @@ const TickMark = styled.span<{ z-index: ${(props) => props.stepValue != null && `${props.stepPosition <= props.stepValue ? "-1" : "0"}`}; `; -const StyledTextInput = styled.div` +const TextInputContainer = styled.div` margin-left: ${(props) => props.theme.inputMarginLeft}; max-width: 70px; `; +const DxcSlider = forwardRef<RefType, SliderPropsType>( + ( + { + label = "", + name = "", + defaultValue, + value, + helperText = "", + minValue = 0, + maxValue = 100, + step = 1, + showLimitsValues = false, + showInput = false, + disabled = false, + marks = false, + onChange, + onDragEnd, + labelFormatCallback, + margin, + size = "fillParent", + }, + ref + ): JSX.Element => { + const labelId = `label-${useId()}`; + const [innerValue, setInnerValue] = useState(defaultValue ?? 0); + const [dragging, setDragging] = useState(false); + const colorsTheme = useContext(HalstackContext); + const isFirefox = navigator.userAgent.indexOf("Firefox") !== -1; + + const minLabel = useMemo( + () => (labelFormatCallback ? labelFormatCallback(minValue) : minValue), + [labelFormatCallback, minValue] + ); + + const maxLabel = useMemo( + () => (labelFormatCallback ? labelFormatCallback(maxValue) : maxValue), + [labelFormatCallback, maxValue] + ); + + const tickMarks = useMemo(() => { + const numberOfMarks = Math.floor(maxValue / step - minValue / step); + const range = maxValue - minValue; + const ticks = []; + + if (marks) { + for (let index = 0; index <= numberOfMarks; index++) { + ticks.push( + <TickMark + disabled={disabled} + stepPosition={(step * index) / range} + stepValue={(value ?? innerValue) / maxValue} + key={`tickmark-${index}`} + /> + ); + } + return ticks; + } + return null; + }, [minValue, maxValue, step, value, innerValue]); + + const handleSliderChange = (event: ChangeEvent<HTMLInputElement>) => { + const intValue = parseInt(event.target.value, 10); + if (intValue !== value || intValue !== innerValue) { + setInnerValue(intValue); + } + onChange?.(intValue); + }; + + const handleSliderDragging = () => { + setDragging(true); + }; + + const handleSliderOnChangeCommitted = (event: MouseEvent<HTMLInputElement>) => { + const intValue = parseInt((event.target as HTMLInputElement).value, 10); + if (dragging) { + setDragging(false); + onDragEnd?.(intValue); + } + }; + + const handlerInputChange = (event: { value: string; error?: string }) => { + const intValue = parseInt(event.value, 10); + if (!Number.isNaN(intValue)) { + if (value == null) setInnerValue(intValue > maxValue ? maxValue : intValue); + onChange?.(intValue > maxValue ? maxValue : intValue); + } + }; + + return ( + <ThemeProvider theme={colorsTheme.slider}> + <Container margin={margin} size={size} ref={ref}> + <Label id={labelId} disabled={disabled}> + {label} + </Label> + <HelperText disabled={disabled}>{helperText}</HelperText> + <SliderContainer> + {showLimitsValues && <MinLabelContainer disabled={disabled}>{minLabel}</MinLabelContainer>} + <SliderInputContainer> + <SliderInput + role="slider" + type="range" + value={value != null && value >= 0 ? value : innerValue} + min={minValue} + max={maxValue} + step={step} + disabled={disabled} + aria-labelledby={labelId} + aria-orientation="horizontal" + aria-valuemax={maxValue} + aria-valuemin={minValue} + aria-valuenow={value != null && value >= 0 ? value : innerValue} + onChange={handleSliderChange} + onMouseUp={handleSliderOnChangeCommitted} + onMouseDown={handleSliderDragging} + /> + {marks && <MarksContainer isFirefox={isFirefox}>{tickMarks}</MarksContainer>} + </SliderInputContainer> + {showLimitsValues && ( + <MaxLabelContainer disabled={disabled} step={step}> + {maxLabel} + </MaxLabelContainer> + )} + {showInput && ( + <TextInputContainer> + <DxcTextInput + name={name} + value={value != null && value >= 0 ? value.toString() : innerValue.toString()} + disabled={disabled} + onChange={handlerInputChange} + size="fillParent" + /> + </TextInputContainer> + )} + </SliderContainer> + </Container> + </ThemeProvider> + ); + } +); + export default DxcSlider; diff --git a/packages/lib/src/switch/Switch.tsx b/packages/lib/src/switch/Switch.tsx index 1666c59294..544e78f623 100644 --- a/packages/lib/src/switch/Switch.tsx +++ b/packages/lib/src/switch/Switch.tsx @@ -8,7 +8,7 @@ import SwitchPropsType, { RefType } from "./types"; const DxcSwitch = forwardRef<RefType, SwitchPropsType>( ( { - defaultChecked, + defaultChecked = false, checked, value, label = "", @@ -25,7 +25,7 @@ const DxcSwitch = forwardRef<RefType, SwitchPropsType>( ): JSX.Element => { const switchId = `switch-${useId()}`; const labelId = `label-${switchId}`; - const [innerChecked, setInnerChecked] = useState(defaultChecked ?? false); + const [innerChecked, setInnerChecked] = useState(defaultChecked); const colorsTheme = useContext(HalstackContext); const translatedLabels = useContext(HalstackLanguageContext); diff --git a/packages/lib/src/text-input/Suggestion.tsx b/packages/lib/src/text-input/Suggestion.tsx index 297300f92a..f5fcd8c00c 100644 --- a/packages/lib/src/text-input/Suggestion.tsx +++ b/packages/lib/src/text-input/Suggestion.tsx @@ -1,19 +1,40 @@ import { memo, useMemo } from "react"; import styled from "styled-components"; import { SuggestionProps } from "./types"; +import { transformSpecialChars } from "./utils"; -const transformSpecialChars = (str: string) => { - const specialCharsRegex = /[\\*()[\]{}+?/]/; - let value = str; - if (specialCharsRegex.test(value)) { - const regexAsString = specialCharsRegex.toString().split(""); - const uniqueSpecialChars = regexAsString.filter((item, index) => regexAsString.indexOf(item) === index); - uniqueSpecialChars.forEach((specialChar) => { - if (str.includes(specialChar)) value = value.replace(specialChar, "\\" + specialChar); - }); +const SuggestionContainer = styled.li<{ + visuallyFocused: SuggestionProps["visuallyFocused"]; +}>` + display: flex; + padding: 0 0.5rem; + line-height: 1.715em; + cursor: pointer; + box-shadow: inset 0 0 0 2px + ${(props) => (props.visuallyFocused ? props.theme.focusListOptionBorderColor : "transparent")}; + + &:hover { + background-color: ${(props) => props.theme.hoverListOptionBackgroundColor}; } - return value; -}; + &:active { + background-color: ${(props) => props.theme.activeListOptionBackgroundColor}; + } +`; + +const StyledSuggestion = styled.span<{ + visuallyFocused: SuggestionProps["visuallyFocused"]; + isLast: SuggestionProps["isLast"]; +}>` + width: 100%; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + padding: 0.25rem 0.5rem 0.188rem 0.5rem; + ${(props) => + props.isLast || props.visuallyFocused + ? `border-bottom: 1px solid transparent` + : `border-bottom: 1px solid ${props.theme.listOptionDividerColor}`}; +`; const Suggestion = ({ id, @@ -53,37 +74,4 @@ const Suggestion = ({ ); }; -const SuggestionContainer = styled.li<{ - visuallyFocused: SuggestionProps["visuallyFocused"]; -}>` - display: flex; - padding: 0 0.5rem; - line-height: 1.715em; - cursor: pointer; - box-shadow: inset 0 0 0 2px - ${(props) => (props.visuallyFocused ? props.theme.focusListOptionBorderColor : "transparent")}; - - &:hover { - background-color: ${(props) => props.theme.hoverListOptionBackgroundColor}; - } - &:active { - background-color: ${(props) => props.theme.activeListOptionBackgroundColor}; - } -`; - -const StyledSuggestion = styled.span<{ - visuallyFocused: SuggestionProps["visuallyFocused"]; - isLast: SuggestionProps["isLast"]; -}>` - width: 100%; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - padding: 0.25rem 0.5rem 0.188rem 0.5rem; - ${(props) => - props.isLast || props.visuallyFocused - ? `border-bottom: 1px solid transparent` - : `border-bottom: 1px solid ${props.theme.listOptionDividerColor}`}; -`; - export default memo(Suggestion); diff --git a/packages/lib/src/text-input/Suggestions.tsx b/packages/lib/src/text-input/Suggestions.tsx index f3fd79e064..d78912df20 100644 --- a/packages/lib/src/text-input/Suggestions.tsx +++ b/packages/lib/src/text-input/Suggestions.tsx @@ -5,6 +5,52 @@ import Suggestion from "./Suggestion"; import { SuggestionsProps } from "./types"; import DxcIcon from "../icon/Icon"; +const SuggestionsContainer = styled.ul<{ error: boolean }>` + box-sizing: border-box; + max-height: 304px; + overflow-y: auto; + margin: 0; + padding: 0.25rem 0; + background-color: ${(props) => + props.error ? props.theme.errorListDialogBackgroundColor : props.theme.listDialogBackgroundColor}; + border: 1px solid + ${(props) => (props.error ? props.theme.errorListDialogBorderColor : props.theme.listDialogBorderColor)}; + + border-radius: 0.25rem; + box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1); + color: ${(props) => props.theme.listOptionFontColor}; + font-family: ${(props) => props.theme.fontFamily}; + font-size: ${(props) => props.theme.listOptionFontSize}; + font-style: ${(props) => props.theme.listOptionFontStyle}; + font-weight: ${(props) => props.theme.listOptionFontWeight}; +`; + +const SuggestionsSystemMessage = styled.span` + display: flex; + padding: 0.25rem 1rem; + color: ${(props) => props.theme.systemMessageFontColor}; + line-height: 1.715em; +`; + +const SuggestionsErrorIcon = styled.span` + display: flex; + flex-wrap: wrap; + align-content: center; + margin-right: 0.5rem; + height: 18px; + width: 18px; + font-size: 18px; + color: ${(props) => props.theme.errorIconColor}; +`; + +const SuggestionsError = styled.span` + display: flex; + padding: 0.25rem 1rem; + align-items: center; + line-height: 1.715em; + color: ${(props) => props.theme.errorListDialogFontColor}; +`; + const Suggestions = ({ id, value, @@ -71,50 +117,4 @@ const Suggestions = ({ ); }; -const SuggestionsContainer = styled.ul<{ error: boolean }>` - box-sizing: border-box; - max-height: 304px; - overflow-y: auto; - margin: 0; - padding: 0.25rem 0; - background-color: ${(props) => - props.error ? props.theme.errorListDialogBackgroundColor : props.theme.listDialogBackgroundColor}; - border: 1px solid - ${(props) => (props.error ? props.theme.errorListDialogBorderColor : props.theme.listDialogBorderColor)}; - - border-radius: 0.25rem; - box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1); - color: ${(props) => props.theme.listOptionFontColor}; - font-family: ${(props) => props.theme.fontFamily}; - font-size: ${(props) => props.theme.listOptionFontSize}; - font-style: ${(props) => props.theme.listOptionFontStyle}; - font-weight: ${(props) => props.theme.listOptionFontWeight}; -`; - -const SuggestionsSystemMessage = styled.span` - display: flex; - padding: 0.25rem 1rem; - color: ${(props) => props.theme.systemMessageFontColor}; - line-height: 1.715em; -`; - -const SuggestionsErrorIcon = styled.span` - display: flex; - flex-wrap: wrap; - align-content: center; - margin-right: 0.5rem; - height: 18px; - width: 18px; - font-size: 18px; - color: ${(props) => props.theme.errorIconColor}; -`; - -const SuggestionsError = styled.span` - display: flex; - padding: 0.25rem 1rem; - align-items: center; - line-height: 1.715em; - color: ${(props) => props.theme.errorListDialogFontColor}; -`; - export default memo(Suggestions); diff --git a/packages/lib/src/text-input/TextInput.tsx b/packages/lib/src/text-input/TextInput.tsx index 56957e107a..99720796f5 100644 --- a/packages/lib/src/text-input/TextInput.tsx +++ b/packages/lib/src/text-input/TextInput.tsx @@ -5,7 +5,6 @@ import { forwardRef, KeyboardEvent, MouseEvent, - ReactNode, useContext, useEffect, useId, @@ -15,7 +14,6 @@ import { } from "react"; import styled, { ThemeProvider } from "styled-components"; import DxcActionIcon from "../action-icon/ActionIcon"; -import { getMargin } from "../common/utils"; import { spaces } from "../common/variables"; import DxcFlex from "../flex/Flex"; import DxcIcon from "../icon/Icon"; @@ -24,59 +22,182 @@ import HalstackContext, { HalstackLanguageContext } from "../HalstackContext"; import useWidth from "../utils/useWidth"; import Suggestions from "./Suggestions"; import TextInputPropsType, { AutosuggestWrapperProps, RefType } from "./types"; +import { + calculateWidth, + hasSuggestions, + isLengthIncorrect, + isNumberIncorrect, + isRequired, + makeCancelable, + patternMismatch, +} from "./utils"; -const sizes = { - small: "240px", - medium: "360px", - large: "480px", - fillParent: "100%", -}; +const TextInputContainer = styled.div<{ + margin: TextInputPropsType["margin"]; + size: TextInputPropsType["size"]; +}>` + box-sizing: border-box; + display: flex; + flex-direction: column; + width: ${(props) => calculateWidth(props.margin, props.size)}; + ${(props) => props.size !== "fillParent" && `min-width:${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] : ""}; + font-family: ${(props) => props.theme.fontFamily}; +`; -const AutosuggestWrapper = ({ condition, wrapper, children }: AutosuggestWrapperProps): JSX.Element => ( - <>{condition ? wrapper(children) : children}</> -); +const Label = styled.label<{ + disabled: TextInputPropsType["disabled"]; + hasHelperText: boolean; +}>` + color: ${(props) => (props.disabled ? props.theme.disabledLabelFontColor : props.theme.labelFontColor)}; + font-size: ${(props) => props.theme.labelFontSize}; + font-style: ${(props) => props.theme.labelFontStyle}; + font-weight: ${(props) => props.theme.labelFontWeight}; + line-height: ${(props) => props.theme.labelLineHeight}; + ${(props) => !props.hasHelperText && `margin-bottom: 0.25rem`} +`; -const calculateWidth = (margin: TextInputPropsType["margin"], size: TextInputPropsType["size"]) => - size === "fillParent" - ? `calc(${sizes[size]} - ${getMargin(margin, "left")} - ${getMargin(margin, "right")})` - : size && sizes[size]; - -const makeCancelable = (promise: Promise<string[]>) => { - let hasCanceled_ = false; - const wrappedPromise = new Promise<string[]>((resolve, reject) => { - promise.then( - (val) => (hasCanceled_ ? reject(Error("Is canceled")) : resolve(val)), - (promiseError) => (hasCanceled_ ? reject(Error("Is canceled")) : reject(promiseError)) - ); - }); - return { - promise: wrappedPromise, - cancel() { - hasCanceled_ = true; - }, - }; -}; +const OptionalLabel = styled.span` + font-weight: ${(props) => props.theme.optionalLabelFontWeight}; +`; + +const HelperText = styled.span<{ disabled: TextInputPropsType["disabled"] }>` + color: ${(props) => (props.disabled ? props.theme.disabledHelperTextFontColor : props.theme.helperTextFontColor)}; + font-size: ${(props) => props.theme.helperTextFontSize}; + font-style: ${(props) => props.theme.helperTextFontStyle}; + font-weight: ${(props) => props.theme.helperTextFontWeight}; + line-height: ${(props) => props.theme.helperTextLineHeight}; + margin-bottom: 0.25rem; +`; + +const InputContainer = styled.div<{ + disabled: TextInputPropsType["disabled"]; + readOnly: TextInputPropsType["readOnly"]; + error: boolean; +}>` + position: relative; + display: flex; + align-items: center; + height: calc(2.5rem - 2px); + padding: 0 0.5rem; + + ${(props) => props.disabled && `background-color: ${props.theme.disabledContainerFillColor};`} + box-shadow: 0 0 0 2px transparent; + border-radius: 4px; + border: 1px solid + ${(props) => + props.disabled + ? props.theme.disabledBorderColor + : props.readOnly + ? props.theme.readOnlyBorderColor + : props.theme.enabledBorderColor}; + ${(props) => + props.error && + !props.disabled && + `border-color: transparent; + box-shadow: 0 0 0 2px ${props.theme.errorBorderColor}; + `} + ${(props) => + !props.disabled + ? ` + &:hover { + border-color: ${ + props.error + ? "transparent" + : props.readOnly + ? props.theme.hoverReadOnlyBorderColor + : props.theme.hoverBorderColor + }; + ${props.error ? `box-shadow: 0 0 0 2px ${props.theme.hoverErrorBorderColor};` : ""} + } + &:focus-within { + border-color: transparent; + box-shadow: 0 0 0 2px ${props.theme.focusBorderColor}; + } + ` + : "cursor: not-allowed;"}; +`; -const hasSuggestions = (suggestions: TextInputPropsType["suggestions"]) => - typeof suggestions === "function" || (suggestions ? suggestions.length > 0 : false); +const Input = styled.input` + height: calc(2.5rem - 2px); + width: 100%; + background: none; + border: none; + outline: none; + padding: 0 0.5rem; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + color: ${(props) => (props.disabled ? props.theme.disabledValueFontColor : props.theme.valueFontColor)}; + font-family: ${(props) => props.theme.fontFamily}; + font-size: ${(props) => props.theme.valueFontSize}; + font-style: ${(props) => props.theme.valueFontStyle}; + font-weight: ${(props) => props.theme.valueFontWeight}; + line-height: 1.5em; + ${(props) => props.disabled && `cursor: not-allowed;`} + + ::placeholder { + color: ${(props) => (props.disabled ? props.theme.disabledPlaceholderFontColor : props.theme.placeholderFontColor)}; + } +`; + +const Prefix = styled.span<{ disabled: TextInputPropsType["disabled"] }>` + height: 1.5rem; + margin-left: 0.25rem; + padding-right: ${(props) => props.theme.prefixDividerPaddingRight}; + ${(props) => { + const color = props.disabled ? props.theme.disabledPrefixColor : props.theme.prefixColor; + return `color: ${color}; border-right: ${props.theme.prefixDividerBorderWidth} ${props.theme.prefixDividerBorderStyle} ${color};`; + }}; + font-size: 1rem; + line-height: 1.5rem; + pointer-events: none; +`; -const isRequired = (value: string, optional: boolean) => value === "" && !optional; +const Suffix = styled.span<{ disabled: TextInputPropsType["disabled"] }>` + height: 1.5rem; + margin: 0 0.25rem; + padding-left: ${(props) => props.theme.suffixDividerPaddingLeft}; + ${(props) => { + const color = props.disabled ? props.theme.disabledSuffixColor : props.theme.suffixColor; + return `color: ${color}; border-left: ${props.theme.suffixDividerBorderWidth} ${props.theme.suffixDividerBorderStyle} ${color};`; + }}; + font-size: 1rem; + line-height: 1.5rem; + pointer-events: none; +`; -const isLengthIncorrect = ( - value: string, - minLength: TextInputPropsType["minLength"], - maxLength: TextInputPropsType["maxLength"] -) => - value != null && ((minLength != null && value.length < minLength) || (maxLength != null && value.length > maxLength)); +const ErrorIcon = styled.span` + display: flex; + flex-wrap: wrap; + align-content: center; + padding: 3px; + height: 18px; + width: 18px; + font-size: 18px; + color: ${(props) => props.theme.errorIconColor}; +`; -const isNumberIncorrect = ( - value: number, - minNumber: TextInputPropsType["minLength"], - maxNumber: TextInputPropsType["maxLength"] -) => (minNumber != null && value < minNumber) || (maxNumber != null && value > maxNumber); +const ErrorMessageContainer = styled.span` + min-height: 1.5em; + color: ${(props) => props.theme.errorMessageColor}; + font-size: 0.75rem; + font-weight: 400; + line-height: 1.5em; + margin-top: 0.25rem; +`; -const patternMismatch = (pattern: TextInputPropsType["pattern"], value: string) => - pattern != null && !new RegExp(pattern).test(value); +const AutosuggestWrapper = ({ condition, wrapper, children }: AutosuggestWrapperProps): JSX.Element => ( + <>{condition ? wrapper(children) : children}</> +); const DxcTextInput = forwardRef<RefType, TextInputPropsType>( ( @@ -112,6 +233,14 @@ const DxcTextInput = forwardRef<RefType, TextInputPropsType>( const autosuggestId = `suggestions-${inputId}`; const errorId = `error-${inputId}`; + const colorsTheme = useContext(HalstackContext); + const translatedLabels = useContext(HalstackLanguageContext); + const numberInputContext = useContext(NumberInputContext); + + const inputRef = useRef<HTMLInputElement | null>(null); + const inputContainerRef = useRef<HTMLDivElement | null>(null); + const actionRef = useRef<HTMLButtonElement | null>(null); + const [innerValue, setInnerValue] = useState(defaultValue); const [isOpen, changeIsOpen] = useState(false); const [isSearching, changeIsSearching] = useState(false); @@ -119,14 +248,7 @@ const DxcTextInput = forwardRef<RefType, TextInputPropsType>( const [filteredSuggestions, changeFilteredSuggestions] = useState<string[]>([]); const [visualFocusIndex, changeVisualFocusIndex] = useState(-1); - const inputRef = useRef<HTMLInputElement | null>(null); - const inputContainerRef = useRef<HTMLDivElement | null>(null); - const actionRef = useRef<HTMLButtonElement | null>(null); - const width = useWidth(inputContainerRef.current); - const colorsTheme = useContext(HalstackContext); - const translatedLabels = useContext(HalstackLanguageContext); - const numberInputContext = useContext(NumberInputContext); const getNumberErrorMessage = (checkedValue: number) => numberInputContext?.minNumber != null && checkedValue < numberInputContext?.minNumber @@ -281,10 +403,10 @@ const DxcTextInput = forwardRef<RefType, TextInputPropsType>( } else { openSuggestions(); if (!isAutosuggestError && !isSearching && filteredSuggestions.length > 0) { - changeVisualFocusIndex((visualFocusedSuggIndex: number) => - visualFocusedSuggIndex < filteredSuggestions.length - 1 - ? visualFocusedSuggIndex + 1 - : visualFocusedSuggIndex === filteredSuggestions.length - 1 + changeVisualFocusIndex((visualFocusedSuggestionIndex: number) => + visualFocusedSuggestionIndex < filteredSuggestions.length - 1 + ? visualFocusedSuggestionIndex + 1 + : visualFocusedSuggestionIndex === filteredSuggestions.length - 1 ? 0 : -1 ); @@ -299,12 +421,12 @@ const DxcTextInput = forwardRef<RefType, TextInputPropsType>( } else { openSuggestions(); if (!isAutosuggestError && !isSearching && filteredSuggestions.length > 0) { - changeVisualFocusIndex((visualFocusedSuggIndex) => - visualFocusedSuggIndex === 0 || visualFocusedSuggIndex === -1 + changeVisualFocusIndex((visualFocusedSuggestionIndex) => + visualFocusedSuggestionIndex === 0 || visualFocusedSuggestionIndex === -1 ? filteredSuggestions.length > 0 ? filteredSuggestions.length - 1 : (suggestions?.length ?? 0) - 1 - : visualFocusedSuggIndex - 1 + : visualFocusedSuggestionIndex - 1 ); } } @@ -579,167 +701,4 @@ const DxcTextInput = forwardRef<RefType, TextInputPropsType>( } ); -const TextInputContainer = styled.div<{ - margin: TextInputPropsType["margin"]; - size: TextInputPropsType["size"]; -}>` - box-sizing: border-box; - display: flex; - flex-direction: column; - width: ${(props) => calculateWidth(props.margin, props.size)}; - ${(props) => props.size !== "fillParent" && `min-width:${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] : ""}; - font-family: ${(props) => props.theme.fontFamily}; -`; - -const Label = styled.label<{ - disabled: TextInputPropsType["disabled"]; - hasHelperText: boolean; -}>` - color: ${(props) => (props.disabled ? props.theme.disabledLabelFontColor : props.theme.labelFontColor)}; - font-size: ${(props) => props.theme.labelFontSize}; - font-style: ${(props) => props.theme.labelFontStyle}; - font-weight: ${(props) => props.theme.labelFontWeight}; - line-height: ${(props) => props.theme.labelLineHeight}; - ${(props) => !props.hasHelperText && `margin-bottom: 0.25rem`} -`; - -const OptionalLabel = styled.span` - font-weight: ${(props) => props.theme.optionalLabelFontWeight}; -`; - -const HelperText = styled.span<{ disabled: TextInputPropsType["disabled"] }>` - color: ${(props) => (props.disabled ? props.theme.disabledHelperTextFontColor : props.theme.helperTextFontColor)}; - font-size: ${(props) => props.theme.helperTextFontSize}; - font-style: ${(props) => props.theme.helperTextFontStyle}; - font-weight: ${(props) => props.theme.helperTextFontWeight}; - line-height: ${(props) => props.theme.helperTextLineHeight}; - margin-bottom: 0.25rem; -`; - -const InputContainer = styled.div<{ - disabled: TextInputPropsType["disabled"]; - readOnly: TextInputPropsType["readOnly"]; - error: boolean; -}>` - position: relative; - display: flex; - align-items: center; - height: calc(2.5rem - 2px); - padding: 0 0.5rem; - - ${(props) => props.disabled && `background-color: ${props.theme.disabledContainerFillColor};`} - box-shadow: 0 0 0 2px transparent; - border-radius: 4px; - border: 1px solid - ${(props) => - props.disabled - ? props.theme.disabledBorderColor - : props.readOnly - ? props.theme.readOnlyBorderColor - : props.theme.enabledBorderColor}; - ${(props) => - props.error && - !props.disabled && - `border-color: transparent; - box-shadow: 0 0 0 2px ${props.theme.errorBorderColor}; - `} - ${(props) => - !props.disabled - ? ` - &:hover { - border-color: ${ - props.error - ? "transparent" - : props.readOnly - ? props.theme.hoverReadOnlyBorderColor - : props.theme.hoverBorderColor - }; - ${props.error ? `box-shadow: 0 0 0 2px ${props.theme.hoverErrorBorderColor};` : ""} - } - &:focus-within { - border-color: transparent; - box-shadow: 0 0 0 2px ${props.theme.focusBorderColor}; - } - ` - : "cursor: not-allowed;"}; -`; - -const Input = styled.input` - height: calc(2.5rem - 2px); - width: 100%; - background: none; - border: none; - outline: none; - padding: 0 0.5rem; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - color: ${(props) => (props.disabled ? props.theme.disabledValueFontColor : props.theme.valueFontColor)}; - font-family: ${(props) => props.theme.fontFamily}; - font-size: ${(props) => props.theme.valueFontSize}; - font-style: ${(props) => props.theme.valueFontStyle}; - font-weight: ${(props) => props.theme.valueFontWeight}; - line-height: 1.5em; - ${(props) => props.disabled && `cursor: not-allowed;`} - - ::placeholder { - color: ${(props) => (props.disabled ? props.theme.disabledPlaceholderFontColor : props.theme.placeholderFontColor)}; - } -`; - -const Prefix = styled.span<{ disabled: TextInputPropsType["disabled"] }>` - height: 1.5rem; - margin-left: 0.25rem; - padding-right: ${(props) => props.theme.prefixDividerPaddingRight}; - ${(props) => { - const color = props.disabled ? props.theme.disabledPrefixColor : props.theme.prefixColor; - return `color: ${color}; border-right: ${props.theme.prefixDividerBorderWidth} ${props.theme.prefixDividerBorderStyle} ${color};`; - }}; - font-size: 1rem; - line-height: 1.5rem; - pointer-events: none; -`; - -const Suffix = styled.span<{ disabled: TextInputPropsType["disabled"] }>` - height: 1.5rem; - margin: 0 0.25rem; - padding-left: ${(props) => props.theme.suffixDividerPaddingLeft}; - ${(props) => { - const color = props.disabled ? props.theme.disabledSuffixColor : props.theme.suffixColor; - return `color: ${color}; border-left: ${props.theme.suffixDividerBorderWidth} ${props.theme.suffixDividerBorderStyle} ${color};`; - }}; - font-size: 1rem; - line-height: 1.5rem; - pointer-events: none; -`; - -const ErrorIcon = styled.span` - display: flex; - flex-wrap: wrap; - align-content: center; - padding: 3px; - height: 18px; - width: 18px; - font-size: 18px; - color: ${(props) => props.theme.errorIconColor}; -`; - -const ErrorMessageContainer = styled.span` - min-height: 1.5em; - color: ${(props) => props.theme.errorMessageColor}; - font-size: 0.75rem; - font-weight: 400; - line-height: 1.5em; - margin-top: 0.25rem; -`; - export default DxcTextInput; diff --git a/packages/lib/src/text-input/utils.ts b/packages/lib/src/text-input/utils.ts new file mode 100644 index 0000000000..3e7bb102f7 --- /dev/null +++ b/packages/lib/src/text-input/utils.ts @@ -0,0 +1,64 @@ +import TextInputPropsType from "./types"; +import { getMargin } from "../common/utils"; + +const sizes = { + small: "240px", + medium: "360px", + large: "480px", + fillParent: "100%", +}; + +export const calculateWidth = (margin: TextInputPropsType["margin"], size: TextInputPropsType["size"]) => + size === "fillParent" + ? `calc(${sizes[size]} - ${getMargin(margin, "left")} - ${getMargin(margin, "right")})` + : size && sizes[size]; + +export const makeCancelable = (promise: Promise<string[]>) => { + let hasCanceled_ = false; + const wrappedPromise = new Promise<string[]>((resolve, reject) => { + promise.then( + (val) => (hasCanceled_ ? reject(Error("Is canceled")) : resolve(val)), + (promiseError) => (hasCanceled_ ? reject(Error("Is canceled")) : reject(promiseError)) + ); + }); + return { + promise: wrappedPromise, + cancel() { + hasCanceled_ = true; + }, + }; +}; + +export const hasSuggestions = (suggestions: TextInputPropsType["suggestions"]) => + typeof suggestions === "function" || (suggestions ? suggestions.length > 0 : false); + +export const isRequired = (value: string, optional: boolean) => value === "" && !optional; + +export const isLengthIncorrect = ( + value: string, + minLength: TextInputPropsType["minLength"], + maxLength: TextInputPropsType["maxLength"] +) => + value != null && ((minLength != null && value.length < minLength) || (maxLength != null && value.length > maxLength)); + +export const isNumberIncorrect = ( + value: number, + minNumber: TextInputPropsType["minLength"], + maxNumber: TextInputPropsType["maxLength"] +) => (minNumber != null && value < minNumber) || (maxNumber != null && value > maxNumber); + +export const patternMismatch = (pattern: TextInputPropsType["pattern"], value: string) => + pattern != null && !new RegExp(pattern).test(value); + +export const transformSpecialChars = (str: string) => { + const specialCharsRegex = /[\\*()[\]{}+?/]/; + let value = str; + if (specialCharsRegex.test(value)) { + const regexAsString = specialCharsRegex.toString().split(""); + const uniqueSpecialChars = regexAsString.filter((item, index) => regexAsString.indexOf(item) === index); + uniqueSpecialChars.forEach((specialChar) => { + if (str.includes(specialChar)) value = value.replace(specialChar, "\\" + specialChar); + }); + } + return value; +}; \ No newline at end of file diff --git a/packages/lib/src/toast/types.ts b/packages/lib/src/toast/types.ts index ec3d7c0837..73f84bce0e 100644 --- a/packages/lib/src/toast/types.ts +++ b/packages/lib/src/toast/types.ts @@ -2,22 +2,45 @@ import { ReactNode } from "react"; import { SVG } from "../common/utils"; type Action = { + /** + * The icon of the action. It can be a string with the name of the icon or an SVG component. + */ icon?: string | SVG; + /** + * The label of the action. + */ label: string; + /** + * The function that will be executed when the user clicks on the action button. + */ onClick: () => void; }; type CommonProps = { + /** + * Tertiary button which performs a custom action, specified by the user. + */ action?: Action; + /** + * Message to be displayed as a toast. + */ message: string; }; type DefaultToast = CommonProps & { + /** + * Material Symbol name or SVG element as the icon that will be placed next to the panel label. + * When using Material Symbols, replace spaces with underscores. + * By default they are outlined if you want it to be filled prefix the symbol name with "filled_". + */ icon?: string | SVG; }; type LoadingToast = CommonProps & { loading: boolean; }; type SemanticToast = CommonProps & { + /** + * Flag that allows to hide the semantic icon of the toast. + */ hideSemanticIcon?: boolean; }; type ToastType = DefaultToast | LoadingToast | SemanticToast; @@ -42,7 +65,17 @@ type ToastPropsType = { hideSemanticIcon?: boolean; }; -type ToastsQueuePropsType = { duration?: number; children: ReactNode }; +type ToastsQueuePropsType = { + /** + * Duration in milliseconds before a toast automatically hides itself. + * The range goes from 3000ms to 5000ms, any other value will not be taken into consideration. + */ + duration?: number; + /** + * Tree of components from which the useToast hook can be triggered. + */ + children: ReactNode; +}; export default ToastPropsType; export type { diff --git a/packages/lib/src/tooltip/Tooltip.tsx b/packages/lib/src/tooltip/Tooltip.tsx index 6afebd6aa2..ea1fdab172 100644 --- a/packages/lib/src/tooltip/Tooltip.tsx +++ b/packages/lib/src/tooltip/Tooltip.tsx @@ -1,7 +1,7 @@ import styled from "styled-components"; import CoreTokens from "../common/coreTokens"; import TooltipPropsType, { TooltipWrapperProps } from "./types"; -import { createContext, useContext } from "react"; +import { useContext } from "react"; import { Root, Trigger, Portal, Arrow, Content } from "@radix-ui/react-tooltip"; import { Provider } from "@radix-ui/react-tooltip"; import { TooltipContext } from "./TooltipContext"; diff --git a/packages/lib/src/wizard/Icons.tsx b/packages/lib/src/wizard/Icons.tsx new file mode 100644 index 0000000000..34398df76c --- /dev/null +++ b/packages/lib/src/wizard/Icons.tsx @@ -0,0 +1,39 @@ +const icons = { + valid: ( + <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 18 18"> + <path data-name="Path 2946" d="M0,0H18V18H0Z" fill="none" /> + <path + data-name="Path 2947" + d="M9.986,4a5.986,5.986,0,1,0,5.986,5.986A5.994,5.994,0,0,0,9.986,4Zm-1.5,9.727L5.5,10.734,6.551,9.679l1.938,1.93L13.42,6.679l1.055,1.063Z" + transform="translate(-0.986 -0.986)" + fill="#eafaef" + opacity="0.999" + /> + <path + data-name="Path 2948" + d="M9.493,2a7.493,7.493,0,1,0,7.493,7.493A7.5,7.5,0,0,0,9.493,2Zm0,13.487a5.994,5.994,0,1,1,5.994-5.994A6,6,0,0,1,9.493,15.487Zm3.439-9.306L7.994,11.119,6.054,9.186,5,10.242l3,3,5.994-5.994Z" + transform="translate(-0.493 -0.493)" + fill="#24a148" + /> + </svg> + ), + invalid: ( + <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 18 18"> + <path data-name="Path 2943" d="M0,0H18V18H0Z" fill="none" /> + <path + data-name="Path 2944" + d="M10,4a6,6,0,1,0,6,6A6.01,6.01,0,0,0,10,4Zm3,7.945L11.945,13,10,11.06,8.059,13,7,11.945,8.944,10,7,8.059,8.059,7,10,8.944,11.945,7,13,8.059,11.06,10Z" + transform="translate(-1.002 -1.002)" + fill="#ffe6e9" + /> + <path + data-name="Path 2945" + d="M11.444,6.5,9.5,8.443,7.558,6.5,6.5,7.558,8.443,9.5,6.5,11.444,7.558,12.5,9.5,10.558,11.444,12.5,12.5,11.444,10.558,9.5,12.5,7.558ZM9.5,2A7.5,7.5,0,1,0,17,9.5,7.494,7.494,0,0,0,9.5,2Zm0,13.5a6,6,0,1,1,6-6A6.009,6.009,0,0,1,9.5,15.5Z" + transform="translate(-0.501 -0.501)" + fill="#d0011b" + /> + </svg> + ), +}; + +export default icons; diff --git a/packages/lib/src/wizard/Wizard.tsx b/packages/lib/src/wizard/Wizard.tsx index ce0a397a06..5e50e4dece 100644 --- a/packages/lib/src/wizard/Wizard.tsx +++ b/packages/lib/src/wizard/Wizard.tsx @@ -1,123 +1,10 @@ -import { useContext, useState } from "react"; +import { useContext, useMemo, useState } from "react"; import styled, { ThemeProvider } from "styled-components"; import { spaces } from "../common/variables"; import DxcIcon from "../icon/Icon"; import HalstackContext from "../HalstackContext"; import WizardPropsType, { StepProps } from "./types"; - -const icons = { - validIcon: ( - <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 18 18"> - <path data-name="Path 2946" d="M0,0H18V18H0Z" fill="none" /> - <path - data-name="Path 2947" - d="M9.986,4a5.986,5.986,0,1,0,5.986,5.986A5.994,5.994,0,0,0,9.986,4Zm-1.5,9.727L5.5,10.734,6.551,9.679l1.938,1.93L13.42,6.679l1.055,1.063Z" - transform="translate(-0.986 -0.986)" - fill="#eafaef" - opacity="0.999" - /> - <path - data-name="Path 2948" - d="M9.493,2a7.493,7.493,0,1,0,7.493,7.493A7.5,7.5,0,0,0,9.493,2Zm0,13.487a5.994,5.994,0,1,1,5.994-5.994A6,6,0,0,1,9.493,15.487Zm3.439-9.306L7.994,11.119,6.054,9.186,5,10.242l3,3,5.994-5.994Z" - transform="translate(-0.493 -0.493)" - fill="#24a148" - /> - </svg> - ), - invalidIcon: ( - <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 18 18"> - <path data-name="Path 2943" d="M0,0H18V18H0Z" fill="none" /> - <path - data-name="Path 2944" - d="M10,4a6,6,0,1,0,6,6A6.01,6.01,0,0,0,10,4Zm3,7.945L11.945,13,10,11.06,8.059,13,7,11.945,8.944,10,7,8.059,8.059,7,10,8.944,11.945,7,13,8.059,11.06,10Z" - transform="translate(-1.002 -1.002)" - fill="#ffe6e9" - /> - <path - data-name="Path 2945" - d="M11.444,6.5,9.5,8.443,7.558,6.5,6.5,7.558,8.443,9.5,6.5,11.444,7.558,12.5,9.5,10.558,11.444,12.5,12.5,11.444,10.558,9.5,12.5,7.558ZM9.5,2A7.5,7.5,0,1,0,17,9.5,7.494,7.494,0,0,0,9.5,2Zm0,13.5a6,6,0,1,1,6-6A6.009,6.009,0,0,1,9.5,15.5Z" - transform="translate(-0.501 -0.501)" - fill="#d0011b" - /> - </svg> - ), -}; - -const DxcWizard = ({ - mode = "horizontal", - defaultCurrentStep, - currentStep, - onStepClick, - steps, - margin, - tabIndex = 0, -}: WizardPropsType): JSX.Element => { - const [innerCurrent, setInnerCurrentStep] = useState(currentStep ?? defaultCurrentStep ?? 0); - const renderedCurrent = currentStep ?? innerCurrent; - const colorsTheme = useContext(HalstackContext); - - const handleStepClick = (newValue: number) => { - setInnerCurrentStep(newValue); - onStepClick?.(newValue); - }; - - return ( - <ThemeProvider theme={colorsTheme.wizard}> - <StepsContainer mode={mode} margin={margin} role="group"> - {steps.map((step, i) => ( - <StepContainer key={`step${i}`} mode={mode} lastStep={i === steps.length - 1}> - <Step - onClick={() => { - handleStepClick(i); - }} - disabled={step.disabled} - mode={mode} - first={i === 0} - last={i === steps.length - 1} - aria-current={renderedCurrent === i ? "step" : "false"} - tabIndex={tabIndex} - > - <StepHeader validityIcon={step.valid !== undefined}> - <IconContainer current={i === renderedCurrent} visited={i < renderedCurrent} disabled={step.disabled}> - {step.icon ? ( - typeof step.icon === "string" ? ( - <DxcIcon icon={step.icon} /> - ) : ( - step.icon - ) - ) : ( - <Number>{i + 1}</Number> - )} - </IconContainer> - {step.valid !== undefined && - (step.valid ? ( - <ValidityIconContainer>{icons.validIcon}</ValidityIconContainer> - ) : ( - <ValidityIconContainer>{icons.invalidIcon}</ValidityIconContainer> - ))} - </StepHeader> - {(step.label || step.description) && ( - <InfoContainer> - {step.label && ( - <Label current={i === renderedCurrent} disabled={step.disabled} visited={i <= innerCurrent}> - {step.label} - </Label> - )} - {step.description && ( - <Description current={i === renderedCurrent} disabled={step.disabled} visited={i <= innerCurrent}> - {step.description} - </Description> - )} - </InfoContainer> - )} - </Step> - {i === steps.length - 1 ? "" : <StepSeparator mode={mode} />} - </StepContainer> - ))} - </StepsContainer> - </ThemeProvider> - ); -}; +import icons from "./Icons"; const StepsContainer = styled.div<{ mode: WizardPropsType["mode"]; @@ -159,6 +46,7 @@ const Step = styled.button<{ display: flex; justify-content: flex-start; align-items: center; + gap: 0.75rem; border: none; border-radius: 0.25rem; background: inherit; @@ -279,10 +167,6 @@ const ValidityIconContainer = styled.div` left: 22.5px; `; -const InfoContainer = styled.div` - margin-left: 12px; -`; - const Label = styled.p<{ current: boolean; visited: boolean; @@ -342,4 +226,78 @@ const StepSeparator = styled.div<{ mode: WizardPropsType["mode"] }>` flex-grow: 1; `; +const DxcWizard = ({ + mode = "horizontal", + defaultCurrentStep = 0, + currentStep, + onStepClick, + steps, + margin, + tabIndex = 0, +}: WizardPropsType): JSX.Element => { + const colorsTheme = useContext(HalstackContext); + const [innerCurrent, setInnerCurrentStep] = useState(defaultCurrentStep); + + const renderedCurrent = useMemo(() => currentStep ?? innerCurrent, [currentStep, innerCurrent]); + + const handleStepClick = (newValue: number) => { + setInnerCurrentStep(newValue); + onStepClick?.(newValue); + }; + + return ( + <ThemeProvider theme={colorsTheme.wizard}> + <StepsContainer mode={mode} margin={margin} role="group"> + {steps.map((step, i) => ( + <StepContainer key={`step${i}`} mode={mode} lastStep={i === steps.length - 1}> + <Step + onClick={() => { + handleStepClick(i); + }} + disabled={step.disabled} + mode={mode} + first={i === 0} + last={i === steps.length - 1} + aria-current={renderedCurrent === i ? "step" : "false"} + tabIndex={tabIndex} + > + <StepHeader validityIcon={step.valid != null}> + <IconContainer current={i === renderedCurrent} visited={i < renderedCurrent} disabled={step.disabled}> + {step.icon ? ( + typeof step.icon === "string" ? ( + <DxcIcon icon={step.icon} /> + ) : ( + step.icon + ) + ) : ( + <Number>{i + 1}</Number> + )} + </IconContainer> + {step.valid != null && ( + <ValidityIconContainer>{step.valid ? icons.valid : icons.invalid}</ValidityIconContainer> + )} + </StepHeader> + {(step.label || step.description) && ( + <div> + {step.label && ( + <Label current={i === renderedCurrent} disabled={step.disabled} visited={i <= innerCurrent}> + {step.label} + </Label> + )} + {step.description && ( + <Description current={i === renderedCurrent} disabled={step.disabled} visited={i <= innerCurrent}> + {step.description} + </Description> + )} + </div> + )} + </Step> + {i === steps.length - 1 ? "" : <StepSeparator mode={mode} />} + </StepContainer> + ))} + </StepsContainer> + </ThemeProvider> + ); +}; + export default DxcWizard;