Skip to content
4 changes: 4 additions & 0 deletions .storybook/_storybook.scss
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,7 @@
.ribbonlink-demo {
padding: 20px;
}

.fileupload-demo {
padding: 20px;
}
1 change: 1 addition & 0 deletions src/__tests__/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ describe('Index', () => {
it('contains all expected elements', () => {
expect(Object.keys(index)).toEqual([
'AccordionMenu',
'FileUpload',
'MaskedInput',
'RibbonLink',
'SubNavigation',
Expand Down
3 changes: 3 additions & 0 deletions src/components.scss
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,6 @@
@import './components/tooltip/Tooltip';
@import './components/ribbon-link/RibbonLink';
@import './components/timeline/Timeline';

// FileUpload
@import './components/file-upload/FileUpload';
38 changes: 38 additions & 0 deletions src/components/file-upload/FileUpload.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
$nhsuk-input-border-colour: #0b0c0c;
$component-padding: nhsuk-spacing(1);

.nhsuk-file-upload {
@include nhsuk-font($size: 19);
margin-left: -$component-padding;
padding: $component-padding;

// The default file upload button in Safari does not
// support setting a custom font-size. Set `-webkit-appearance`
// to `button` to drop out of the native appearance so the
// font-size is set to 19px
// https://bugs.webkit.org/show_bug.cgi?id=224746
&::-webkit-file-upload-button {
-webkit-appearance: button;
color: inherit;
font: inherit;
}

&:focus {
outline: $nhsuk-focus-width solid $nhsuk-focus-color;
// Use `box-shadow` to add border instead of changing `border-width`
// (which changes element size) and since `outline` is already used for the
// yellow focus state.
box-shadow: inset 0 0 0 4px $nhsuk-input-border-colour;
}

// Set "focus-within" to fix https://bugzilla.mozilla.org/show_bug.cgi?id=1430196
// so that component receives focus in Firefox.
// This can't be set together with `:focus` as all versions of IE fail
// to recognise `focus-within` and don't set any styles from the block
// when it's a selector.
&:focus-within {
outline: $nhsuk-focus-width solid $nhsuk-focus-color;

box-shadow: inset 0 0 0 4px $nhsuk-input-border-colour;
}
}
21 changes: 21 additions & 0 deletions src/components/file-upload/FileUpload.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import classNames from 'classnames';
import { ErrorMessage, Hint, Label } from 'nhsuk-react-components';
import React, { HTMLProps } from 'react';

interface FileUploadProps extends HTMLProps<HTMLDivElement> {
error?: string;
hint?: string;
}

const FileUpload: React.FC<FileUploadProps> = ({ error, hint, children, id, ...rest }) => {
return (
<div className={classNames('nhsuk-form-group', { 'nhsuk-form-group--error': error })} {...rest}>
<Label htmlFor={id}>{children}</Label>
{error && <ErrorMessage>{error}</ErrorMessage>}
{hint && <Hint>{hint}</Hint>}
<input aria-describedby={id} id="file-upload" className="nhsuk-file-upload" type="file" />
</div>
);
};

export default FileUpload;
28 changes: 28 additions & 0 deletions src/components/file-upload/__tests__/FileUpload.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import React from 'react';
import { mount, shallow } from 'enzyme';
import FileUpload from '../FileUpload';

describe('FileUpload', () => {
it('Matches snapshot', () => {
const component = shallow(<FileUpload>Upload</FileUpload>);
expect(component).toMatchSnapshot();
component.unmount();
});
it('With Error', () => {
const component = mount(<FileUpload error="something wrong">Upload</FileUpload>);
expect(component).toMatchSnapshot();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd want a couple more tests just to check that certain parts of the conditional rendering is working properly - i.e. check for a span.nhsuk-error-message to exist etc.


const renderedComponent = component.render();
expect(renderedComponent.find('span').prop('class')).toBe('nhsuk-error-message');

component.unmount();
});
it('With Hint', () => {
const component = mount(<FileUpload hint="Format: JPG">Upload</FileUpload>);
expect(component).toMatchSnapshot();

const renderedComponent = component.render();
expect(renderedComponent.find('span').prop('class')).toBe('nhsuk-hint');
component.unmount();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`FileUpload Matches snapshot 1`] = `
<div
className="nhsuk-form-group"
>
<Label>
Upload
</Label>
<input
className="nhsuk-file-upload"
id="file-upload"
type="file"
/>
</div>
`;

exports[`FileUpload With Error 1`] = `
<FileUpload
error="something wrong"
>
<div
className="nhsuk-form-group nhsuk-form-group--error"
>
<Label>
<BaseLabel>
<label
className="nhsuk-label"
>
Upload
</label>
</BaseLabel>
</Label>
<ErrorMessage
role="alert"
visuallyHiddenText="Error: "
>
<span
className="nhsuk-error-message"
role="alert"
>
<span
className="nhsuk-u-visually-hidden"
>
Error:
</span>
something wrong
</span>
</ErrorMessage>
<input
className="nhsuk-file-upload"
id="file-upload"
type="file"
/>
</div>
</FileUpload>
`;

exports[`FileUpload With Hint 1`] = `
<FileUpload
hint="Format: JPG"
>
<div
className="nhsuk-form-group"
>
<Label>
<BaseLabel>
<label
className="nhsuk-label"
>
Upload
</label>
</BaseLabel>
</Label>
<Hint>
<span
className="nhsuk-hint"
>
Format: JPG
</span>
</Hint>
<input
className="nhsuk-file-upload"
id="file-upload"
type="file"
/>
</div>
</FileUpload>
`;
3 changes: 3 additions & 0 deletions src/components/file-upload/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import FileUpload from './FileUpload';

export default FileUpload;
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export { default as AccordionMenu } from './components/accordion-menu';
export { default as FileUpload } from './components/file-upload';
export { default as MaskedInput } from './components/masked-input';
export { default as RibbonLink } from './components/ribbon-link';
export { default as SubNavigation } from './components/sub-navigation';
Expand Down
22 changes: 22 additions & 0 deletions stories/FileUpload.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import React from 'react';
import { storiesOf } from '@storybook/react';
import { FileUpload } from '../src';

const stories = storiesOf('File Upload', module);

stories
.add('Standard', () => (
<div className="fileupload-demo">
<FileUpload>Upload a file</FileUpload>
</div>
))
.add('With Error', () => (
<div className="fileupload-demo">
<FileUpload error="Invalid file type">Upload a file</FileUpload>
</div>
))
.add('With Hint', () => (
<div className="fileupload-demo">
<FileUpload hint="Maximum file size 2GB">Upload a File</FileUpload>
</div>
));