Unverified Commit 724ea6f7 authored by Tor Raswill's avatar Tor Raswill Committed by GitHub
Browse files

feat: add file input component (#5036)



* #3047 add file input component

A file input component that also supports the custom prop.

Adds FormFile,  FormFileInput, FormFileLabel

Adds types

Updates docs

* #3047 remove bs-custom-file-input script

remove bs-custom-file-input script and import, remove useEffect since it is not used in FormFile for anything else.

* #3047 rename buttontext

rename buttontext to buttonText

* #3047 update docs

Update docs with how to customize the inner markup

* #3047 fix missed case for buttonText

* #3047 Update FileApi.js

Update FileApi.js with more comprehensive example

* #3047 add lang prop

The lang prop allows translating the Browse button text via SCSS as per the bootstrap documentation.

Add types and docs

* Update FileApi.js

fix conflicting Ids for elements

* #3047 Update forms.js

Update docs with
* recommended script for managing visible output of FormFile custom
* Better readability of FormFile docs when customizing the output

* #3047 Create FormFileSpec.js

Added test for FormFile

* #3047 removed buttonText prop

This commit removes the buttonText prop, the types for it and the test.

Instead it forwards the data-browse attribute to the underlying components.

Updates docs.

This is to be more uniform with TWBS.

* #3047 fix types

Added simple type test and added File to Form.d.ts

* Update simple.test.tsx

Fix lint/prettier error after removing ignore line from docs

* #3047 fix conflicting ID's

* #3047 FormFile docs

Updated to make sure that
* The example in the API for the "customizing" version" docs prints Input before Label since custom is not set
* Updates the description for the data-browse attribute
* Adds queries for the API docs for FormFile, FormFileInput and FormFileLabel

* Update FormFile.js

Add error for data-browse if custom is not set

* Add test for data-browse and typescript ref

* Added test for data-browse attribute
* Add FormFile to types export
* Add FormFile ref test to simple.test.tsx

* Clean up innerRefs

Per comments from @bpas247

* Update FormFile.js

Update the description printed in the API section

* Update FormFile.js

* add inputAs with desc
* rewrite as with new desc
* add type def to data-browse
* set form-file as default class instead of form-group when not setting custom
* use as like in other components to create wrapping div

* Rename FileButtonTextSCSS

rename one to incorrect since MacOS does not carer about casing in filenames

* Rename FileButtonTextScss

Rename it to its correct name, update imports and references

* Update FormFileSpec.js

Update tests to
* test changed class
* test inputAs
* test as with another element type
Co-authored-by: default avatarTor Raswill <tor.raswill@dqc.se>
parent 0f5c7987
......@@ -2,6 +2,7 @@ import classNames from 'classnames';
import PropTypes from 'prop-types';
import React from 'react';
import FormCheck from './FormCheck';
import FormFile from './FormFile';
import FormControl from './FormControl';
import FormGroup from './FormGroup';
import FormLabel from './FormLabel';
......@@ -80,6 +81,7 @@ Form.Row = createWithBsPrefix('form-row');
Form.Group = FormGroup;
Form.Control = FormControl;
Form.Check = FormCheck;
Form.File = FormFile;
Form.Switch = Switch;
Form.Label = FormLabel;
Form.Text = FormText;
......
import classNames from 'classnames';
import PropTypes from 'prop-types';
import React, { useContext, useMemo } from 'react';
import all from 'prop-types-extra/lib/all';
import Feedback from './Feedback';
import FormFileInput from './FormFileInput';
import FormFileLabel from './FormFileLabel';
import FormContext from './FormContext';
import { useBootstrapPrefix } from './ThemeProvider';
const propTypes = {
/**
* @default 'form-file'
*/
bsPrefix: PropTypes.string,
/**
* A seperate bsPrefix used for custom controls
*
* @default 'custom-file'
*/
bsCustomPrefix: PropTypes.string,
/**
* The wrapping HTML element to use when rendering the FormFile.
*
* @type {('div'|elementType)}
*/
as: PropTypes.elementType,
/**
* The underlying HTML element to use when rendering the FormFile.
*
* @type {('input'|elementType)}
*/
inputAs: PropTypes.elementType,
/** A HTML id attribute, necessary for proper form accessibility. */
id: PropTypes.string,
/**
* Provide a function child to manually handle the layout of the FormFile's inner components.
*
* If not using the custom prop <code>FormFile.Label></code> should be before <code><FormFile.Input isInvalid /></code>
* ```jsx
* <FormFile>
* <FormFile.Label>Allow us to contact you?</FormFile.Label>
* <FormFile.Input isInvalid />
* <Feedback type="invalid">Yo this is required</Feedback>
* </FormFile>
* ```
*
* If using the custom prop <code><FormFile.Input isInvalid /></code> should be before <code>FormFile.Label></code>
* ```jsx
* <FormFile custom>
* <FormFile.Input isInvalid />
* <FormFile.Label>Allow us to contact you?</FormFile.Label>
* <Feedback type="invalid">Yo this is required</Feedback>
* </FormFile>
* ```
*/
children: PropTypes.node,
disabled: PropTypes.bool,
label: PropTypes.node,
/** Use Bootstrap's custom form elements to replace the browser defaults */
custom: PropTypes.bool,
/** Manually style the input as valid */
isValid: PropTypes.bool.isRequired,
/** Manually style the input as invalid */
isInvalid: PropTypes.bool.isRequired,
/** A message to display when the input is in a validation state */
feedback: PropTypes.node,
/**
* The string for the "Browse" text label when using custom file input
*
* @type string
*/
'data-browse': all(
PropTypes.string,
({ custom, 'data-browse': dataBrowse }) =>
dataBrowse && !custom
? Error(
'`data-browse` attribute value will only be used when custom is `true`',
)
: null,
),
/** The language for the button when using custom file input and SCSS based strings */
lang: all(PropTypes.string, ({ custom, lang }) =>
lang && !custom
? Error('`lang` can only be set when custom is `true`')
: null,
),
};
const defaultProps = {
disabled: false,
isValid: false,
isInvalid: false,
};
const FormFile = React.forwardRef(
(
{
id,
bsPrefix,
bsCustomPrefix,
disabled,
isValid,
isInvalid,
feedback,
className,
style,
label,
children,
custom,
lang,
'data-browse': dataBrowse,
// Need to define the default "as" during prop destructuring to be compatible with styled-components github.com/react-bootstrap/react-bootstrap/issues/3595
as: Component = 'div',
inputAs = 'input',
...props
},
ref,
) => {
bsPrefix = custom
? useBootstrapPrefix(bsCustomPrefix, 'custom')
: useBootstrapPrefix(bsPrefix, 'form-file');
const type = 'file';
const { controlId } = useContext(FormContext);
const innerFormContext = useMemo(
() => ({
controlId: id || controlId,
custom,
}),
[controlId, custom, id],
);
const hasLabel = label != null && label !== false && !children;
const input = (
<FormFileInput
{...props}
ref={ref}
isValid={isValid}
isInvalid={isInvalid}
disabled={disabled}
as={inputAs}
lang={lang}
/>
);
return (
<FormContext.Provider value={innerFormContext}>
<Component
style={style}
className={classNames(
className,
bsPrefix,
custom && `custom-${type}`,
)}
>
{children || (
<>
{custom ? (
<>
{input}
{hasLabel && (
<FormFileLabel data-browse={dataBrowse}>
{label}
</FormFileLabel>
)}
</>
) : (
<>
{hasLabel && <FormFileLabel>{label}</FormFileLabel>}
{input}
</>
)}
{(isValid || isInvalid) && (
<Feedback type={isValid ? 'valid' : 'invalid'}>
{feedback}
</Feedback>
)}
</>
)}
</Component>
</FormContext.Provider>
);
},
);
FormFile.displayName = 'FormFile';
FormFile.propTypes = propTypes;
FormFile.defaultProps = defaultProps;
FormFile.Input = FormFileInput;
FormFile.Label = FormFileLabel;
export default FormFile;
import classNames from 'classnames';
import PropTypes from 'prop-types';
import React, { useContext } from 'react';
import FormContext from './FormContext';
import { useBootstrapPrefix } from './ThemeProvider';
const propTypes = {
/**
* @default 'form-file-input'
*/
bsPrefix: PropTypes.string,
/**
* A seperate bsPrefix used for custom controls
*
* @default 'custom-file-input'
*/
bsCustomPrefix: PropTypes.string,
/**
* The underlying HTML element to use when rendering the FormFileInput.
*
* @type {('input'|elementType)}
*/
as: PropTypes.elementType,
/** A HTML id attribute, necessary for proper form accessibility. */
id: PropTypes.string,
/** Manually style the input as valid */
isValid: PropTypes.bool.isRequired,
/** Manually style the input as invalid */
isInvalid: PropTypes.bool.isRequired,
/** The language for the button when using custom file input and SCSS based strings */
lang: PropTypes.string,
};
const FormFileInput = React.forwardRef(
(
{
id,
bsPrefix,
bsCustomPrefix,
className,
isValid,
isInvalid,
lang,
// Need to define the default "as" during prop destructuring to be compatible with styled-components github.com/react-bootstrap/react-bootstrap/issues/3595
as: Component = 'input',
...props
},
ref,
) => {
const { controlId, custom } = useContext(FormContext);
const type = 'file';
bsPrefix = custom
? useBootstrapPrefix(bsCustomPrefix, 'custom-file-input')
: useBootstrapPrefix(bsPrefix, 'form-control-file');
return (
<Component
{...props}
ref={ref}
id={id || controlId}
type={type}
lang={lang}
className={classNames(
className,
bsPrefix,
isValid && 'is-valid',
isInvalid && 'is-invalid',
)}
/>
);
},
);
FormFileInput.displayName = 'FormFileInput';
FormFileInput.propTypes = propTypes;
export default FormFileInput;
import classNames from 'classnames';
import PropTypes from 'prop-types';
import React, { useContext } from 'react';
import FormContext from './FormContext';
import { useBootstrapPrefix } from './ThemeProvider';
const propTypes = {
/**
* @default 'form-file-input'
*/
bsPrefix: PropTypes.string,
/**
* A seperate bsPrefix used for custom controls
*
* @default 'custom-file-label'
*/
bsCustomPrefix: PropTypes.string,
/** The HTML for attribute for associating the label with an input */
htmlFor: PropTypes.string,
/** The string for the "Browse" text label when using custom file input */
'data-browse': PropTypes.string,
};
const FormFileLabel = React.forwardRef(
({ bsPrefix, bsCustomPrefix, className, htmlFor, ...props }, ref) => {
const { controlId, custom } = useContext(FormContext);
bsPrefix = custom
? useBootstrapPrefix(bsCustomPrefix, 'custom-file-label')
: useBootstrapPrefix(bsPrefix, 'form-file-label');
return (
<label // eslint-disable-line jsx-a11y/label-has-associated-control
{...props}
ref={ref}
htmlFor={htmlFor || controlId}
className={classNames(className, bsPrefix)}
data-browse={props['data-browse']}
/>
);
},
);
FormFileLabel.displayName = 'FormFileLabel';
FormFileLabel.propTypes = propTypes;
export default FormFileLabel;
......@@ -26,6 +26,7 @@ export Fade from './Fade';
export Form from './Form';
export FormControl from './FormControl';
export FormCheck from './FormCheck';
export FormFile from './FormFile';
export Switch from './Switch';
export FormGroup from './FormGroup';
export FormLabel from './FormLabel';
......
import { mount } from 'enzyme';
import React from 'react';
import FormFile from '../src/FormFile';
describe('<FormFile>', () => {
it('should render correctly', () => {
let wrapper = mount(
<FormFile id="foo" name="foo" label="My label" className="my-file" />,
);
wrapper
.assertSingle('div.form-file.my-file')
.assertSingle('input[type="file"][name="foo"]');
wrapper
.assertSingle('label.form-file-label[htmlFor="foo"]')
.text()
.should.equal('My label');
});
it('should support isValid', () => {
mount(<FormFile isValid />).assertSingle('.is-valid');
});
it('should support isInvalid', () => {
mount(<FormFile isInvalid />).assertSingle('.is-invalid');
});
it('should support ref forwarding', () => {
class Container extends React.Component {
render() {
return (
<FormFile
ref={ref => {
this.input = ref;
}}
/>
);
}
}
const instance = mount(<Container />).instance();
expect(instance.input.tagName).to.equal('INPUT');
});
it('should supports custom', () => {
const wrapper = mount(<FormFile custom label="My label" />);
wrapper
.assertSingle('div.custom-file')
.assertSingle('input.custom-file-input');
wrapper.assertSingle('label.custom-file-label');
});
it('should supports lang when custom', () => {
const wrapper = mount(<FormFile custom lang="en" label="My label" />);
expect(wrapper.prop('lang')).to.equal('en');
});
it('should supports data-browse when custom', () => {
const wrapper = mount(
<FormFile custom data-browse="foo" label="My label" />,
);
expect(wrapper.prop('data-browse')).to.equal('foo');
});
it('should support "inputAs"', () => {
const Surrogate = ({ className = '', ...rest }) => (
<input className={`extraClass ${className}'`} {...rest} />
);
const wrapper = mount(<FormFile inputAs={Surrogate} />);
wrapper.assertSingle('input.extraClass[type="file"]');
});
it('Should have div as default component', () => {
const wrapper = mount(<FormFile />);
expect(wrapper.find('div').length).to.equal(1);
});
it('should support "as"', () => {
// eslint-disable-next-line no-unused-vars
const wrapperEl = document.createElement('wrapper-element');
const Surrogate = ({ className = '', ...rest }) => (
<wrapper-element className={`extraClass ${className}'`} {...rest} />
);
const wrapper = mount(<FormFile as={Surrogate} />);
wrapper.assertSingle('wrapper-element.extraClass');
});
});
import * as React from 'react';
import FormCheck from './FormCheck';
import FormFile from './FormFile';
import FormControl from './FormControl';
import FormGroup from './FormGroup';
import FormLabel from './FormLabel';
......@@ -25,6 +26,7 @@ declare class Form<
static Group: typeof FormGroup;
static Control: typeof FormControl;
static Check: typeof FormCheck;
static File: typeof FormFile;
static Label: typeof FormLabel;
static Text: typeof FormText;
}
......
import * as React from 'react';
import FormFileInput from './FormFileInput';
import FormFileLabel from './FormFileLabel';
import { BsPrefixComponent } from './helpers';
export interface FormFileProps {
bsCustomPrefix?: string;
id?: string;
disabled?: boolean;
label?: React.ReactNode;
custom?: boolean;
isValid?: boolean;
isInvalid?: boolean;
feedback?: React.ReactNode;
lang?: string;
}
declare class FormFile<
As extends React.ElementType = 'input'
> extends BsPrefixComponent<As, FormFileProps> {
static Input: typeof FormFileInput;
static Label: typeof FormFileLabel;
}
export default FormFile;
import * as React from 'react';
import { BsPrefixComponent } from './helpers';
export interface FormFileInputProps {
id?: string;
isValid?: boolean;
isInvalid?: boolean;
lang?: string;
}
declare class FormFileInput<
As extends React.ElementType = 'input'
> extends BsPrefixComponent<As, FormFileInputProps> {}
export default FormFileInput;
import * as React from 'react';
import { BsPrefixComponent } from './helpers';
export interface FormFileLabelProps {
htmlFor?: string;
}
declare class FormFileLabel extends BsPrefixComponent<
'label',
FormFileLabelProps
> {}
export default FormFileLabel;
......@@ -61,6 +61,7 @@ export { default as Fade, FadeProps } from './Fade';
export { default as Form, FormProps } from './Form';
export { default as FormControl, FormControlProps } from './FormControl';
export { default as FormCheck, FormCheckProps } from './FormCheck';
export { default as FormFile, FormFileProps } from './FormFile';
export { default as FormGroup, FormGroupProps } from './FormGroup';
export { default as FormLabel, FormLabelProps } from './FormLabel';
export { default as FormText, FormTextProps } from './FormText';
......
......@@ -18,6 +18,7 @@ import {
Dropdown,
DropdownButton,
Form,
FormFile,
FormControl,
InputGroup,
ListGroup,
......@@ -222,6 +223,13 @@ import {
<Form.Control type="text" placeholder="Hoizontal" />
</Col>
</Form.Group>
<Form.File id="custom-file" label="Custom file input" custom />
<Form.File
ref={React.createRef<HTMLInputElement & FormFile>()}
id="custom-file-ref"
label="Custom file input"
custom
/>
</Form>;
<div>
......
<Form>
<Form.File // prettier-ignore
id="custom-file"
label="Custom file input"
custom
/>
</Form>;
<Form>
<div className="mb-3">
<Form.File id="formcheck-api-custom" custom>
<Form.File.Input isValid />
<Form.File.Label data-browse="Button text">
Custom file input
</Form.File.Label>
<Form.Control.Feedback type="valid">You did it!</Form.Control.Feedback>
</Form.File>
</div>
<div className="mb-3">
<Form.File id="formcheck-api-regular">
<Form.File.Label>Regular file input</Form.File.Label>
<Form.File.Input />
</Form.File>
</div>
</Form>;
<Form>
<Form.File // prettier-ignore
id="custom-file-translate-html"
label="Voeg je document toe"
data-browse="Bestand kiezen"
custom
/>
</Form>;
<Form>
<Form.File // prettier-ignore
id="custom-file-translate-scss"
label="Custom file input"