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..309ea05fd5 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,54 @@ 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); + $dropzone-offset: $dropzone-padding + $govuk-border-width-form-element; + + // 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; + 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 { + // 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..d97ab3d8f0 --- /dev/null +++ b/packages/govuk-frontend/src/govuk/components/file-upload/file-upload.mjs @@ -0,0 +1,298 @@ +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 + */ + $wrapper + + /** + * @private + */ + $button + + /** + * @private + */ + $status + + /** + * @private + * @type {FileUploadConfig} + */ + // eslint-disable-next-line + // @ts-ignore + config + + /** @private */ + i18n + + /** + * @param {Element | null} $root - File input element + * @param {FileUploadConfig} [config] - File Upload config + */ + constructor($root, config = {}) { + super($root) + + if (!(this.$root instanceof HTMLInputElement)) { + return + } + + 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, 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(this.$root, 'lang') + }) + + this.$label = document.querySelector(`[for="${this.$root.id}"]`) + + if (!this.$label) { + throw new ElementError({ + component: FileUpload, + identifier: 'No label' + }) + } + + // Wrapping element. This defines the boundaries of our drag and drop area. + const $wrapper = document.createElement('div') + $wrapper.className = 'govuk-file-upload-wrapper' + + // 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.$root.insertAdjacentElement('afterend', $wrapper) + + // Move the native file input to inside of the wrapper + $wrapper.insertAdjacentElement('afterbegin', this.$root) + + // Make all these new variables available to the module + this.$wrapper = $wrapper + this.$button = $button + this.$status = $status + + // 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)) + + // 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('dragenter', this.onDragEnter.bind(this)) + document.addEventListener('dragleave', this.onDragLeaveOrDrop.bind(this)) + } + + /** + * Check if the value of the underlying input has changed + */ + onChange() { + if (!('files' in this.$root)) { + return + } + + if (!this.$root.files) { + return + } + + // 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 + this.$status.innerText = this.i18n.t('filesSelectedDefault') + } else if ( + // If there is 1 file, just show the file name + fileCount === 1 + ) { + // 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', { + count: fileCount + }) + } + } + + /** + * When the button is clicked, emulate clicking the actual, hidden file input + */ + onClick() { + if (this.$label instanceof HTMLElement) { + this.$label.click() + } + } + + /** + * 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 + */ + 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') + } + + /** + * When a dragged file leaves the container, or the file is dropped, + * remove the visual indicator. + */ + onDragLeaveOrDrop() { + // eslint-disable-next-line + // @ts-ignore + 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. + */ + 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: { + // 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' + } + } + }) + + /** + * 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.puppeteer.test.js b/packages/govuk-frontend/src/govuk/components/file-upload/file-upload.puppeteer.test.js new file mode 100644 index 0000000000..a0172ae79d --- /dev/null +++ b/packages/govuk-frontend/src/govuk/components/file-upload/file-upload.puppeteer.test.js @@ -0,0 +1,331 @@ +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") + }) + }) + }) + + 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 fde79e6588..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 @@ -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 @@ -73,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 @@ -89,6 +105,37 @@ 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 + 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: 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 @@ -107,13 +154,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 @@ -130,8 +170,28 @@ 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 + multiple: true + selectFilesButtonText: Dewiswch ffeil + filesSelectedDefaultText: Dim ffeiliau wedi'u dewis + filesSelectedText: + 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 + 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..5d098d57da 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 %} @@ -42,10 +43,23 @@ {% if params.formGroup.beforeInput %} {{ params.formGroup.beforeInput.html | safe | trim | indent(2) if params.formGroup.beforeInput.html else params.formGroup.beforeInput.text }} {% endif %} - {% if params.formGroup.afterInput %} {{ params.formGroup.afterInput.html | safe | trim | indent(2) if params.formGroup.afterInput.html else params.formGroup.afterInput.text }} 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..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 @@ -59,6 +59,20 @@ 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 disabled', () => { + const $ = render('file-upload', examples.disabled) + + const $component = $('.govuk-file-upload') + expect($component.attr('disabled')).toBeTruthy() + }) + it('renders with attributes', () => { const $ = render('file-upload', examples.attributes) diff --git a/packages/govuk-frontend/src/govuk/init.jsdom.test.mjs b/packages/govuk-frontend/src/govuk/init.jsdom.test.mjs index b7206342f4..b3ed66b9aa 100644 --- a/packages/govuk-frontend/src/govuk/init.jsdom.test.mjs +++ b/packages/govuk-frontend/src/govuk/init.jsdom.test.mjs @@ -14,6 +14,7 @@ jest.mock(`./components/character-count/character-count.mjs`) jest.mock(`./components/checkboxes/checkboxes.mjs`) jest.mock(`./components/error-summary/error-summary.mjs`) jest.mock(`./components/exit-this-page/exit-this-page.mjs`) +jest.mock(`./components/file-upload/file-upload.mjs`) jest.mock(`./components/header/header.mjs`) jest.mock(`./components/notification-banner/notification-banner.mjs`) jest.mock(`./components/password-input/password-input.mjs`) @@ -38,6 +39,7 @@ describe('initAll', () => { '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';