From 4764d10d4cbe42f5f4f0e13ee16cc8041bb3c1c0 Mon Sep 17 00:00:00 2001 From: beeps Date: Thu, 12 Sep 2024 14:53:35 +0100 Subject: [PATCH 01/14] [WIP] Spike progressively enhanced file upload --- packages/govuk-frontend/src/govuk/all.mjs | 1 + .../src/govuk/all.puppeteer.test.js | 1 + .../govuk/components/file-upload/_index.scss | 48 ++++ .../components/file-upload/file-upload.mjs | 229 ++++++++++++++++++ .../components/file-upload/file-upload.yaml | 40 ++- .../govuk/components/file-upload/template.njk | 2 +- .../src/govuk/init.jsdom.test.mjs | 2 + packages/govuk-frontend/src/govuk/init.mjs | 5 + .../tasks/build/package.unit.test.mjs | 1 + 9 files changed, 321 insertions(+), 8 deletions(-) create mode 100644 packages/govuk-frontend/src/govuk/components/file-upload/file-upload.mjs diff --git a/packages/govuk-frontend/src/govuk/all.mjs b/packages/govuk-frontend/src/govuk/all.mjs index fe0de924eb..d9918711cb 100644 --- a/packages/govuk-frontend/src/govuk/all.mjs +++ b/packages/govuk-frontend/src/govuk/all.mjs @@ -5,6 +5,7 @@ export { CharacterCount } from './components/character-count/character-count.mjs export { Checkboxes } from './components/checkboxes/checkboxes.mjs' export { ErrorSummary } from './components/error-summary/error-summary.mjs' export { ExitThisPage } from './components/exit-this-page/exit-this-page.mjs' +export { FileUpload } from './components/file-upload/file-upload.mjs' export { Header } from './components/header/header.mjs' export { NotificationBanner } from './components/notification-banner/notification-banner.mjs' export { PasswordInput } from './components/password-input/password-input.mjs' diff --git a/packages/govuk-frontend/src/govuk/all.puppeteer.test.js b/packages/govuk-frontend/src/govuk/all.puppeteer.test.js index 6d6a61491b..ef255fd897 100644 --- a/packages/govuk-frontend/src/govuk/all.puppeteer.test.js +++ b/packages/govuk-frontend/src/govuk/all.puppeteer.test.js @@ -71,6 +71,7 @@ describe('GOV.UK Frontend', () => { 'Component', 'ErrorSummary', 'ExitThisPage', + 'FileUpload', 'Header', 'NotificationBanner', 'PasswordInput', diff --git a/packages/govuk-frontend/src/govuk/components/file-upload/_index.scss b/packages/govuk-frontend/src/govuk/components/file-upload/_index.scss index 5862ab9cc3..4b20003587 100644 --- a/packages/govuk-frontend/src/govuk/components/file-upload/_index.scss +++ b/packages/govuk-frontend/src/govuk/components/file-upload/_index.scss @@ -46,4 +46,52 @@ cursor: not-allowed; } } + + .govuk-file-upload-wrapper { + display: inline-flex; + align-items: baseline; + position: relative; + } + + .govuk-file-upload-wrapper--show-dropzone { + $dropzone-padding: govuk-spacing(2); + + margin-top: -$dropzone-padding; + margin-left: -$dropzone-padding; + padding: $dropzone-padding; + outline: 2px dotted govuk-colour("mid-grey"); + background-color: govuk-colour("light-grey"); + + .govuk-file-upload__button, + .govuk-file-upload__status { + // When the dropzone is hovered over, make these aspects not accept + // mouse events, so dropped files fall through to the input beneath them + pointer-events: none; + } + } + + .govuk-file-upload-wrapper .govuk-file-upload { + // Make the native control take up the entire space of the element, but + // invisible and behind the other elements until we need it + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + margin: 0; + padding: 0; + opacity: 0; + } + + .govuk-file-upload__button { + width: auto; + margin-bottom: 0; + flex-grow: 0; + flex-shrink: 0; + } + + .govuk-file-upload__status { + margin-bottom: 0; + margin-left: govuk-spacing(2); + } } diff --git a/packages/govuk-frontend/src/govuk/components/file-upload/file-upload.mjs b/packages/govuk-frontend/src/govuk/components/file-upload/file-upload.mjs new file mode 100644 index 0000000000..00076df49b --- /dev/null +++ b/packages/govuk-frontend/src/govuk/components/file-upload/file-upload.mjs @@ -0,0 +1,229 @@ +import { closestAttributeValue } from '../../common/closest-attribute-value.mjs' +import { mergeConfigs } from '../../common/index.mjs' +import { normaliseDataset } from '../../common/normalise-dataset.mjs' +import { ElementError } from '../../errors/index.mjs' +import { GOVUKFrontendComponent } from '../../govuk-frontend-component.mjs' +import { I18n } from '../../i18n.mjs' + +/** + * File upload component + * + * @preserve + */ +export class FileUpload extends GOVUKFrontendComponent { + /** + * @private + * @type {HTMLInputElement} + */ + $input + + /** + * @private + * @type {HTMLElement} + */ + $wrapper + + /** + * @private + * @type {HTMLButtonElement} + */ + $button + + /** + * @private + * @type {HTMLElement} + */ + $status + + /** + * @private + * @type {FileUploadConfig} + */ + config + + /** @private */ + i18n + + /** + * @param {Element | null} $input - File input element + * @param {FileUploadConfig} [config] - File Upload config + */ + constructor($input, config = {}) { + super() + + if (!($input instanceof HTMLInputElement)) { + throw new ElementError({ + componentName: 'File upload', + element: $input, + expectedType: 'HTMLInputElement', + identifier: 'Root element (`$module`)' + }) + } + + if ($input.type !== 'file') { + throw new ElementError('File upload: Form field must be of type `file`.') + } + + this.config = mergeConfigs( + FileUpload.defaults, + config, + normaliseDataset(FileUpload, $input.dataset) + ) + + this.i18n = new I18n(this.config.i18n, { + // Read the fallback if necessary rather than have it set in the defaults + locale: closestAttributeValue($input, 'lang') + }) + + $input.addEventListener('change', this.onChange.bind(this)) + this.$input = $input + + // Wrapping element. This defines the boundaries of our drag and drop area. + const $wrapper = document.createElement('div') + $wrapper.className = 'govuk-file-upload-wrapper' + $wrapper.addEventListener('dragover', this.onDragOver.bind(this)) + $wrapper.addEventListener('dragleave', this.onDragLeaveOrDrop.bind(this)) + $wrapper.addEventListener('drop', this.onDragLeaveOrDrop.bind(this)) + + // Create the file selection button + const $button = document.createElement('button') + $button.className = + 'govuk-button govuk-button--secondary govuk-file-upload__button' + $button.type = 'button' + $button.innerText = this.i18n.t('selectFilesButton') + $button.addEventListener('click', this.onClick.bind(this)) + + // Create status element that shows what/how many files are selected + const $status = document.createElement('span') + $status.className = 'govuk-body govuk-file-upload__status' + $status.innerText = this.i18n.t('filesSelectedDefault') + $status.setAttribute('role', 'status') + + // Assemble these all together + $wrapper.insertAdjacentElement('beforeend', $button) + $wrapper.insertAdjacentElement('beforeend', $status) + + // Inject all this *after* the native file input + this.$input.insertAdjacentElement('afterend', $wrapper) + + // Move the native file input to inside of the wrapper + $wrapper.insertAdjacentElement('afterbegin', this.$input) + + // Make all these new variables available to the module + this.$wrapper = $wrapper + this.$button = $button + this.$status = $status + + // Bind change event to the underlying input + this.$input.addEventListener('change', this.onChange.bind(this)) + } + + /** + * Check if the value of the underlying input has changed + */ + onChange() { + if (!this.$input.files) { + return + } + + const fileCount = this.$input.files.length + + if (fileCount === 0) { + // If there are no files, show the default selection text + this.$status.innerText = this.i18n.t('filesSelectedDefault') + } else if ( + // If there is 1 file, just show the file name + fileCount === 1 + ) { + this.$status.innerText = this.$input.files[0].name + } else { + // Otherwise, tell the user how many files are selected + this.$status.innerText = this.i18n.t('filesSelected', { + count: fileCount + }) + } + } + + /** + * When the button is clicked, emulate clicking the actual, hidden file input + */ + onClick() { + this.$input.click() + } + + /** + * When a file is dragged over the container, show a visual indicator that a + * file can be dropped here. + */ + onDragOver() { + this.$wrapper.classList.add('govuk-file-upload-wrapper--show-dropzone') + } + + /** + * When a dragged file leaves the container, or the file is dropped, + * remove the visual indicator. + */ + onDragLeaveOrDrop() { + this.$wrapper.classList.remove('govuk-file-upload-wrapper--show-dropzone') + } + + /** + * Name for the component used when initialising using data-module attributes. + */ + static moduleName = 'govuk-file-upload' + + /** + * File upload default config + * + * @see {@link FileUploadConfig} + * @constant + * @type {FileUploadConfig} + */ + static defaults = Object.freeze({ + i18n: { + selectFilesButton: 'Choose file', + filesSelectedDefault: 'No file chosen', + filesSelected: { + one: '%{count} file chosen', + other: '%{count} files chosen' + } + } + }) + + /** + * File upload config schema + * + * @constant + * @satisfies {Schema} + */ + static schema = Object.freeze({ + properties: { + i18n: { type: 'object' } + } + }) +} + +/** + * File upload config + * + * @see {@link FileUpload.defaults} + * @typedef {object} FileUploadConfig + * @property {FileUploadTranslations} [i18n=FileUpload.defaults.i18n] - File upload translations + */ + +/** + * File upload translations + * + * @see {@link FileUpload.defaults.i18n} + * @typedef {object} FileUploadTranslations + * + * Messages used by the component + * @property {string} [selectFiles] - Text of button that opens file browser + * @property {TranslationPluralForms} [filesSelected] - Text indicating how + * many files have been selected + */ + +/** + * @typedef {import('../../common/index.mjs').Schema} Schema + * @typedef {import('../../i18n.mjs').TranslationPluralForms} TranslationPluralForms + */ diff --git a/packages/govuk-frontend/src/govuk/components/file-upload/file-upload.yaml b/packages/govuk-frontend/src/govuk/components/file-upload/file-upload.yaml index fde79e6588..131b005bb1 100644 --- a/packages/govuk-frontend/src/govuk/components/file-upload/file-upload.yaml +++ b/packages/govuk-frontend/src/govuk/components/file-upload/file-upload.yaml @@ -89,6 +89,31 @@ examples: name: file-upload-1 label: text: Upload a file + - name: allows multiple files + options: + id: file-upload-1 + name: file-upload-1 + label: + text: Upload a file + attributes: + multiple: true + - name: allows image files only + options: + id: file-upload-1 + name: file-upload-1 + label: + text: Upload a file + attributes: + accept: 'image/*' + - name: allows direct media capture + description: Currently only works on mobile devices. + options: + id: file-upload-1 + name: file-upload-1 + label: + text: Upload a file + attributes: + capture: 'user' - name: with hint text options: id: file-upload-2 @@ -107,13 +132,6 @@ examples: text: Your photo may be in your Pictures, Photos, Downloads or Desktop folder. Or in an app like iPhoto. errorMessage: text: Error message goes here - - name: with value - options: - id: file-upload-4 - name: file-upload-4 - value: C:\fakepath\myphoto.jpg - label: - text: Upload a photo - name: with label as page heading options: id: file-upload-1 @@ -132,6 +150,14 @@ examples: classes: extra-class # Hidden examples are not shown in the review app, but are used for tests and HTML fixtures + - name: with value + hidden: true + options: + id: file-upload-4 + name: file-upload-4 + value: C:\fakepath\myphoto.jpg + label: + text: Upload a photo - name: attributes hidden: true options: diff --git a/packages/govuk-frontend/src/govuk/components/file-upload/template.njk b/packages/govuk-frontend/src/govuk/components/file-upload/template.njk index a3b11c7b90..a8276f98c8 100644 --- a/packages/govuk-frontend/src/govuk/components/file-upload/template.njk +++ b/packages/govuk-frontend/src/govuk/components/file-upload/template.njk @@ -42,7 +42,7 @@ {% if params.formGroup.beforeInput %} {{ params.formGroup.beforeInput.html | safe | trim | indent(2) if params.formGroup.beforeInput.html else params.formGroup.beforeInput.text }} {% endif %} - { 'character-count', 'error-summary', 'exit-this-page', + 'file-upload', 'notification-banner', 'password-input' ] diff --git a/packages/govuk-frontend/src/govuk/init.mjs b/packages/govuk-frontend/src/govuk/init.mjs index 380cdcd233..6ba2e78fdb 100644 --- a/packages/govuk-frontend/src/govuk/init.mjs +++ b/packages/govuk-frontend/src/govuk/init.mjs @@ -5,6 +5,7 @@ import { CharacterCount } from './components/character-count/character-count.mjs import { Checkboxes } from './components/checkboxes/checkboxes.mjs' import { ErrorSummary } from './components/error-summary/error-summary.mjs' import { ExitThisPage } from './components/exit-this-page/exit-this-page.mjs' +import { FileUpload } from './components/file-upload/file-upload.mjs' import { Header } from './components/header/header.mjs' import { NotificationBanner } from './components/notification-banner/notification-banner.mjs' import { PasswordInput } from './components/password-input/password-input.mjs' @@ -44,6 +45,7 @@ function initAll(config) { [Checkboxes], [ErrorSummary, config.errorSummary], [ExitThisPage, config.exitThisPage], + [FileUpload, config.fileUpload], [Header], [NotificationBanner, config.notificationBanner], [PasswordInput, config.passwordInput], @@ -176,6 +178,7 @@ export { initAll, createAll } * @property {CharacterCountConfig} [characterCount] - Character Count config * @property {ErrorSummaryConfig} [errorSummary] - Error Summary config * @property {ExitThisPageConfig} [exitThisPage] - Exit This Page config + * @property {FileUploadConfig} [fileUpload] - File Upload config * @property {NotificationBannerConfig} [notificationBanner] - Notification Banner config * @property {PasswordInputConfig} [passwordInput] - Password input config */ @@ -191,6 +194,8 @@ export { initAll, createAll } * @typedef {import('./components/error-summary/error-summary.mjs').ErrorSummaryConfig} ErrorSummaryConfig * @typedef {import('./components/exit-this-page/exit-this-page.mjs').ExitThisPageConfig} ExitThisPageConfig * @typedef {import('./components/exit-this-page/exit-this-page.mjs').ExitThisPageTranslations} ExitThisPageTranslations + * @typedef {import('./components/file-upload/file-upload.mjs').FileUploadConfig} FileUploadConfig + * @typedef {import('./components/file-upload/file-upload.mjs').FileUploadTranslations} FileUploadTranslations * @typedef {import('./components/notification-banner/notification-banner.mjs').NotificationBannerConfig} NotificationBannerConfig * @typedef {import('./components/password-input/password-input.mjs').PasswordInputConfig} PasswordInputConfig */ diff --git a/packages/govuk-frontend/tasks/build/package.unit.test.mjs b/packages/govuk-frontend/tasks/build/package.unit.test.mjs index 20d571d47d..462904addd 100644 --- a/packages/govuk-frontend/tasks/build/package.unit.test.mjs +++ b/packages/govuk-frontend/tasks/build/package.unit.test.mjs @@ -187,6 +187,7 @@ describe('packages/govuk-frontend/dist/', () => { export { Checkboxes } from './components/checkboxes/checkboxes.mjs'; export { ErrorSummary } from './components/error-summary/error-summary.mjs'; export { ExitThisPage } from './components/exit-this-page/exit-this-page.mjs'; + export { FileUpload } from './components/file-upload/file-upload.mjs'; export { Header } from './components/header/header.mjs'; export { NotificationBanner } from './components/notification-banner/notification-banner.mjs'; export { PasswordInput } from './components/password-input/password-input.mjs'; From 187ee1a1941cbcd70f6ac72ccb2d65a69ef54c0f Mon Sep 17 00:00:00 2001 From: Owen Jones Date: Fri, 4 Oct 2024 15:40:49 +0100 Subject: [PATCH 02/14] click label instead of input --- .../govuk/components/file-upload/file-upload.mjs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/packages/govuk-frontend/src/govuk/components/file-upload/file-upload.mjs b/packages/govuk-frontend/src/govuk/components/file-upload/file-upload.mjs index 00076df49b..ff19fa83a6 100644 --- a/packages/govuk-frontend/src/govuk/components/file-upload/file-upload.mjs +++ b/packages/govuk-frontend/src/govuk/components/file-upload/file-upload.mjs @@ -75,6 +75,15 @@ export class FileUpload extends GOVUKFrontendComponent { locale: closestAttributeValue($input, 'lang') }) + this.$label = document.querySelector(`[for="${$input.id}"]`) + + if (!this.$label) { + throw new ElementError({ + componentName: 'File upload', + identifier: 'No label' + }) + } + $input.addEventListener('change', this.onChange.bind(this)) this.$input = $input @@ -148,7 +157,9 @@ export class FileUpload extends GOVUKFrontendComponent { * When the button is clicked, emulate clicking the actual, hidden file input */ onClick() { - this.$input.click() + if (this.$label instanceof HTMLElement) { + this.$label.click() + } } /** From 9cfd3be7dce2768646f98695b2b4bba0836b0d75 Mon Sep 17 00:00:00 2001 From: Owen Jones Date: Wed, 16 Oct 2024 17:56:23 +0100 Subject: [PATCH 03/14] Add aria-hidden and tabindex -1 to input This change is mutated by a lot of mess from me trying to make typescript not upset with me --- .../components/file-upload/file-upload.mjs | 85 +++++++++++-------- 1 file changed, 48 insertions(+), 37 deletions(-) diff --git a/packages/govuk-frontend/src/govuk/components/file-upload/file-upload.mjs b/packages/govuk-frontend/src/govuk/components/file-upload/file-upload.mjs index ff19fa83a6..eb696558aa 100644 --- a/packages/govuk-frontend/src/govuk/components/file-upload/file-upload.mjs +++ b/packages/govuk-frontend/src/govuk/components/file-upload/file-upload.mjs @@ -13,25 +13,16 @@ import { I18n } from '../../i18n.mjs' export class FileUpload extends GOVUKFrontendComponent { /** * @private - * @type {HTMLInputElement} - */ - $input - - /** - * @private - * @type {HTMLElement} */ $wrapper /** * @private - * @type {HTMLButtonElement} */ $button /** * @private - * @type {HTMLElement} */ $status @@ -39,60 +30,53 @@ export class FileUpload extends GOVUKFrontendComponent { * @private * @type {FileUploadConfig} */ + // eslint-disable-next-line + // @ts-ignore config /** @private */ i18n /** - * @param {Element | null} $input - File input element + * @param {Element | null} $root - File input element * @param {FileUploadConfig} [config] - File Upload config */ - constructor($input, config = {}) { - super() + constructor($root, config = {}) { + super($root) - if (!($input instanceof HTMLInputElement)) { - throw new ElementError({ - componentName: 'File upload', - element: $input, - expectedType: 'HTMLInputElement', - identifier: 'Root element (`$module`)' - }) + if (!(this.$root instanceof HTMLInputElement)) { + return } - if ($input.type !== 'file') { - throw new ElementError('File upload: Form field must be of type `file`.') + if (this.$root.type !== 'file') { + throw new ElementError( + 'File upload: Form field must be an input of type `file`.' + ) } this.config = mergeConfigs( FileUpload.defaults, config, - normaliseDataset(FileUpload, $input.dataset) + normaliseDataset(FileUpload, this.$root.dataset) ) this.i18n = new I18n(this.config.i18n, { // Read the fallback if necessary rather than have it set in the defaults - locale: closestAttributeValue($input, 'lang') + locale: closestAttributeValue(this.$root, 'lang') }) - this.$label = document.querySelector(`[for="${$input.id}"]`) + this.$label = document.querySelector(`[for="${this.$root.id}"]`) if (!this.$label) { throw new ElementError({ - componentName: 'File upload', + component: FileUpload, identifier: 'No label' }) } - $input.addEventListener('change', this.onChange.bind(this)) - this.$input = $input - // Wrapping element. This defines the boundaries of our drag and drop area. const $wrapper = document.createElement('div') $wrapper.className = 'govuk-file-upload-wrapper' - $wrapper.addEventListener('dragover', this.onDragOver.bind(this)) - $wrapper.addEventListener('dragleave', this.onDragLeaveOrDrop.bind(this)) - $wrapper.addEventListener('drop', this.onDragLeaveOrDrop.bind(this)) // Create the file selection button const $button = document.createElement('button') @@ -113,29 +97,50 @@ export class FileUpload extends GOVUKFrontendComponent { $wrapper.insertAdjacentElement('beforeend', $status) // Inject all this *after* the native file input - this.$input.insertAdjacentElement('afterend', $wrapper) + this.$root.insertAdjacentElement('afterend', $wrapper) // Move the native file input to inside of the wrapper - $wrapper.insertAdjacentElement('afterbegin', this.$input) + $wrapper.insertAdjacentElement('afterbegin', this.$root) // Make all these new variables available to the module this.$wrapper = $wrapper this.$button = $button this.$status = $status + // with everything set up, apply attributes to programmatically hide the input + this.$root.setAttribute('aria-hidden', 'true') + this.$root.setAttribute('tabindex', '-1') + // Bind change event to the underlying input - this.$input.addEventListener('change', this.onChange.bind(this)) + this.$root.addEventListener('change', this.onChange.bind(this)) + this.$wrapper.addEventListener('dragover', this.onDragOver.bind(this)) + this.$wrapper.addEventListener( + 'dragleave', + this.onDragLeaveOrDrop.bind(this) + ) + this.$wrapper.addEventListener('drop', this.onDragLeaveOrDrop.bind(this)) } /** * Check if the value of the underlying input has changed */ onChange() { - if (!this.$input.files) { + if (!('files' in this.$root)) { + return + } + + if (!this.$root.files) { return } - const fileCount = this.$input.files.length + // eslint-disable-next-line + // @ts-ignore + const fileCount = this.$root.files.length // eslint-disable-line + + // trying to appease typescript + if (!this.$status || !this.i18n) { + return + } if (fileCount === 0) { // If there are no files, show the default selection text @@ -144,7 +149,9 @@ export class FileUpload extends GOVUKFrontendComponent { // If there is 1 file, just show the file name fileCount === 1 ) { - this.$status.innerText = this.$input.files[0].name + // eslint-disable-next-line + // @ts-ignore + this.$status.innerText = this.$root.files[0].name // eslint-disable-line } else { // Otherwise, tell the user how many files are selected this.$status.innerText = this.i18n.t('filesSelected', { @@ -167,6 +174,8 @@ export class FileUpload extends GOVUKFrontendComponent { * file can be dropped here. */ onDragOver() { + // eslint-disable-next-line + // @ts-ignore this.$wrapper.classList.add('govuk-file-upload-wrapper--show-dropzone') } @@ -175,6 +184,8 @@ export class FileUpload extends GOVUKFrontendComponent { * remove the visual indicator. */ onDragLeaveOrDrop() { + // eslint-disable-next-line + // @ts-ignore this.$wrapper.classList.remove('govuk-file-upload-wrapper--show-dropzone') } From 480e7d04dd9706e5cd796c3175648c4f8725cd2e Mon Sep 17 00:00:00 2001 From: beeps Date: Fri, 18 Oct 2024 14:25:59 +0100 Subject: [PATCH 04/14] Add `multiple` Nunjucks parameter Setting this via the `attributes` parameter raises complaints from the test that lints our HTML output. Should quieten that failing test. --- .../src/govuk/components/file-upload/file-upload.yaml | 7 +++++-- .../src/govuk/components/file-upload/template.njk | 1 + .../src/govuk/components/file-upload/template.test.js | 7 +++++++ 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/packages/govuk-frontend/src/govuk/components/file-upload/file-upload.yaml b/packages/govuk-frontend/src/govuk/components/file-upload/file-upload.yaml index 131b005bb1..1222eba773 100644 --- a/packages/govuk-frontend/src/govuk/components/file-upload/file-upload.yaml +++ b/packages/govuk-frontend/src/govuk/components/file-upload/file-upload.yaml @@ -15,6 +15,10 @@ params: type: boolean required: false description: If `true`, file input will be disabled. + - name: multiple + type: boolean + required: false + description: If `true`, a user may select multiple files at the same time. The exact mechanism to do this differs depending on operating system. - name: describedBy type: string required: false @@ -95,8 +99,7 @@ examples: name: file-upload-1 label: text: Upload a file - attributes: - multiple: true + multiple: true - name: allows image files only options: id: file-upload-1 diff --git a/packages/govuk-frontend/src/govuk/components/file-upload/template.njk b/packages/govuk-frontend/src/govuk/components/file-upload/template.njk index a8276f98c8..8f1638c567 100644 --- a/packages/govuk-frontend/src/govuk/components/file-upload/template.njk +++ b/packages/govuk-frontend/src/govuk/components/file-upload/template.njk @@ -45,6 +45,7 @@ {% if params.formGroup.afterInput %} diff --git a/packages/govuk-frontend/src/govuk/components/file-upload/template.test.js b/packages/govuk-frontend/src/govuk/components/file-upload/template.test.js index 0aaee979a9..038c34d7be 100644 --- a/packages/govuk-frontend/src/govuk/components/file-upload/template.test.js +++ b/packages/govuk-frontend/src/govuk/components/file-upload/template.test.js @@ -59,6 +59,13 @@ describe('File upload', () => { expect($component.attr('aria-describedby')).toMatch('test-target-element') }) + it('renders with multiple', () => { + const $ = render('file-upload', examples['allows multiple files']) + + const $component = $('.govuk-file-upload') + expect($component.attr('multiple')).toBeTruthy() + }) + it('renders with attributes', () => { const $ = render('file-upload', examples.attributes) From af41330a4fd122413d0ccb163ae177ff60b81b87 Mon Sep 17 00:00:00 2001 From: beeps Date: Fri, 18 Oct 2024 16:30:53 +0100 Subject: [PATCH 05/14] Tweaks to dropzone styles --- .../src/govuk/components/file-upload/_index.scss | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/govuk-frontend/src/govuk/components/file-upload/_index.scss b/packages/govuk-frontend/src/govuk/components/file-upload/_index.scss index 4b20003587..309ea05fd5 100644 --- a/packages/govuk-frontend/src/govuk/components/file-upload/_index.scss +++ b/packages/govuk-frontend/src/govuk/components/file-upload/_index.scss @@ -55,12 +55,14 @@ .govuk-file-upload-wrapper--show-dropzone { $dropzone-padding: govuk-spacing(2); + $dropzone-offset: $dropzone-padding + $govuk-border-width-form-element; - margin-top: -$dropzone-padding; - margin-left: -$dropzone-padding; + // Add negative margins to all sides so that content doesn't jump due to + // the addition of the padding and border. + margin: -$dropzone-offset; padding: $dropzone-padding; - outline: 2px dotted govuk-colour("mid-grey"); - background-color: govuk-colour("light-grey"); + border: $govuk-border-width-form-element dashed $govuk-input-border-colour; + background-color: $govuk-body-background-colour; .govuk-file-upload__button, .govuk-file-upload__status { From f2bccb341d3a847b405be19c0ec28cef4d9aa461 Mon Sep 17 00:00:00 2001 From: beeps Date: Mon, 21 Oct 2024 09:33:08 +0100 Subject: [PATCH 06/14] Remove setting aria-hidden Chromium actively ignores this attribute on file inputs and throws a warning in the console --- .../src/govuk/components/file-upload/file-upload.mjs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/govuk-frontend/src/govuk/components/file-upload/file-upload.mjs b/packages/govuk-frontend/src/govuk/components/file-upload/file-upload.mjs index eb696558aa..f59f84eb9c 100644 --- a/packages/govuk-frontend/src/govuk/components/file-upload/file-upload.mjs +++ b/packages/govuk-frontend/src/govuk/components/file-upload/file-upload.mjs @@ -107,8 +107,7 @@ export class FileUpload extends GOVUKFrontendComponent { this.$button = $button this.$status = $status - // with everything set up, apply attributes to programmatically hide the input - this.$root.setAttribute('aria-hidden', 'true') + // Prevent the hidden input being tabbed to by keyboard users this.$root.setAttribute('tabindex', '-1') // Bind change event to the underlying input From e5b9c3877e8cf95122f061acdae94fe3ef7a5276 Mon Sep 17 00:00:00 2001 From: beeps Date: Mon, 21 Oct 2024 10:56:49 +0100 Subject: [PATCH 07/14] Syncronise disabled state between input and button Adds a mutation observer that looks to see if the `disabled` attribute of the input changes and, if so, updates the button to match --- .../components/file-upload/file-upload.mjs | 39 +++++++++++++++++++ .../components/file-upload/file-upload.yaml | 7 ++++ .../components/file-upload/template.test.js | 7 ++++ 3 files changed, 53 insertions(+) diff --git a/packages/govuk-frontend/src/govuk/components/file-upload/file-upload.mjs b/packages/govuk-frontend/src/govuk/components/file-upload/file-upload.mjs index f59f84eb9c..44683c2c84 100644 --- a/packages/govuk-frontend/src/govuk/components/file-upload/file-upload.mjs +++ b/packages/govuk-frontend/src/govuk/components/file-upload/file-upload.mjs @@ -110,6 +110,10 @@ export class FileUpload extends GOVUKFrontendComponent { // Prevent the hidden input being tabbed to by keyboard users this.$root.setAttribute('tabindex', '-1') + // Syncronise the `disabled` state between the button and underlying input + this.updateDisabledState() + this.observeDisabledState() + // Bind change event to the underlying input this.$root.addEventListener('change', this.onChange.bind(this)) this.$wrapper.addEventListener('dragover', this.onDragOver.bind(this)) @@ -188,6 +192,41 @@ export class FileUpload extends GOVUKFrontendComponent { this.$wrapper.classList.remove('govuk-file-upload-wrapper--show-dropzone') } + /** + * Create a mutation observer to check if the input's attributes altered. + */ + observeDisabledState() { + const observer = new MutationObserver((mutationList) => { + for (const mutation of mutationList) { + console.log('mutation', mutation) + if ( + mutation.type === 'attributes' && + mutation.attributeName === 'disabled' + ) { + this.updateDisabledState() + } + } + }) + + observer.observe(this.$root, { + attributes: true + }) + } + + /** + * Synchronise the `disabled` state between the input and replacement button. + */ + updateDisabledState() { + if ( + !(this.$root instanceof HTMLInputElement) || + !(this.$button instanceof HTMLButtonElement) + ) { + return + } + + this.$button.disabled = this.$root.disabled + } + /** * Name for the component used when initialising using data-module attributes. */ diff --git a/packages/govuk-frontend/src/govuk/components/file-upload/file-upload.yaml b/packages/govuk-frontend/src/govuk/components/file-upload/file-upload.yaml index 1222eba773..7aeb371ddd 100644 --- a/packages/govuk-frontend/src/govuk/components/file-upload/file-upload.yaml +++ b/packages/govuk-frontend/src/govuk/components/file-upload/file-upload.yaml @@ -117,6 +117,13 @@ examples: text: Upload a file attributes: capture: 'user' + - name: is disabled + options: + id: file-upload-1 + name: file-upload-1 + label: + text: Upload a file + disabled: true - name: with hint text options: id: file-upload-2 diff --git a/packages/govuk-frontend/src/govuk/components/file-upload/template.test.js b/packages/govuk-frontend/src/govuk/components/file-upload/template.test.js index 038c34d7be..2afe406039 100644 --- a/packages/govuk-frontend/src/govuk/components/file-upload/template.test.js +++ b/packages/govuk-frontend/src/govuk/components/file-upload/template.test.js @@ -66,6 +66,13 @@ describe('File upload', () => { expect($component.attr('multiple')).toBeTruthy() }) + it('renders with multiple', () => { + const $ = render('file-upload', examples['is disabled']) + + const $component = $('.govuk-file-upload') + expect($component.attr('disabled')).toBeTruthy() + }) + it('renders with attributes', () => { const $ = render('file-upload', examples.attributes) From 79900833acc3b366ee58cbba6a30714558da930a Mon Sep 17 00:00:00 2001 From: beeps Date: Mon, 21 Oct 2024 18:19:31 +0100 Subject: [PATCH 08/14] Add i18n parameters --- .../govuk/components/file-upload/file-upload.yaml | 11 +++++++++++ .../src/govuk/components/file-upload/template.njk | 13 +++++++++++++ 2 files changed, 24 insertions(+) diff --git a/packages/govuk-frontend/src/govuk/components/file-upload/file-upload.yaml b/packages/govuk-frontend/src/govuk/components/file-upload/file-upload.yaml index 7aeb371ddd..de7508994d 100644 --- a/packages/govuk-frontend/src/govuk/components/file-upload/file-upload.yaml +++ b/packages/govuk-frontend/src/govuk/components/file-upload/file-upload.yaml @@ -158,6 +158,17 @@ examples: text: Upload a file formGroup: classes: extra-class + - name: translated + options: + id: file-upload-1 + name: file-upload-1 + label: + text: Llwythwch ffeil i fyny + selectFilesButtonText: Dewiswch ffeil + filesSelectedDefaultText: Dim ffeiliau wedi'u dewis + filesSelectedText: + one: "%{count} ffeil wedi'i dewis" + other: "%{count} ffeil wedi'u dewis" # Hidden examples are not shown in the review app, but are used for tests and HTML fixtures - name: with value diff --git a/packages/govuk-frontend/src/govuk/components/file-upload/template.njk b/packages/govuk-frontend/src/govuk/components/file-upload/template.njk index 8f1638c567..e2235907ca 100644 --- a/packages/govuk-frontend/src/govuk/components/file-upload/template.njk +++ b/packages/govuk-frontend/src/govuk/components/file-upload/template.njk @@ -1,4 +1,5 @@ {% from "../../macros/attributes.njk" import govukAttributes %} +{% from "../../macros/i18n.njk" import govukI18nAttributes %} {% from "../error-message/macro.njk" import govukErrorMessage %} {% from "../hint/macro.njk" import govukHint %} {% from "../label/macro.njk" import govukLabel %} @@ -47,6 +48,18 @@ {%- if params.disabled %} disabled{% endif %} {%- if params.multiple %} multiple{% endif %} {%- if describedBy %} aria-describedby="{{ describedBy }}"{% endif %} + {{- govukI18nAttributes({ + key: 'select-files-button', + message: params.selectFilesButtonText + }) -}} + {{- govukI18nAttributes({ + key: 'files-selected-default', + message: params.filesSelectedDefaultText + }) -}} + {{- govukI18nAttributes({ + key: 'files-selected', + message: params.filesSelectedText + }) -}} {{- govukAttributes(params.attributes) }}> {% if params.formGroup.afterInput %} {{ params.formGroup.afterInput.html | safe | trim | indent(2) if params.formGroup.afterInput.html else params.formGroup.afterInput.text }} From b9eb7db442330cad73eeaabc4a8d4f8406c97c9a Mon Sep 17 00:00:00 2001 From: beeps Date: Tue, 22 Oct 2024 10:24:06 +0100 Subject: [PATCH 09/14] Write JavaScript functionality tests --- .../file-upload/file-upload.puppeteer.test.js | 286 ++++++++++++++++++ .../components/file-upload/file-upload.yaml | 3 +- 2 files changed, 288 insertions(+), 1 deletion(-) create mode 100644 packages/govuk-frontend/src/govuk/components/file-upload/file-upload.puppeteer.test.js diff --git a/packages/govuk-frontend/src/govuk/components/file-upload/file-upload.puppeteer.test.js b/packages/govuk-frontend/src/govuk/components/file-upload/file-upload.puppeteer.test.js new file mode 100644 index 0000000000..cf502b4879 --- /dev/null +++ b/packages/govuk-frontend/src/govuk/components/file-upload/file-upload.puppeteer.test.js @@ -0,0 +1,286 @@ +const { render } = require('@govuk-frontend/helpers/puppeteer') +const { getExamples } = require('@govuk-frontend/lib/components') + +const inputSelector = '.govuk-file-upload' +const wrapperSelector = '.govuk-file-upload-wrapper' +const buttonSelector = '.govuk-file-upload__button' +const statusSelector = '.govuk-file-upload__status' + +describe('/components/file-upload', () => { + let examples + + beforeAll(async () => { + examples = await getExamples('file-upload') + }) + + describe('/components/file-upload/preview', () => { + describe('when JavaScript is unavailable or fails', () => { + beforeAll(async () => { + await page.setJavaScriptEnabled(false) + }) + + afterAll(async () => { + await page.setJavaScriptEnabled(true) + }) + + it('renders an unmodified file input', async () => { + await render(page, 'file-upload', examples.default) + + const inputType = await page.$eval(inputSelector, (el) => + el.getAttribute('type') + ) + expect(inputType).toBe('file') + }) + + it('does not inject additional elements', async () => { + await render(page, 'file-upload', examples.default) + + const $wrapperElement = await page.$(wrapperSelector) + const $buttonElement = await page.$(buttonSelector) + const $statusElement = await page.$(statusSelector) + + expect($wrapperElement).toBeNull() + expect($buttonElement).toBeNull() + expect($statusElement).toBeNull() + }) + }) + + describe('when JavaScript is available', () => { + describe('on page load', () => { + beforeAll(async () => { + await render(page, 'file-upload', examples.default) + }) + + describe('wrapper element', () => { + it('renders the wrapper element', async () => { + const wrapperElement = await page.$eval(wrapperSelector, (el) => el) + + expect(wrapperElement).toBeDefined() + }) + + it('moves the file input inside of the wrapper element', async () => { + const inputElementParent = await page.$eval( + inputSelector, + (el) => el.parentNode + ) + const wrapperElement = await page.$eval(wrapperSelector, (el) => el) + + expect(inputElementParent).toStrictEqual(wrapperElement) + }) + }) + + describe('file input', () => { + it('sets tabindex to -1', async () => { + const inputElementTabindex = await page.$eval(inputSelector, (el) => + el.getAttribute('tabindex') + ) + + expect(inputElementTabindex).toBe('-1') + }) + }) + + describe('choose file button', () => { + it('renders the button element', async () => { + const buttonElement = await page.$eval(buttonSelector, (el) => el) + const buttonElementType = await page.$eval(buttonSelector, (el) => + el.getAttribute('type') + ) + + expect(buttonElement).toBeDefined() + expect(buttonElementType).toBe('button') + }) + + it('renders the button with default text', async () => { + const buttonElementText = await page.$eval(buttonSelector, (el) => + el.innerHTML.trim() + ) + + expect(buttonElementText).toBe('Choose file') + }) + }) + + describe('status element', () => { + it('renders the status element', async () => { + const statusElement = await page.$eval(statusSelector, (el) => el) + + expect(statusElement).toBeDefined() + }) + + it('renders the status element with role', async () => { + const statusElementRole = await page.$eval(statusSelector, (el) => + el.getAttribute('role') + ) + + expect(statusElementRole).toBe('status') + }) + + it('renders the status element with default text', async () => { + const statusElementText = await page.$eval(statusSelector, (el) => + el.innerHTML.trim() + ) + + expect(statusElementText).toBe('No file chosen') + }) + }) + }) + + describe('when clicking the choose file button', () => { + it('opens the file picker', async () => { + // It doesn't seem to be possible to check if the file picker dialog + // opens as an isolated test, so this test clicks the button, tries to + // set a value in the file chooser, then checks if that value was set + // on the input as expected. + const testFilename = 'test.gif' + await render(page, 'file-upload', examples.default) + + const [fileChooser] = await Promise.all([ + page.waitForFileChooser(), + page.click(buttonSelector) + ]) + + await fileChooser.accept([testFilename]) + + const inputElementValue = await page.$eval( + inputSelector, + (el) => + // @ts-ignore + el.value + ) + + // For Windows and backward compatibility, the values of file inputs + // are always formatted starting with `C:\\fakepath\\` + expect(inputElementValue).toBe(`C:\\fakepath\\${testFilename}`) + }) + }) + + describe('when selecting a file', () => { + const testFilename = 'fakefile.txt' + + beforeEach(async () => { + await render(page, 'file-upload', examples.default) + + const [fileChooser] = await Promise.all([ + page.waitForFileChooser(), + page.click(inputSelector) + ]) + await fileChooser.accept([testFilename]) + }) + + it('updates the file input value', async () => { + const inputElementValue = await page.$eval( + inputSelector, + (el) => + // @ts-ignore + el.value + ) + + const inputElementFiles = await page.$eval( + inputSelector, + (el) => + // @ts-ignore + el.files + ) + + // For Windows and backward compatibility, the values of file inputs + // are always formatted starting with `C:\\fakepath\\` + expect(inputElementValue).toBe(`C:\\fakepath\\${testFilename}`) + + // Also check the files object + expect(inputElementFiles[0]).toBeDefined() + }) + + it('updates the filename in the status element', async () => { + const statusElementText = await page.$eval(statusSelector, (el) => + el.innerHTML.trim() + ) + + expect(statusElementText).toBe(testFilename) + }) + }) + + describe('when selecting multiple files', () => { + beforeEach(async () => { + await render(page, 'file-upload', examples['allows multiple files']) + + const [fileChooser] = await Promise.all([ + page.waitForFileChooser(), + page.click(inputSelector) + ]) + await fileChooser.accept(['testfile1.txt', 'testfile2.pdf']) + }) + + it('updates the file input value', async () => { + const inputElementValue = await page.$eval( + inputSelector, + (el) => + // @ts-ignore + el.value + ) + + const inputElementFiles = await page.$eval( + inputSelector, + (el) => + // @ts-ignore + el.files + ) + + // For Windows and backward compatibility, the values of file inputs + // are always formatted starting with `C:\\fakepath\\` + // + // Additionally, `value` will only ever return the first file selected + expect(inputElementValue).toBe(`C:\\fakepath\\testfile1.txt`) + + // Also check the files object + expect(inputElementFiles[0]).toBeDefined() + expect(inputElementFiles[1]).toBeDefined() + }) + + it('shows the number of files selected in the status element', async () => { + const statusElementText = await page.$eval(statusSelector, (el) => + el.innerHTML.trim() + ) + + expect(statusElementText).toBe('2 files chosen') + }) + }) + + describe('i18n', () => { + beforeEach(async () => { + await render(page, 'file-upload', examples.translated) + }) + + it('uses the correct translation for the choose file button', async () => { + const buttonText = await page.$eval(buttonSelector, (el) => + el.innerHTML.trim() + ) + + expect(buttonText).toBe('Dewiswch ffeil') + }) + + describe('status element', () => { + it('uses the correct translation when no files are selected', async () => { + const statusText = await page.$eval(statusSelector, (el) => + el.innerHTML.trim() + ) + + expect(statusText).toBe("Dim ffeiliau wedi'u dewis") + }) + + it('uses the correct translation when multiple files are selected', async () => { + const [fileChooser] = await Promise.all([ + page.waitForFileChooser(), + page.click(inputSelector) + ]) + await fileChooser.accept(['testfile1.txt', 'testfile2.pdf']) + + const statusText = await page.$eval(statusSelector, (el) => + el.innerHTML.trim() + ) + + expect(statusText).toBe("2 ffeil wedi'u dewis") + }) + }) + }) + }) + }) +}) diff --git a/packages/govuk-frontend/src/govuk/components/file-upload/file-upload.yaml b/packages/govuk-frontend/src/govuk/components/file-upload/file-upload.yaml index de7508994d..f7624a00a0 100644 --- a/packages/govuk-frontend/src/govuk/components/file-upload/file-upload.yaml +++ b/packages/govuk-frontend/src/govuk/components/file-upload/file-upload.yaml @@ -164,11 +164,12 @@ examples: name: file-upload-1 label: text: Llwythwch ffeil i fyny + multiple: true selectFilesButtonText: Dewiswch ffeil filesSelectedDefaultText: Dim ffeiliau wedi'u dewis filesSelectedText: - one: "%{count} ffeil wedi'i dewis" other: "%{count} ffeil wedi'u dewis" + one: "%{count} ffeil wedi'i dewis" # Hidden examples are not shown in the review app, but are used for tests and HTML fixtures - name: with value From fbc34fefa2e17e3c5554cc526630d2452b158d2c Mon Sep 17 00:00:00 2001 From: beeps Date: Tue, 22 Oct 2024 10:24:17 +0100 Subject: [PATCH 10/14] Fix component not rendering plural objects correctly --- .../src/govuk/components/file-upload/template.njk | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/govuk-frontend/src/govuk/components/file-upload/template.njk b/packages/govuk-frontend/src/govuk/components/file-upload/template.njk index e2235907ca..5d098d57da 100644 --- a/packages/govuk-frontend/src/govuk/components/file-upload/template.njk +++ b/packages/govuk-frontend/src/govuk/components/file-upload/template.njk @@ -58,7 +58,7 @@ }) -}} {{- govukI18nAttributes({ key: 'files-selected', - message: params.filesSelectedText + messages: params.filesSelectedText }) -}} {{- govukAttributes(params.attributes) }}> {% if params.formGroup.afterInput %} From ae765c8f5cedfad30fdfc4bd5b86249149cb6339 Mon Sep 17 00:00:00 2001 From: beeps Date: Tue, 22 Oct 2024 17:02:04 +0100 Subject: [PATCH 11/14] Add Nunjucks parameter documentation --- .../src/govuk/components/file-upload/file-upload.mjs | 2 ++ .../govuk/components/file-upload/file-upload.yaml | 12 ++++++++++++ 2 files changed, 14 insertions(+) diff --git a/packages/govuk-frontend/src/govuk/components/file-upload/file-upload.mjs b/packages/govuk-frontend/src/govuk/components/file-upload/file-upload.mjs index 44683c2c84..919d1689f1 100644 --- a/packages/govuk-frontend/src/govuk/components/file-upload/file-upload.mjs +++ b/packages/govuk-frontend/src/govuk/components/file-upload/file-upload.mjs @@ -244,6 +244,8 @@ export class FileUpload extends GOVUKFrontendComponent { selectFilesButton: 'Choose file', filesSelectedDefault: 'No file chosen', filesSelected: { + // the 'one' string isn't used as the component displays the filename + // instead, however it's here for coverage's sake one: '%{count} file chosen', other: '%{count} files chosen' } diff --git a/packages/govuk-frontend/src/govuk/components/file-upload/file-upload.yaml b/packages/govuk-frontend/src/govuk/components/file-upload/file-upload.yaml index f7624a00a0..a641799e45 100644 --- a/packages/govuk-frontend/src/govuk/components/file-upload/file-upload.yaml +++ b/packages/govuk-frontend/src/govuk/components/file-upload/file-upload.yaml @@ -77,6 +77,18 @@ params: type: string required: true description: HTML to add after the input. If `html` is provided, the `text` option will be ignored. + - name: selectFilesButtonText + type: string + required: false + description: The text of the button that opens the file picker. JavaScript enhanced version of the component only. Default is "Choose file". + - name: filesSelected + type: object + required: false + description: The text to display when multiple files has been chosen by the user. JavaScript enhanced version of the component only. The component will replace the `%{count}` placeholder with the number of files selected. This is a [pluralised list of messages](https://frontend.design-system.service.gov.uk/localise-govuk-frontend). + - name: filesSelectedDefault + type: string + required: false + description: The text to display when no file has been chosen by the user. JavaScript enhanced version of the component only. Default is "No file chosen". - name: classes type: string required: false From 33891bc598e7c75a4e00eb8825ffebc7b34b0866 Mon Sep 17 00:00:00 2001 From: beeps Date: Tue, 22 Oct 2024 17:42:21 +0100 Subject: [PATCH 12/14] Add tests for disable state syncronisation --- .../file-upload/file-upload.puppeteer.test.js | 45 +++++++++++++++++++ .../components/file-upload/file-upload.yaml | 2 +- .../components/file-upload/template.test.js | 4 +- 3 files changed, 48 insertions(+), 3 deletions(-) diff --git a/packages/govuk-frontend/src/govuk/components/file-upload/file-upload.puppeteer.test.js b/packages/govuk-frontend/src/govuk/components/file-upload/file-upload.puppeteer.test.js index cf502b4879..a0172ae79d 100644 --- a/packages/govuk-frontend/src/govuk/components/file-upload/file-upload.puppeteer.test.js +++ b/packages/govuk-frontend/src/govuk/components/file-upload/file-upload.puppeteer.test.js @@ -281,6 +281,51 @@ describe('/components/file-upload', () => { }) }) }) + + describe('disabled state syncing', () => { + it('disables the button if the input is disabled on page load', async () => { + await render(page, 'file-upload', examples.disabled) + + const buttonDisabled = await page.$eval(buttonSelector, (el) => + el.hasAttribute('disabled') + ) + + expect(buttonDisabled).toBeTruthy() + }) + + it('disables the button if the input is disabled programatically', async () => { + await render(page, 'file-upload', examples.default) + + await page.$eval(inputSelector, (el) => + el.setAttribute('disabled', '') + ) + + const buttonDisabledAfter = await page.$eval(buttonSelector, (el) => + el.hasAttribute('disabled') + ) + + expect(buttonDisabledAfter).toBeTruthy() + }) + + it('enables the button if the input is enabled programatically', async () => { + await render(page, 'file-upload', examples.disabled) + + const buttonDisabledBefore = await page.$eval(buttonSelector, (el) => + el.hasAttribute('disabled') + ) + + await page.$eval(inputSelector, (el) => + el.removeAttribute('disabled') + ) + + const buttonDisabledAfter = await page.$eval(buttonSelector, (el) => + el.hasAttribute('disabled') + ) + + expect(buttonDisabledBefore).toBeTruthy() + expect(buttonDisabledAfter).toBeFalsy() + }) + }) }) }) }) diff --git a/packages/govuk-frontend/src/govuk/components/file-upload/file-upload.yaml b/packages/govuk-frontend/src/govuk/components/file-upload/file-upload.yaml index a641799e45..d7e0852316 100644 --- a/packages/govuk-frontend/src/govuk/components/file-upload/file-upload.yaml +++ b/packages/govuk-frontend/src/govuk/components/file-upload/file-upload.yaml @@ -129,7 +129,7 @@ examples: text: Upload a file attributes: capture: 'user' - - name: is disabled + - name: disabled options: id: file-upload-1 name: file-upload-1 diff --git a/packages/govuk-frontend/src/govuk/components/file-upload/template.test.js b/packages/govuk-frontend/src/govuk/components/file-upload/template.test.js index 2afe406039..1a30309449 100644 --- a/packages/govuk-frontend/src/govuk/components/file-upload/template.test.js +++ b/packages/govuk-frontend/src/govuk/components/file-upload/template.test.js @@ -66,8 +66,8 @@ describe('File upload', () => { expect($component.attr('multiple')).toBeTruthy() }) - it('renders with multiple', () => { - const $ = render('file-upload', examples['is disabled']) + it('renders with disabled', () => { + const $ = render('file-upload', examples.disabled) const $component = $('.govuk-file-upload') expect($component.attr('disabled')).toBeTruthy() From 16960ef59e224a9c134ed5f5a49d0358724df7f5 Mon Sep 17 00:00:00 2001 From: beeps Date: Thu, 24 Oct 2024 09:44:21 +0100 Subject: [PATCH 13/14] Show dropzone when dragover page and not input --- .../src/govuk/components/file-upload/file-upload.mjs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/govuk-frontend/src/govuk/components/file-upload/file-upload.mjs b/packages/govuk-frontend/src/govuk/components/file-upload/file-upload.mjs index 919d1689f1..737235c764 100644 --- a/packages/govuk-frontend/src/govuk/components/file-upload/file-upload.mjs +++ b/packages/govuk-frontend/src/govuk/components/file-upload/file-upload.mjs @@ -116,12 +116,13 @@ export class FileUpload extends GOVUKFrontendComponent { // Bind change event to the underlying input this.$root.addEventListener('change', this.onChange.bind(this)) - this.$wrapper.addEventListener('dragover', this.onDragOver.bind(this)) - this.$wrapper.addEventListener( - 'dragleave', - this.onDragLeaveOrDrop.bind(this) - ) + + // When a file is dropped on the input this.$wrapper.addEventListener('drop', this.onDragLeaveOrDrop.bind(this)) + + // When a file is dragged over the page (or dragged off the page) + document.addEventListener('dragover', this.onDragOver.bind(this)) + document.addEventListener('dragleave', this.onDragLeaveOrDrop.bind(this)) } /** From 980b1c6fbc1bc170d03614183e989902a07b9a4f Mon Sep 17 00:00:00 2001 From: beeps Date: Fri, 25 Oct 2024 13:19:26 +0100 Subject: [PATCH 14/14] Change dragover to dragenter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Dragenter is fired only once when the drag first touches the page, whereas dragover fires continuously. As we’re only toggling the visbility of the dropzone, dragenter will suffice for our needs. --- .../src/govuk/components/file-upload/file-upload.mjs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/govuk-frontend/src/govuk/components/file-upload/file-upload.mjs b/packages/govuk-frontend/src/govuk/components/file-upload/file-upload.mjs index 737235c764..d97ab3d8f0 100644 --- a/packages/govuk-frontend/src/govuk/components/file-upload/file-upload.mjs +++ b/packages/govuk-frontend/src/govuk/components/file-upload/file-upload.mjs @@ -121,7 +121,7 @@ export class FileUpload extends GOVUKFrontendComponent { this.$wrapper.addEventListener('drop', this.onDragLeaveOrDrop.bind(this)) // When a file is dragged over the page (or dragged off the page) - document.addEventListener('dragover', this.onDragOver.bind(this)) + document.addEventListener('dragenter', this.onDragEnter.bind(this)) document.addEventListener('dragleave', this.onDragLeaveOrDrop.bind(this)) } @@ -176,8 +176,14 @@ export class FileUpload extends GOVUKFrontendComponent { /** * When a file is dragged over the container, show a visual indicator that a * file can be dropped here. + * + * @param {DragEvent} event - the drag event */ - onDragOver() { + onDragEnter(event) { + // Check if the thing being dragged is a file (and not text or something + // else), we only want to indicate files. + console.log(event) + // eslint-disable-next-line // @ts-ignore this.$wrapper.classList.add('govuk-file-upload-wrapper--show-dropzone')