diff --git a/src/Form/BSFeedback.jsx b/src/Form/BSFeedback.jsx new file mode 100644 index 0000000000..3d0044182f --- /dev/null +++ b/src/Form/BSFeedback.jsx @@ -0,0 +1,45 @@ +import classNames from 'classnames'; +import React from 'react'; +import PropTypes from 'prop-types'; +// import { BsPrefixProps, BsPrefixRefForwardingComponent } from './helpers'; + +const propTypes = { + /** + * Specify whether the feedback is for valid or invalid fields + * + * @type {('valid'|'invalid')} + */ + type: PropTypes.string, + + /** Display feedback as a tooltip. */ + tooltip: PropTypes.bool, + + as: PropTypes.elementType, +}; + +const Feedback = React.forwardRef( + ( + { + as: Component = 'div', + className, + type = 'valid', + tooltip = false, + ...props + }, + ref, + ) => ( + + ), +); + +Feedback.displayName = 'Feedback'; +Feedback.propTypes = propTypes; + +export default Feedback; diff --git a/src/Form/BSForm.jsx b/src/Form/BSForm.jsx new file mode 100644 index 0000000000..c0b37d1318 --- /dev/null +++ b/src/Form/BSForm.jsx @@ -0,0 +1,92 @@ +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'; +import FormText from './FormText'; +import Switch from './Switch'; +// import { useBootstrapPrefix } from './ThemeProvider'; +import createWithBsPrefix from './createWithBsPrefix'; +// import { BsPrefixProps, BsPrefixRefForwardingComponent } from './helpers'; + +const FormRow = createWithBsPrefix('form-row'); + +const propTypes = { + /** + * @default {'form'} + */ + bsPrefix: PropTypes.string, + + /** + * The Form `ref` will be forwarded to the underlying element, + * which means, unless it's rendered `as` a composite component, + * it will be a DOM node, when resolved. + * + * @type {ReactRef} + * @alias ref + */ + _ref: PropTypes.any, + + /** + * Display the series of labels, form controls, + * and buttons on a single horizontal row + */ + inline: PropTypes.bool, + + /** + * Mark a form as having been validated. Setting it to `true` will + * toggle any validation styles on the forms elements. + */ + validated: PropTypes.bool, + as: PropTypes.elementType, +}; + +const defaultProps = { + inline: false, +}; + +const FormImpl = (React.forwardRef( + ( + { + bsPrefix, + inline, + className, + validated, + as: Component = 'form', + ...props + }, + ref, + ) => { + // bsPrefix = useBootstrapPrefix(bsPrefix, 'form'); + bsPrefix = 'form'; + return ( + + ); + }, +)); + +FormImpl.displayName = 'Form'; +FormImpl.propTypes = propTypes; +FormImpl.defaultProps = defaultProps; + +FormImpl.Row = FormRow; +FormImpl.Group = FormGroup; +FormImpl.Control = FormControl; +FormImpl.Check = FormCheck; +FormImpl.File = FormFile; +FormImpl.Switch = Switch; +FormImpl.Label = FormLabel; +FormImpl.Text = FormText; + +export default FormImpl; diff --git a/src/Form/BSFormCheck.jsx b/src/Form/BSFormCheck.jsx new file mode 100644 index 0000000000..9347c6aeaa --- /dev/null +++ b/src/Form/BSFormCheck.jsx @@ -0,0 +1,207 @@ +import classNames from 'classnames'; +import PropTypes from 'prop-types'; +// import all from 'prop-types-extra/lib/all'; +import React, { useContext, useMemo } from 'react'; +import Feedback from './BSFeedback'; +import FormCheckInput from './FormCheckInput'; +import FormCheckLabel from './FormCheckLabel'; +import FormContext from './BSFormContext'; +// import { useBootstrapPrefix } from './ThemeProvider'; + +const propTypes = { + /** + * @default 'form-check' + */ + bsPrefix: PropTypes.string, + + /** + * A seperate bsPrefix used for custom controls + * + * @default 'custom-control' + */ + bsCustomPrefix: PropTypes.string, + + /** + * The FormCheck `ref` will be forwarded to the underlying input element, + * which means it will be a DOM node, when resolved. + * + * @type {ReactRef} + * @alias ref + */ + _ref: PropTypes.any, + + /** + * The underlying HTML element to use when rendering the FormCheck. + * + * @type {('input'|elementType)} + */ + as: PropTypes.elementType, + + /** + * A HTML id attribute, necessary for proper form accessibility. + * An id is recommended for allowing label clicks to toggle the check control. + * + * This is **required** for custom check controls or when `type="switch"` due to + * how they are rendered. + */ + id: PropTypes.string, + + /** + * Provide a function child to manually handle the layout of the FormCheck's inner components. + * + * ```jsx + * + * + * Allow us to contact you? + * Yo this is required + * + * ``` + */ + children: PropTypes.node, + + /** + * Groups controls horizontally with other `FormCheck`s. + */ + inline: PropTypes.bool, + + /** + * Disables the control. + */ + disabled: PropTypes.bool, + + /** + * `title` attribute for the underlying `FormCheckLabel`. + */ + title: PropTypes.string, + + /** + * Label for the control. + */ + label: PropTypes.node, + + /** Use Bootstrap's custom form elements to replace the browser defaults */ + // custom: all(PropTypes.bool, ({ custom, id }) => (custom && !id ? Error('Custom check controls require an id to work') : null)), + + /** + * The type of checkable. + * @type {('radio' | 'checkbox' | 'switch')} + */ + type: all( + PropTypes.oneOf(['radio', 'checkbox', 'switch']).isRequired, + ({ type, custom }) => (type === 'switch' && custom === false + ? Error('`custom` cannot be set to `false` when the type is `switch`') + : null), + ({ type, id }) => (type === 'switch' && !id + ? Error('`id` must be defined when the type is `switch`') + : null), + ), + + /** Manually style the input as valid */ + isValid: PropTypes.bool, + + /** Manually style the input as invalid */ + isInvalid: PropTypes.bool, + + /** Display feedback as a tooltip. */ + feedbackTooltip: PropTypes.bool, + + /** A message to display when the input is in a validation state */ + feedback: PropTypes.node, +}; + +const FormCheck = (React.forwardRef( + ( + { + id, + bsPrefix, + bsCustomPrefix, + inline = false, + disabled = false, + isValid = false, + isInvalid = false, + feedbackTooltip = false, + feedback, + className, + style, + title = '', + type = 'checkbox', + label, + children, + custom: propCustom, + as = 'input', + ...props + }, + ref, + ) => { + const custom = type === 'switch' ? true : propCustom; + const [prefix, defaultPrefix] = custom + ? [bsCustomPrefix, 'custom-control'] + : [bsPrefix, 'form-check']; + + // bsPrefix = useBootstrapPrefix(prefix, defaultPrefix); + bsPrefix = 'form-check'; + + const { controlId } = useContext(FormContext); + const innerFormContext = useMemo( + () => ({ + controlId: id || controlId, + custom, + }), + [controlId, custom, id], + ); + + const hasLabel = custom || (label != null && label !== false && !children); + + const input = ( + + ); + + return ( + +
+ {children || ( + <> + {input} + {hasLabel && ( + {label} + )} + {(isValid || isInvalid) && ( + + {feedback} + + )} + + )} +
+
+ ); + }, +)); + +FormCheck.displayName = 'FormCheck'; +FormCheck.propTypes = propTypes; + +FormCheck.Input = FormCheckInput; +FormCheck.Label = FormCheckLabel; + +export default FormCheck; diff --git a/src/Form/BSFormContext.jsx b/src/Form/BSFormContext.jsx new file mode 100644 index 0000000000..12f9ca46b3 --- /dev/null +++ b/src/Form/BSFormContext.jsx @@ -0,0 +1,7 @@ +import React from 'react'; + +const FormContext = React.createContext({ + controlId: undefined, +}); + +export default FormContext; diff --git a/src/Form/BSFormControl.jsx b/src/Form/BSFormControl.jsx new file mode 100644 index 0000000000..be564f6abc --- /dev/null +++ b/src/Form/BSFormControl.jsx @@ -0,0 +1,187 @@ +import classNames from 'classnames'; +import PropTypes from 'prop-types'; +// import all from 'prop-types-extra/lib/all'; +import React, { useContext } from 'react'; +// import warning from 'warning'; +import Feedback from './BSFeedback'; +import FormContext from './BSFormContext'; +// import { useBootstrapPrefix } from './ThemeProvider'; +// import { BsPrefixProps, BsPrefixRefForwardingComponent } from './helpers'; + +const propTypes = { + /** + * @default {'form-control'} + */ + bsPrefix: PropTypes.string, + + /** + * A seperate bsPrefix used for custom controls + * + * @default 'custom' + */ + bsCustomPrefix: PropTypes.string, + + /** + * The FormControl `ref` will be forwarded to the underlying input element, + * which means unless `as` is a composite component, + * it will be a DOM node, when resolved. + * + * @type {ReactRef} + * @alias ref + */ + _ref: PropTypes.any, + /** + * Input size variants + * + * @type {('sm'|'lg')} + */ + size: PropTypes.string, + + /** + * The size attribute of the underlying HTML element. + * Specifies the visible width in characters if `as` is `'input'`. + * Specifies the number of visible options if `as` is `'select'`. + */ + htmlSize: PropTypes.number, + + /** + * The underlying HTML element to use when rendering the FormControl. + * + * @type {('input'|'textarea'|'select'|elementType)} + */ + as: PropTypes.elementType, + + /** + * Render the input as plain text. Generally used along side `readOnly`. + */ + plaintext: PropTypes.bool, + + /** Make the control readonly */ + readOnly: PropTypes.bool, + + /** Make the control disabled */ + disabled: PropTypes.bool, + + /** + * The `value` attribute of underlying input + * + * @controllable onChange + * */ + value: PropTypes.oneOfType([ + PropTypes.string, + PropTypes.arrayOf(PropTypes.string), + PropTypes.number, + ]), + + /** A callback fired when the `value` prop changes */ + onChange: PropTypes.func, + + /** + * Use Bootstrap's custom form elements to replace the browser defaults + * @type boolean + */ + // custom: all(PropTypes.bool, ({ as, type, custom }) => (custom === true && type !== 'range' && as !== 'select' + // ? Error( + // '`custom` can only be set to `true` when the input type is `range`, or `select`', + // ) + // : null)), + + /** + * The HTML input `type`, which is only relevant if `as` is `'input'` (the default). + */ + type: PropTypes.string, + + /** + * Uses `controlId` from `` if not explicitly specified. + */ + id: PropTypes.string, + + /** Add "valid" validation styles to the control */ + isValid: PropTypes.bool, + + /** Add "invalid" validation styles to the control and accompanying label */ + isInvalid: PropTypes.bool, +}; + +const FormControl = React.forwardRef( + ( + { + bsPrefix, + bsCustomPrefix, + type, + size, + htmlSize, + id, + // eslint-disable-next-line react/prop-types + className, + isValid = false, + isInvalid = false, + plaintext, + readOnly, + custom, + as: Component = 'input', + ...props + }, + ref, + ) => { + const { controlId } = useContext(FormContext); + const [prefix, defaultPrefix] = custom + ? [bsCustomPrefix, 'custom'] + : [bsPrefix, 'form-control']; + + // eslint-disable-next-line no-param-reassign + // bsPrefix = useBootstrapPrefix(prefix, defaultPrefix); + bsPrefix = 'form-control'; + + let classes; + if (plaintext) { + classes = { [`${bsPrefix}-plaintext`]: true }; + } else if (type === 'file') { + classes = { [`${bsPrefix}-file`]: true }; + } else if (type === 'range') { + classes = { [`${bsPrefix}-range`]: true }; + } else if (Component === 'select' && custom) { + classes = { + [`${bsPrefix}-select`]: true, + [`${bsPrefix}-select-${size}`]: size, + }; + } else { + classes = { + [bsPrefix]: true, + [`${bsPrefix}-${size}`]: size, + }; + } + + console.warn( + controlId == null || !id, + '`controlId` is ignored on `` when `id` is specified.', + ); + + // warning( + // controlId == null || !id, + // '`controlId` is ignored on `` when `id` is specified.', + // ); + + return ( + + ); + }, +); + +FormControl.displayName = 'FormControl'; +FormControl.propTypes = propTypes; + +export default Object.assign(FormControl, { Feedback }); diff --git a/src/Form/FormControl.jsx b/src/Form/FormControl.jsx index 54fc69fca7..a87d5d6a23 100644 --- a/src/Form/FormControl.jsx +++ b/src/Form/FormControl.jsx @@ -1,7 +1,8 @@ import React, { useCallback, useEffect } from 'react'; import PropTypes from 'prop-types'; import classNames from 'classnames'; -import RBFormControl from 'react-bootstrap/FormControl'; +// import RBFormControl from 'react-bootstrap/FormControl'; +import RBFormControl from './BSFormControl'; import { IMaskInput } from 'react-imask'; import { useFormGroupContext } from './FormGroupContext'; import FormControlFeedback from './FormControlFeedback'; diff --git a/src/Form/createWithBsPrefix.jsx b/src/Form/createWithBsPrefix.jsx new file mode 100644 index 0000000000..ae9d07b993 --- /dev/null +++ b/src/Form/createWithBsPrefix.jsx @@ -0,0 +1,44 @@ +import classNames from 'classnames'; +// import camelize from 'dom-helpers/camelize'; +import React from 'react'; +// import { useBootstrapPrefix } from './ThemeProvider'; + +function camelize(str) { + return str + .replace(/[-_\s]+(.)?/g, (match, char) => (char ? char.toUpperCase() : '')) + .replace(/^[A-Z]/, (char) => char.toLowerCase()); +} + +const pascalCase = (str) => str[0].toUpperCase() + camelize(str).slice(1); + +// TODO: emstricten & fix the typing here! `createWithBsPrefix...` +export default function createWithBsPrefix( + prefix, + { + displayName = pascalCase(prefix), + Component, + defaultProps, + } = {}, +) { + const BsComponent = React.forwardRef( + ( + { + className, bsPrefix, as: Tag = Component || 'div', ...props + }, + ref, + ) => { + // const resolvedPrefix = useBootstrapPrefix(bsPrefix, prefix); + const resolvedPrefix = 'bs'; + return ( + + ); + }, + ); + BsComponent.defaultProps = defaultProps; + BsComponent.displayName = displayName; + return BsComponent; +}