Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

File upload component #272

Merged
merged 14 commits into from
Mar 29, 2023
Merged
Binary file added src/assets/images/file-upload-complete.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/assets/images/file-upload-multiple.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/assets/images/file-upload-progress.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions src/components/_all.scss
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
@import 'callout/callout';
@import 'card/card';
@import 'content-block/content-block';
@import 'file-upload/file-upload';
@import 'filters/filters';
@import 'footer/footer';
@import 'form/form';
Expand Down
8 changes: 8 additions & 0 deletions src/components/file-upload/_file-upload.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<label class="nsw-form__label{{#if required}} nsw-form__required{{/if}}" for="{{id}}">{{label}}{{#if required}}<span class="sr-only"> (required)</span>{{/if}}</label>{{#if helper-text}}
<span class="nsw-form__helper" id="{{id}}-helper-text">{{{helper-text}}}</span>{{/if}}{{#if error-text}}
<span class="nsw-form__helper nsw-form__helper--error" id="{{id}}-error-text"><span class="material-icons nsw-material-icons" focusable="false" aria-hidden="true">cancel</span>{{error-text}}</span>{{/if}}{{#if valid-text}}
<span class="nsw-form__helper nsw-form__helper--valid" id="{{id}}-valid-text"><span class="material-icons nsw-material-icons" focusable="false" aria-hidden="true">check_circle</span>{{valid-text}}</span>{{/if}}
<div class="nsw-file-upload{{#if js}} js-file-upload{{/if}}"{{#if replace}} data-replace-files{{/if}}>
<input type="file" class="nsw-file-upload__input" name="{{id}}" id="{{id}}"{{#if helper-text}} aria-describedby="{{#if helper-text}}{{id}}-helper-text{{/if}}"{{/if}}{{#if error-text}} aria-invalid="true"{{/if}}{{#if accept}} accept="image/*,.pdf"{{/if}}{{#if multiple}} multiple{{/if}}>
<label for="{{id}}" class="nsw-file-upload__label nsw-button nsw-button--dark-outline-solid">Select file</label>
</div>
56 changes: 56 additions & 0 deletions src/components/file-upload/_file-upload.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
.nsw-file-upload {
margin-top: rem(8px);
display: block;

&__label {
margin: 0;
}

&__input {
position: absolute;
opacity: 0;

&:disabled {
+ .nsw-file-upload__label {
opacity: 0.4;
cursor: not-allowed;
}
}

&:focus + .nsw-file-upload__label {
@include nsw-focus;
}
}

&__list {
display: none;
margin: 0;
padding: 0;
border: 0;

&.active {
display: block;
}
}

&__item {
display: flex;
max-width: rem(500px);
align-items: center;
justify-content: space-between;
background-color: var(--nsw-off-white);
border-radius: var(--nsw-border-radius);
padding: rem(8px);

&:not(:last-child) {
margin-bottom: rem(8px);
}
}

&__item-filename {
margin-left: rem(8px);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
74 changes: 74 additions & 0 deletions src/components/file-upload/_guidance.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
---
title: File upload
layout: blank-layout.hbs
---

<h2>Usage</h2>
<p>Use file upload when a user is required to upload a file from their device to a specific location. Only ask users to upload files when it's critical to the delivery of your service.</p>

<p>Do:</p>

<ul>
<li>Use help text to highlight any input restrictions to the users upfront, for example format or size</li>
<li>Allow multiple file formats as not everyone has access to the same software</li>
<li>Be considerate of asking for large files, as some users may have limited band width or data</li>
<li>Let users know exactly what errors occurred during an upload process so they can fix them</li>
</ul>

<h3>When to avoid</h3>
<p>Avoid asking users to provide documents if you don't require them. Be considerate of file size and don't ask users to upload large files.</p>

<h2>How this component works</h2>

<h3>Truncate file names</h3>
<p>Truncate file names when they extend beyond the width of the parent element</p>

<h3>Upload status</h3>

<div class="nsw-m-top-xs nsw-in-page-alert nsw-in-page-alert--info nsw-in-page-alert--compact">
<span class="material-icons nsw-material-icons nsw-in-page-alert__icon" focusable="false" aria-hidden="true">info</span>
<div class="nsw-in-page-alert__content">
<p class="nsw-small">This functionailty is not included in the base component.</p>
</div>
</div>

<p>Where a user would benefit from knowing the status of their upload consider adding indicators.</p>

<div class="nsw-grid">
<div class="nsw-col nsw-col-md-6">
<figure class="nsw-media nsw-media--light nsw-m-top-xs">
<img src="/assets/images/file-upload-progress.png" alt="A loader showing the process is pending.">
</figure>
<div class="nsw-bg--brand-light nsw-p-sm">
<p>A loader can show the process is pending.</p>
</div>
</div>
<div class="nsw-col nsw-col-md-6">
<figure class="nsw-media nsw-media--light nsw-m-top-xs">
<img src="/assets/images/file-upload-complete.png" alt="A tick showing the process is complete.">
</figure>
<div class="nsw-bg--brand-light nsw-p-sm">
<p>A tick can show the process is complete.</p>
</div>
</div>
</div>

<h3>Multiple uploads</h3>
<p>Clearly communicate if a user can upload multiple files and consider displaying additional files vertically.</p>

<div class="nsw-grid">
<div class="nsw-col nsw-col-md-6">
<figure class="nsw-media nsw-media--light nsw-m-top-xs">
<img src="/assets/images/file-upload-multiple.png" alt="A loader showing the process is pending.">
</figure>
<div class="nsw-bg--brand-light nsw-p-sm">
<p>Clearly group the files and allow users to remove each individually.</p>
</div>
</div>
</div>

<h3>Error messages</h3>
<p>Error messages inform the user when there is an issue with the data they have input. Provide helpful error messages to clearly explain to the user what the issue is and how they can effectively address the errors. Check out <a href="https://design-system.service.gov.uk/components/file-upload/#error-messages">GOV.UK Design System's error messages for file uploads</a> for some best practice examples.</p>

<h2>Accessibility</h2>
<p>All components are responsive and meet WCAG 2.1 AA accessibility guidelines.</p>
34 changes: 34 additions & 0 deletions src/components/file-upload/blank.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
---
title: File upload
width: narrow
page: true
---

{{#>_layout-container}}
<div class="nsw-form">
<div class="nsw-form__group">
{{>_file-upload
js=true
id="file-upload-helper"
label="Upload drivers license"
helper-text="Formats accepted: JPG, PNG or PDF <br /> File size must not exceed 350MB"
accept=true
multiple=true
replace=true
}}
</div>
</div>

<div class="nsw-form">
<div class="nsw-form__group">
{{>_file-upload
js=true
id="file-upload-error"
label="Upload drivers license"
helper-text="Formats accepted: JPG, PNG or PDF <br /> File size must not exceed 350MB"
error-text="The selected file must be smaller than 350MB"
accept=true
}}
</div>
</div>
{{/_layout-container}}
87 changes: 87 additions & 0 deletions src/components/file-upload/file-upload.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
/* eslint-disable max-len */
class FileUpload {
constructor(element) {
this.element = element
this.input = this.element.querySelector('.nsw-file-upload__input')
this.label = this.element.querySelector('.nsw-file-upload__label')
this.multipleUpload = this.input.hasAttribute('multiple')
this.replaceFiles = this.element.hasAttribute('data-replace-files')
this.filesList = false
}

init() {
if (!this.input) return

this.input.addEventListener('change', () => {
if (this.input.value === '') return
this.updateFileList()
})
}

createFileList() {
const ul = document.createElement('ul')
ul.classList.add('nsw-file-upload__list')
this.label.insertAdjacentElement('afterend', ul)
this.filesList = this.element.querySelector('.nsw-file-upload__list')
}

createFileItem(file) {
const li = document.createElement('li')
li.classList.add('nsw-file-upload__item')
const html = `
<span class="nsw-file-upload__item-filename"></span>
<button type="button" class="nsw-icon-button">
<span class="sr-only">Remove file</span>
<span class="material-icons nsw-material-icons" focusable="false" aria-hidden="true">cancel</span>
</button>`

li.insertAdjacentHTML('afterbegin', html)
li.querySelector('.nsw-file-upload__item-filename').textContent = this.constructor.truncateString(file.name, 50)
return li.outerHTML
}

updateFileList() {
if (!this.filesList) {
this.createFileList()
}

this.filesList.classList.add('active')

let string = ''
for (let i = 0; i < this.input.files.length; i += 1) {
const file = this.input.files[i]
string = this.createFileItem(file) + string
}

if (this.replaceFiles) {
this.filesList.innerHTML = string
} else {
this.filesList.insertAdjacentHTML('beforeend', string)
}

this.removeFile()
}

removeFile() {
this.filesList.addEventListener('click', (event) => {
if (!event.target.closest('.nsw-icon-button')) return
event.preventDefault()
const item = event.target.closest('.nsw-file-upload__item')

item.remove()

if (event.currentTarget.children.length === 0) {
this.filesList.classList.remove('active')
}
})
}

static truncateString(str, num) {
if (str.length <= num) {
return str
}
return `${str.slice(0, num)}...`
}
}

export default FileUpload
40 changes: 40 additions & 0 deletions src/components/file-upload/index.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
---
title: File upload
width: narrow
tabs: true
directory: file-upload
intro: File uploaders help users to select and upload files.
meta-description: File uploaders help users to select and upload files.
meta-index: true
---

{{#>_docs-example}}
<div class="nsw-form">
<div class="nsw-form__group">
{{>_file-upload
js=true
id="file-upload-helper"
label="Upload drivers license"
helper-text="Formats accepted: JPG, PNG or PDF <br /> File size must not exceed 350MB"
accept=true
multiple=true
}}
</div>
</div>
{{/_docs-example}}

<h3>Error messages</h3>
{{#>_docs-example}}
<div class="nsw-form">
<div class="nsw-form__group">
{{>_file-upload
js=true
id="file-upload-error"
label="Upload drivers license"
helper-text="Formats accepted: JPG, PNG or PDF <br /> File size must not exceed 350MB"
error-text="The selected file must be smaller than 350MB"
accept=true
}}
</div>
</div>
{{/_docs-example}}
10 changes: 9 additions & 1 deletion src/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import Navigation from './components/main-nav/main-nav'
import Accordion from './components/accordion/accordion'
import Dialog from './components/dialog/dialog'
import Filters from './components/filters/filters'
import FileUpload from './components/file-upload/file-upload'
import Tabs from './components/tabs/tabs'
import GlobalAlert from './components/global-alert/global-alert'

Expand Down Expand Up @@ -32,6 +33,7 @@ function initSite() {
const closeSearchButton = document.querySelectorAll('.js-close-search')
const accordions = document.querySelectorAll('.js-accordion')
const dialogs = document.querySelectorAll('.js-dialog')
const fileUpload = document.querySelectorAll('.js-file-upload')
const filters = document.querySelectorAll('.js-filters')
const tabs = document.querySelectorAll('.js-tabs')
const globalAlert = document.querySelectorAll('.js-global-alert')
Expand All @@ -55,6 +57,12 @@ function initSite() {
new Dialog(element).init()
})

if (fileUpload) {
fileUpload.forEach((element) => {
new FileUpload(element).init()
})
}

if (filters) {
filters.forEach((element) => {
new Filters(element).init()
Expand All @@ -75,5 +83,5 @@ function initSite() {
}

export {
initSite, SiteSearch, Navigation, Accordion, Tabs, GlobalAlert,
initSite, SiteSearch, Navigation, Accordion, Tabs, GlobalAlert, Dialog, Filters, FileUpload,
}
1 change: 1 addition & 0 deletions src/main.scss
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
@import 'components/callout/callout';
@import 'components/card/card';
@import 'components/content-block/content-block';
@import 'components/file-upload/file-upload';
@import 'components/filters/filters';
@import 'components/footer/footer';
@import 'components/form/form';
Expand Down