Skip to content

Commit

Permalink
2643 - va-file-input-multiple (#1184)
Browse files Browse the repository at this point in the history
* WIP: Add va-file-input-multi files and begin setting up layout

* change [uswds='true'] to :not([uswds=false])

* Fix handleChange logic and cleanup code

* Create headless prop to denote va-file-input is part of a composite component and won't render certain parts of UI

* Styled for when files are uploaded

* Add storybook example for custom validation. Do not emit vaChange from va-file-input when internally validated

* style when no files are uploaded

* add multiple file input tests

* WIP: Add more examples of va-file-input-multiple to storybook page

* Revert to using slots instead of children for additionalInfo functionality

* initial set up to get child/sloted nodes

* initial set up to get child/sloted nodes-2

* Clean up multiple mentions in single file input. fix flaky test

* test using innerHTML

* clean up tests

* revert individual file input slot system

* Update file-input logic to try and use dynamically named slots

* remove empty docs

* additional info variant/works instorybook

* does not show additional content  prior to adding a file

* Update styling and storybook examples

* Add JSDoc comments, move FileIndex interface to new file, remove code for debugging and TODO comments

* Fix additional merge issues

* Add documentation from new methods in merge

* bump to 9.3.0

* Remove tests which are not valid after restructuring when errors show. Bump package.json version

---------

Co-authored-by: Alex Taker <[email protected]>
Co-authored-by: Ray Messina <[email protected]>
  • Loading branch information
3 people authored Jun 18, 2024
1 parent d22625f commit b88a8d9
Show file tree
Hide file tree
Showing 12 changed files with 885 additions and 94 deletions.
308 changes: 308 additions & 0 deletions packages/storybook/stories/va-file-input-multiple-uswds.stories.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,308 @@
/* eslint-disable react/prop-types */
import React, {useState} from 'react';
import {VaFileInputMultiple} from '@department-of-veterans-affairs/web-components/react-bindings';
import {getWebComponentDocs, propStructure} from './wc-helpers';

const fileInputDocs = getWebComponentDocs('va-file-input-multiple');

export default {
title: 'Components/File input multiple USWDS',
id: 'uswds/va-file-input-multiple',
parameters: {
componentSubtitle: `va-file-input-multiple web component`,

},
};

const defaultArgs = {
'label': 'Select a file to upload',
'name': 'my-file-input',
'accept': null,
'required': false,
'errors': [],
'enable-analytics': false,
'hint': 'You can upload a .pdf, .gif, .jpg, .bmp, or .txt file.',
'vaMultipleChange': null,
'header-size': null,
'children': null
};

const Template = ({
label,
name,
accept,
errors,
required,
hint,
'enable-analytics': enableAnalytics,
vaMultipleChange,
headerSize,
additional
}) => {
return (
<VaFileInputMultiple
label={label}
name={name}
accept={accept}
required={required}
errors={errors}
hint={hint}
enable-analytics={enableAnalytics}
onVaMultipleChange={vaMultipleChange}
header-size={headerSize}
children={additional}
/>
);
};

export const Default = Template.bind(null);
Default.args = { ...defaultArgs };
Default.argTypes = propStructure(fileInputDocs);

export const Accept = Template.bind(null);
Accept.args = {
...defaultArgs,
label: 'Select PDF files',
hint: 'All files in this list must be PDFs',
accept: '.pdf'
};

export const HeaderSize = Template.bind(null);
HeaderSize.args = {
...defaultArgs,
label: 'Custom sized header',
hint: 'Numbers from 1-6 correspond to an H1-H6 tag',
headerSize: 1
};

const additionalInfoContent = (
<div>
<va-select className="hydrated" uswds label='What kind of file is this?' required>
<option key="1" value="1">Public Document</option>
<option key="2" value="2">Private Document</option>
</va-select>
</div>);

const AdditionalInfoTemplate = ({
label,
name,
accept,
errors,
required,
hint,
'enable-analytics': enableAnalytics,
vaMultipleChange,
headerSize,
additional
}) => {
return (
<>
<VaFileInputMultiple
label={label}
name={name}
accept={accept}
required={required}
errors={errors}
hint={hint}
enable-analytics={enableAnalytics}
onVaMultipleChange={vaMultipleChange}
header-size={headerSize}>
{additional}
</VaFileInputMultiple>
<hr/>
<div>
<p>To add additional fields associated with some file, you can pass custom content into the slot of this component and it will render in each file input.</p>
<p>This example showcases how to include custom content, such as dropdowns, within the file input component.</p>
</div>
<div className="vads-u-margin-top--2">
<pre className="vads-u-font-size--sm vads-u-background-color--gray-lightest vads-u-padding--2">
<code>
{`const additionalInfoContent = (
<div>
<va-select className="hydrated" uswds label='What kind of file is this?' required>
<option key="1" value="1">Public Document</option>
<option key="2" value="2">Private Document</option>
</va-select>
</div>
);
<VaFileInputMultiple ... >
{additionalInfoContent}
</VaFileInputMultiple>`}
</code>
</pre>
<a
href="https://github.com/department-of-veterans-affairs/component-library/tree/main/packages/storybook/stories"
target="_blank"
>
View validation code in our repo
</a>
</div>
</>
);
};

export const AdditionalInfo = AdditionalInfoTemplate.bind(null);
AdditionalInfo.args = {
...defaultArgs,
label: 'Label Header',
additional: additionalInfoContent
}

const ErrorsTemplate = ({label, name, hint}) => {
const [errorsList, setErrorsList] = useState([]);

function setErrorForEachFile(event) {
const fileEntries = event.detail.files;
const errors = fileEntries.map((file, index) => {
if (!file) {
return '';
}
return 'Error for index ' + index;
});
setErrorsList(errors);
}

return (
<>
<VaFileInputMultiple
label={label}
name={name}
hint={hint}
errors={errorsList}
onVaMultipleChange={setErrorForEachFile}
/>
<hr />
<div>
<p>Parent components are responsible for managing error states through a dedicated error array. Each index in this array corresponds to a file input, with the value at each index representing the error state for that specific file. This setup allows for the dynamic display of errors based on real-time validation of each file as it is processed.</p>
</div>
<div className="vads-u-margin-top--2">
<pre className="vads-u-font-size--sm vads-u-background-color--gray-lightest vads-u-padding--2">
<code>
{`const [errorsList, setErrorsList] = useState([]);
function setErrorForEachFile(event) {
const fileEntries = event.detail.files;
const errors = fileEntries.map((file, index) => {
if (!file) {
return '';
}
return 'Error for index ' + index;
});
setErrorsList(errors);
}
return (
<VaFileInputMultiple
...
errors={errorsList}
onVaMultipleChange={setErrorForEachFile}
/>`}
</code>
</pre>
<a
href="https://github.com/department-of-veterans-affairs/component-library/tree/main/packages/storybook/stories"
target="_blank"
>
View validation code in our repo
</a>
</div>
</>
)
};

export const Errors = ErrorsTemplate.bind(null);
Errors.args = {
...defaultArgs,
label: 'Label Header',
}

const CustomValidationTemplate = ({ label, name, accept, hint }) => {
const [errorsList, setErrorsList] = useState([]);

function validateFileContents(event) {
let errors = [];
const fileEntries = event.detail.files;

fileEntries.forEach(fileEntry => {
if (fileEntry) {
let error = '';

if (fileEntry.size > 2 * 1024 * 1024) { // 2MB = 2 * 1024 * 1024 bytes
error = "File size cannot be greater than 2MB";
}

errors.push(error);
} else {
errors.push(''); // Add an empty error if no fileEntry
}
});

setErrorsList(errors);
}

return (
<>
<VaFileInputMultiple
label={label}
name={name}
hint={hint}
errors={errorsList}
accept={accept}
onVaMultipleChange={validateFileContents}
/>
<hr />
<div>
<p>The parent component can capture the files from the onVaMultipleChange event, validate the files, and dynamically set errors. Each file must have a corresponding entry in the errors array prop, even if the entry is an empty string indicating no errors.</p>
<p>This example demonstrates custom validation logic to show an error if the file size exceeds 2MB. Validation occurs when a file is added or removed. </p>
</div>
<div className="vads-u-margin-top--2">
<pre className="vads-u-font-size--sm vads-u-background-color--gray-lightest vads-u-padding--2">
<code>
{`const [errorsList, setErrorsList] = useState([]);
function validateFileContents(event) {
let errors = [];
const fileEntries = event.detail.files;
fileEntries.forEach(fileEntry => {
if (fileEntry) {
let error = '';
if (fileEntry.size > 2 * 1024 * 1024) { // 2MB = 2 * 1024 * 1024 bytes
error = "File size cannot be greater than 2MB";
}
errors.push(error);
} else {
errors.push(''); // Add an empty error if no fileEntry
}
});
setErrorsList(errors);
}
<VaFileInputMultiple
...
errors={errorsList}
onVaMultipleChange={validateFileContents}
/>`}
</code>
</pre>
<a
href="https://github.com/department-of-veterans-affairs/component-library/tree/main/packages/storybook/stories"
target="_blank"
>
View validation code in our repo
</a>
</div>
</>
);
};

export const CustomValidation = CustomValidationTemplate.bind(null);
CustomValidation.args = {
...defaultArgs,
label: 'Upload files which are smaller than 2 MB',
hint: 'Select any file type',
}
16 changes: 9 additions & 7 deletions packages/storybook/stories/va-file-input-uswds.stories.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,9 @@ const defaultArgs = {
'error': '',
'enable-analytics': false,
'hint': 'You can upload a .pdf, .gif, .jpg, .bmp, or .txt file.',
'vaChange': event =>
alert(`File change event received: ${event?.detail?.files[0]?.name}`),
'vaChange': null,
'uswds': true,
'headerSize': null,
'header-size': null,
'children': null
};

Expand Down Expand Up @@ -55,7 +54,7 @@ const Template = ({
enable-analytics={enableAnalytics}
onVaChange={vaChange}
uswds={uswds}
headerSize={headerSize}
header-size={headerSize}
children={children}
/>
);
Expand Down Expand Up @@ -100,7 +99,7 @@ HeaderLabel.args = {
required: true
}

const additionalInfoSlotContent = (
const additionalInfoContent = (
<div>
<va-select className="hydrated" uswds label='What kind of file is this?' required>
<option key="1" value="1">Public Document</option>
Expand All @@ -112,7 +111,7 @@ export const AdditionalInfo = Template.bind(null);
AdditionalInfo.args = {
...defaultArgs,
label: 'Label Header',
children: additionalInfoSlotContent
children: additionalInfoContent
}

const CustomValidationTemplate = ({ label, name, accept, required, error, hint }) => {
Expand Down Expand Up @@ -206,4 +205,7 @@ CustomValidation.args = {
}

export const WithAnalytics = Template.bind(null);
WithAnalytics.args = { ...defaultArgs, 'enable-analytics': true };
WithAnalytics.args = {
...defaultArgs,
'enable-analytics': true
};
2 changes: 1 addition & 1 deletion packages/web-components/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@department-of-veterans-affairs/web-components",
"version": "9.2.0",
"version": "10.0.0",
"description": "Stencil Component Starter",
"main": "dist/index.cjs.js",
"module": "dist/index.js",
Expand Down
Loading

0 comments on commit b88a8d9

Please sign in to comment.