Skip to content

Commit

Permalink
Merge pull request #5305 from alphagov/spike-enhanced-file-upload
Browse files Browse the repository at this point in the history
Add progressively enhanced File Upload component
  • Loading branch information
romaricpascal authored Mar 3, 2025
2 parents a753f46 + e366b44 commit 4984bfe
Show file tree
Hide file tree
Showing 18 changed files with 1,749 additions and 10 deletions.
48 changes: 48 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,54 @@ For advice on how to use these release notes see [our guidance on staying up to

### New features

#### Use our improved File upload component

We've added a [JavaScript enhancement to the File upload component](https://design-system.service.gov.uk/components/file-upload/#using-the-improved-file-upload-component) which:

- makes the component easier to use for drag and drop
- allows the text of the component to be translated
- fixes accessibility issues for users of Dragon, a speech recognition software

This improvement is opt-in, as it's a substantial visual change which risks shifting other content on the page.

To enable this improvement for your users, you'll first need to update the markup of your File upload component:

- if you use our Nunjucks macro, using the new `javascript` option of `govukFileUpload`

```njk
{{ govukFileUpload({
id: "file-upload",
name: "photo",
label: {
text: "Upload your photo"
},
javascript: true
}) }}
```

- if you're using HTML, wrapping the `<input type="file">` of the File upload markup in a `<div class="govuk-drop-zone" data-module="govuk-file-upload">`

```html
<div class="govuk-form-group">
<label class="govuk-label" for="file-upload-1">
Upload your photo
</label>
<div class="govuk-drop-zone" data-module="govuk-file-upload">
<input class="govuk-file-upload" id="file-upload" name="photo" type="file">
</div>
</div>
```

If you're importing components individually in your JavaScript, which we recommend for better performance, you'll then need to import and initialise the new `FileUpload` component.

```js
import {FileUpload} from 'govuk-frontend'

createAll(FileUpload)
```

This change was introduced in [pull request #5305: Add progressively enhanced File Upload component](https://github.com/alphagov/govuk-frontend/pull/5305)

#### Form control components now have default `id` attributes

If you're using the included Nunjucks macros, the Text input, Textarea, Password input, Character count, File upload, and Select components now automatically use the value of the `name` parameter for the `id` parameter.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,10 @@
"text": "Problem with file",
"href": "#file"
},
{
"text": "Problem with file (enhanced)",
"href": "#file-enhanced"
},
{
"text": "Problem with radios",
"href": "#radios"
Expand Down Expand Up @@ -192,6 +196,18 @@
}
}) }}

{{ govukFileUpload({
label: {
"text": 'Label for enhanced file upload'
},
id: "file-enhanced",
name: "file-enhanced",
errorMessage: {
"text": "Problem with file"
},
javascript: true
}) }}

{{ govukRadios({
fieldset: {
legend: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
<li><a href="#date" class="govuk-link">Date pattern</a></li>
<li><a href="#select" class="govuk-link">Select box</a></li>
<li><a href="#file" class="govuk-link">File upload</a></li>
<li><a href="#file-enhanced" class="govuk-link">File upload (enhanced)</a></li>
</ul>

<form action="/" method="post">
Expand Down Expand Up @@ -174,5 +175,17 @@
}) -}}
{% endcall %}

{% call govukFieldset() %}
<span id="file-enhanced"></span>
{{- govukFileUpload({
id: 'file-upload-2',
name: 'file-upload-2',
label: {
text: 'Upload a file'
},
javascript: true
}) -}}
{% endcall %}

</form>
{% endblock %}
Original file line number Diff line number Diff line change
Expand Up @@ -340,6 +340,24 @@
}
}) }}

