diff --git a/apps/website/pages/components/file-input/code.tsx b/apps/website/pages/components/file-input/code.tsx new file mode 100644 index 0000000000..4e08285722 --- /dev/null +++ b/apps/website/pages/components/file-input/code.tsx @@ -0,0 +1,19 @@ +import Head from "next/head"; +import type { ReactElement } from "react"; +import FileInputCodePage from "screens/components/file-input/code/FileInputCodePage"; +import FileInputPageLayout from "screens/components/file-input/FileInputPageLayout"; + +const Code = () => { + return ( + <> + + File Input Code — Halstack Design System + + + + ); +}; + +Code.getLayout = (page: ReactElement) => {page}; + +export default Code; diff --git a/apps/website/pages/components/file-input/index.tsx b/apps/website/pages/components/file-input/index.tsx index b97e6a2ba0..4fdd08d2b0 100644 --- a/apps/website/pages/components/file-input/index.tsx +++ b/apps/website/pages/components/file-input/index.tsx @@ -1,21 +1,19 @@ import Head from "next/head"; import type { ReactElement } from "react"; -import FileInputCodePage from "screens/components/file-input/code/FileInputCodePage"; import FileInputPageLayout from "screens/components/file-input/FileInputPageLayout"; +import FileInputOverviewPage from "screens/components/file-input/overview/FileInputOverviewPage"; -const Usage = () => { +const Index = () => { return ( <> File Input — Halstack Design System - + ); }; -Usage.getLayout = function getLayout(page: ReactElement) { - return {page}; -}; +Index.getLayout = (page: ReactElement) => {page}; -export default Usage; +export default Index; diff --git a/apps/website/pages/components/file-input/specifications.tsx b/apps/website/pages/components/file-input/specifications.tsx deleted file mode 100644 index 17cf3c16e3..0000000000 --- a/apps/website/pages/components/file-input/specifications.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import Head from "next/head"; -import type { ReactElement } from "react"; -import FileInputSpecsPage from "screens/components/file-input/specs/FileInputSpecsPage"; -import FileInputPageLayout from "screens/components/file-input/FileInputPageLayout"; - -const Specifications = () => { - return ( - <> - - File Input Specs — Halstack Design System - - - - ); -}; - -Specifications.getLayout = function getLayout(page: ReactElement) { - return {page}; -}; - -export default Specifications; diff --git a/apps/website/pages/components/file-input/usage.tsx b/apps/website/pages/components/file-input/usage.tsx deleted file mode 100644 index 55a30c850a..0000000000 --- a/apps/website/pages/components/file-input/usage.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import Head from "next/head"; -import type { ReactElement } from "react"; -import FileInputPageLayout from "screens/components/file-input/FileInputPageLayout"; -import FileInputUsagePage from "screens/components/file-input/usage/FileInputUsagePage"; - -const Usage = () => { - return ( - <> - - File Input Usage — Halstack Design System - - - - ); -}; - -Usage.getLayout = function getLayout(page: ReactElement) { - return {page}; -}; - -export default Usage; diff --git a/apps/website/screens/components/file-input/FileInputPageLayout.tsx b/apps/website/screens/components/file-input/FileInputPageLayout.tsx index 01012df243..62e75f4326 100644 --- a/apps/website/screens/components/file-input/FileInputPageLayout.tsx +++ b/apps/website/screens/components/file-input/FileInputPageLayout.tsx @@ -6,12 +6,8 @@ import { ReactNode } from "react"; const FileInputPageHeading = ({ children }: { children: ReactNode }) => { const tabs = [ - { label: "Code", path: "/components/file-input" }, - { label: "Usage", path: "/components/file-input/usage" }, - { - label: "Specifications", - path: "/components/file-input/specifications", - }, + { label: "Overview", path: "/components/file-input" }, + { label: "Code", path: "/components/file-input/code" }, ]; return ( @@ -20,11 +16,10 @@ const FileInputPageHeading = ({ children }: { children: ReactNode }) => { - The file input component is used to choose files from any location in the local machine and update those - files to the server where the application is hosted. It is a common procedure in applications where files - are required, like documents, images, or other information in digital formats. + File inputs are used to allow users to upload one or more files from their local device to an application in + a structured and accessible way. - + {children} diff --git a/apps/website/screens/components/file-input/overview/FileInputOverviewPage.tsx b/apps/website/screens/components/file-input/overview/FileInputOverviewPage.tsx new file mode 100644 index 0000000000..44d0e37aec --- /dev/null +++ b/apps/website/screens/components/file-input/overview/FileInputOverviewPage.tsx @@ -0,0 +1,281 @@ +import { DxcParagraph, DxcBulletedList, DxcFlex, DxcTable, DxcImage } from "@dxc-technology/halstack-react"; +import QuickNavContainer from "@/common/QuickNavContainer"; +import QuickNavContainerLayout from "@/common/QuickNavContainerLayout"; +import DocFooter from "@/common/DocFooter"; +import Image from "@/common/Image"; +import Code from "@/common/Code"; +import anatomy from "./images/fileInput_anatomy.png"; +import Example from "@/common/example/Example"; +import variants from "./examples/variants"; + +const sections = [ + { + title: "Introduction", + content: ( + <> + + The file input component is a key UI element for collecting digital files such as documents, + images, videos or other media types. It plays a critical role in workflows where users must provide + supplemental information—like uploading resumes, profile pictures, supporting documents or media attachments. + + + Unlike text or number inputs, file inputs trigger system-level dialogs and are limited in how their visual + appearance can be customized. However, they provide built-in functionality for browsing, selecting and + preparing files for upload. Developers can also configure constraints such as file type (e.g.,{" "} + .jpg, .pdf) or maximum file size to ensure data quality and improve server-side + processing. + + + Clear instructions, visual feedback and validation help users understand what types of files are accepted, + whether multiple files are allowed, and if an upload was successful. Proper labeling and accessibility + support—including keyboard navigation and screen reader compatibility—are essential to providing an inclusive + experience. + + + File inputs enhance functionality in forms by enabling richer user interactions and expanding the types of + data an application can accept. + + + ), + }, + { + title: "Anatomy", + content: ( + <> + File input's anatomy + + + Label (Optional): a descriptive text that helps users understand what information + is expected in the input field. It should be clear, concise and placed near the input for better + readability. + + + Optional indicator (Optional): a small indicator that signals the input field is + not mandatory. It helps users know they can leave the field empty without causing validation errors. + + + Helper text (Optional): additional text placed below the input label that provides + guidance, examples or explanations to assist users in filling out the field correctly. + + + Container: the visual boundary that encloses the file input area. It helps define the drop + zone and clickable area and should visually reflect interaction states such as focus, hover or error. + + + File input button: instructional or informative text inside the drop area (e.g., “Select + files or drop files here”) that helps orient users and encourage interaction. + + + File item container: displays the name of the file(s) selected or dropped. + + + Uploaded file preview (Optional): a visual element placed before the file input + area, often used to display a file icon or category label to reinforce the expected content. + + + Error message: appears below the input field when validation fails (e.g., unsupported file + type or size). The message should clearly explain the issue and how to fix it. + + + Remove action: allows users to delete a selected file before submission. + + + + ), + }, + { + title: "Form inputs", + content: ( + <> + + Form inputs are essential UI elements that allow users to interact with digital products by{" "} + entering or selecting data. Choosing the right input type and structure is key to designing + efficient, user-friendly forms that support task completion and data accuracy. + + + A form input (also known as a form field) is used to capture user data. Common input types include text + fields, date pickers, number fields, radio buttons, checkboxes, toggles and dropdowns. Forms should always + include a submission method—such as a submit button, link or keyboard trigger—to complete the interaction. + + + ), + subSections: [ + { + title: "Shared input characteristics", + content: ( + <> + + Although input fields vary in type and purpose, they often share a common set of features: + + + + Helper text: additional information displayed below the field to guide the user in + providing the correct input. + + + Optional label: inputs that are not mandatory can be marked with an "Optional" tag to + set clear expectations. + + + + ), + }, + { + title: "Common input states", + content: ( + <> + Most inputs can also present standard interactive or informative states: + + + Disabled: this state prevents users from interacting with the field. It’s typically + used when a value is not applicable or editable under certain conditions or roles. + + + Error: when a user enters invalid or incomplete data, the input shows an error state, + often accompanied by a helpful message to guide corrections. + + + + ), + }, + ], + }, + { + title: "Using file inputs", + content: ( + <> + + File inputs are commonly used in applications and forms where users are required to upload documents, images, + or other digital files. Our file input component is designed to be flexible and accessible, supporting both + drag-and-drop interactions and traditional file browsing. It enables designers and developers to handle a + variety of file-based use cases while maintaining usability, clarity and strong user feedback. + + + In this section, we will explore the key characteristics and behaviors of our file input component to help you + understand how to use it effectively and accessibly in your product. + + + ), + subSections: [ + { + title: "Uploading files", + content: ( + <> + + Uploading files is the core interaction of the file input component. Users can either click to open the + file picker or drag-and-drop files directly into the designated area. The component supports single and + multiple file uploads, provides visual feedback once a file is selected and often includes the ability to + remove files before submission. + + + Clear affordances, immediate validation (e.g., for file type or size) and the option to undo or remove + uploads help users remain in control and confident in the submission process. + + + ), + }, + { + title: "Variants", + content: ( + <> + + The file input component is available in multiple variants to support a wide range of use cases and + interaction patterns. Each variant adapts to different interface needs, whether it's a compact + multi-device form or a file-intensive desktop experience. Designers and developers should select the most + appropriate variant based on context, layout constraints and user expectations. + + + + Below is a summary of the available file input variants and their recommended use cases: + + + + + Name + Use case + + + + + + File + + + Use the file variant when designing for multi-device compatibility. It offers a + minimal, straightforward interface ideal for responsive layouts and mobile-first designs. + + + + + Filedrop + + + Use in large or complex forms when designing only for desktop. It + combines both click-to-upload and drag-and-drop functionality in a compact inline layout. + + + + + Dropzone + + + Choose the dropzone when the primary goal is file uploading—such as media galleries + or document submission tools. It provides a large, visually prominent area optimized for dragging + and dropping files. + + + + + + ), + }, + ], + }, + { + title: "Best practices", + content: ( + <> + + + Always use clear labels: provide descriptive and specific labels that indicate the type of + file expected (e.g., “Upload profile picture” or “Attach supporting document”). Avoid generic terms like + “Upload.” + + + Use helper text to guide users: add helper text below the label when users need more + context about accepted file types (e.g., PDF, PNG), maximum file size or quantity limitations. This reduces + the likelihood of upload errors. + + + Display file previews when useful: for visual content like images, enabling preview + thumbnails can improve user confidence by providing a direct visual confirmation rather than just a file + name. This is especially helpful when uploading multiple similar images. + + + Use single-line layout for compact spaces: when working with constrained or dense layouts, + the single-file file input variant keeps the selected file inline with the field, avoiding vertical + expansion and keeping the form tidy. + + + Validate early and clearly: display error messages immediately if a file does not meet + requirements (e.g., incorrect format or file too large). Use clear, concise language that explains how to + fix the issue. + + + + ), + }, +]; + +const FileInputOverviewPage = () => { + return ( + + + + + + + ); +}; + +export default FileInputOverviewPage; diff --git a/apps/website/screens/components/file-input/usage/examples/variants.ts b/apps/website/screens/components/file-input/overview/examples/variants.ts similarity index 100% rename from apps/website/screens/components/file-input/usage/examples/variants.ts rename to apps/website/screens/components/file-input/overview/examples/variants.ts diff --git a/apps/website/screens/components/file-input/overview/images/fileInput_anatomy.png b/apps/website/screens/components/file-input/overview/images/fileInput_anatomy.png new file mode 100644 index 0000000000..3c60479fe9 Binary files /dev/null and b/apps/website/screens/components/file-input/overview/images/fileInput_anatomy.png differ diff --git a/apps/website/screens/components/file-input/specs/FileInputSpecsPage.tsx b/apps/website/screens/components/file-input/specs/FileInputSpecsPage.tsx deleted file mode 100644 index adb8cfbe8d..0000000000 --- a/apps/website/screens/components/file-input/specs/FileInputSpecsPage.tsx +++ /dev/null @@ -1,725 +0,0 @@ -import { DxcParagraph, DxcBulletedList, DxcFlex, DxcTable } from "@dxc-technology/halstack-react"; -import Figure from "@/common/Figure"; -import DocFooter from "@/common/DocFooter"; -import Code from "@/common/Code"; -import Image from "@/common/Image"; -import QuickNavContainer from "@/common/QuickNavContainer"; -import QuickNavContainerLayout from "@/common/QuickNavContainerLayout"; -import fileInputStatesFile from "./images/input_file_states_file.png"; -import fileInputStatesFiledrop from "./images/input_file_states_filedrop.png"; -import fileInputStatesDropzone from "./images/input_file_states_dropzone.png"; -import fileInputStatesFileItem from "./images/input_file_states_fileitem.png"; -import fileInputAnatomy from "./images/input_file_anatomy.png"; -import fileInputFileFileItemPreview from "./images/input_file_fileitem_preview.png"; -import fileInputFileSingleFile from "./images/input_file_single_file.png"; -import fileInputSpecs from "./images/input_file_specs.png"; - -const sections = [ - { - title: "Specifications", - content: ( -
- File input design specifications -
- ), - }, - { - title: "States", - content: ( - The component file input is made-up of an input (type: file) and a file-item(s). - ), - subSections: [ - { - title: "File", - content: ( - <> - - The element has the following states: enabled, hover,{" "} - focus, active and disabled. - -
- File variant states -
- - ), - }, - { - title: "Filedrop", - content: ( - <> - - The element has the following states: enabled, hover,{" "} - focus, active, dragover and disabled. - -
- Filedrop variant states -
- - ), - }, - { - title: "Dropzone", - content: ( - <> - - The element has the following states: enabled, hover,{" "} - focus, active, dragover and disabled. - -
- Dropzone variant states -
- - ), - }, - { - title: "File items", - content: ( - <> - - The element has the following states: enabled, hover,{" "} - focus, active, loading and error. - -
- File item states -
- - ), - }, - ], - }, - { - title: "Anatomy", - content: ( - <> - File input anatomy - - Label - Drag and drop area - Error message - Error indicator - Action - Remove file - Helper text - file input button - File preview - File name - File item container - - - ), - }, - { - title: "File item with preview", - content: ( - <> - - When the files to upload are mainly images, the preview can provide more feedback to the user rather than the - name of the file, preventing errors loading content. - -
- File item with preview -
- - ), - }, - { - title: "Single-file file input", - content: ( - <> - - In order to provide a compact version of the file input component to accommodate any layout restriction, the - variant file displays the file name in the same row instead of growing vertically. - -
- File variant -
- - ), - }, - { - title: "Design tokens", - subSections: [ - { - title: "Color", - subSections: [ - { - title: "Base", - content: ( - - - - Component token - Element - Core token - Value - - - - - - dropBorderColor - - Drag and drop area - - color-black - - #000000 - - - - fileItemBorderColor - - File item - - color-grey-300 - - #cccccc - - - - deleteFileItemColor - - File item - - color-black - - #000000 - - - - fileNameFontColor - - File name - - color-black - - #000000 - - - - filePreviewBackgroundColor - - File preview - - color-grey-100 - - #f2f2f2 - - - - filePreviewIconColor - - File preview icon - - color-black - - #000000 - - - - labelFontColor - - Label - - color-black - - #000000 - - - - helperTextFontColor - - Helper text - - color-black - - #000000 - - - - dropLabelFontColor - - Drop label - - color-black - - #000000 - - - - ), - }, - { - title: "Interactive", - content: ( - - - - Component token - Element - Core token - Value - - - - - - disabledLabelFontColor - - Label:disabled - - color-grey-500 - - #999999 - - - - disabledHelperTextFontColor - - Helper text:disabled - - color-grey-500 - - #999999 - - - - disabledDropLabelFontColor - - Drop label:disabled - - color-grey-500 - - #999999 - - - - focusDropBorderColor - - Dnd border:focus - - color-blue-600 - - #0095ff - - - - disabledDropBorderColor - - Dnd border:disabled - - color-grey-500 - - #999999 - - - - dragoverDropBackgroundColor - - Dnd fill:dragover - - color-blue-50 - - #f5fbff - - - - hoverDeleteFileItemBackgroundColor - - File item icon:hover - - color-grey-100-a - - #0000000d - - - - focusDeleteFileItemBorderColor - - File item icon:focus - - color-blue-600 - - #0095ff - - - - activeDeleteFileItemBackgroundColor - - File item icon:active - - color-grey-300-a - - #00000033 - - - - errorFileItemBorderColor - - File item container:error - - color-red-700 - - #d0011b - - - - errorFileItemBackgroundColor - - File item container:error - - color-red-50 - - #fff5f6 - - - - errorFilePreviewBackgroundColor - - File item preview:error - - color-red-200 - - #ffccd3 - - - - errorFilePreviewIconColor - - File item preview icon:error - - color-red-700 - - #d0011b - - - - errorMessageFontColor - - File item:error - - color-red-700 - - #d0011b - - - - ), - }, - ], - }, - { - title: "Typography", - content: ( - - - - Property - Element - Core token - Value - - - - - - font-family - - Label - - font-family-sans - - Open Sans - - - - font-size - - Label - - font-scale-02 - - 0.875rem / 14px - - - - font-weight - - Label - - font-bold - - 600 - - - - line-height - - Label - - font-leading-loose-01 - - 1.75em - - - - font-family - - File item - - font-family-sans - - Open Sans - - - - font-size - - File item - - font-scale-02 - - 0.875rem / 14px - - - - font-weight - - File item - - font-regular - - 400 - - - - line-height - - File item - - font-leading-normal - - 1.5em - - - - font-family - - Helper text - - font-family-sans - - Open Sans - - - - font-size - - Helper text - - font-scale-01 - - 12px - - - - font-weight - - Helper text - - font-regular - - 400 - - - - line-height - - Helper text - - font-leading-normal - - 1.5em - - - - font-family - - Drop label - - font-family-sans - - Open Sans - - - - font-size - - Drop label - - font-scale-03 - - 1rem / 16px - - - - font-weight - - Drop label - - font-regular - - 400 - - - - font-family - - Error message - - font-family-sans - - Open Sans - - - - font-size - - Error message - - font-scale-01 - - 0.75rem / 12px - - - - font-weight - - Error message - - font-regular - - 400 - - - - line-height - - Error message - - font-leading-normal - - 1.5em - - - - ), - }, - { - title: "Border", - content: ( - - - - Property - Element - Core token - Value - - - - - - border-style - - Drag and drop area - - border-style-dashed - - dashed - - - - border-width - - Drag and drop area - - border-width-1 - - 1px - - - - border-radius - - Drag and drop area - - border-radius-large - - 0.375rem / 6px - - - - border-style - - File item - - border-style-solid - - solid - - - - border-width - - File item - - border-width-1 - - 1px - - - - border-radius - - File item - - border-radius-medium - - 0.25rem / 4px - - - - box-shadow - - File item icon:focus - - - 0 0 0 2px - - - - box-shadow - - Drag and drop area:dragover - - - 0 0 0 2px - - - - ), - }, - ], - }, -]; - -const FileInputSpecsPage = () => { - return ( - - - - - - - ); -}; - -export default FileInputSpecsPage; diff --git a/apps/website/screens/components/file-input/specs/images/input_file_anatomy.png b/apps/website/screens/components/file-input/specs/images/input_file_anatomy.png deleted file mode 100644 index 3fdd727e63..0000000000 Binary files a/apps/website/screens/components/file-input/specs/images/input_file_anatomy.png and /dev/null differ diff --git a/apps/website/screens/components/file-input/specs/images/input_file_fileitem_preview.png b/apps/website/screens/components/file-input/specs/images/input_file_fileitem_preview.png deleted file mode 100644 index 38a058392f..0000000000 Binary files a/apps/website/screens/components/file-input/specs/images/input_file_fileitem_preview.png and /dev/null differ diff --git a/apps/website/screens/components/file-input/specs/images/input_file_single_file.png b/apps/website/screens/components/file-input/specs/images/input_file_single_file.png deleted file mode 100644 index b84cdb4942..0000000000 Binary files a/apps/website/screens/components/file-input/specs/images/input_file_single_file.png and /dev/null differ diff --git a/apps/website/screens/components/file-input/specs/images/input_file_specs.png b/apps/website/screens/components/file-input/specs/images/input_file_specs.png deleted file mode 100644 index 171a55142c..0000000000 Binary files a/apps/website/screens/components/file-input/specs/images/input_file_specs.png and /dev/null differ diff --git a/apps/website/screens/components/file-input/specs/images/input_file_states_dropzone.png b/apps/website/screens/components/file-input/specs/images/input_file_states_dropzone.png deleted file mode 100644 index c877969c05..0000000000 Binary files a/apps/website/screens/components/file-input/specs/images/input_file_states_dropzone.png and /dev/null differ diff --git a/apps/website/screens/components/file-input/specs/images/input_file_states_file.png b/apps/website/screens/components/file-input/specs/images/input_file_states_file.png deleted file mode 100644 index 87f8e1748b..0000000000 Binary files a/apps/website/screens/components/file-input/specs/images/input_file_states_file.png and /dev/null differ diff --git a/apps/website/screens/components/file-input/specs/images/input_file_states_filedrop.png b/apps/website/screens/components/file-input/specs/images/input_file_states_filedrop.png deleted file mode 100644 index 97eca0231f..0000000000 Binary files a/apps/website/screens/components/file-input/specs/images/input_file_states_filedrop.png and /dev/null differ diff --git a/apps/website/screens/components/file-input/specs/images/input_file_states_fileitem.png b/apps/website/screens/components/file-input/specs/images/input_file_states_fileitem.png deleted file mode 100644 index 7ebe659fca..0000000000 Binary files a/apps/website/screens/components/file-input/specs/images/input_file_states_fileitem.png and /dev/null differ diff --git a/apps/website/screens/components/file-input/usage/FileInputUsagePage.tsx b/apps/website/screens/components/file-input/usage/FileInputUsagePage.tsx deleted file mode 100644 index ecd3b2fdf2..0000000000 --- a/apps/website/screens/components/file-input/usage/FileInputUsagePage.tsx +++ /dev/null @@ -1,95 +0,0 @@ -import { DxcParagraph, DxcBulletedList, DxcFlex, DxcTable } from "@dxc-technology/halstack-react"; -import QuickNavContainer from "@/common/QuickNavContainer"; -import QuickNavContainerLayout from "@/common/QuickNavContainerLayout"; -import DocFooter from "@/common/DocFooter"; -import variants from "./examples/variants"; -import Example from "@/common/example/Example"; - -const sections = [ - { - title: "Usage", - content: Considerations for the file input component use:, - subSections: [ - { - title: "Do's", - content: ( - - - Provide a meaningful label and helper text to help the user understand the files expected. - - - When displaying errors, provide feedback about the type of error using the error message. - - - When the file input process fails, provide useful information instead of showing an error message using - technical or undetermined information (e.g. '0x94 ERROR_PATH_BUSY'). - - - ), - }, - - { - title: "Don'ts", - content: ( - - - Use the file input component to upload multiple files inside a modal dialog. - - - Use a variant with drag and drop functionality when designing for mobile devices. - - - ), - }, - ], - }, - { - title: "Variants", - content: ( - <> - - - - - Name - Use case - - - - - - File - - Use the file variant when designing for multidevice - - - - Filedrop - - Use in large or complex forms when designing only for desktop - - - - Dropzone - - Choose the dropzone when the main purpose of the content is to file input files/images - - - - - ), - }, -]; - -const FileInputUsagePage = () => { - return ( - - - - - - - ); -}; - -export default FileInputUsagePage; diff --git a/packages/lib/src/file-input/FileInput.stories.tsx b/packages/lib/src/file-input/FileInput.stories.tsx index be4fbdbb9f..14b7c254ad 100644 --- a/packages/lib/src/file-input/FileInput.stories.tsx +++ b/packages/lib/src/file-input/FileInput.stories.tsx @@ -1,8 +1,8 @@ import { Meta, StoryObj } from "@storybook/react"; import ExampleContainer from "../../.storybook/components/ExampleContainer"; import Title from "../../.storybook/components/Title"; -import { HalstackProvider } from "../HalstackContext"; import DxcFileInput from "./FileInput"; +import { userEvent, within } from "@storybook/test"; export default { title: "File Input", @@ -70,12 +70,6 @@ const filesExamples = [ }, ]; -const opinionatedTheme = { - fileInput: { - fontColor: "#000000", - }, -}; - const FileInput = () => ( <> @@ -513,88 +507,36 @@ const FileInput = () => ( margin="xxlarge" /> </ExampleContainer> - <Title title="Opinionated theme" theme="light" level={2} /> - <ExampleContainer> - <HalstackProvider theme={opinionatedTheme}> - <Title title="Single file" theme="light" level={4} /> - <DxcFileInput - label="File input" - helperText="Please select files" - value={fileExample} - multiple={false} - callbackFile={() => {}} - /> - </HalstackProvider> - </ExampleContainer> - <ExampleContainer> - <HalstackProvider theme={opinionatedTheme}> - <Title title="Invalid single file" theme="light" level={4} /> - <DxcFileInput - label="File input" - helperText="Please select files" - value={fileExampleError} - multiple={false} - callbackFile={() => {}} - /> - </HalstackProvider> - </ExampleContainer> - <ExampleContainer> - <Title title="Single file" theme="light" level={4} /> - <HalstackProvider theme={opinionatedTheme}> - <DxcFileInput - mode="filedrop" - label="File input" - helperText="Please select files" - value={fileExample} - multiple={false} - callbackFile={() => {}} - /> - </HalstackProvider> - </ExampleContainer> - <ExampleContainer> - <Title title="Invalid single file" theme="light" level={4} /> - <HalstackProvider theme={opinionatedTheme}> - <DxcFileInput - mode="filedrop" - label="File input" - helperText="Please select files" - value={fileExampleError} - multiple={false} - callbackFile={() => {}} - /> - </HalstackProvider> - </ExampleContainer> - <ExampleContainer> - <Title title="Single file" theme="light" level={4} /> - <HalstackProvider theme={opinionatedTheme}> - <DxcFileInput - label="File input" - helperText="Please select files" - mode="dropzone" - value={fileExample} - callbackFile={() => {}} - multiple={false} - /> - </HalstackProvider> - </ExampleContainer> - <ExampleContainer> - <Title title="Invalid single file" theme="light" level={4} />{" "} - <HalstackProvider theme={opinionatedTheme}> - <DxcFileInput - label="File input" - helperText="Please select files" - mode="dropzone" - value={fileExampleError} - callbackFile={() => {}} - multiple={false} - /> - </HalstackProvider> - </ExampleContainer> </> ); +// const EllipsisError = () => { +// return ( +// <> +// <ExampleContainer> +// <Title title="Ellipsis error" theme="light" level={4} /> +// <DxcFileInput +// label="File input" +// helperText="Please select files" +// value={filesExamples} +// callbackFile={() => {}} +// /> +// </ExampleContainer> +// </> +// ); +// }; type Story = StoryObj<typeof DxcFileInput>; +// TODO: fix this test related to the tooltip when the error message has ellipsis +// export const FileInputEllipsisInError: Story = { +// render: EllipsisError, +// play: async ({ canvasElement }) => { +// const canvas = within(canvasElement); +// await userEvent.hover(canvas.getByText((text) => text.startsWith("This error message"))); +// await userEvent.hover(canvas.getByText((text) => text.startsWith("This error message"))); +// }, +// }; + export const Chromatic: Story = { render: FileInput, }; diff --git a/packages/lib/src/file-input/FileInput.tsx b/packages/lib/src/file-input/FileInput.tsx index 1b1301536a..2108c79c18 100644 --- a/packages/lib/src/file-input/FileInput.tsx +++ b/packages/lib/src/file-input/FileInput.tsx @@ -1,11 +1,13 @@ import { useCallback, useContext, useEffect, useId, useState, forwardRef, DragEvent, ChangeEvent } from "react"; -import styled, { ThemeProvider } from "styled-components"; +import styled from "styled-components"; import DxcButton from "../button/Button"; import { spaces } from "../common/variables"; import FileItem from "./FileItem"; import FileInputPropsType, { FileData, RefType } from "./types"; -import HalstackContext, { HalstackLanguageContext } from "../HalstackContext"; +import { HalstackLanguageContext } from "../HalstackContext"; import { getFilePreview, isFileIncluded } from "./utils"; +import HelperText from "../styles/forms/HelperText"; +import Label from "../styles/forms/Label"; const FileInputContainer = styled.div<{ margin: FileInputPropsType["margin"] }>` display: flex; @@ -22,27 +24,13 @@ const FileInputContainer = styled.div<{ margin: FileInputPropsType["margin"] }>` 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; + props.singleFileMode + ? "flex-direction: row; column-gap: var(--spacing-gap-xs);" + : "flex-direction: column; row-gap: var(--spacing-gap-xs);"} + margin-top: var(--spacing-gap-xs); `; const ValueInput = styled.input` @@ -52,14 +40,14 @@ const ValueInput = styled.input` const FileItemListContainer = styled.div` display: flex; flex-direction: column; - row-gap: 0.25rem; + row-gap: var(--spacing-gap-xs); `; const Container = styled.div` display: flex; flex-direction: column; - row-gap: 0.25rem; - margin-top: 0.25rem; + row-gap: var(--spacing-gap-xs); + margin-top: var(--spacing-gap-xs); `; const DragDropArea = styled.div<{ @@ -71,26 +59,21 @@ const DragDropArea = styled.div<{ 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;"} + ? "flex-direction: row; column-gap: var(--spacing-gap-s);" + : "justify-content: center; flex-direction: column; row-gap: var(--spacing-gap-s); 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"}; + padding: ${(props) => (props.mode === "filedrop" ? `var(--spacing-padding-xxs)` : "var(--spacing-padding-m)")}; 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)}; + border-radius: var(--border-radius-m); + border-width: var(--border-width-s); + border-style: var(--border-style-outline); + border-color: var(--border-color-neutral-dark); ${(props) => props.isDragging && ` - background-color: ${props.theme.dragoverDropBackgroundColor}; - border-color: transparent; - box-shadow: 0 0 0 2px ${props.theme.focusDropBorderColor}; + background-color: var(--color-bg-secondary-lightest); + border: var(--border-width-m) var(--border-style-default) var(--border-color-secondary-medium); `} cursor: ${(props) => props.disabled && "not-allowed"}; `; @@ -102,29 +85,20 @@ const DropzoneLabel = styled.div<{ disabled: FileInputPropsType["disabled"] }>` 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}; + color: ${(props) => (props.disabled ? "var(--color-fg-neutral-medium)" : "var(--color-fg-neutral-dark)")}; + font-family: var(--typography-font-family); + font-size: var(--typography-helper-text-m); + font-weight: var(--typography-helper-text-regular); `; 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; + color: ${(props) => (props.disabled ? "var(--color-fg-neutral-medium)" : "var(--color-fg-neutral-dark)")}; + font-family: var(--typography-font-family); + font-size: var(--typography-helper-text-m); + font-weight: var(--typography-helper-text-regular); `; const DxcFileInput = forwardRef<RefType, FileInputPropsType>( @@ -151,7 +125,6 @@ const DxcFileInput = forwardRef<RefType, FileInputPropsType>( const [isDragging, setIsDragging] = useState(false); const [files, setFiles] = useState<FileData[]>([]); const fileInputId = `file-input-${useId()}`; - const colorsTheme = useContext(HalstackContext); const translatedLabels = useContext(HalstackLanguageContext); const checkFileSize = (file: File) => { @@ -252,121 +225,116 @@ const DxcFileInput = forwardRef<RefType, FileInputPropsType>( }, [value]); return ( - <ThemeProvider theme={colorsTheme.fileInput}> - <FileInputContainer margin={margin} ref={ref}> - <Label htmlFor={fileInputId} disabled={disabled}> - {label} - </Label> - <HelperText disabled={disabled}>{helperText}</HelperText> - {mode === "file" ? ( - <FileContainer singleFileMode={!multiple && files.length === 1}> - <ValueInput - id={fileInputId} - type="file" - accept={accept} - multiple={multiple} - onChange={selectFiles} - disabled={disabled} - readOnly - /> + <FileInputContainer margin={margin} ref={ref}> + <Label htmlFor={fileInputId} disabled={disabled}> + {label} + </Label> + <HelperText disabled={disabled}>{helperText}</HelperText> + {mode === "file" ? ( + <FileContainer singleFileMode={!multiple && files.length === 1}> + <ValueInput + id={fileInputId} + type="file" + accept={accept} + multiple={multiple} + onChange={selectFiles} + disabled={disabled} + readOnly + /> + <DxcButton + mode="secondary" + label={ + buttonLabel ?? + (multiple + ? translatedLabels.fileInput.multipleButtonLabelDefault + : translatedLabels.fileInput.singleButtonLabelDefault) + } + onClick={handleClick} + disabled={disabled} + size={{ width: "fitContent", height: "medium" }} + tabIndex={tabIndex} + /> + {files.length > 0 && ( + <FileItemListContainer role="list"> + {files.map((file, index) => ( + <FileItem + fileName={file.file.name} + error={file.error} + singleFileMode={!multiple && files.length === 1} + showPreview={mode === "file" && !multiple ? false : showPreview} + preview={file.preview ?? ""} + type={file.file.type} + onDelete={onDelete} + tabIndex={tabIndex} + key={`file-${index}`} + /> + ))} + </FileItemListContainer> + )} + </FileContainer> + ) : ( + <Container> + <ValueInput + id={fileInputId} + type="file" + accept={accept} + multiple={multiple} + onChange={selectFiles} + disabled={disabled} + readOnly + /> + <DragDropArea + isDragging={isDragging} + disabled={disabled} + mode={mode} + onDrop={handleDrop} + onDragEnter={handleDragIn} + onDragOver={handleDrag} + onDragLeave={handleDragOut} + > <DxcButton mode="secondary" - label={ - buttonLabel ?? - (multiple - ? translatedLabels.fileInput.multipleButtonLabelDefault - : translatedLabels.fileInput.singleButtonLabelDefault) - } + label={buttonLabel ?? translatedLabels.fileInput.dropAreaButtonLabelDefault} onClick={handleClick} disabled={disabled} - size={{ width: "fitContent" }} - tabIndex={tabIndex} + size={{ width: "fitContent", height: "medium" }} /> - {files.length > 0 && ( - <FileItemListContainer role="list"> - {files.map((file, index) => ( - <FileItem - fileName={file.file.name} - error={file.error} - singleFileMode={!multiple && files.length === 1} - showPreview={mode === "file" && !multiple ? false : showPreview} - preview={file.preview ?? ""} - type={file.file.type} - onDelete={onDelete} - tabIndex={tabIndex} - key={`file-${index}`} - /> - ))} - </FileItemListContainer> - )} - </FileContainer> - ) : ( - <Container> - <ValueInput - id={fileInputId} - type="file" - accept={accept} - multiple={multiple} - onChange={selectFiles} - disabled={disabled} - readOnly - /> - <DragDropArea - isDragging={isDragging} - disabled={disabled} - mode={mode} - onDrop={handleDrop} - onDragEnter={handleDragIn} - onDragOver={handleDrag} - onDragLeave={handleDragOut} - > - <DxcButton - mode="secondary" - label={buttonLabel ?? translatedLabels.fileInput.dropAreaButtonLabelDefault} - onClick={handleClick} - disabled={disabled} - size={{ width: "fitContent" }} - /> - {mode === "dropzone" ? ( - <DropzoneLabel disabled={disabled}> - {dropAreaLabel ?? - (multiple - ? translatedLabels.fileInput.multipleDropAreaLabelDefault - : translatedLabels.fileInput.singleDropAreaLabelDefault)} - </DropzoneLabel> - ) : ( - <FiledropLabel disabled={disabled}> - {dropAreaLabel ?? - (multiple - ? translatedLabels.fileInput.multipleDropAreaLabelDefault - : translatedLabels.fileInput.singleDropAreaLabelDefault)} - </FiledropLabel> - )} - </DragDropArea> - {files.length > 0 && ( - <FileItemListContainer role="list"> - {files.map((file, index) => ( - <FileItem - fileName={file.file.name} - error={file.error} - singleFileMode={false} - showPreview={showPreview} - preview={file.preview ?? ""} - type={file.file.type} - onDelete={onDelete} - tabIndex={tabIndex} - key={`file-${index}`} - /> - ))} - </FileItemListContainer> + {mode === "dropzone" ? ( + <DropzoneLabel disabled={disabled}> + {dropAreaLabel ?? + (multiple + ? translatedLabels.fileInput.multipleDropAreaLabelDefault + : translatedLabels.fileInput.singleDropAreaLabelDefault)} + </DropzoneLabel> + ) : ( + <FiledropLabel disabled={disabled}> + {dropAreaLabel ?? + (multiple + ? translatedLabels.fileInput.multipleDropAreaLabelDefault + : translatedLabels.fileInput.singleDropAreaLabelDefault)} + </FiledropLabel> )} - </Container> - )} - {mode === "file" && !multiple && files.length === 1 && files[0]?.error && ( - <ErrorMessage>{files[0].error}</ErrorMessage> - )} - </FileInputContainer> - </ThemeProvider> + </DragDropArea> + {files.length > 0 && ( + <FileItemListContainer role="list"> + {files.map((file, index) => ( + <FileItem + fileName={file.file.name} + error={file.error} + singleFileMode={false} + showPreview={showPreview} + preview={file.preview ?? ""} + type={file.file.type} + onDelete={onDelete} + tabIndex={tabIndex} + key={`file-${index}`} + /> + ))} + </FileItemListContainer> + )} + </Container> + )} + </FileInputContainer> ); } ); diff --git a/packages/lib/src/file-input/FileItem.tsx b/packages/lib/src/file-input/FileItem.tsx index 02737b5006..a305f63451 100644 --- a/packages/lib/src/file-input/FileItem.tsx +++ b/packages/lib/src/file-input/FileItem.tsx @@ -1,10 +1,18 @@ -import { memo, useContext, useId } from "react"; +import { memo, MouseEvent, useContext, useId, useState } 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 { HalstackLanguageContext } from "../HalstackContext"; +import { TooltipWrapper } from "../tooltip/Tooltip"; + +const ListItem = styled.li` + list-style: none; + display: flex; + flex-direction: column; + gap: var(--spacing-gap-xs); +`; const MainContainer = styled.div<{ error: FileItemProps["error"]; @@ -13,18 +21,19 @@ const MainContainer = styled.div<{ }>` box-sizing: border-box; display: flex; - justify-content: center; - gap: 0.75rem; + align-items: center; + gap: var(--spacing-gap-m); width: ${(props) => (props.singleFileMode ? "230px" : "320px")}; + height: ${(props) => (props.singleFileMode || !props.showPreview) && "var(--height-m)"}; padding: ${(props) => - props.showPreview - ? `calc(8px - ${props.theme.fileItemBorderThickness})` - : `calc(8px - ${props.theme.fileItemBorderThickness}) calc(8px - ${props.theme.fileItemBorderThickness}) calc(8px - ${props.theme.fileItemBorderThickness}) 16px`}; - ${(props) => (props.error ? `background-color: ${props.theme.errorFileItemBackgroundColor};` : "")}; - border-color: ${(props) => (props.error ? props.theme.errorFileItemBorderColor : props.theme.fileItemBorderColor)}; - border-width: ${(props) => props.theme.fileItemBorderThickness}; - border-style: ${(props) => props.theme.fileItemBorderStyle}; - border-radius: ${(props) => props.theme.fileItemBorderRadius}; + props.showPreview && !props.singleFileMode + ? `var(--spacing-padding-xs) var(--spacing-padding-s)` + : `0px var(--spacing-padding-s)`}; + ${(props) => props.error && `background-color: var(--color-bg-error-lightest)`}; + border-color: ${(props) => (props.error ? `var(--border-color-error-medium)` : `var(--border-color-neutral-light)`)}; + border-width: ${(props) => (props.error ? `var(--border-width-m)` : `var(--border-width-s)`)}; + border-style: var(--border-style-default); + border-radius: var(--border-radius-s); `; const ImagePreview = styled.img` @@ -39,17 +48,16 @@ const IconPreview = styled.span<{ error: FileItemProps["error"] }>` display: flex; align-items: center; justify-content: center; - background-color: ${(props) => - props.error ? props.theme.errorFilePreviewBackgroundColor : props.theme.filePreviewBackgroundColor}; + background-color: ${(props) => (props.error ? `var(--color-bg-error-light) ` : `var(--color-bg-neutral-light)`)}; width: 48px; height: 48px; padding: 15px; border-radius: 2px; - color: ${(props) => (props.error ? props.theme.errorFilePreviewIconColor : props.theme.filePreviewIconColor)}; - font-size: 18px; + color: ${(props) => (props.error ? `var(--color-fg-error-medium)` : `var(--color-fg-neutral-strong) `)}; + font-size: var(--height-xs); svg { - height: 18px; - width: 18px; + width: 20px; + height: var(--height-xs); } `; @@ -58,38 +66,43 @@ const FileItemContent = styled.div` display: grid; grid-template-columns: auto min-content; grid-template-rows: min-content auto; - column-gap: 0.25rem; + column-gap: var(--spacing-gap-s); `; const FileName = styled.span` align-self: center; - color: ${(props) => props.theme.fileNameFontColor}; - font-family: ${(props) => props.theme.fileItemFontFamily}; - font-size: ${(props) => props.theme.fileItemFontSize}; - font-weight: ${(props) => props.theme.fileItemFontWeight}; - line-height: ${(props) => props.theme.fileItemLineHeight}; + color: var(--color-fg-neutral-dark); + font-family: var(--typography-font-family); + font-size: var(--typography-helper-text-m); + font-weight: var(--typography-helper-text-regular); white-space: pre; overflow: hidden; text-overflow: ellipsis; `; +const ErrorMessageContainer = styled.div<{ singleFileMode: FileItemProps["singleFileMode"] }>` + display: flex; + align-items: center; + gap: var(--spacing-gap-xs); + color: var(--color-fg-error-medium); + max-width: ${(props) => (props.singleFileMode ? "230px" : "320px")}; +`; const ErrorIcon = styled.span` display: flex; flex-wrap: wrap; align-content: center; - padding: 3px; - height: 18px; - width: 18px; - font-size: 18px; - color: #d0011b; + font-size: var(--height-xs); `; -const ErrorMessage = styled.span` - 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}; +const ErrorMessage = styled.div` + display: block; + color: var(--color-fg-error-medium); + font-family: var(--typography-font-family); + font-size: var(--typography-helper-text-s); + font-weight: var(--typography-helper-text-regular); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; `; const FileItem = ({ @@ -103,40 +116,48 @@ const FileItem = ({ tabIndex, }: FileItemProps): JSX.Element => { const translatedLabels = useContext(HalstackLanguageContext); + const [hasTooltip, setHasTooltip] = useState(false); const fileNameId = useId(); + const handleOnMouseEnter = (event: MouseEvent<HTMLSpanElement>) => { + const text = event.currentTarget; + setHasTooltip(text.scrollWidth > text.clientWidth); + }; + 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 && ( + <ListItem> + <MainContainer error={error} singleFileMode={singleFileMode} showPreview={showPreview}> + {showPreview && + (type.includes("image") ? ( + <ImagePreview src={preview} alt={`Preview of ${fileName}`} /> + ) : ( + <IconPreview aria-labelledby={fileNameId} error={error} role="img"> + <DxcIcon icon={preview} /> + </IconPreview> + ))} + <FileItemContent> + <FileName id={fileNameId}>{fileName}</FileName> + <DxcFlex> + <DxcActionIcon + onClick={() => onDelete(fileName)} + icon="close" + tabIndex={tabIndex} + title={translatedLabels.fileInput.deleteFileActionTitle} + /> + </DxcFlex> + </FileItemContent> + </MainContainer> + {error && ( + <TooltipWrapper condition={hasTooltip} label={error}> + <ErrorMessageContainer role="alert" aria-live="assertive" singleFileMode={singleFileMode}> <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> + <ErrorMessage onMouseEnter={handleOnMouseEnter}>{error}</ErrorMessage> + </ErrorMessageContainer> + </TooltipWrapper> + )} + </ListItem> ); };