Skip to content

Commit

Permalink
feat: File Input (#1218)
Browse files Browse the repository at this point in the history
Co-authored-by: Vincent Smedinga <[email protected]>
Co-authored-by: Aram <[email protected]>
  • Loading branch information
3 people authored May 24, 2024
1 parent d7316e8 commit 7b6ba98
Show file tree
Hide file tree
Showing 11 changed files with 264 additions and 0 deletions.
9 changes: 9 additions & 0 deletions packages/css/src/components/file-input/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<!-- @license CC0-1.0 -->

# File Input

Allows the user to upload one or more files from their device.

## Visual considerations

The filename label and button are displayed in the language of the browser and can vary between browsers and operating systems.
68 changes: 68 additions & 0 deletions packages/css/src/components/file-input/file-input.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/**
* @license EUPL-1.2+
* Copyright Gemeente Amsterdam
*/

@import "../../common/text-rendering";

@mixin reset-button {
border: 0;
border-radius: 0; // Reset rounded borders on iOS devices
box-sizing: border-box;
}

.ams-file-input {
background-color: var(--ams-file-input-background-color);
border: var(--ams-file-input-border);
color: var(--ams-file-input-color);
cursor: var(--ams-file-input-cursor);
font-family: var(--ams-file-input-font-family);
font-size: var(--ams-file-input-font-size);
font-weight: var(--ams-file-input-font-weight);
line-height: var(--ams-file-input-line-height);
max-inline-size: calc(100% - var(--ams-file-input-padding-inline) * 2);
outline-offset: 0.25rem; // Double the default focus outline offset to compensate for the dashed border
padding-block: var(--ams-file-input-padding-block);
padding-inline: var(--ams-file-input-padding-inline);
touch-action: manipulation;

@include text-rendering;
}

.ams-file-input:disabled {
color: var(--ams-file-input-disabled-color);
cursor: var(--ams-file-input-disabled-cursor);
}

.ams-file-input::file-selector-button {
appearance: none; // Reset default appearance on iOS devices
background-color: var(--ams-file-input-file-selector-button-background-color);
box-shadow: var(--ams-file-input-file-selector-button-box-shadow);
color: var(--ams-file-input-file-selector-button-color);
cursor: var(--ams-file-input-file-selector-button-cursor);
font-family: inherit;
font-size: inherit; // iOS specific fix
font-weight: inherit;
margin-inline-end: var(--ams-file-input-file-selector-button-margin-inline-end);
padding-block: var(--ams-file-input-file-selector-button-padding-block);
padding-inline: var(--ams-file-input-file-selector-button-padding-inline);

@media screen and (-ms-high-contrast: active), screen and (forced-colors: active) {
border: var(
--ams-file-input-file-selector-button-forced-color-mode-border
); // add border because forced colors changes box-shadow to none
}

@include reset-button;
}

.ams-file-input:disabled::file-selector-button {
box-shadow: var(--ams-file-input-file-selector-button-disabled-box-shadow);
color: var(--ams-file-input-disabled-color);
cursor: var(--ams-file-input-file-selector-button-disabled-cursor);
}

.ams-file-input:not(:disabled):hover::file-selector-button {
box-shadow: var(--ams-file-input-file-selector-button-hover-box-shadow);
color: var(--ams-file-input-file-selector-button-hover-color);
}
1 change: 1 addition & 0 deletions packages/css/src/components/index.scss
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
*/

/* Append here */
@import "./file-input/file-input";
@import "./field/field";
@import "./select/select";
@import "./time-input/time-input";
Expand Down
37 changes: 37 additions & 0 deletions packages/react/src/FileInput/FileInput.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { render } from '@testing-library/react'
import { createRef } from 'react'
import { FileInput } from './FileInput'
import '@testing-library/jest-dom'

describe('File input', () => {
it('renders', () => {
const { container } = render(<FileInput />)
const component = container.querySelector('input[type="file"]')

expect(component).toBeInTheDocument()
expect(component).toBeVisible()
})

it('renders a design system BEM class name', () => {
const { container } = render(<FileInput />)
const component = container.querySelector('input[type="file"]')

expect(component).toHaveClass('ams-file-input')
})

it('renders an additional class name', () => {
const { container } = render(<FileInput className="extra" />)
const component = container.querySelector('input[type="file"]')

expect(component).toHaveClass('ams-file-input extra')
})

it('supports ForwardRef in React', () => {
const ref = createRef<HTMLInputElement>()

const { container } = render(<FileInput ref={ref} />)
const component = container.querySelector('input[type="file"]')

expect(ref.current).toBe(component)
})
})
18 changes: 18 additions & 0 deletions packages/react/src/FileInput/FileInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/**
* @license EUPL-1.2+
* Copyright Gemeente Amsterdam
*/

import clsx from 'clsx'
import { forwardRef } from 'react'
import type { ForwardedRef, InputHTMLAttributes } from 'react'

export type FileInputProps = InputHTMLAttributes<HTMLInputElement>

export const FileInput = forwardRef(
({ className, ...restProps }: FileInputProps, ref: ForwardedRef<HTMLInputElement>) => (
<input {...restProps} ref={ref} className={clsx('ams-file-input', className)} type="file" />
),
)

FileInput.displayName = 'FileInput'
5 changes: 5 additions & 0 deletions packages/react/src/FileInput/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<!-- @license CC0-1.0 -->

# React File Input component

[File Input documentation](../../../css/src/components/file-input/README.md)
2 changes: 2 additions & 0 deletions packages/react/src/FileInput/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { FileInput } from './FileInput'
export type { FileInputProps } from './FileInput'
1 change: 1 addition & 0 deletions packages/react/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
*/

/* Append here */
export * from './FileInput'
export * from './Field'
export * from './Select'
export * from './TimeInput'
Expand Down
42 changes: 42 additions & 0 deletions proprietary/tokens/src/components/ams/file-input.tokens.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
{
"ams": {
"file-input": {
"background-color": { "value": "{ams.color.primary-white}" },
"border": { "value": "{ams.border.width.sm} dashed {ams.color.neutral-grey3}" },
"color": { "value": "{ams.color.primary-black}" },
"cursor": { "value": "{ams.action.activate.cursor}" },
"font-family": { "value": "{ams.text.font-family}" },
"font-size": { "value": "{ams.text.level.5.font-size}" },
"font-weight": { "value": "{ams.text.font-weight.normal}" },
"line-height": { "value": "{ams.text.level.5.line-height}" },
"outline-offset": { "value": "{ams.focus.outline-offset}" },
"padding-block": { "value": "{ams.space.inside.md}" },
"padding-inline": { "value": "{ams.space.inside.md}" },
"disabled": {
"color": { "value": "{ams.color.neutral-grey2}" },
"cursor": { "value": "{ams.action.disabled.cursor}" }
},
"file-selector-button": {
"background-color": { "value": "{ams.color.primary-white}" },
"box-shadow": { "value": "inset 0 0 0 {ams.border.width.md} {ams.color.primary-blue}" },
"color": { "value": "{ams.color.primary-blue}" },
"cursor": { "value": "{ams.action.activate.cursor}" },
"margin-inline-end": { "value": "{ams.space.inside.md}" },
"padding-block": { "value": "{ams.space.inside.xs}" },
"padding-inline": { "value": "{ams.space.inside.md}" },
"hover": {
"box-shadow": { "value": "inset 0 0 0 {ams.border.width.lg} {ams.color.dark-blue}" },
"color": { "value": "{ams.color.dark-blue}" }
},
"disabled": {
"box-shadow": { "value": "inset 0 0 0 {ams.border.width.md} {ams.color.neutral-grey2}" },
"color": { "value": "{ams.color.neutral-grey2}" },
"cursor": { "value": "{ams.action.disabled.cursor}" }
},
"forced-color-mode": {
"border": { "value": "{ams.border.width.md} solid" }
}
}
}
}
}
29 changes: 29 additions & 0 deletions storybook/src/components/FileInput/FileInput.docs.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { Canvas, Controls, Markdown, Meta, Primary } from "@storybook/blocks";
import * as FileInputStories from "./FileInput.stories.tsx";
import README from "../../../../packages/css/src/components/file-input/README.md?raw";

<Meta of={FileInputStories} />

<Markdown>{README}</Markdown>

<Primary />

<Controls />

## Multiple Files

Allow multiple files to be selected. The label will update to show the number of files selected.

<Canvas of={FileInputStories.Multiple} />

## Accept

Limit the types of files that can be selected. Some examples are `image/*`, `video/*`, or `audio/*`. To limit to a specific file type, use the MIME type, such as `application/pdf`.

- [MDN File Input](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/file#limiting_accepted_file_types): More examples

<Canvas of={FileInputStories.Accept} />

## Disabled

<Canvas of={FileInputStories.Disabled} />
52 changes: 52 additions & 0 deletions storybook/src/components/FileInput/FileInput.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/**
* @license EUPL-1.2+
* Copyright Gemeente Amsterdam
*/

import { FileInput } from '@amsterdam/design-system-react/src'
import { Meta, StoryObj } from '@storybook/react'

const meta = {
title: 'Components/Forms/File Input',
component: FileInput,
args: {
accept: undefined,
multiple: false,
disabled: false,
},
argTypes: {
accept: {
control: {
type: 'text',
},
},
multiple: {
control: {
type: 'boolean',
},
},
disabled: {
control: {
type: 'boolean',
},
},
},
} satisfies Meta<typeof FileInput>

export default meta

type Story = StoryObj<typeof meta>

export const Default: Story = {}

export const Multiple: Story = {
args: { multiple: true },
}

export const Accept: Story = {
args: { accept: 'application/pdf' },
}

export const Disabled: Story = {
args: { disabled: true },
}

0 comments on commit 7b6ba98

Please sign in to comment.