{{ govukFileUpload({
id: "file-upload-1",
name: "file-upload-1",
label: {
text: "Llwythwch ffeil i fyny"
},
javascript: true,
chooseFilesButtonText: "Dewiswch ffeil",
dropInstructionText: "neu ollwng ffeil",
noFileChosenText: "Dim ffeil wedi'i dewis",
multipleFilesChosenText: {
other: "%{count} ffeil wedi'u dewis",
one: "%{count} ffeil wedi'i dewis"
},
enteredDropZoneText: "Wedi mynd i mewn i'r parth gollwng",
leftDropZoneText: "Parth gollwng i'r chwith"
}) }}

{{ govukFooter({
classes: "govuk-!-margin-bottom-4",
navigation: [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,8 @@ scenario: |
hint: {
text: "Your photo must be at least 50KB and no more than 10MB"
},
errorMessage: errors["photo"]
errorMessage: errors["photo"],
javascript: true
}) }}

{{ govukCheckboxes({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,8 @@ scenario: |
hint: {
text: "Your photo must be at least 50KB and no more than 10MB"
},
errorMessage: errors["photo"]
errorMessage: errors["photo"],
javascript: true
}) }}

{{ govukCheckboxes({
Expand Down
1 change: 1 addition & 0 deletions packages/govuk-frontend/src/govuk/all.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
1 change: 1 addition & 0 deletions packages/govuk-frontend/src/govuk/all.puppeteer.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ describe('GOV.UK Frontend', () => {
'ConfigurableComponent',
'ErrorSummary',
'ExitThisPage',
'FileUpload',
'Header',
'NotificationBanner',
'PasswordInput',
Expand Down
167 changes: 167 additions & 0 deletions packages/govuk-frontend/src/govuk/components/file-upload/_index.scss
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@
@import "../label/index";

@include govuk-exports("govuk/component/file-upload") {
$file-upload-border-width: 2px;
$component-padding: govuk-spacing(1);
$empty-button-background-colour: govuk-colour("white");
$empty-pseudo-button-background-colour: govuk-colour("light-grey");
$empty-status-background-colour: govuk-tint(govuk-colour("blue"), 70%);

.govuk-file-upload {
@include govuk-font($size: 19);
Expand Down Expand Up @@ -46,4 +50,167 @@
cursor: not-allowed;
}
}

.govuk-drop-zone {
display: block;
position: relative;
z-index: 0;
background-color: $govuk-body-background-colour;
}

// required because disabling pointer events
// on the button means that the cursor style
// be applied on the button itself
.govuk-drop-zone--disabled {
cursor: not-allowed;
}

.govuk-file-upload-button__pseudo-button {
width: auto;
margin-right: govuk-spacing(2);
margin-bottom: $govuk-border-width-form-element + 1;
flex-shrink: 0;
}

.govuk-file-upload-button__instruction {
margin-top: govuk-spacing(2) - ($govuk-border-width-form-element + 1);
margin-bottom: 0;
text-align: left;
}

.govuk-file-upload-button__status {
display: block;
margin-bottom: govuk-spacing(2);
padding: govuk-spacing(3) govuk-spacing(2);
background-color: govuk-colour("white");
text-align: left;
}

// bugs documented with button using flex
// https://github.com/philipwalton/flexbugs#flexbug-9
// so we need a container here
.govuk-file-upload-button__pseudo-button-container {
display: flex;
align-items: baseline;
flex-wrap: wrap;
}

.govuk-file-upload-button {
width: 100%;
// align the padding to be same as notification banner and error summary accounting for the thicker borders
padding: (govuk-spacing(3) + $govuk-border-width - $file-upload-border-width);
border: $file-upload-border-width govuk-colour("mid-grey") solid;
background-color: govuk-colour("light-grey");
cursor: pointer;

@include govuk-media-query($from: tablet) {
padding: (govuk-spacing(4) + $govuk-border-width - $file-upload-border-width);
}

.govuk-file-upload-button__pseudo-button {
background-color: govuk-colour("white");
}

&:hover {
background-color: govuk-tint(govuk-colour("mid-grey"), 20%);

.govuk-file-upload-button__pseudo-button {
background-color: govuk-shade(govuk-colour("light-grey"), 10%);
}

.govuk-file-upload-button__status {
background-color: govuk-tint(govuk-colour("blue"), 80%);
}
}

&:active,
&:focus {
border: $file-upload-border-width solid govuk-colour("black");
outline: $govuk-focus-width solid $govuk-focus-colour;
// Ensure outline appears outside of the element
outline-offset: 0;
background-color: govuk-tint(govuk-colour("mid-grey"), 20%);
// Double the border by adding its width again. Use `box-shadow` for this
// instead of changing `border-width` - this is for consistency with
// components such as textarea where we avoid changing `border-width` as
// it will change the element size. Also, `outline` cannot be utilised
// here as it is already used for the yellow focus state.
box-shadow: inset 0 0 0 $govuk-border-width-form-element;

.govuk-file-upload-button__pseudo-button {
background-color: $govuk-focus-colour;
box-shadow: 0 2px 0 govuk-colour("black");
}

&:hover .govuk-file-upload-button__pseudo-button {
border-color: $govuk-focus-colour;
outline: 3px solid transparent;
background-color: govuk-colour("light-grey");
box-shadow: inset 0 0 0 1px $govuk-focus-colour;
}
}
}

.govuk-file-upload-button--empty {
border-style: dashed;
background-color: $empty-button-background-colour;

.govuk-file-upload-button__pseudo-button {
background-color: $empty-pseudo-button-background-colour;
}

.govuk-file-upload-button__status {
color: govuk-shade(govuk-colour("blue"), 60%);
background-color: $empty-status-background-colour;
}

&:hover,
&:focus,
&:active {
background-color: govuk-colour("light-grey");

.govuk-file-upload-button__status {
background-color: govuk-tint(govuk-colour("blue"), 80%);
}
}
}

.govuk-file-upload-button--dragging {
border-style: solid;
border-color: govuk-colour("black");

// extra specificity to apply when
// empty
&.govuk-file-upload-button {
background-color: govuk-tint(govuk-colour("mid-grey"), 20%);
}

&.govuk-file-upload-button--empty {
background-color: govuk-colour("light-grey");
}

&.govuk-file-upload-button--empty:not(:disabled) .govuk-file-upload-button__status,
&.govuk-file-upload-button--empty .govuk-file-upload-button__pseudo-button {
background-color: govuk-colour("white");
}

.govuk-file-upload-button__pseudo-button {
background-color: govuk-shade(govuk-colour("light-grey"), 10%);
}
}

.govuk-file-upload-button:disabled {
pointer-events: none;
opacity: 0.5;

background-color: $empty-button-background-colour;

.govuk-file-upload-button__pseudo-button {
background-color: $empty-pseudo-button-background-colour;
}

.govuk-file-upload-button__status {
background-color: $empty-status-background-colour;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,37 @@ describe('/components/file-upload', () => {
const examples = await getExamples('file-upload')

for (const exampleName in examples) {
await render(page, 'file-upload', examples[exampleName])
// JavaScript enhancements being optional, some examples will not have
// any element with `data-module="govuk-file-upload"`. This causes an error
// as `render` assumes that if a component is exported by GOV.UK Frontend
// its rendered markup will have a `data-module` and tried to initialise
// the JavaScript component, even if no element with the right `data-module`
// is on the page.
//
// Because of this, we need to filter `ElementError` thrown by the JavaScript
// component to examples that actually run the JavaScript enhancements
try {
await render(page, 'file-upload', examples[exampleName])
} catch (e) {
const macroOptions = /** @type {MacroOptions} */ (
examples[exampleName].context
)

const exampleUsesJavaScript = macroOptions.javascript
const exampleLackedRoot = e.message.includes(
'govuk-file-upload: Root element'
)

if (!exampleLackedRoot || exampleUsesJavaScript) {
throw e
}
}
await expect(axe(page)).resolves.toHaveNoViolations()
}
}, 120000)
})
})

/**
* @import {MacroOptions} from '@govuk-frontend/lib/components'
*/
Loading

0 comments on commit 4984bfe

Please sign in to comment.