Skip to content

Commit

Permalink
feat: add file picker package (#2674)
Browse files Browse the repository at this point in the history
* feat: add file picker package

* chore: make file picker composable

* chore: fix labels

* chore: update stories, use siblingBox

* chore: tests

* chore: finish tests

* chore(box): add box prop and sibling box type

* chore: changesets

* chore: pr feedback

* docs: file picker

* chore: fix disabled example

* chore: fix docs and border radius issue

* chore: design updates

* chore: dont wrap button

* chore: update tests from style update

* chore: sarahs doc feedback

* chore: extra period
  • Loading branch information
nkrantz authored Sep 21, 2022
1 parent 312a3cc commit 9825576
Show file tree
Hide file tree
Showing 26 changed files with 1,973 additions and 1,213 deletions.
6 changes: 6 additions & 0 deletions .changeset/cool-lies-impress.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@twilio-paste/core': patch
'@twilio-paste/sibling-box': patch
---

[Sibling Box] adds type 'file' for use in the File Picker package
6 changes: 6 additions & 0 deletions .changeset/late-tables-shop.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@twilio-paste/core': minor
'@twilio-paste/file-picker': major
---

[File Picker]: creates the File Picker package and adds it to the system
6 changes: 6 additions & 0 deletions .changeset/sour-birds-sleep.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@twilio-paste/core': patch
'@twilio-paste/box': patch
---

[Box] adds `accept` prop for use in the File Picker package
1 change: 1 addition & 0 deletions .codesandbox/ci.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
"/packages/paste-core/primitives/disclosure",
"/packages/paste-core/components/display-pill-group",
"/packages/paste-libraries/dropdown",
"/packages/paste-core/components/file-picker",
"/packages/paste-core/layout/flex",
"/packages/paste-core/components/form-pill-group",
"/packages/paste-core/layout/grid",
Expand Down
2 changes: 2 additions & 0 deletions packages/paste-codemods/tools/.cache/mappings.json
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@
"useDisclosureState": "@twilio-paste/core/disclosure",
"DisplayPill": "@twilio-paste/core/display-pill-group",
"DisplayPillGroup": "@twilio-paste/core/display-pill-group",
"FilePicker": "@twilio-paste/core/file-picker",
"FilePickerButton": "@twilio-paste/core/file-picker",
"FormPill": "@twilio-paste/core/form-pill-group",
"FormPillGroup": "@twilio-paste/core/form-pill-group",
"useFormPillState": "@twilio-paste/core/form-pill-group",
Expand Down
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import * as React from 'react';
import {render, screen} from '@testing-library/react';
import {Default, Disabled, Required, Customized} from '../stories/index.stories';

describe('FilePicker', () => {
it('should render', () => {
const {getByText} = render(<Default data-testid="test-file-picker" />);
expect(getByText('Upload a file')).toBeDefined();
expect(screen.getByTestId('test-file-picker')).toHaveAttribute('type', 'file');
});
it('should render as disabled', () => {
render(<Disabled data-testid="test-file-picker" />);
expect(screen.getByTestId('test-file-picker').getAttribute('aria-disabled')).toBe('true');
});
it('should render as required', () => {
render(<Required data-testid="test-file-picker" />);
expect(screen.getByTestId('test-file-picker').getAttribute('aria-required')).toBe('true');
});
it('should set aria-describedby on the file description text', () => {
render(<Default data-testid="test-file-picker" />);
const textId = screen.getByText('No file uploaded').getAttribute('id');
expect(screen.getByTestId('test-file-picker')).toHaveAttribute('aria-describedby', textId);
});
});

describe('FilePicker customization', () => {
it('should set an element data attribute on File Picker', () => {
const {container} = render(<Default />);
expect(container.querySelector('[data-paste-element="FILEPICKER"]')).toBeInTheDocument();
expect(container.querySelector('[data-paste-element="FILEPICKER_BUTTON"]')).toBeInTheDocument();
expect(container.querySelector('[data-paste-element="FILEPICKER_TEXT"]')).toBeInTheDocument();
});
it('should set a custom element data attribute on File Picker', () => {
const {container} = render(<Default element="MY_FILEPICKER" />);
expect(container.querySelector('[data-paste-element="MY_FILEPICKER"]')).toBeInTheDocument();
expect(container.querySelector('[data-paste-element="MY_FILEPICKER_BUTTON"]')).toBeInTheDocument();
expect(container.querySelector('[data-paste-element="MY_FILEPICKER_TEXT"]')).toBeInTheDocument();
});
it('should add custom styling to File Picker', () => {
const {container} = render(<Customized />);
expect(container.querySelector('[data-paste-element="FILEPICKER"]')).toHaveStyle(
"font-family: 'Fira Mono',Courier,monospace"
);
expect(container.querySelector('[data-paste-element="FILEPICKER_BUTTON"]')).toHaveStyle(
'background-color: rgba(242, 47, 70, 0.1)'
);
expect(container.querySelector('[data-paste-element="FILEPICKER_TEXT"]')).toHaveStyle(
'margin: 0px 0.75rem 0px 0.125rem'
);
});
it('should add custom styling to a custom named File Picker', () => {
const {container} = render(<Customized element="MY_FILEPICKER" />);
expect(container.querySelector('[data-paste-element="MY_FILEPICKER"]')).toHaveStyle(
"font-family: 'Fira Mono',Courier,monospace"
);
expect(container.querySelector('[data-paste-element="MY_FILEPICKER_BUTTON"]')).toHaveStyle(
'background-color: rgba(242, 47, 70, 0.1)'
);
expect(container.querySelector('[data-paste-element="MY_FILEPICKER_TEXT"]')).toHaveStyle(
'margin: 0px 0.75rem 0px 0.125rem'
);
});
});
3 changes: 3 additions & 0 deletions packages/paste-core/components/file-picker/build.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
const {build} = require('../../../../tools/build/esbuild');

build(require('./package.json'));
53 changes: 53 additions & 0 deletions packages/paste-core/components/file-picker/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
{
"name": "@twilio-paste/file-picker",
"version": "0.0.0",
"category": "interaction",
"status": "production",
"description": "A File Picker is a form element used to upload files.",
"author": "Twilio Inc.",
"license": "MIT",
"main:dev": "src/index.tsx",
"main": "dist/index.js",
"module": "dist/index.es.js",
"types": "dist/index.d.ts",
"sideEffects": false,
"publishConfig": {
"access": "public"
},
"files": [
"dist"
],
"scripts": {
"build": "yarn clean && NODE_ENV=production node build.js && tsc",
"build:js": "NODE_ENV=development node build.js",
"build:props": "typedoc --tsconfig ./tsconfig.json --json ./dist/prop-types.json",
"clean": "rm -rf ./dist",
"tsc": "tsc"
},
"peerDependencies": {
"@twilio-paste/box": "^7.0.0",
"@twilio-paste/button": "^11.1.1",
"@twilio-paste/design-tokens": "^8.0.0",
"@twilio-paste/sibling-box": "^6.0.0",
"@twilio-paste/style-props": "^6.0.0",
"@twilio-paste/text": "^7.0.0",
"@twilio-paste/theme": "^8.0.0",
"@twilio-paste/uid-library": "^0.2.5",
"prop-types": "^15.7.2",
"react": "^17.0.2",
"react-dom": "^17.0.2"
},
"devDependencies": {
"@twilio-paste/box": "^7.0.0",
"@twilio-paste/button": "^11.1.1",
"@twilio-paste/design-tokens": "^8.0.0",
"@twilio-paste/sibling-box": "^6.0.0",
"@twilio-paste/style-props": "^6.0.0",
"@twilio-paste/text": "^7.0.0",
"@twilio-paste/theme": "^8.0.0",
"@twilio-paste/uid-library": "^0.2.5",
"prop-types": "^15.7.2",
"react": "^17.0.2",
"react-dom": "^17.0.2"
}
}
110 changes: 110 additions & 0 deletions packages/paste-core/components/file-picker/src/FilePicker.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import * as React from 'react';
import * as PropTypes from 'prop-types';
import {Text} from '@twilio-paste/text';
import {useUID} from '@twilio-paste/uid-library';
import {Box, safelySpreadBoxProps} from '@twilio-paste/box';
import type {BoxProps} from '@twilio-paste/box';
import {SiblingBox} from '@twilio-paste/sibling-box';

export interface FilePickerProps extends React.HTMLAttributes<HTMLInputElement>, Pick<BoxProps, 'element'> {
accept?: string;
children: React.ReactElement;
disabled?: boolean;
i18nNoSelectionText?: string;
required?: boolean;
}

const FilePicker = React.forwardRef<HTMLInputElement, FilePickerProps>(
(
{
element = 'FILEPICKER',
accept,
id = useUID(),
children,
disabled = false,
i18nNoSelectionText = 'No file uploaded',
required = false,
...props
},
ref
) => {
const [fileDescription, setFileDescription] = React.useState(i18nNoSelectionText);

const textId = useUID();

const handleChange = (evt: any): void => {
const file = evt.target.files[0].name;
setFileDescription(file);
};

return (
<>
<Box
// The actual <input type="file"/> is hidden but still appears in the DOM
{...safelySpreadBoxProps(props)}
id={id}
ref={ref}
as="input"
type="file"
accept={accept}
aria-disabled={disabled}
disabled={disabled}
aria-required={required}
aria-describedby={textId}
size="size0"
border="none"
overflow="hidden"
padding="space0"
margin="space0"
whiteSpace="nowrap"
textTransform="none"
position="absolute"
clip="rect(0 0 0 0)"
onChange={handleChange}
/>
<label htmlFor={id}>
<SiblingBox
element={element}
as="span"
type="file"
borderRadius="borderRadius10"
padding="space20"
paddingLeft="space30"
boxShadow="shadowBorderWeak"
_focusSibling={{
borderRadius: 'borderRadius10',
padding: 'space20',
paddingLeft: 'space30',
boxShadow: 'shadowFocus',
}}
>
<Text
id={textId}
as="span"
color={disabled ? 'colorTextWeaker' : 'currentColor'}
marginRight="space40"
fontWeight="fontWeightMedium"
element={`${element}_TEXT`}
>
{fileDescription}
</Text>
{React.cloneElement(children, {disabled: disabled, element: `${element}_BUTTON`})}
</SiblingBox>
</label>
</>
);
}
);

FilePicker.displayName = 'FilePicker';

FilePicker.propTypes = {
accept: PropTypes.string,
children: PropTypes.element.isRequired,
element: PropTypes.string,
disabled: PropTypes.bool,
i18nNoSelectionText: PropTypes.string,
required: PropTypes.bool,
};

export {FilePicker};
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import * as React from 'react';
import * as PropTypes from 'prop-types';
import type {ButtonProps} from '@twilio-paste/button';
import {Button} from '@twilio-paste/button';
import {Box} from '@twilio-paste/box';

const FilePickerButton = React.forwardRef<HTMLButtonElement, ButtonProps>(
({children, element, disabled, ...props}, ref) => {
return (
<Box whiteSpace="nowrap">
<Button {...props} element={element} ref={ref} size="small" disabled={disabled} as="span" type="button">
{children}
</Button>
</Box>
);
}
);

FilePickerButton.displayName = 'FilePickerButton';

FilePickerButton.propTypes = {
element: PropTypes.string,
children: PropTypes.node.isRequired,
disabled: PropTypes.bool,
};

export {FilePickerButton};
2 changes: 2 additions & 0 deletions packages/paste-core/components/file-picker/src/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './FilePicker';
export * from './FilePickerButton';
Loading

0 comments on commit 9825576

Please sign in to comment.