diff --git a/.storybook/styles/application.scss b/.storybook/styles/application.scss index 296bffa0..3fe04659 100644 --- a/.storybook/styles/application.scss +++ b/.storybook/styles/application.scss @@ -48,10 +48,9 @@ $output-bourbon-deprecation-warnings: false; @import 'components/progress-circle'; @import 'components/login'; @import 'components/pagination'; -@import 'components/sidebar-content'; @import 'components/radio'; @import 'components/tabs'; +@import 'components/visually-hidden'; // Page Styles @import 'pages/main'; -@import 'pages/styleguide'; diff --git a/.storybook/styles/components/_forms.scss b/.storybook/styles/components/_forms.scss index 4a7486e4..9c756c44 100755 --- a/.storybook/styles/components/_forms.scss +++ b/.storybook/styles/components/_forms.scss @@ -1,7 +1,7 @@ /*----------------------- -Fieldset +Fieldsets and Field Wrappers -----------------------*/ -fieldset { +fieldset, .field-wrapper { display: inline-block; @include rem(margin-top, 15px); position: relative; @@ -64,12 +64,17 @@ Input Icon @include rem(padding-left, 30px); } - i { - @include position(absolute, 1px null null 10px); - background-repeat: no-repeat; - background-size: 15px; - height: 20px; - width: 20px; + .input-wrapper { + position: relative; + + i { + @include position(absolute, 3px null null 10px); + background-repeat: no-repeat; + background-size: 15px; + display: inline-block; + height: 20px; + width: 20px; + } } .mail-icon:after { @@ -404,29 +409,6 @@ Button Area width: 135px; } -/*----------------------- -Input Icon ------------------------*/ -.icon-label { - position: relative; - input { - @include rem(padding-left, 30px); - } - - .input-wrapper { - position: relative; - - i { - @include position(absolute, 3px null null 10px); - background-repeat: no-repeat; - background-size: 15px; - display: inline-block; - height: 20px; - width: 20px; - } - } -} - /*----------------------- Custom Dropdown Select -----------------------*/ @@ -571,13 +553,8 @@ Custom Dropdown Select } } - fieldset.checkbox, - fieldset.CheckboxGroup { - width: 100% !important; - - input { - border: 1px solid $white-med; - } + .checkbox { + width: 100%; } } diff --git a/.storybook/styles/components/_sidebar-content.scss b/.storybook/styles/components/_sidebar-content.scss deleted file mode 100644 index 47d882fa..00000000 --- a/.storybook/styles/components/_sidebar-content.scss +++ /dev/null @@ -1,107 +0,0 @@ -.sidebar-content { - display: none; - - @media ($tablet-landscape) { - display: block; - background-color: $white-base; - box-shadow: 0 0 18px 0 rgba(232, 232, 232, 0.5); - //@include position(fixed, 0px null 0px 0px); - overflow: scroll; - margin-top: 70px; - width: 345px; - @include rem(padding, 20px); - } - - input, - select { - border: 1px solid $grey-base; - height: 50px; - @include rem(padding-left, 8px); - } - - select { - background-position: right 10px top 50%; - color: $grey-base; - } - - .choose-degree { - @include rem(margin-top, 20px); - .radio { - display: inline-block; - @include rem(margin-right, 30px); - } - } - - .coupled-inputs { - fieldset { - display: inline-block; - width: 42%; - vertical-align: bottom; - } - } - h3 { - text-align: center; - text-transform: uppercase; - letter-spacing: 1px; - } - - li { - display: inline-block; - } -} - -.purchase-credits { - hr { - @include rem(margin, 50px 0 40px); - } - - .content-center { - h3 { - font-weight: $bold; - } - - p { - @include rem(margin-bottom, 0); - } - - li:first-child { - font-weight: $bold; - float: left; - font-size: $s-big; - } - - li:last-child { - color: $green-base; - float: right; - font-size: $s-large; - } - - ul { - @include rem(margin-bottom, 23px); - } - } - - .selected-credits { - border: 1px solid $green-base; - } - - .mobile-credits { - display: block; - - @media ($tablet-landscape) { - display: none; - } - - p { - @include rem(margin-bottom, 0px); - } - - h3 { - font-size: 20px; - @include rem(margin-bottom, 20px); - text-transform: uppercase; - letter-spacing: 1px; - text-align: center; - } - } -} diff --git a/.storybook/styles/components/_visually-hidden.scss b/.storybook/styles/components/_visually-hidden.scss new file mode 100644 index 00000000..07d36f86 --- /dev/null +++ b/.storybook/styles/components/_visually-hidden.scss @@ -0,0 +1,17 @@ +// Visually Hidden Element +// https://www.a11yproject.com/posts/2013-01-11-how-to-hide-content/ + +// In the rare case that this is applied to an interactive element, show the +// element temporarily when it receives focus (for keyboard users) + +.visually-hidden:not(:focus):not(:active) { + clip: rect(0 0 0 0) !important; + clip-path: inset(50%) !important; + height: 1px !important; + width: 1px !important; + overflow: hidden !important; + position: absolute !important; + white-space: nowrap !important; + border: 0 !important; + padding: 0 !important; +} \ No newline at end of file diff --git a/.storybook/styles/pages/_styleguide.scss b/.storybook/styles/pages/_styleguide.scss deleted file mode 100644 index 254aa178..00000000 --- a/.storybook/styles/pages/_styleguide.scss +++ /dev/null @@ -1,53 +0,0 @@ -/*----------------------- -Styleguide ------------------------*/ -.styleguide { - .styleguide-fields fieldset { - margin-top: 0px; - } - - h5:not(:first-child) { - @include rem(margin-top, 20px); - } - - .styleguide-sidebar { - .side-navigation { - margin-top: 0px; - position: relative; - } - } - .styleguide-header { - .navigation { - position: relative; - } - } - - &.content { - &.with-sidebar { - .content-container { - .card { - @media ($tablet-landscape) { - float: inherit; - width: 100%; - } - } - } - } - } -} - -.code-header { - border-bottom: 1px solid $white-med; - @include rem(margin-bottom, 20px); - @include rem(padding-bottom, 20px); - - &:not(:first-child) { - @include rem(margin-top, 60px); - } -} - -xmp { - background-color: rgba(lighten($blue-base, 35%), 0.5); - @include rem(padding, 20px); - white-space: pre-wrap; -} diff --git a/README.md b/README.md index 67ea2114..6f4adee6 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,7 @@ Documentation and usage info can be found in [docs.md](docs.md). - [v7.0.0](migration-guides/v7.0.0.md) - [v8.0.0](migration-guides/v8.0.0.md) - [v9.0.0](migration-guides/v9.0.0.md) +- [v10.0.0](migration-guides/v10.0.0.md) ## Contribution diff --git a/docs.md b/docs.md index 5b4f5a85..9f64b0f2 100644 --- a/docs.md +++ b/docs.md @@ -34,126 +34,123 @@ * [DateInput][30] * [Parameters][31] * [Examples][32] -* [DropdownCheckboxGroup][33] +* [FileInput][33] * [Parameters][34] * [Examples][35] -* [FileInput][36] - * [Parameters][37] - * [Examples][38] -* [HiddenInput][39] +* [HiddenInput][36] + * [Examples][37] +* [Input][38] + * [Parameters][39] * [Examples][40] -* [Input][41] +* [IconInput][41] * [Parameters][42] * [Examples][43] -* [IconInput][44] +* [MaskedInput][44] * [Parameters][45] * [Examples][46] -* [MaskedInput][47] +* [RangeInput][47] * [Parameters][48] * [Examples][49] -* [RangeInput][50] +* [RadioGroup][50] * [Parameters][51] * [Examples][52] -* [RadioGroup][53] +* [Select][53] * [Parameters][54] * [Examples][55] -* [Select][56] +* [Switch][56] * [Parameters][57] * [Examples][58] -* [Switch][59] +* [Textarea][59] * [Parameters][60] * [Examples][61] -* [Textarea][62] +* [ErrorLabel][62] * [Parameters][63] * [Examples][64] -* [ErrorLabel][65] +* [InputError][65] * [Parameters][66] * [Examples][67] -* [InputError][68] +* [InputLabel][68] * [Parameters][69] * [Examples][70] -* [InputLabel][71] +* [LabeledField][71] * [Parameters][72] * [Examples][73] -* [LabeledField][74] - * [Parameters][75] - * [Examples][76] -* [blurDirty][77] +* [blurDirty][74] + * [Examples][75] +* [convertNameToLabel][76] + * [Parameters][77] * [Examples][78] -* [convertNameToLabel][79] - * [Parameters][80] - * [Examples][81] -* [fieldOptionsType][82] -* [fieldOptionGroupsType][83] -* [fieldPropTypesWithValue][84] - * [Parameters][85] - * [Examples][86] -* [defaultValueTypes][87] -* [fieldPropTypes][88] -* [radioGroupPropTypes][89] -* [checkboxGroupPropTypes][90] -* [omitLabelProps][91] +* [fieldOptionsType][79] +* [fieldOptionGroupsType][80] +* [fieldPropTypesWithValue][81] + * [Parameters][82] + * [Examples][83] +* [defaultValueTypes][84] +* [fieldPropTypes][85] +* [radioGroupPropTypes][86] +* [checkboxGroupPropTypes][87] +* [omitLabelProps][88] + * [Parameters][89] + * [Examples][90] +* [replaceEmptyStringValue][91] * [Parameters][92] * [Examples][93] -* [replaceEmptyStringValue][94] +* [Table][94] * [Parameters][95] * [Examples][96] -* [Table][97] +* [SortableTable][97] * [Parameters][98] * [Examples][99] -* [SortableTable][100] +* [TableColumn][100] * [Parameters][101] * [Examples][102] -* [TableColumn][103] +* [FlashMessage][103] * [Parameters][104] * [Examples][105] -* [FlashMessage][106] +* [FlashMessageContainer][106] * [Parameters][107] * [Examples][108] -* [FlashMessageContainer][109] - * [Parameters][110] - * [Examples][111] -* [Spinner][112] +* [Spinner][109] + * [Examples][110] +* [LoadingContainer][111] + * [Parameters][112] * [Examples][113] -* [LoadingContainer][114] +* [compareAtPath][114] * [Parameters][115] * [Examples][116] -* [compareAtPath][117] +* [generateInputErrorId][117] * [Parameters][118] * [Examples][119] -* [generateInputErrorId][120] +* [serializeOptions][120] * [Parameters][121] * [Examples][122] -* [serializeOptions][123] +* [serializeOptionGroups][123] * [Parameters][124] * [Examples][125] -* [serializeOptionGroups][126] +* [stripNamespace][126] * [Parameters][127] * [Examples][128] -* [stripNamespace][129] +* [triggerOnKeys][129] * [Parameters][130] * [Examples][131] -* [triggerOnKeys][132] +* [Modal][132] * [Parameters][133] * [Examples][134] -* [Modal][135] +* [cloudinaryUploader][135] * [Parameters][136] * [Examples][137] -* [cloudinaryUploader][138] - * [Parameters][139] - * [Examples][140] ## ColorPicker -A control component for picking a hex color value. Built using the [react-color][141] `ChromePicker`. +A control component for picking a hex color value. Built using the [react-color][138] `ChromePicker`. ### Parameters -* `value` **[String][142]?** The hex value of the selected color -* `onChange` **[Function][143]?** A function called with the new hex value when a color is selected -* `onOpen` **[Function][143]?** A function called when the picker is expanded -* `onClose` **[Function][143]?** A function called when the picker is closed -* `active` **[Boolean][144]?** A boolean that controls whether the picker is expanded or not. +* `value` **[String][139]?** The hex value of the selected color +* `onChange` **[Function][140]?** A function called with the new hex value when a color is selected +* `onOpen` **[Function][140]?** A function called when the picker is expanded +* `onClose` **[Function][140]?** A function called when the picker is closed +* `active` **[Boolean][141]?** A boolean that controls whether the picker is expanded or not. ### Examples @@ -177,15 +174,15 @@ A control component for navigating between multiple numbered pages. ### Parameters -* `value` **[Number][145]** The number of the current page (optional, default `1`) -* `onChange` **[Function][143]?** A function called with the new value when a page is clicked. -* `min` **[Number][145]** The number of the first page (optional, default `1`) -* `max` **[Number][145]** The number of the last page. (optional, default `1`) -* `alwaysShow` **[Boolean][144]** Always show the component, even when there's only one page visible. (optional, default `false`) -* `pagesShown` **[Number][145]** The number of pages to display around (and including) the current page (optional, default `3`) -* `previousLabel` **[String][142]** The text of the "previous page" button (optional, default `'Prev'`) -* `nextLabel` **[String][142]** The text of the "next page" button (optional, default `'Next'`) -* `delimiter` **[String][142]** The delimiter that will be shown when there are hidden pages (optional, default `'...'`) +* `value` **[Number][142]** The number of the current page (optional, default `1`) +* `onChange` **[Function][140]?** A function called with the new value when a page is clicked. +* `min` **[Number][142]** The number of the first page (optional, default `1`) +* `max` **[Number][142]** The number of the last page. (optional, default `1`) +* `alwaysShow` **[Boolean][141]** Always show the component, even when there's only one page visible. (optional, default `false`) +* `pagesShown` **[Number][142]** The number of pages to display around (and including) the current page (optional, default `3`) +* `previousLabel` **[String][139]** The text of the "previous page" button (optional, default `'Prev'`) +* `nextLabel` **[String][139]** The text of the "next page" button (optional, default `'Next'`) +* `delimiter` **[String][139]** The delimiter that will be shown when there are hidden pages (optional, default `'...'`) ### Examples @@ -212,11 +209,11 @@ A control component for navigating among multiple tabs ### Parameters -* `vertical` **[Boolean][144]?** A boolean setting the `className` of the `ul` to 'horizontal' (default), or 'vertical', which determines the alignment of the tabs (optional, default `false`) -* `options` **[Array][146]** An array of tab values (strings or key-value pairs) -* `value` **([String][142] | [Number][145])** The value of the current tab -* `onChange` **[Function][143]?** A function called with the new value when a tab is clicked -* `activeClassName` **[String][142]?** The class of the active tab, (optional, default `active`) +* `vertical` **[Boolean][141]?** A boolean setting the `className` of the `ul` to 'horizontal' (default), or 'vertical', which determines the alignment of the tabs (optional, default `false`) +* `options` **[Array][143]** An array of tab values (strings or key-value pairs) +* `value` **([String][139] | [Number][142])** The value of the current tab +* `onChange` **[Function][140]?** A function called with the new value when a tab is clicked +* `activeClassName` **[String][139]?** The class of the active tab, (optional, default `active`) ### Examples @@ -248,12 +245,12 @@ If a className is provided to the component, it will be appended to the conditio ### Parameters -* `invalid` **[Boolean][144]?** Whether or not a related form is invalid (will set aria-disabled when `true`) -* `pristine` **[Boolean][144]?** Whether or not a related form is pristine (will set aria-disabled when `true`) -* `variant` **[String][142]** A descriptive string that will be appended to the button's class with format `button-` (optional, default `"primary"`) -* `submitting` **[Boolean][144]?** Whether or not a related form is submitting (will give button class `'in-progress` when `true`) -* `type` **[Boolean][144]** The [type][147] attribute of the button element (optional, default `"button"`) -* `children` **[Function][143]?** Any React component(s) being wrapped by the button +* `invalid` **[Boolean][141]?** Whether or not a related form is invalid (will set aria-disabled when `true`) +* `pristine` **[Boolean][141]?** Whether or not a related form is pristine (will set aria-disabled when `true`) +* `variant` **[String][139]** A descriptive string that will be appended to the button's class with format `button-` (optional, default `"primary"`) +* `submitting` **[Boolean][141]?** Whether or not a related form is submitting (will give button class `'in-progress` when `true`) +* `type` **[Boolean][141]** The [type][144] attribute of the button element (optional, default `"button"`) +* `children` **[Function][140]?** Any React component(s) being wrapped by the button ### Examples @@ -280,8 +277,8 @@ If a `className` is provided to the component, it will be appended to the defaul ### Parameters -* `className` **[String][142]?** A class to add to the wrapper -* `children` **[Function][143]?** The React component(s) being wrapped +* `className` **[String][139]?** A class to add to the wrapper +* `children` **[Function][140]?** The React component(s) being wrapped ### Examples @@ -332,8 +329,8 @@ This input only accepts and stores boolean values. ### Parameters -* `input` **[Object][148]** A `redux-form` [input][149] object -* `meta` **[Object][148]** A `redux-form` [meta][150] object +* `input` **[Object][145]** A `redux-form` [input][146] object +* `meta` **[Object][145]** A `redux-form` [meta][147] object ### Examples @@ -367,10 +364,11 @@ Clicking an unselected checkbox adds its value to this array, and clicking a sel ### Parameters -* `input` **[Object][148]** A `redux-form` [input][149] object -* `meta` **[Object][148]** A `redux-form` [meta][150] object -* `options` **[Array][146]** An array of checkbox values (strings, numbers, or key-value pairs) -* `checkboxInputProps` **[Object][148]** An object of key-value pairs representing props to pass down to all checkbox inputs (optional, default `{}`) +* `input` **[Object][145]** A `redux-form` [input][146] object +* `meta` **[Object][145]** A `redux-form` [meta][147] object +* `options` **[Array][143]** An array of checkbox values (strings, numbers, or key-value pairs) +* `checkboxInputProps` **[Object][145]** An object of key-value pairs representing props to pass down to all checkbox inputs (optional, default `{}`) +* `dropdown` **[Boolean][141]** A flag indicating whether the checkbox options are displayed in a dropdown container or not (optional, default `false`) ### Examples @@ -425,7 +423,7 @@ export default TodoForm ## CloudinaryFileInput -A wrapper around a file input component (defaults to [FileInput][36]) that automatically uploads files to cloudinary via the [cloudinaryUploader][151] HOC. +A wrapper around a file input component (defaults to [FileInput][33]) that automatically uploads files to cloudinary via the [cloudinaryUploader][148] HOC. The value of this input will only get set upon successful upload. The shape of the value will be of a file object or an array of file objects with the `url` set to the public URL of the uploaded file. The full response from Cloudinary is accessible via the value's `meta.cloudinary` key. @@ -436,12 +434,12 @@ or via the `CLOUDINARY_CLOUD_NAME` and `CLOUDINARY_BUCKET` env vars (recommended ### Parameters -* `input` **[Object][148]** A `redux-form` [input][149] object -* `meta` **[Object][148]** A `redux-form` [meta][150] object -* `fileInput` **[Function][143]** A component that gets wrapped with Cloudinary upload logic (optional, default `FileInput`) -* `multiple` **[Boolean][144]** A flag indicating whether or not to accept multiple files (optional, default `false`) -* `onUploadSuccess` **[Function][143]** A handler that gets invoked with the response from a successful upload to Cloudinary (optional, default `noop`) -* `onUploadFailure` **[Function][143]** A handler that gets invoked with the error from a failed upload to Cloudinary (optional, default `noop`) +* `input` **[Object][145]** A `redux-form` [input][146] object +* `meta` **[Object][145]** A `redux-form` [meta][147] object +* `fileInput` **[Function][140]** A component that gets wrapped with Cloudinary upload logic (optional, default `FileInput`) +* `multiple` **[Boolean][141]** A flag indicating whether or not to accept multiple files (optional, default `false`) +* `onUploadSuccess` **[Function][140]** A handler that gets invoked with the response from a successful upload to Cloudinary (optional, default `noop`) +* `onUploadFailure` **[Function][140]** A handler that gets invoked with the error from a failed upload to Cloudinary (optional, default `noop`) ### Examples @@ -470,8 +468,8 @@ The value of this input is a hex color string. ### Parameters -* `input` **[Object][148]** A `redux-form` [input][149] object -* `meta` **[Object][148]** A `redux-form` [meta][150] object +* `input` **[Object][145]** A `redux-form` [input][146] object +* `meta` **[Object][145]** A `redux-form` [meta][147] object ### Examples @@ -493,21 +491,21 @@ function UserForm ({ handleSubmit, pristine, invalid, submitting }) { ## DateInput -An input component that wraps a `DatePicker` component from the [react-datepicker][152] library. +An input component that wraps a `DatePicker` component from the [react-datepicker][149] library. This wrapper adds the following functionality to `DatePicker`: * Adapts it to receive `redux-form`-style input props. * Adds name and error labels. With the exception of the `input` and `meta` props, all props are passed down to the `DatePicker` component. -A full list of props supported by this component can be found [here][153]. Note that unfortunately `aria-*` props are **not** supported. +A full list of props supported by this component can be found [here][150]. Note that unfortunately `aria-*` props are **not** supported. -*Note: this component requires special styles in order to render correctly. To include these styles in your project, follow the directions in the main [README][154] file.* +*Note: this component requires special styles in order to render correctly. To include these styles in your project, follow the directions in the main [README][151] file.* ### Parameters -* `input` **[Object][148]** A `redux-form` [input][149] object -* `meta` **[Object][148]** A `redux-form` [meta][150] object +* `input` **[Object][145]** A `redux-form` [input][146] object +* `meta` **[Object][145]** A `redux-form` [meta][147] object ### Examples @@ -527,51 +525,6 @@ function BirthdayForm ({ handleSubmit }) { // Will render datepicker with label "Birthday" and placeholder "mm/dd/yyyy" ``` -## DropdownCheckboxGroup - -A group of checkboxes that can be used in a `redux-form`-controlled form. -Wraps the [CheckboxGroup][21] component in a [DropdownSelect][155] component, which displays the selected values as a list. -Options are displayed in a scrollable `Select`-style dropdown container. - -The value of each checkbox is specified via the `options` prop. This prop can either be: - -* An array of strings -* An array of key-value pairs: `{ key, value }` - -The value of the entire `DropdownCheckboxGroup` component is an **array** containing the values of the selected checkboxes. -Clicking an unselected checkbox adds its value to this array, and clicking a selected checkbox removes its value from this array. - -### Parameters - -* `input` **[Object][148]** A `redux-form` [input][149] object -* `meta` **[Object][148]** A `redux-form` [meta][150] object -* `options` **[Array][146]** An array of checkbox values (strings or key-value pairs) - -### Examples - -```javascript -function InterestsForm ({ handleSubmit, pristine, invalid, submitting }) { - return ( -
- - - Submit - - - ) -} - -export default TodoForm -``` - ## FileInput A file input that can be used in a `redux-form`-controlled form. @@ -588,18 +541,18 @@ A component passed using `previewComponent` will receive the following props: ### Parameters -* `input` **[Object][148]** A `redux-form` [input][149] object -* `meta` **[Object][148]** A `redux-form` [meta][150] object -* `readFiles` **[Function][143]** A callback that is fired with new files and is expected to return an array of file objects with the `url` key set to the "read" value. This can be either a data URL or the public URL from a 3rd party API (optional, default `readFilesAsDataUrls`) -* `multiple` **[Boolean][144]** A flag indicating whether or not to accept multiple files (optional, default `false`) -* `accept` **[String][142]?** Value that defines the file types the file input should accept (e.g., ".doc,.docx"). More info: [https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/file#accept][156] -* `capture` **(`"user"` | `"environment"`)?** Value that specifies which camera to use, if the accept attribute indicates the input type of image or video. This is not available for all devices (e.g., desktops). More info: [https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/file#capture][157] -* `onRemove` **[Function][143]** A callback fired when a file is removed (optional, default `noop`) -* `previewComponent` **[Function][143]** A custom component that is used to display a preview of each attached file (optional, default `RenderPreview`) -* `removeComponent` **[Function][143]** A custom component that receives `value` and `onRemove` props (optional, default `RemoveButton`) -* `thumbnail` **[String][142]?** A placeholder image to display before the file is loaded -* `hidePreview` **[Boolean][144]** A flag indicating whether or not to hide the file preview (optional, default `false`) -* `selectText` **[String][142]?** An override for customizing the text that is displayed on the input's label. Defaults to 'Select File' or 'Select File(s)' depending on the `multiple` prop value +* `input` **[Object][145]** A `redux-form` [input][146] object +* `meta` **[Object][145]** A `redux-form` [meta][147] object +* `readFiles` **[Function][140]** A callback that is fired with new files and is expected to return an array of file objects with the `url` key set to the "read" value. This can be either a data URL or the public URL from a 3rd party API (optional, default `readFilesAsDataUrls`) +* `multiple` **[Boolean][141]** A flag indicating whether or not to accept multiple files (optional, default `false`) +* `accept` **[String][139]?** Value that defines the file types the file input should accept (e.g., ".doc,.docx"). More info: [https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/file#accept][152] +* `capture` **(`"user"` | `"environment"`)?** Value that specifies which camera to use, if the accept attribute indicates the input type of image or video. This is not available for all devices (e.g., desktops). More info: [https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/file#capture][153] +* `onRemove` **[Function][140]** A callback fired when a file is removed (optional, default `noop`) +* `previewComponent` **[Function][140]** A custom component that is used to display a preview of each attached file (optional, default `RenderPreview`) +* `removeComponent` **[Function][140]** A custom component that receives `value` and `onRemove` props (optional, default `RemoveButton`) +* `thumbnail` **[String][139]?** A placeholder image to display before the file is loaded +* `hidePreview` **[Boolean][141]** A flag indicating whether or not to hide the file preview (optional, default `false`) +* `selectText` **[String][139]?** An override for customizing the text that is displayed on the input's label. Defaults to 'Select File' or 'Select File(s)' depending on the `multiple` prop value ### Examples @@ -625,7 +578,7 @@ function HeadshotForm ({ handleSubmit, pristine, invalid, submitting }) { An Input component that is hidden from the page. The input element is hidden with CSS instead of using `type="hidden` so that Cypress can still access its value. -Aside from being hidden, this component is identical to [Input][41], +Aside from being hidden, this component is identical to [Input][38], and will take the same props. ### Examples @@ -653,9 +606,9 @@ Any children passed to this component will be rendered within this wrapper. ### Parameters -* `input` **[Object][148]** A `redux-form` [input][149] object -* `meta` **[Object][148]** A `redux-form` [meta][150] object -* `type` **[String][142]?** A string to specify the type of input element (defaults to `text`) +* `input` **[Object][145]** A `redux-form` [input][146] object +* `meta` **[Object][145]** A `redux-form` [meta][147] object +* `type` **[String][139]?** A string to specify the type of input element (defaults to `text`) ### Examples @@ -678,16 +631,16 @@ function UserForm ({ handleSubmit, pristine, invalid, submitting }) { ## IconInput -A wrapper around the [Input][41] component that adds an icon to the input. +A wrapper around the [Input][38] component that adds an icon to the input. This icon is rendered as an `` tag, with a dynamic class based on the `icon` prop. -For example, given an `icon` prop of `"twitter"`, the component will render an [Input][41] with child ``. +For example, given an `icon` prop of `"twitter"`, the component will render an [Input][38] with child ``. -Additionally, the fieldset of this [Input][41] will be given the class `"icon-label"` for styling purposes. +Additionally, the wrapping div of this [Input][38] will be given the class `"icon-label"` for styling purposes. ### Parameters -* `icon` **[String][142]** The name of the icon associated with the input +* `icon` **[String][139]** The name of the icon associated with the input ### Examples @@ -711,15 +664,15 @@ function TwitterForm ({ handleSubmit, pristine, invalid, submitting }) { ## MaskedInput -A masked input that can be used in a `redux-form`-controlled form. Built on top of [cleave.js][158]. +A masked input that can be used in a `redux-form`-controlled form. Built on top of [cleave.js][154]. ### Parameters -* `input` **[Object][148]** A `redux-form` [input][149] object -* `meta` **[Object][148]** A `redux-form` [meta][150] object -* `maskOptions` **[Object][148]** An object of options to pass to the underlying `Cleave` instance. [(supported options)][159] (optional, default `{}`) -* `onInit` **[Function][143]** A function that will be invoked with the object representing the class when the input is initialized (optional, default `null`) -* `htmlRef` **([Function][143] | [Object][148])** A stable reference that can be used to access the DOM node of the underlying input (optional, default `null`) +* `input` **[Object][145]** A `redux-form` [input][146] object +* `meta` **[Object][145]** A `redux-form` [meta][147] object +* `maskOptions` **[Object][145]** An object of options to pass to the underlying `Cleave` instance. [(supported options)][155] (optional, default `{}`) +* `onInit` **[Function][140]** A function that will be invoked with the object representing the class when the input is initialized (optional, default `null`) +* `htmlRef` **([Function][140] | [Object][145])** A stable reference that can be used to access the DOM node of the underlying input (optional, default `null`) ### Examples @@ -746,12 +699,12 @@ A range input that can be used in a `redux-form`-controlled form. ### Parameters -* `input` **[Object][148]** A `redux-form` [input][149] object -* `meta` **[Object][148]** A `redux-form` [meta][150] object -* `min` **[Number][145]** The minumum attribute of the slider control (optional, default `0`) -* `max` **[Number][145]** The maximum attribute of the slider control (optional, default `100`) -* `step` **[Number][145]** The step attribute of the slider control (optional, default `1`) -* `hideRangeValue` **[Boolean][144]** A boolean representing whether or not to display the range value (optional, default `false`) +* `input` **[Object][145]** A `redux-form` [input][146] object +* `meta` **[Object][145]** A `redux-form` [meta][147] object +* `min` **[Number][142]** The minumum attribute of the slider control (optional, default `0`) +* `max` **[Number][142]** The maximum attribute of the slider control (optional, default `100`) +* `step` **[Number][142]** The step attribute of the slider control (optional, default `1`) +* `hideRangeValue` **[Boolean][141]** A boolean representing whether or not to display the range value (optional, default `false`) ### Examples @@ -789,10 +742,10 @@ The value of the entire `RadioGroup` component is the value of the currently sel ### Parameters -* `input` **[Object][148]** A `redux-form` [input][149] object -* `meta` **[Object][148]** A `redux-form` [meta][150] object -* `options` **[Array][146]** An array of radio button values (strings, numbers, booleans, or key-value pairs) -* `radioInputProps` **[Object][148]** An object of key-value pairs representing props to pass down to all radio inputs (optional, default `{}`) +* `input` **[Object][145]** A `redux-form` [input][146] object +* `meta` **[Object][145]** A `redux-form` [meta][147] object +* `options` **[Array][143]** An array of radio button values (strings, numbers, booleans, or key-value pairs) +* `radioInputProps` **[Object][145]** An object of key-value pairs representing props to pass down to all radio inputs (optional, default `{}`) ### Examples @@ -867,12 +820,12 @@ The value of the `Select` component will be the same as the value of the selecte ### Parameters -* `input` **[Object][148]** A `redux-form` [input][149] object -* `meta` **[Object][148]** A `redux-form` [meta][150] object -* `options` **[Array][146]** An array of option values (strings, numbers, or key-value pairs). This prop will be ignored if `optionGroups` is present. -* `optionGroups` **[Array][146]** An array of option group objects -* `placeholder` **[String][142]** A string to display as a placeholder option. Pass in `false` to hide the placeholder option. (optional, default `'Select'`) -* `enablePlaceholderOption` **[Boolean][144]** A flag indicating that the placeholder option should not be `disabled` (optional, default `false`) +* `input` **[Object][145]** A `redux-form` [input][146] object +* `meta` **[Object][145]** A `redux-form` [meta][147] object +* `options` **[Array][143]** An array of option values (strings, numbers, or key-value pairs). This prop will be ignored if `optionGroups` is present. +* `optionGroups` **[Array][143]** An array of option group objects +* `placeholder` **[String][139]** A string to display as a placeholder option. Pass in `false` to hide the placeholder option. (optional, default `'Select'`) +* `enablePlaceholderOption` **[Boolean][141]** A flag indicating that the placeholder option should not be `disabled` (optional, default `false`) ### Examples @@ -925,14 +878,14 @@ A switch input that can be used in a `redux-form`-controlled form. This input only accepts and stores boolean values. -See the [react-switch][160] documentation for additional styling properties. +See the [react-switch][156] documentation for additional styling properties. ### Parameters -* `input` **[Object][148]** A `redux-form` [input][149] object -* `meta` **[Object][148]** A `redux-form` [meta][150] object -* `checkedIcon` **([Element][161] | [Boolean][144])** An icon displayed when the switch is checked. Set to `false` if no check icon is desired. -* `uncheckedIcon` **([Element][161] | [Boolean][144])** An icon displayed when the switch is unchecked. Set to `false` if no uncheck icon is desired. +* `input` **[Object][145]** A `redux-form` [input][146] object +* `meta` **[Object][145]** A `redux-form` [meta][147] object +* `checkedIcon` **([Element][157] | [Boolean][141])** An icon displayed when the switch is checked. Set to `false` if no check icon is desired. +* `uncheckedIcon` **([Element][157] | [Boolean][141])** An icon displayed when the switch is unchecked. Set to `false` if no uncheck icon is desired. ### Examples @@ -958,10 +911,10 @@ Can forward ref down to textarea input and optionally displays a character count ### Parameters -* `input` **[Object][148]** A `redux-form` [input][149] object -* `meta` **[Object][148]** A `redux-form` [meta][150] object -* `maxLength` **[Number][145]?** The maximum allowed length of the input -* `hideCharacterCount` **[Boolean][144]** Whether to hide the character count if given a maxLength (optional, default `false`) +* `input` **[Object][145]** A `redux-form` [input][146] object +* `meta` **[Object][145]** A `redux-form` [meta][147] object +* `maxLength` **[Number][142]?** The maximum allowed length of the input +* `hideCharacterCount` **[Boolean][141]** Whether to hide the character count if given a maxLength (optional, default `false`) * `forwardedRef` **Ref?** A ref to be forwarded to `textarea` input (standard `ref` cannot currently be forwarded) ### Examples @@ -989,7 +942,7 @@ A label for displaying error message. ### Parameters -* `children` **[String][142]** A message to display +* `children` **[String][139]** A message to display ### Examples @@ -1013,9 +966,9 @@ function MyView () { A dynamic error label associated with an input component. -NOTE: direct use of this component is deprecated as of v4.1.0 due to its dependency on redux-form. Please use [ErrorLabel][65] instead. +NOTE: direct use of this component is deprecated as of v4.1.0 due to its dependency on redux-form. Please use [ErrorLabel][62] instead. -This component is used within [LabeledField][74], and therefore is incorporated into most `lp-components` input components by default. +This component is used within [LabeledField][71], and therefore is incorporated into most `lp-components` input components by default. The error label uses the following rules to determine how it will be displayed: @@ -1029,10 +982,10 @@ In addition to the props below, any extra props will be passed directly to the i ### Parameters -* `error` **([String][142] | [Array][146])** An error message or array of error messages to display -* `invalid` **[Boolean][144]** Whether the associated input has an invalid value -* `touched` **[String][142]** Whether the associated input has been touched -* `name` **[String][142]** The name of the input (used to generate a unique ID) +* `error` **([String][139] | [Array][143])** An error message or array of error messages to display +* `invalid` **[Boolean][141]** Whether the associated input has an invalid value +* `touched` **[String][139]** Whether the associated input has been touched +* `name` **[String][139]** The name of the input (used to generate a unique ID) ### Examples @@ -1061,7 +1014,7 @@ function ValidatedInput ({ A dynamic label associated with an input component. -This component is used within [LabeledField][74], and therefore is incorporated into most `lp-components` input components by default. +This component is used within [LabeledField][71], and therefore is incorporated into most `lp-components` input components by default. The text of the label is set using the following rules: @@ -1070,21 +1023,21 @@ The text of the label is set using the following rules: * Else If the `label` prop is set to a string, the label will display that text * Otherwise, the label will be set using the `name` prop. -If `name` is used to set the text, it will be stripped of its prefixes and converted to [start case][162]. +If `name` is used to set the text, it will be stripped of its prefixes and converted to [start case][158]. For instance: `'person.firstName'` becomes `'First Name'` -Note: When using third party form libraries (e.g., [Redux Form][163]), it's likely that setting the `required` prop will turn on the browser's automatic validation, which could cause the library to behave unexpectedly. If the browser validation behavior is causing issues, then add a `noValidate` prop to the form to [turn off][164] automatic validation. (e.g., `
`) +Note: When using third party form libraries (e.g., [Redux Form][159]), it's likely that setting the `required` prop will turn on the browser's automatic validation, which could cause the library to behave unexpectedly. If the browser validation behavior is causing issues, then add a `noValidate` prop to the form to [turn off][160] automatic validation. (e.g., `
`) ### Parameters -* `name` **[String][142]** The name of the associated input -* `id` **[String][142]** The id of the associated input (defaults to name) (optional, default `name`) -* `hint` **[String][142]?** A usage hint for the associated input -* `label` **([String][142] | [Boolean][144])?** Custom text for the label -* `tooltip` **[String][142]?** A message to display in a tooltip -* `required` **[Boolean][144]?** A boolean value to indicate whether the field is required -* `requiredIndicator` **[String][142]?** Custom character to denote a field is required (optional, default `''`) +* `name` **[String][139]** The name of the associated input +* `id` **[String][139]** The id of the associated input (defaults to name) (optional, default `name`) +* `hint` **[String][139]?** A usage hint for the associated input +* `label` **([String][139] | [Boolean][141])?** Custom text for the label +* `tooltip` **[String][139]?** A message to display in a tooltip +* `required` **[Boolean][141]?** A boolean value to indicate whether the field is required +* `requiredIndicator` **[String][139]?** Custom character to denote a field is required (optional, default `''`) ### Examples @@ -1112,19 +1065,20 @@ function EmailInput ({ ## LabeledField -A fieldset wrapper for redux-form controlled inputs. This wrapper adds a label component (defaults to [InputLabel][71]) -above the wrapped component and an error component below (defaults to [InputError][68]). Additionally, it adds the class `"error"` -to the fieldset if the input is touched and invalid. +A wrapper for redux-form controlled inputs. This wrapper adds a label component (defaults to [InputLabel][68]) +above the wrapped component and an error component below (defaults to [InputError][65]). Additionally, it adds the class `"error"` +to the wrapper if the input is touched and invalid. In order to populate the `InputLabel` and `InputError` correctly, you should pass all the props of the corresponding input to this component. To prevent label-specific props from being passed to the input itself, -use the [omitLabelProps][91] helper. +use the [omitLabelProps][88] helper. ### Parameters -* `hideErrorLabel` **[Boolean][144]?** A boolean determining whether to hide the error label on input error (optional, default `false`) -* `labelComponent` **[Function][143]** A custom label component for the input (optional, default `InputLabel`) -* `errorComponent` **[Function][143]** A custom error component for the input (optional, default `InputError`) +* `hideErrorLabel` **[Boolean][141]** A boolean determining whether to hide the error label on input error (optional, default `false`) +* `labelComponent` **[Function][140]** A custom label component for the input (optional, default `InputLabel`) +* `errorComponent` **[Function][140]** A custom error component for the input (optional, default `InputError`) +* `as` **[String][139]** A string that determines the element type of the wrapper (optional, default `'div'`) ### Examples @@ -1208,7 +1162,7 @@ by stripping its namespace and converting it to start case. ### Parameters -* `name` **[String][142]** A redux-form field name +* `name` **[String][139]** A redux-form field name ### Examples @@ -1217,24 +1171,24 @@ convertNameToLabel('example') // -> 'Example' convertNameToLabel('person.firstName') // -> 'First Name' ``` -Returns **[String][142]** A user-friendly field label +Returns **[String][139]** A user-friendly field label ## fieldOptionsType -A constant representing the `PropTypes` of the `options` prop for select components, e.g., [Select][56] and [CheckboxGroup][21] +A constant representing the `PropTypes` of the `options` prop for select components, e.g., [Select][53] and [CheckboxGroup][21] Type: PropTypes ## fieldOptionGroupsType -A constant representing the `PropTypes` of the `optionGroups` prop for select components, e.g., [Select][56] +A constant representing the `PropTypes` of the `optionGroups` prop for select components, e.g., [Select][53] Type: PropTypes ## fieldPropTypesWithValue -A function that takes `PropTypes` for a `redux-form` [input][149] object. -Returns an object containing all `PropTypes` for `redux-form` [Field][165] components. +A function that takes `PropTypes` for a `redux-form` [input][146] object. +Returns an object containing all `PropTypes` for `redux-form` [Field][161] components. ### Parameters @@ -1264,36 +1218,36 @@ fieldPropTypesWithValue(valuePropType) // } ``` -Returns **[Object][148]** `PropTypes` for `redux-form` [input][149] and [meta][150] objects +Returns **[Object][145]** `PropTypes` for `redux-form` [input][146] and [meta][147] objects ## defaultValueTypes -A constant representing default `PropTypes` for `redux-form` [Field][165] values. +A constant representing default `PropTypes` for `redux-form` [Field][161] values. Default types are either `number` or `string`. Type: PropTypes ## fieldPropTypes -An object containing the default `PropTypes` for `redux-form` [Field][165] components. +An object containing the default `PropTypes` for `redux-form` [Field][161] components. -Type: [Object][148] +Type: [Object][145] ## radioGroupPropTypes -A constant representing the `PropTypes` of the `input` prop for the radio group component, e.g., [RadioGroup][53] +A constant representing the `PropTypes` of the `input` prop for the radio group component, e.g., [RadioGroup][50] Type: PropTypes ## checkboxGroupPropTypes -A constant representing the `PropTypes` of the `input` prop for checkbox group components, e.g., [CheckboxGroup][21] and [DropdownCheckboxGroup][33] +A constant representing the `PropTypes` of the `input` prop for checkbox group components, e.g., [CheckboxGroup][21] Type: PropTypes ## omitLabelProps -A function that takes a form component `props` object and returns the `props` object with [InputLabel][71] props omitted. +A function that takes a form component `props` object and returns the `props` object with [InputLabel][68] props omitted. Created in order to prevent these props from being passed down to the input component through `...rest`. Omits the following props: @@ -1307,7 +1261,7 @@ Omits the following props: ### Parameters -* `props` **[Object][148]** A props object +* `props` **[Object][145]** A props object ### Examples @@ -1341,7 +1295,7 @@ function Input (props) { } ``` -Returns **[Object][148]** `props` object with [InputLabel][71] props omitted +Returns **[Object][145]** `props` object with [InputLabel][68] props omitted ## replaceEmptyStringValue @@ -1378,11 +1332,11 @@ export default compose( ## Table A component for displaying data in a table. -This component's behavior is largely determined by the [TableColumn][103] components that are passed to it. +This component's behavior is largely determined by the [TableColumn][100] components that are passed to it. ### Parameters -* `data` **[Array][146]** An array of objects to display in the table- one object per row (optional, default `[]`) +* `data` **[Array][143]** An array of objects to display in the table- one object per row (optional, default `[]`) ### Examples @@ -1401,19 +1355,19 @@ function PersonTable ({ people }) { ## SortableTable A component for displaying sortable data in a table. -This component's behavior is largely determined by the [TableColumn][103] components that are passed to it. +This component's behavior is largely determined by the [TableColumn][100] components that are passed to it. ### Parameters -* `data` **[Array][146]** An array of objects to display in the table- one object per row (optional, default `[]`) -* `initialColumn` **[Number][145]** The name of the column that's initially selected (optional, default `''`) -* `initialAscending` **[Boolean][144]** The sort direction of the initial column (optional, default `true`) -* `disableReverse` **[Boolean][144]** Disables automatic reversing of descending sorts (optional, default `false`) -* `disableSort` **[Boolean][144]** A flag to disable sorting on all columns and hide sorting arrows. (optional, default `false`) -* `controlled` **[Boolean][144]** A flag to disable sorting on all columns, while keeping the sorting arrows. Used when sorting is controlled by an external source. (optional, default `false`) -* `onChange` **[Function][143]?** A callback that will be fired when the sorting state changes -* `rowComponent` **[Function][143]?** A custom row component for the table. Will be passed the `data` for the row, several internal table states (the current column being sorted (sortPath), whether ascending sort is active or not (ascending), the sorting function (sortFunc), and the value getter (valueGetter)) as well as `children` to render. -* `headerComponent` **[Function][143]?** A custom header component for the table. Will be passed the configuration of the corresponding column, as well as the current `sortPath` / `ascending` and an `onClick` handler. May be overridden by a custom `headerComponent` for a column. +* `data` **[Array][143]** An array of objects to display in the table- one object per row (optional, default `[]`) +* `initialColumn` **[Number][142]** The name of the column that's initially selected (optional, default `''`) +* `initialAscending` **[Boolean][141]** The sort direction of the initial column (optional, default `true`) +* `disableReverse` **[Boolean][141]** Disables automatic reversing of descending sorts (optional, default `false`) +* `disableSort` **[Boolean][141]** A flag to disable sorting on all columns and hide sorting arrows. (optional, default `false`) +* `controlled` **[Boolean][141]** A flag to disable sorting on all columns, while keeping the sorting arrows. Used when sorting is controlled by an external source. (optional, default `false`) +* `onChange` **[Function][140]?** A callback that will be fired when the sorting state changes +* `rowComponent` **[Function][140]?** A custom row component for the table. Will be passed the `data` for the row, several internal table states (the current column being sorted (sortPath), whether ascending sort is active or not (ascending), the sorting function (sortFunc), and the value getter (valueGetter)) as well as `children` to render. +* `headerComponent` **[Function][140]?** A custom header component for the table. Will be passed the configuration of the corresponding column, as well as the current `sortPath` / `ascending` and an `onClick` handler. May be overridden by a custom `headerComponent` for a column. ### Examples @@ -1431,20 +1385,20 @@ function PersonTable ({ people }) { ## TableColumn -A component used to pass column information to a [Table][97] or [SortableTable][100]. +A component used to pass column information to a [Table][94] or [SortableTable][97]. ### Parameters -* `name` **[String][142]** The key of the value to display in the column from each data object -* `label` **[String][142]?** The text that will be displayed in the column header. Defaults to `name`. -* `sortFunc` **[Function][143]?** The function that will be used to sort the table data when the column is selected -* `component` **[Function][143]?** A custom cell component for the column. Will be passed the `key`, `name`, `value` and `data` for the row. -* `headerComponent` **[Function][143]?** A custom header component for the column. Will be passed the configuration of the column, as well as the current `sortPath` / `ascending` and an `onClick` handler. `onClick` must be appended to allow for sorting functionality. -* `onClick` **[Function][143]?** A function that will be called `onClick` on every cell in the column. -* `format` **[Function][143]?** A function that formats the value displayed in each cell in the column -* `disabled` **[Boolean][144]?** A flag that disables sorting for the column -* `placeholder` **[String][142]?** A string that will be displayed if the value of the cell is `undefined` or `null` -* `valueGetter` **[Function][143]?** A function that will return a cell's value derived from each data object. Will be passed the `data` for the row. +* `name` **[String][139]** The key of the value to display in the column from each data object +* `label` **[String][139]?** The text that will be displayed in the column header. Defaults to `name`. +* `sortFunc` **[Function][140]?** The function that will be used to sort the table data when the column is selected +* `component` **[Function][140]?** A custom cell component for the column. Will be passed the `key`, `name`, `value` and `data` for the row. +* `headerComponent` **[Function][140]?** A custom header component for the column. Will be passed the configuration of the column, as well as the current `sortPath` / `ascending` and an `onClick` handler. `onClick` must be appended to allow for sorting functionality. +* `onClick` **[Function][140]?** A function that will be called `onClick` on every cell in the column. +* `format` **[Function][140]?** A function that formats the value displayed in each cell in the column +* `disabled` **[Boolean][141]?** A flag that disables sorting for the column +* `placeholder` **[String][139]?** A string that will be displayed if the value of the cell is `undefined` or `null` +* `valueGetter` **[Function][140]?** A function that will return a cell's value derived from each data object. Will be passed the `data` for the row. ### Examples @@ -1474,9 +1428,9 @@ A component that displays a flash message. ### Parameters -* `children` **[String][142]** The flash message that will be displayed. -* `isError` **[Boolean][144]** A flag to indicate whether the message is an error message. (optional, default `false`) -* `onDismiss` **[Function][143]?** A callback for dismissing the flash message. The dismiss button will only be shown if this callback is provided. +* `children` **[String][139]** The flash message that will be displayed. +* `isError` **[Boolean][141]** A flag to indicate whether the message is an error message. (optional, default `false`) +* `onDismiss` **[Function][140]?** A callback for dismissing the flash message. The dismiss button will only be shown if this callback is provided. ### Examples @@ -1497,14 +1451,14 @@ function MyView () { ## FlashMessageContainer -A component that displays multiple flash messages generated by [redux-flash][166]. +A component that displays multiple flash messages generated by [redux-flash][162]. Most apps will need only one of these containers at the top level. Will pass down any additional props to the inner `FlashMessage` components. ### Parameters -* `messages` **[Object][148]** The flash messages that will be displayed. -* `limit` **[Number][145]?** Maximum number of concurrent messages to display +* `messages` **[Object][145]** The flash messages that will be displayed. +* `limit` **[Number][142]?** Maximum number of concurrent messages to display ### Examples @@ -1550,7 +1504,7 @@ depending on whether `isLoading` is true or false ### Parameters -* `isLoading` **[Boolean][144]** Whether the inner component should be indicated as loading (optional, default `false`) +* `isLoading` **[Boolean][141]** Whether the inner component should be indicated as loading (optional, default `false`) ### Examples @@ -1573,8 +1527,8 @@ certain path, and runs given comparison function on those values. ### Parameters -* `path` **[String][142]** Name of the path to values -* `func` **[Function][143]** Comparison function to run on values at specified path +* `path` **[String][139]** Name of the path to values +* `func` **[Function][140]** Comparison function to run on values at specified path ### Examples @@ -1596,7 +1550,7 @@ people.sort(ageComparator) // ] ``` -Returns **[Function][143]** Comparison function +Returns **[Function][140]** Comparison function ## generateInputErrorId @@ -1605,7 +1559,7 @@ is centralized to facilitate reference by multiple input components. ### Parameters -* `name` **[String][142]** The name of the input +* `name` **[String][139]** The name of the input ### Examples @@ -1617,7 +1571,7 @@ generateInputErrorId(name) // 'cardNumberError' ``` -Returns **[String][142]** String representing error id +Returns **[String][139]** String representing error id ## serializeOptions @@ -1626,7 +1580,7 @@ Function that transforms string options into object options with keys of ### Parameters -* `optionArray` **[Array][146]** Array of option values +* `optionArray` **[Array][143]** Array of option values ### Examples @@ -1638,7 +1592,7 @@ serializeOptions(options) // [{ key: 'apple', value: 'apple' }, { key: 'banana', value: 'banana' }] ``` -Returns **[Array][146]** Array of object options +Returns **[Array][143]** Array of object options ## serializeOptionGroups @@ -1647,7 +1601,7 @@ object options with keys of `key` and `value` ### Parameters -* `optionGroupArray` **[Array][146]** Array of option values +* `optionGroupArray` **[Array][143]** Array of option values ### Examples @@ -1671,7 +1625,7 @@ serializeOptionGroups(optionGroups) // ] ``` -Returns **[Array][146]** Array of object group options +Returns **[Array][143]** Array of object group options ## stripNamespace @@ -1681,7 +1635,7 @@ Returns the argument if it is undefined or not a string. ### Parameters -* `str` **[String][142]** Namespaced string +* `str` **[String][139]** Namespaced string ### Examples @@ -1693,14 +1647,14 @@ stripNamespace(namespace) // 'name' ``` -Returns **[String][142]** String with namespace removed +Returns **[String][139]** String with namespace removed ## triggerOnKeys ### Parameters -* `fn` **[Function][143]** The function to trigger -* `keys` **([String][142] | [Array][146]<[String][142]>)** String or Array of keys +* `fn` **[Function][140]** The function to trigger +* `keys` **([String][139] | [Array][143]<[String][139]>)** String or Array of keys ### Examples @@ -1709,23 +1663,23 @@ const triggerOnEnter = triggerOnKeys(() => console.log('Hi'), ['Enter']) function MyExample () { return } ``` -Returns **[Function][143]** Returns a function that takes an event and watches for keys +Returns **[Function][140]** Returns a function that takes an event and watches for keys ## Modal -A modal component with a built-in close button. Uses [`react-modal`][167] under the hood, and can accept any props `react-modal` does. +A modal component with a built-in close button. Uses [`react-modal`][163] under the hood, and can accept any props `react-modal` does. -Unlike `react-modal`, this component does not require an `isOpen` prop to render. However, that prop can still be used in the case where animations are necessary- see [this issue][168]. +Unlike `react-modal`, this component does not require an `isOpen` prop to render. However, that prop can still be used in the case where animations are necessary- see [this issue][164]. Note: this component requires custom styles. These styles can be imported from the `lib/styles` folder as shown inn the example below. ### Parameters -* `onClose` **[Function][143]** A handler for closing the modal. May be triggered via the close button, and outside click, or a key press. -* `className` **([String][142] | [Object][148])** Additional class to append to the base class of the modal (modal-inner). See [React Modal's style documentation][169] for more details. (optional, default `""`) -* `overlayClassName` **([String][142] | [Object][148])** Additional class to append to the base class of the modal overlay (modal-fade-screen). See [React Modal's style documentation][169] for more details. (optional, default `""`) -* `isOpen` **[Boolean][144]** A flag for showing the modal. (optional, default `true`) -* `preventClose` **[Boolean][144]** A flag for preventing the modal from being closed (close button, escape, or overlay click). (optional, default `false`) +* `onClose` **[Function][140]** A handler for closing the modal. May be triggered via the close button, and outside click, or a key press. +* `className` **([String][139] | [Object][145])** Additional class to append to the base class of the modal (modal-inner). See [React Modal's style documentation][165] for more details. (optional, default `""`) +* `overlayClassName` **([String][139] | [Object][145])** Additional class to append to the base class of the modal overlay (modal-fade-screen). See [React Modal's style documentation][165] for more details. (optional, default `""`) +* `isOpen` **[Boolean][141]** A flag for showing the modal. (optional, default `true`) +* `preventClose` **[Boolean][141]** A flag for preventing the modal from being closed (close button, escape, or overlay click). (optional, default `false`) ### Examples @@ -1753,7 +1707,7 @@ function MyView () { ## cloudinaryUploader -A function that returns a React HOC for uploading files to (Cloudinary)\[[https://cloudinary.com][170]]. +A function that returns a React HOC for uploading files to (Cloudinary)\[[https://cloudinary.com][166]]. `cloudinaryUploader` exposes the following props to the wrapped component: @@ -1762,15 +1716,15 @@ A function that returns a React HOC for uploading files to (Cloudinary)\[[https: ### Parameters -* `cloudName` **[string][142]** The name of the Cloudinary cloud to upload to. Can also be set via `CLOUDINARY_CLOUD_NAME` in `process.env`. -* `bucket` **[string][142]** The name of the Cloudinary bucket to upload to. Can also be set via `CLOUDINARY_BUCKET` in `process.env`. -* `apiAdapter` **[Object][148]** Adapter that responds to `post` with a Promise -* `uploadPreset` **[string][142]** The name of the Cloudinary upload preset. Can also be set via `CLOUDINARY_UPLOAD_PRESET` in `process.env`. (optional, default `default`) -* `endpoint` **[string][142]** The endpoint for the upload request. Can also be set via `CLOUDINARY_ENDPOINT` in `process.env`. (optional, default `https://api.cloudinary.com/v1_1/`) -* `fileType` **[string][142]** The type of file. (optional, default `auto`) -* `cloudinaryPublicId` **[string][142]?** The name of the file stored in Cloudinary. -* `createPublicId` **[string][142]?** A function to generate a custom public id for the uploaded file. This function is passed the file object and is expected to return a string. Overridden by the `cloudinaryPublicId` prop. -* `requestOptions` **[object][148]** Options for the request, as specified by (`lp-requests`)\[[https://github.com/LaunchPadLab/lp-requests/blob/master/src/http/http.js][171]]. (optional, default `DEFAULT_REQUEST_OPTIONS`) +* `cloudName` **[string][139]** The name of the Cloudinary cloud to upload to. Can also be set via `CLOUDINARY_CLOUD_NAME` in `process.env`. +* `bucket` **[string][139]** The name of the Cloudinary bucket to upload to. Can also be set via `CLOUDINARY_BUCKET` in `process.env`. +* `apiAdapter` **[Object][145]** Adapter that responds to `post` with a Promise +* `uploadPreset` **[string][139]** The name of the Cloudinary upload preset. Can also be set via `CLOUDINARY_UPLOAD_PRESET` in `process.env`. (optional, default `default`) +* `endpoint` **[string][139]** The endpoint for the upload request. Can also be set via `CLOUDINARY_ENDPOINT` in `process.env`. (optional, default `https://api.cloudinary.com/v1_1/`) +* `fileType` **[string][139]** The type of file. (optional, default `auto`) +* `cloudinaryPublicId` **[string][139]?** The name of the file stored in Cloudinary. +* `createPublicId` **[string][139]?** A function to generate a custom public id for the uploaded file. This function is passed the file object and is expected to return a string. Overridden by the `cloudinaryPublicId` prop. +* `requestOptions` **[object][145]** Options for the request, as specified by (`lp-requests`)\[[https://github.com/LaunchPadLab/lp-requests/blob/master/src/http/http.js][167]]. (optional, default `DEFAULT_REQUEST_OPTIONS`) ### Examples @@ -1802,7 +1756,7 @@ export default compose( )(CloudinaryFileInput) ``` -Returns **[Function][143]** A HOC that can be used to wrap a component. +Returns **[Function][140]** A HOC that can be used to wrap a component. [1]: #colorpicker @@ -1868,280 +1822,272 @@ Returns **[Function][143]** A HOC that can be used to wrap a component. [32]: #examples-10 -[33]: #dropdowncheckboxgroup +[33]: #fileinput [34]: #parameters-10 [35]: #examples-11 -[36]: #fileinput +[36]: #hiddeninput -[37]: #parameters-11 +[37]: #examples-12 -[38]: #examples-12 +[38]: #input -[39]: #hiddeninput +[39]: #parameters-11 [40]: #examples-13 -[41]: #input +[41]: #iconinput [42]: #parameters-12 [43]: #examples-14 -[44]: #iconinput +[44]: #maskedinput [45]: #parameters-13 [46]: #examples-15 -[47]: #maskedinput +[47]: #rangeinput [48]: #parameters-14 [49]: #examples-16 -[50]: #rangeinput +[50]: #radiogroup [51]: #parameters-15 [52]: #examples-17 -[53]: #radiogroup +[53]: #select [54]: #parameters-16 [55]: #examples-18 -[56]: #select +[56]: #switch [57]: #parameters-17 [58]: #examples-19 -[59]: #switch +[59]: #textarea [60]: #parameters-18 [61]: #examples-20 -[62]: #textarea +[62]: #errorlabel [63]: #parameters-19 [64]: #examples-21 -[65]: #errorlabel +[65]: #inputerror [66]: #parameters-20 [67]: #examples-22 -[68]: #inputerror +[68]: #inputlabel [69]: #parameters-21 [70]: #examples-23 -[71]: #inputlabel +[71]: #labeledfield [72]: #parameters-22 [73]: #examples-24 -[74]: #labeledfield +[74]: #blurdirty -[75]: #parameters-23 +[75]: #examples-25 -[76]: #examples-25 +[76]: #convertnametolabel -[77]: #blurdirty +[77]: #parameters-23 [78]: #examples-26 -[79]: #convertnametolabel +[79]: #fieldoptionstype -[80]: #parameters-24 +[80]: #fieldoptiongroupstype -[81]: #examples-27 +[81]: #fieldproptypeswithvalue -[82]: #fieldoptionstype +[82]: #parameters-24 -[83]: #fieldoptiongroupstype +[83]: #examples-27 -[84]: #fieldproptypeswithvalue +[84]: #defaultvaluetypes -[85]: #parameters-25 +[85]: #fieldproptypes -[86]: #examples-28 +[86]: #radiogroupproptypes -[87]: #defaultvaluetypes +[87]: #checkboxgroupproptypes -[88]: #fieldproptypes +[88]: #omitlabelprops -[89]: #radiogroupproptypes +[89]: #parameters-25 -[90]: #checkboxgroupproptypes +[90]: #examples-28 -[91]: #omitlabelprops +[91]: #replaceemptystringvalue [92]: #parameters-26 [93]: #examples-29 -[94]: #replaceemptystringvalue +[94]: #table [95]: #parameters-27 [96]: #examples-30 -[97]: #table +[97]: #sortabletable [98]: #parameters-28 [99]: #examples-31 -[100]: #sortabletable +[100]: #tablecolumn [101]: #parameters-29 [102]: #examples-32 -[103]: #tablecolumn +[103]: #flashmessage [104]: #parameters-30 [105]: #examples-33 -[106]: #flashmessage +[106]: #flashmessagecontainer [107]: #parameters-31 [108]: #examples-34 -[109]: #flashmessagecontainer +[109]: #spinner -[110]: #parameters-32 +[110]: #examples-35 -[111]: #examples-35 +[111]: #loadingcontainer -[112]: #spinner +[112]: #parameters-32 [113]: #examples-36 -[114]: #loadingcontainer +[114]: #compareatpath [115]: #parameters-33 [116]: #examples-37 -[117]: #compareatpath +[117]: #generateinputerrorid [118]: #parameters-34 [119]: #examples-38 -[120]: #generateinputerrorid +[120]: #serializeoptions [121]: #parameters-35 [122]: #examples-39 -[123]: #serializeoptions +[123]: #serializeoptiongroups [124]: #parameters-36 [125]: #examples-40 -[126]: #serializeoptiongroups +[126]: #stripnamespace [127]: #parameters-37 [128]: #examples-41 -[129]: #stripnamespace +[129]: #triggeronkeys [130]: #parameters-38 [131]: #examples-42 -[132]: #triggeronkeys +[132]: #modal [133]: #parameters-39 [134]: #examples-43 -[135]: #modal +[135]: #cloudinaryuploader [136]: #parameters-40 [137]: #examples-44 -[138]: #cloudinaryuploader - -[139]: #parameters-41 - -[140]: #examples-45 - -[141]: https://casesandberg.github.io/react-color/ - -[142]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String +[138]: https://casesandberg.github.io/react-color/ -[143]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Statements/function +[139]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String -[144]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean +[140]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Statements/function -[145]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Number +[141]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean -[146]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array +[142]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Number -[147]: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button#attr-type +[143]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array -[148]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object +[144]: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button#attr-type -[149]: http://redux-form.com/6.5.0/docs/api/Field.md/#input-props +[145]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object -[150]: http://redux-form.com/6.5.0/docs/api/Field.md/#meta-props +[146]: http://redux-form.com/6.5.0/docs/api/Field.md/#input-props -[151]: https://github.com/LaunchPadLab/lp-hoc/blob/master/docs.md#cloudinaryuploader +[147]: http://redux-form.com/6.5.0/docs/api/Field.md/#meta-props -[152]: https://github.com/Hacker0x01/react-datepicker +[148]: https://github.com/LaunchPadLab/lp-hoc/blob/master/docs.md#cloudinaryuploader -[153]: https://github.com/Hacker0x01/react-datepicker/blob/master/docs/datepicker.md +[149]: https://github.com/Hacker0x01/react-datepicker -[154]: README.md#dateinput-styles +[150]: https://github.com/Hacker0x01/react-datepicker/blob/master/docs/datepicker.md -[155]: DropdownSelect +[151]: README.md#dateinput-styles -[156]: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/file#accept +[152]: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/file#accept -[157]: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/file#capture +[153]: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/file#capture -[158]: https://github.com/nosir/cleave.js +[154]: https://github.com/nosir/cleave.js -[159]: https://github.com/nosir/cleave.js/blob/master/doc/options.md +[155]: https://github.com/nosir/cleave.js/blob/master/doc/options.md -[160]: https://github.com/markusenglund/react-switch +[156]: https://github.com/markusenglund/react-switch -[161]: https://developer.mozilla.org/docs/Web/API/Element +[157]: https://developer.mozilla.org/docs/Web/API/Element -[162]: https://en.wikipedia.org/wiki/Letter_case#Stylistic_or_specialised_usage +[158]: https://en.wikipedia.org/wiki/Letter_case#Stylistic_or_specialised_usage -[163]: https://redux-form.com +[159]: https://redux-form.com -[164]: https://developer.mozilla.org/en-US/docs/Learn/HTML/Forms/Form_validation#Validating_forms_using_JavaScript +[160]: https://developer.mozilla.org/en-US/docs/Learn/HTML/Forms/Form_validation#Validating_forms_using_JavaScript -[165]: http://redux-form.com/6.5.0/docs/api/Field.md/ +[161]: http://redux-form.com/6.5.0/docs/api/Field.md/ -[166]: https://github.com/LaunchPadLab/redux-flash +[162]: https://github.com/LaunchPadLab/redux-flash -[167]: https://github.com/reactjs/react-modal +[163]: https://github.com/reactjs/react-modal -[168]: https://github.com/reactjs/react-modal/issues/25 +[164]: https://github.com/reactjs/react-modal/issues/25 -[169]: http://reactcommunity.org/react-modal/styles/classes/#for-the-content-and-overlay +[165]: http://reactcommunity.org/react-modal/styles/classes/#for-the-content-and-overlay -[170]: https://cloudinary.com +[166]: https://cloudinary.com -[171]: https://github.com/LaunchPadLab/lp-requests/blob/master/src/http/http.js +[167]: https://github.com/LaunchPadLab/lp-requests/blob/master/src/http/http.js diff --git a/migration-guides/v.10.0.0.md b/migration-guides/v.10.0.0.md new file mode 100644 index 00000000..177879e6 --- /dev/null +++ b/migration-guides/v.10.0.0.md @@ -0,0 +1,86 @@ +# v10.0.0 Migration Guide + +This version contains the following breaking changes: + +1. All inputs, except for `CheckboxGroup` and `RadioGroup`, are now wrapped in a `div` +2. The `LabeledField` component is now rendered as a `div` by default and accepts an optional prop `as` that can overwrite the HTML element +3. The `DropdownCheckboxGroup` component is now `CheckboxGroup` with prop `dropdown`=`true` +4. The labels are now rendered correctly when dropdown options are selected for the `CheckboxGroup` component and they are sorted from oldest (left) to newest (right) +5. The `CheckboxGroup` and `RadioGroup` legends now rely on `visually-hidden` class styles to hide the label from the view + +Further explanation of each item is detailed below. + +--- +## 1. All inputs, except for `CheckboxGroup` and `RadioGroup`, are now wrapped in a 'div' +`fieldset` no longer wraps each input unless when grouping related form controls (i.e., `CheckboxGroup` and `RadioGroup`). This affects base styles in _forms.scss and any custom rules your code might have that rely on the outdated structure. + +```html + +
+ + + +
+ +
+
+ + +
+ + + +
+ +
+
+``` + +## 2. The `LabeledField` component is now rendered as a `div` by default and accepts an optional prop `as` that can overwrite the HTML element +If you are **not** using `LabeledField` with a group of inputs, do not overwrite the wrapper's element, as it should not be `fieldset`. You may need to update your code that relies on the outdated structure. If you are using `LabeledField` with a group of inputs, pass in 'fieldset' for the `as` prop and make sure the custom label component has `legend` as its first child ([source](https://www.w3.org/TR/WCAG20-TECHS/H71.html#:~:text=The%20first%20element%20inside%20the,related%20radio%20buttons%20and%20checkboxes))- you can reference the `CheckboxGroup` or `RadioGroup` component as an example. The only valid options for the `as` prop are 'div' and 'fieldset'. + +## 3. The `DropdownCheckboxGroup` component is now `CheckboxGroup` with prop `dropdown`=`true` +Due to high functionality overlap between the two components, `DropdownCheckboxGroup` has been removed and instead `CheckboxGroup` now accepts the optional `dropdown` prop that defaults to `false`. When the prop's value is set to `true`, the checkbox options appear in a dropdown container, just like it did in `DropdownCheckboxGroup`. + +```jsx +const inputProps = { + name: 'person.checkboxOptions', + value: '', + onChange: action('field changed'), +} +const options = [ + { key: 'First Option', value: '1' }, + { key: 'Second Option', value: '2' }, + { key: 'Third Option', value: '3' }, +] + +// Before: +import { DropdownCheckboxGroup } from 'lp-components' + + + +// After: +import { CheckboxGroup } from 'lp-components' + + +``` + +## 4. The labels are now rendered correctly when dropdown options are selected for the `CheckboxGroup` component and they are sorted from oldest (left) to newest (right) +Previously, the `DropdownCheckboxGroup` component displayed the `value` of the selected option rather than the `key` which corresponds to its label. Additionally, the selected options appeared from newest to oldest, contrary to the typical expected behavior. If you've implemented workarounds, please remove them. + +## 5. The `CheckboxGroup` and `RadioGroup` legends now rely on `visually-hidden` class styles to hide the label from the view +To follow the accessibility guideline of `fieldset` having one `legend` as its direct child, changes have been made in `CheckboxGroup` and `RadioGroup` to always render the `legend` and if `label=false` is passed into the components, the class name "visually-hidden" is added to the legend in order to visually hide it. Please make sure the following class styles are included in your project's root stylesheet: + +```css +.visually-hidden:not(:focus):not(:active) { + clip: rect(0 0 0 0) !important; + clip-path: inset(50%) !important; + height: 1px !important; + width: 1px !important; + overflow: hidden !important; + position: absolute !important; + white-space: nowrap !important; + border: 0 !important; + padding: 0 !important; +} +``` \ No newline at end of file diff --git a/package.json b/package.json index ca09c10d..d21bbc2e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@launchpadlab/lp-components", - "version": "9.3.1", + "version": "10.0.0", "engines": { "node": "^18.12 || ^20.0" }, @@ -26,8 +26,7 @@ "docs": "documentation build src/index.js -f md -o docs.md", "format": "prettier --write \"(src|test|stories|.storybook)/**/*.+(js|jsx|json|scss)\"", "lint": "eslint src --max-warnings=0", - "prepare": "husky install", - "prepublishOnly": "yarn run clean && yarn run build", + "prepare": "husky install && yarn run clean && yarn run build", "storybook": "yarn && start-storybook -p 6006", "test": "jest --coverage", "size": "yarn build && size-limit", diff --git a/src/forms/helpers/dropdown-select.js b/src/forms/helpers/dropdown-select.js index 780e5c11..f3aabd9e 100644 --- a/src/forms/helpers/dropdown-select.js +++ b/src/forms/helpers/dropdown-select.js @@ -8,27 +8,38 @@ const propTypes = { children: PropTypes.node, className: PropTypes.string, selectedValues: PropTypes.arrayOf(PropTypes.string), + options: PropTypes.arrayOf( + PropTypes.shape({ + key: PropTypes.string.isRequired, + value: PropTypes.string.isRequired, + }) + ), } const defaultProps = { className: '', selectedValues: [], + options: [], } -// Wraps the `DropdownCheckboxGroup` component +// Wraps the `CheckboxGroup` component when dropdown is set to true. -function DropdownSelect({ children, className, selectedValues }) { +function DropdownSelect({ children, className, selectedValues, options }) { const [expanded, toggleExpanded] = useToggle() return ( toggleExpanded(false)}> -
+
options.find((o) => o.value === v).key) + return labels.length ? labels.join(', ') : 'None' } export default DropdownSelect diff --git a/src/forms/helpers/field-prop-types.js b/src/forms/helpers/field-prop-types.js index 01076354..e090bb3f 100644 --- a/src/forms/helpers/field-prop-types.js +++ b/src/forms/helpers/field-prop-types.js @@ -128,7 +128,7 @@ export const radioGroupPropTypes = fieldPropTypesWithValue( /** * - * A constant representing the `PropTypes` of the `input` prop for checkbox group components, e.g., {@link CheckboxGroup} and {@link DropdownCheckboxGroup} + * A constant representing the `PropTypes` of the `input` prop for checkbox group components, e.g., {@link CheckboxGroup} * * @constant {PropTypes} checkboxGroupPropTypes * diff --git a/src/forms/inputs/checkbox-group.js b/src/forms/inputs/checkbox-group.js index c9bd90a6..6162a3c9 100644 --- a/src/forms/inputs/checkbox-group.js +++ b/src/forms/inputs/checkbox-group.js @@ -7,6 +7,7 @@ import { omitLabelProps, replaceEmptyStringValue, convertNameToLabel, + DropdownSelect, } from '../helpers' import { LabeledField } from '../labels' import { @@ -15,6 +16,7 @@ import { serializeOptions, compose, } from '../../utils' +import classnames from 'classnames' /** * @@ -34,6 +36,7 @@ import { * @param {Object} meta - A `redux-form` [meta](http://redux-form.com/6.5.0/docs/api/Field.md/#meta-props) object * @param {Array} options - An array of checkbox values (strings, numbers, or key-value pairs) * @param {Object} [checkboxInputProps={}] - An object of key-value pairs representing props to pass down to all checkbox inputs + * @param {Boolean} [dropdown=false] - A flag indicating whether the checkbox options are displayed in a dropdown container or not * @example * * function TodoForm ({ handleSubmit, pristine, invalid, submitting }) { @@ -88,17 +91,45 @@ const propTypes = { className: PropTypes.string, checkboxInputProps: PropTypes.object, options: fieldOptionsType, + dropdown: PropTypes.bool, } const defaultProps = { className: 'CheckboxGroup', checkboxInputProps: {}, options: [], + dropdown: false, } -function CheckboxGroupLegend({ name, label }) { - if (label === false) return null - return {label || convertNameToLabel(name)} +function CheckboxGroupLegend({ + label, + name, + required, + requiredIndicator, + hint, +}) { + return ( + + {label || convertNameToLabel(name)} + {required && requiredIndicator && ( + + )} + {hint && {hint}} + + ) +} + +function CheckboxOptionsContainer({ children, dropdown, ...rest }) { + if (dropdown) + return ( + + {children} + + ) + + return children } function CheckboxGroup(props) { @@ -108,6 +139,7 @@ function CheckboxGroup(props) { options, className, checkboxInputProps, + dropdown, ...rest } = props const inputProps = omitLabelProps(rest) @@ -118,22 +150,28 @@ function CheckboxGroup(props) { return function (checked) { // Add or remove option value from array of values, depending on whether it's checked const newValueArray = checked - ? addToArray([option.value], value) + ? addToArray(value, [option.value]) : removeFromArray([option.value], value) return onChange(newValueArray) } } + return ( - {optionObjects.map((option, i) => { - return ( - + {optionObjects.map((option) => ( + - ) - })} + ))} + ) } diff --git a/src/forms/inputs/dropdown-checkbox-group.js b/src/forms/inputs/dropdown-checkbox-group.js deleted file mode 100644 index 3ef988b2..00000000 --- a/src/forms/inputs/dropdown-checkbox-group.js +++ /dev/null @@ -1,74 +0,0 @@ -import React from 'react' -import CheckboxGroup from './checkbox-group' -import { - checkboxGroupPropTypes, - DropdownSelect, - fieldOptionsType, -} from '../helpers' -import { InputLabel } from '../labels' - -/** - * - * A group of checkboxes that can be used in a `redux-form`-controlled form. - * Wraps the {@link CheckboxGroup} component in a {@link DropdownSelect} component, which displays the selected values as a list. - * Options are displayed in a scrollable `Select`-style dropdown container. - * - * The value of each checkbox is specified via the `options` prop. This prop can either be: - * - An array of strings - * - An array of key-value pairs: `{ key, value }` - * - * The value of the entire `DropdownCheckboxGroup` component is an **array** containing the values of the selected checkboxes. - * Clicking an unselected checkbox adds its value to this array, and clicking a selected checkbox removes its value from this array. - * - * @name DropdownCheckboxGroup - * @type Function - * @param {Object} input - A `redux-form` [input](http://redux-form.com/6.5.0/docs/api/Field.md/#input-props) object - * @param {Object} meta - A `redux-form` [meta](http://redux-form.com/6.5.0/docs/api/Field.md/#meta-props) object - * @param {Array} options - An array of checkbox values (strings or key-value pairs) - * @example - * - * function InterestsForm ({ handleSubmit, pristine, invalid, submitting }) { - * return ( - *
- * - * - * Submit - * - * - * ) - * } - * - * export default TodoForm - */ - -const propTypes = { - ...checkboxGroupPropTypes, - options: fieldOptionsType, -} - -function DropdownCheckboxGroup(props) { - const { - input: { name, value }, - label, - } = props - return ( -
- - - - -
- ) -} - -DropdownCheckboxGroup.propTypes = propTypes - -export default DropdownCheckboxGroup diff --git a/src/forms/inputs/icon-input.js b/src/forms/inputs/icon-input.js index 12aae8ce..22fd7304 100644 --- a/src/forms/inputs/icon-input.js +++ b/src/forms/inputs/icon-input.js @@ -10,7 +10,7 @@ import classnames from 'classnames' * This icon is rendered as an `` tag, with a dynamic class based on the `icon` prop. * For example, given an `icon` prop of `"twitter"`, the component will render an {@link Input} with child ``. * - * Additionally, the fieldset of this {@link Input} will be given the class `"icon-label"` for styling purposes. + * Additionally, the wrapping div of this {@link Input} will be given the class `"icon-label"` for styling purposes. * * @name IconInput * @type Function diff --git a/src/forms/inputs/index.js b/src/forms/inputs/index.js index 470734be..a3326504 100644 --- a/src/forms/inputs/index.js +++ b/src/forms/inputs/index.js @@ -3,7 +3,6 @@ export CheckboxGroup from './checkbox-group' export CloudinaryFileInput from './cloudinary-file-input' export ColorInput from './color-input' export DateInput from './date-input' -export DropdownCheckboxGroup from './dropdown-checkbox-group' export FileInput from './file-input' export HiddenInput from './hidden-input' export Input from './input' diff --git a/src/forms/inputs/radio-group.js b/src/forms/inputs/radio-group.js index 2c01e131..cfb232c4 100644 --- a/src/forms/inputs/radio-group.js +++ b/src/forms/inputs/radio-group.js @@ -8,6 +8,7 @@ import { } from '../helpers' import { LabeledField } from '../labels' import { serializeOptions, filterInvalidDOMProps } from '../../utils' +import classnames from 'classnames' /** * @@ -89,9 +90,18 @@ const defaultProps = { radioInputProps: {}, } -function RadioGroupLegend({ label, name }) { - if (label === false) return null - return {label || convertNameToLabel(name)} +function RadioGroupLegend({ label, name, required, requiredIndicator, hint }) { + return ( + + {label || convertNameToLabel(name)} + {required && requiredIndicator && ( + + )} + {hint && {hint}} + + ) } // This should never be used by itself, so it does not exist as a separate export @@ -137,13 +147,14 @@ function RadioGroup(props) { - {optionObjects.map((option, i) => { + {optionObjects.map((option) => { return ( - @@ -104,7 +109,7 @@ function LabeledField({ {!hideErrorLabel && ( )} - + ) } diff --git a/stories/forms/inputs/checkbox-group.story.js b/stories/forms/inputs/checkbox-group.story.js index 74700ea8..01cc524c 100644 --- a/stories/forms/inputs/checkbox-group.story.js +++ b/stories/forms/inputs/checkbox-group.story.js @@ -75,3 +75,11 @@ storiesOf('CheckboxGroup', module) }} /> )) + .add('with dropdown', () => ( + + )) diff --git a/stories/forms/inputs/dropdown-checkbox-group.story.js b/stories/forms/inputs/dropdown-checkbox-group.story.js deleted file mode 100644 index 00f62b96..00000000 --- a/stories/forms/inputs/dropdown-checkbox-group.story.js +++ /dev/null @@ -1,59 +0,0 @@ -import React from 'react' -import { storiesOf } from '@storybook/react' -import { action } from '@storybook/addon-actions' -import { DropdownCheckboxGroup as StaticDropdownCheckboxGroup } from 'src' -import dynamicInput from '../../dynamic-input' - -const DropdownCheckboxGroup = dynamicInput({ - initialValue: '', - valuePath: 'input.value', - onChangePath: 'input.onChange', -})(StaticDropdownCheckboxGroup) - -const inputProps = { - name: 'person.checkboxOptions', - value: '', - onChange: action('field changed'), -} - -const options = [ - { key: 'First Option', value: '1' }, - { key: 'Second Option', value: '2' }, - { key: 'Third Option', value: '3' }, -] - -storiesOf('DropdownCheckboxGroup', module) - .add('with default label', () => ( - - )) - .add('with custom label', () => ( - - )) - .add('with no label', () => ( - - )) - .add('with error', () => ( - - )) - .add('with empty options', () => ( - - )) diff --git a/test/forms/inputs/checkbox-group.test.js b/test/forms/inputs/checkbox-group.test.js index ddc206ec..60782e04 100644 --- a/test/forms/inputs/checkbox-group.test.js +++ b/test/forms/inputs/checkbox-group.test.js @@ -1,10 +1,13 @@ import React, { useState } from 'react' import { CheckboxGroup } from '../../../src/' -import { render, screen } from '@testing-library/react' +import { render, screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' +const name = 'testGroup' +const formattedName = 'Test Group' + const WrappedCheckboxGroup = (props) => { - const [value, setValue] = useState([]) + const [value, setValue] = useState(props.value || []) const options = [ { key: 'First Option', value: '1' }, @@ -14,114 +17,192 @@ const WrappedCheckboxGroup = (props) => { const defaultProps = { input: { - name: 'test', - value: value, + name, + value, onChange: setValue, }, meta: {}, - options: options, + options, } return } -test('CheckboxGroup adds value to array when unselected option clicked', async () => { - const user = userEvent.setup() - - render() - - const checkbox1 = screen.getByRole('checkbox', { name: 'First Option' }) - const checkbox2 = screen.getByRole('checkbox', { name: 'Second Option' }) - const checkbox3 = screen.getByRole('checkbox', { name: 'Third Option' }) - - await user.click(checkbox2) - await user.click(checkbox3) +describe('CheckboxGroup', () => { + test('adds value to array when unselected option clicked', async () => { + const user = userEvent.setup() + + render() + + const checkbox1 = screen.getByRole('checkbox', { name: 'First Option' }) + const checkbox2 = screen.getByRole('checkbox', { name: 'Second Option' }) + const checkbox3 = screen.getByRole('checkbox', { name: 'Third Option' }) + + await user.click(checkbox2) + await user.click(checkbox3) + + expect(checkbox1).not.toBeChecked() + expect(checkbox2).toBeChecked() + expect(checkbox3).toBeChecked() + }) + + test('removes value from array when selected option clicked', async () => { + const user = userEvent.setup() + + render() + + const checkbox2 = screen.getByRole('checkbox', { name: 'Second Option' }) + await user.click(checkbox2) + + expect(checkbox2).toBeChecked() + + await user.click(checkbox2) + + expect(checkbox2).not.toBeChecked() + }) + + test("has a legend with the group's name by default", () => { + const props = { + input: { + name, + value: '', + }, + meta: {}, + } + render() - expect(checkbox1).not.toBeChecked() - expect(checkbox2).toBeChecked() - expect(checkbox3).toBeChecked() -}) - -test('CheckboxGroup removes value to array when selected option clicked', async () => { - const user = userEvent.setup() + expect(screen.getByText(formattedName)).toBeInTheDocument() + }) - render() + test("has a legend with the group's label (when provided)", () => { + const props = { + input: { + name, + value: '', + }, + label: 'Different Name', + meta: {}, + } + render() - const checkbox2 = screen.getByRole('checkbox', { name: 'Second Option' }) - await user.click(checkbox2) + expect(screen.getByText('Different Name')).toBeInTheDocument() + }) - expect(checkbox2).toBeChecked() + test('does not pass class to children', () => { + const props = { + input: { + name, + value: '', + }, + meta: {}, + options: ['TOGGLED_OPTION'], + className: 'custom-class', + } + render() - await user.click(checkbox2) + const checkboxGroup = screen.getByRole('group', { name: formattedName }) + const checkbox = screen.getByRole('checkbox') - expect(checkbox2).not.toBeChecked() -}) + expect(checkboxGroup).toHaveClass('custom-class') + expect(checkbox).not.toHaveClass('custom-class') + }) -test("CheckboxGroup has a legend with the group's name by default", () => { - const props = { - input: { - name: 'testGroup', - value: '', - }, - meta: {}, - } - render() + test('passes down props to children', () => { + const props = { + input: { + name, + value: '', + }, + meta: {}, + options: ['TOGGLED_OPTION'], + className: 'custom-group-class', + checkboxInputProps: { + className: 'custom-input-class', + }, + } + render() - expect(screen.getByText('Test Group')).toBeInTheDocument() -}) + const checkboxGroup = screen.getByRole('group', { name: formattedName }) + const checkbox = screen.getByRole('checkbox') -test("CheckboxGroup has a legend with the group's label (when provided)", () => { - const props = { - input: { - name: 'testGroup', - value: '', - }, - label: 'Checkbox Group', - meta: {}, - } - render() + expect(checkboxGroup).toHaveClass('custom-group-class') + expect(checkbox).not.toHaveClass('custom-group-class') + expect(checkbox).toHaveClass('custom-input-class') + }) + + test('with dropdown = true adds value to array when unselected option clicked', async () => { + const user = userEvent.setup() + + render() + + const select = screen.getByRole('button') + await user.click(select) + + const firstCheckbox = screen.getByLabelText('First Option') + await user.click(firstCheckbox) + + const thirdCheckbox = screen.getByLabelText('Third Option') + await user.click(thirdCheckbox) + + expect(firstCheckbox).toBeChecked() + expect(thirdCheckbox).toBeChecked() + + const selectValueLabel = screen.getByText('First Option, Third Option') + expect(selectValueLabel).toBeInTheDocument() + }) + + test('with dropdown = true removes value from array when selected option clicked', async () => { + const user = userEvent.setup() + + render() + + const select = screen.getByRole('button') + await user.click(select) + + const firstCheckbox = screen.getByLabelText('First Option') + await user.click(firstCheckbox) + + expect(firstCheckbox).not.toBeChecked() + + const selectValueLabel = screen.getByText('None') + expect(selectValueLabel).toBeInTheDocument() + }) + + test('with dropdown = true sets menu no longer active when clicked outside', async () => { + const user = userEvent.setup() + + render() + + const select = screen.getByRole('button') + await user.click(select) + + expect(select.nextSibling).toHaveClass('options', 'is-active') + + const fieldset = screen.getAllByRole('group').at(0) + + user.click(fieldset) + await waitFor(() => { + expect(select.nextSibling).not.toHaveClass('is-active') + }) + }) + + test('does not show required indicator when no custom required indicator provided', () => { + render() + expect(screen.getByText(formattedName).textContent).toEqual(formattedName) + }) - expect(screen.getByText('Checkbox Group')).toBeInTheDocument() -}) - -test('CheckboxGroup does not pass class to children', () => { - const props = { - input: { - name: 'testGroup', - value: '', - }, - meta: {}, - options: ['TOGGLED_OPTION'], - className: 'custom-class', - } - render() - - const checkboxGroup = screen.getByRole('group', { name: 'Test Group' }) - const checkbox = screen.getByRole('checkbox') - - expect(checkboxGroup).toHaveClass('custom-class') - expect(checkbox).not.toHaveClass('custom-class') -}) - -test('CheckboxGroup passes down props to children', () => { - const props = { - input: { - name: 'testGroup', - value: '', - }, - meta: {}, - options: ['TOGGLED_OPTION'], - className: 'custom-group-class', - checkboxInputProps: { - className: 'custom-input-class', - }, - } - render() + test('shows custom indicator when required true and custom requiredIndicator provided', () => { + render() + expect(screen.getByText('*')).toBeInTheDocument() + }) - const checkboxGroup = screen.getByRole('group', { name: 'Test Group' }) - const checkbox = screen.getByRole('checkbox') + test('hides custom indicator when required false and custom requiredIndicator provided', () => { + render() + expect(screen.queryByText('*')).not.toBeInTheDocument() + }) - expect(checkboxGroup).toHaveClass('custom-group-class') - expect(checkbox).not.toHaveClass('custom-group-class') - expect(checkbox).toHaveClass('custom-input-class') + test('shows hint when hint provided', () => { + render() + expect(screen.getByText(formattedName)).toHaveTextContent('hint') + }) }) diff --git a/test/forms/inputs/checkbox.test.js b/test/forms/inputs/checkbox.test.js index 4ddce878..4a30d967 100644 --- a/test/forms/inputs/checkbox.test.js +++ b/test/forms/inputs/checkbox.test.js @@ -9,7 +9,7 @@ const WrappedCheckbox = () => { const props = { input: { name: 'test', - value: value, + value, onChange: () => setValue(!value), }, meta: {}, diff --git a/test/forms/inputs/cloudinary-file-input.test.js b/test/forms/inputs/cloudinary-file-input.test.js index 52c8bf68..a61b0bf3 100644 --- a/test/forms/inputs/cloudinary-file-input.test.js +++ b/test/forms/inputs/cloudinary-file-input.test.js @@ -5,7 +5,7 @@ import { CloudinaryFileInput } from '../../../src/' const name = 'name.of.field' const value = { name: 'existingFileName', url: 'value of field' } -const onChange = () => { } +const onChange = () => {} const input = { name, value, onChange } const PUBLIC_URL = 'url-of-uploaded-file' const uploadResponse = { url: PUBLIC_URL } @@ -32,11 +32,11 @@ test('CloudinaryFileInput adds uploadStatus to className', () => { cloudName, bucket, } - render() - - const fieldset = screen.getByRole('group') - expect(fieldset).toHaveClass(className) - expect(fieldset).toHaveClass(uploadStatus) + const { + container: { firstChild: wrapper }, + } = render() + expect(wrapper).toHaveClass(className) + expect(wrapper).toHaveClass(uploadStatus) }) test('CloudinaryFileInput sets returned url within value', async () => { diff --git a/test/forms/inputs/date-input.test.js b/test/forms/inputs/date-input.test.js index 24f2ca73..da7a3605 100644 --- a/test/forms/inputs/date-input.test.js +++ b/test/forms/inputs/date-input.test.js @@ -14,8 +14,8 @@ const WrappedDateInput = (props) => { const defaultProps = { input: { - name: name, - value: value, + name, + value, onChange: setValue, onBlur: noop, }, @@ -86,7 +86,7 @@ test("DateInput defaults tabbable item to today's date", async () => { expect(current).toHaveProperty('tabIndex', 0) }) -test("DateInput sets empty input to an empty string", async () => { +test('DateInput sets empty input to an empty string', async () => { const user = userEvent.setup() const onChange = jest.fn() const props = { input: { ...input, onChange, onBlur: noop }, meta: {} } @@ -99,7 +99,7 @@ test("DateInput sets empty input to an empty string", async () => { expect(onChange).toHaveBeenCalledWith('') }) -test("DateInput invokes onBlur when focus changes", async () => { +test('DateInput invokes onBlur when focus changes', async () => { const user = userEvent.setup() const onBlur = jest.fn() const onChange = jest.fn() @@ -116,4 +116,4 @@ test("DateInput invokes onBlur when focus changes", async () => { await user.tab() expect(onBlur).toHaveBeenCalledTimes(1) -}) \ No newline at end of file +}) diff --git a/test/forms/inputs/dropdown-checkbox-group.test.js b/test/forms/inputs/dropdown-checkbox-group.test.js deleted file mode 100644 index 57a6126d..00000000 --- a/test/forms/inputs/dropdown-checkbox-group.test.js +++ /dev/null @@ -1,81 +0,0 @@ -import React, { useState } from 'react' -import { DropdownCheckboxGroup } from '../../../src/' -import { render, screen, waitFor } from '@testing-library/react' -import userEvent from '@testing-library/user-event' - -const WrappedDropdownCheckboxGroup = (props) => { - const [value, setValue] = useState(props.value || []) - - const options = [ - { key: 'First Option', value: '1' }, - { key: 'Second Option', value: '2' }, - { key: 'Third Option', value: '3' }, - ] - - const defaultProps = { - input: { - name: 'test', - value: value, - onChange: setValue, - }, - meta: {}, - options, - } - - return -} - -test('DropdownCheckboxGroup adds value to array when unselected option clicked', async () => { - const user = userEvent.setup() - - render() - const select = screen.getByRole('button') - await user.click(select) - - const firstCheckbox = screen.getByLabelText('First Option') - await user.click(firstCheckbox) - - const thirdCheckbox = screen.getByLabelText('Third Option') - await user.click(thirdCheckbox) - - expect(firstCheckbox).toBeChecked() - expect(thirdCheckbox).toBeChecked() - - const selectValueLabel = screen.getByText('3, 1') - expect(selectValueLabel).toBeInTheDocument() -}) - -test('DropdownCheckboxGroup removes value from array when selected option clicked', async () => { - const user = userEvent.setup() - - render() - - const select = screen.getByRole('button') - await user.click(select) - - const firstCheckbox = screen.getByLabelText('First Option') - await user.click(firstCheckbox) - - expect(firstCheckbox).not.toBeChecked() - - const selectValueLabel = screen.getByText('None') - expect(selectValueLabel).toBeInTheDocument() -}) - -test('DropdownCheckboxGroup sets menu no longer active when clicked outside', async () => { - const user = userEvent.setup() - - render() - - const select = screen.getByRole('button') - await user.click(select) - - expect(select.nextSibling).toHaveClass('options', 'is-active') - - const fieldset = screen.getAllByRole('group').at(0) - - user.click(fieldset) - await waitFor(() => { - expect(select.nextSibling).not.toHaveClass('is-active') - }) -}) \ No newline at end of file diff --git a/test/forms/inputs/icon-input.test.js b/test/forms/inputs/icon-input.test.js index 6ed6cf4a..ed2b3a66 100644 --- a/test/forms/inputs/icon-input.test.js +++ b/test/forms/inputs/icon-input.test.js @@ -7,11 +7,9 @@ const value = 'value of field' const onChange = () => {} const props = { input: { name, value, onChange }, meta: {}, icon: 'foo' } -test('IconInput adds class "icon-label" to surrounding fieldset', () => { - render() - const fieldset = screen.getByRole('group') - - expect(fieldset).toHaveClass('icon-label') +test('IconInput adds class "icon-label" to surrounding container', () => { + const { container } = render() + expect(container.firstChild).toHaveClass('icon-label') }) test('IconInput renders tag with correct class', () => { diff --git a/test/forms/inputs/radio-group.test.js b/test/forms/inputs/radio-group.test.js index 8792e84b..da20778e 100644 --- a/test/forms/inputs/radio-group.test.js +++ b/test/forms/inputs/radio-group.test.js @@ -3,131 +3,201 @@ import { render, screen } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { RadioGroup } from '../../../src/' -test('RadioGroup changes value when buttons are clicked', async () => { - const user = userEvent.setup() - const onChange = jest.fn() - const props = { - input: { - name: 'test', - value: '', - onChange, - }, - meta: {}, - options: ['Option 1', 'Option 2'], - } - render() - const radioOptions = screen.getAllByRole('radio') - await user.click(radioOptions.at(0)) - expect(onChange).toHaveBeenCalledWith('Option 1') - await user.click(radioOptions.at(1)) - expect(onChange).toHaveBeenCalledWith('Option 2') -}) +const name = 'testGroup' +const formattedName = 'Test Group' -test("RadioGroup's inputs all have the same name", () => { - const name = 'sameName' - const props = { - input: { - name, - value: '', - }, - meta: {}, - options: ['Option 1', 'Option 2'], - } - render() - const radioOptions = screen.getAllByRole('radio') - expect(radioOptions.at(0)).toHaveAttribute('name', name) - expect(radioOptions.at(1)).toHaveAttribute('name', name) -}) +describe('RadioGroup', () => { + test('changes value when buttons are clicked', async () => { + const user = userEvent.setup() + const onChange = jest.fn() + const props = { + input: { + name, + value: '', + onChange, + }, + meta: {}, + options: ['Option 1', 'Option 2'], + } + render() + const radioOptions = screen.getAllByRole('radio') + await user.click(radioOptions.at(0)) + expect(onChange).toHaveBeenCalledWith('Option 1') + await user.click(radioOptions.at(1)) + expect(onChange).toHaveBeenCalledWith('Option 2') + }) -test("RadioGroup input has a value that matches the corresponding option's value", () => { - const options = ['Option 1', 'Option 2'] - const props = { - input: { - name: 'test', - value: '', - }, - meta: {}, - options, - } - render() - expect(screen.getByRole('radio', { name: options.at(0) })).toHaveAttribute('value', options.at(0)) - expect(screen.getByRole('radio', { name: options.at(1) })).toHaveAttribute('value', options.at(1)) -}) + test('inputs all have the same name', () => { + const props = { + input: { + name, + value: '', + }, + meta: {}, + options: ['Option 1', 'Option 2'], + } + render() + const radioOptions = screen.getAllByRole('radio') + expect(radioOptions.at(0)).toHaveAttribute('name', name) + expect(radioOptions.at(1)).toHaveAttribute('name', name) + }) -test("RadioGroup has a legend with the input's name start-cased by default", () => { - const name = 'sameName' - const props = { - input: { - name, - value: '', - }, - meta: {}, - options: ['Option 1', 'Option 2'], - } - render() - expect(screen.getByRole('group', { name: 'Same Name' })).toBeInTheDocument() -}) + test("input has a value that matches the corresponding option's value", () => { + const options = ['Option 1', 'Option 2'] + const props = { + input: { + name: 'test', + value: '', + }, + meta: {}, + options, + } + render() + expect(screen.getByRole('radio', { name: options.at(0) })).toHaveAttribute( + 'value', + options.at(0) + ) + expect(screen.getByRole('radio', { name: options.at(1) })).toHaveAttribute( + 'value', + options.at(1) + ) + }) -test("RadioGroup does not have a legend when label is `false`", () => { - const name = 'sameName' - const props = { - input: { - name, - value: '', - }, - meta: {}, - options: ['Option 1', 'Option 2'], - label: false, - } - render() - expect(screen.queryByRole('group', { name: 'Same Name' })).not.toBeInTheDocument() -}) + test("has a legend with the input's name start-cased by default", () => { + const props = { + input: { + name, + value: '', + }, + meta: {}, + options: ['Option 1', 'Option 2'], + } + render() + expect( + screen.getByRole('group', { name: formattedName }) + ).toBeInTheDocument() + }) -test("RadioGroup has a legend with the group's label (when provided)", () => { - const name = 'sameName' - const props = { - input: { - name, - value: '', - }, - meta: {}, - label: 'Different Name', - options: ['Option 1', 'Option 2'], - } - render() - expect(screen.getByRole('group', { name: 'Different Name' })).toBeInTheDocument() -}) + test('has a legend with visually-hidden class when label is `false`', () => { + const props = { + input: { + name, + value: '', + }, + meta: {}, + options: ['Option 1', 'Option 2'], + label: false, + } + render() + const fieldset = screen.queryByRole('group', { name: formattedName }) + expect(fieldset).toBeInTheDocument() + expect(fieldset.firstChild).toHaveClass('visually-hidden') + }) -test('RadioGroup does not pass down class name', () => { - const props = { - input: { - name: 'test', - value: '', - }, - meta: {}, - options: ['Option 1'], - className: 'custom-radio', - } - render() - expect(screen.getByRole('group', { name: 'Test' })).toHaveClass('custom-radio') - expect(screen.getByRole('radio')).not.toHaveClass('custom-radio') -}) + test("has a legend with the group's label (when provided)", () => { + const props = { + input: { + name, + value: '', + }, + meta: {}, + label: 'Different Name', + options: ['Option 1', 'Option 2'], + } + render() + expect( + screen.getByRole('group', { name: 'Different Name' }) + ).toBeInTheDocument() + }) + + test('does not pass down class name', () => { + const props = { + input: { + name: 'test', + value: '', + }, + meta: {}, + options: ['Option 1'], + className: 'custom-radio', + } + render() + expect(screen.getByRole('group', { name: 'Test' })).toHaveClass( + 'custom-radio' + ) + expect(screen.getByRole('radio')).not.toHaveClass('custom-radio') + }) + + test('passes down props to children', () => { + const props = { + input: { + name, + value: '', + }, + meta: {}, + options: ['Option 1', 'Option 2'], + className: 'custom-radio-group', + 'data-test': 'true', + radioInputProps: { className: 'custom-radio-input' }, + } + render() + screen.getAllByRole('radio').forEach((el) => { + expect(el).toHaveClass('custom-radio-input') + expect(el).toHaveAttribute('data-test', 'true') + }) + }) + + test('does not show required indicator when no custom required indicator provided', () => { + const props = { + input: { + name, + value: '', + }, + meta: {}, + required: true, + } + + render() + expect(screen.getByText(formattedName).textContent).toEqual(formattedName) + }) + + test('shows custom indicator when required true and custom requiredIndicator provided', () => { + const props = { + input: { + name, + value: '', + }, + meta: {}, + required: true, + requiredIndicator: '*', + } + render() + expect(screen.getByText('*')).toBeInTheDocument() + }) + + test('hides custom indicator when required false and custom requiredIndicator provided', () => { + const props = { + input: { + name, + value: '', + }, + meta: {}, + required: false, + requiredIndicator: '*', + } + render() + expect(screen.queryByText('*')).not.toBeInTheDocument() + }) -test('RadioGroup passes down props to children', () => { - const props = { - input: { - name: 'test', - value: '', - }, - meta: {}, - options: ['Option 1', 'Option 2'], - className: 'custom-radio-group', - 'data-test': 'true', - radioInputProps: { className: 'custom-radio-input' }, - } - render() - screen.getAllByRole('radio').forEach((el) => { - expect(el).toHaveClass('custom-radio-input') - expect(el).toHaveAttribute('data-test', 'true') + test('shows hint when hint provided', () => { + const props = { + input: { + name, + value: '', + }, + meta: {}, + hint: 'hint', + } + render() + expect(screen.getByText(formattedName)).toHaveTextContent('hint') }) }) diff --git a/test/forms/inputs/select.test.js b/test/forms/inputs/select.test.js index a95aba7a..0e072900 100644 --- a/test/forms/inputs/select.test.js +++ b/test/forms/inputs/select.test.js @@ -83,7 +83,9 @@ test('Select has a disabled placeholder by default', () => { meta: {}, } render() const optionGroup = screen.getByRole('group', { name: options.name }) - expect(within(optionGroup).getByRole('option', { name: 'testOption'})).toBeInTheDocument() + expect( + within(optionGroup).getByRole('option', { name: 'testOption' }) + ).toBeInTheDocument() }) test('Select adds an aria-describedby attribute when there is an input error', () => { @@ -119,7 +123,9 @@ test('Select adds an aria-describedby attribute when there is an input error', ( options: [OPTION], } render( - const props = { input: { name: 'foo' }, meta: {} } - render( - - - - ) - const fieldset = screen.getByRole('group') - expect(fieldset).toBeInTheDocument() - expect(fieldset).not.toHaveClass('error') -}) +describe('LabeledField', () => { + test('wraps children in container with field-wrapper class by default', () => { + const Wrapped = () => + const props = { input: { name: 'foo' }, meta: {} } + const { + container: { firstChild: wrapper }, + } = render( + + + + ) + expect(wrapper).toBeInTheDocument() + expect(wrapper).toHaveClass('field-wrapper') + expect(wrapper).not.toHaveClass('error') + }) -test('adds error class when touched and invalid', () => { - const Wrapped = () => - const props = { - input: { name: 'foo' }, - meta: { touched: true, invalid: true }, - } - render( - - - - ) - expect(screen.getByRole('group')).toHaveClass('error') -}) + test('wraps children in fieldset when explicitly set', () => { + const CheckboxLegend = () => Foo + const props = { + input: { name: 'foo' }, + meta: {}, + as: 'fieldset', + labelComponent: CheckboxLegend, + } + const Options = () => ( + <> +
+ + +
+
+ + +
+ + ) + render( + + + + ) + const fieldset = screen.getByRole('group', { name: 'Foo' }) + expect(fieldset).toBeInTheDocument() + expect(fieldset).not.toHaveClass('error') + }) -test('adds disabled class when disabled', () => { - const Wrapped = () => - const props = { input: { name: 'foo' }, meta: {}, disabled: true } - render( - - - - ) - expect(screen.getByRole('group')).toHaveClass('disabled') -}) + test('adds error class when touched and invalid', () => { + const Wrapped = () => + const props = { + input: { name: 'foo' }, + meta: { touched: true, invalid: true }, + } + const { + container: { firstChild: wrapper }, + } = render( + + + + ) + expect(wrapper).toHaveClass('error') + }) -test('adds InputLabel and InputError', () => { - const Wrapped = () => - const props = { - input: { name: 'foo' }, - meta: { touched: true, invalid: true }, - error: "Required" - } - render( - - - - ) - // InputLabel - expect(screen.getByText('Foo')).toBeInTheDocument() - // InputError - expect(screen.getByText('Required')).toBeInTheDocument() -}) + test('adds disabled class when disabled', () => { + const Wrapped = () => + const props = { + input: { name: 'foo' }, + meta: {}, + disabled: true, + } + const { + container: { firstChild: wrapper }, + } = render( + + + + ) + expect(wrapper).toHaveClass('disabled') + }) -test('hides error label with hideErrorLabel option', () => { - const Wrapped = () => - const props = { - input: { name: 'foo' }, - meta: { touched: true, invalid: true }, - error: 'Required', - hideErrorLabel: true, - } - render( - - - - ) - expect(screen.queryByText('Required')).not.toBeInTheDocument() -}) + test('adds InputLabel and InputError', () => { + const Wrapped = () => + const props = { + input: { name: 'foo' }, + meta: { touched: true, invalid: true }, + error: 'Required', + } + render( + + + + ) + // InputLabel + expect(screen.getByText('Foo')).toBeInTheDocument() + // InputError + expect(screen.getByText('Required')).toBeInTheDocument() + }) -test('adds a custom label component', () => { - const Wrapped = () => - const LabelComponent = () => - const props = { - input: { - name: 'foo', - }, - meta: {}, - labelComponent: LabelComponent, - } - - render( - - - - ) - expect(screen.getByText('This is a custom label')).toBeInTheDocument() -}) + test('hides error label with hideErrorLabel option', () => { + const Wrapped = () => + const props = { + input: { name: 'foo' }, + meta: { touched: true, invalid: true }, + error: 'Required', + hideErrorLabel: true, + } + render( + + + + ) + expect(screen.queryByText('Required')).not.toBeInTheDocument() + }) -test('passes custom props to a custom label component', () => { - const Wrapped = () => - // eslint-disable-next-line - const LabelComponent = ({ customHint }) => ( - - ) - const props = { - input: { - name: 'foo', - }, - meta: {}, - customHint: 'Hi!', - labelComponent: LabelComponent, - } - - render( - - - - ) - expect(screen.getByText(props.customHint)).toBeInTheDocument() -}) + test('adds a custom label component', () => { + const Wrapped = () => + const LabelComponent = () => + const props = { + input: { + name: 'foo', + }, + meta: {}, + labelComponent: LabelComponent, + } -test('considers a custom label component to have higher precedence than a label prop', () => { - const Wrapped = () => - const LabelComponent = () => - const props = { - input: { - name: 'foo', - }, - label: 'Standard Label', - meta: {}, - labelComponent: LabelComponent, - } - - render( - - - - ) - expect(screen.getByText('This is a custom label')).toBeInTheDocument() - expect(screen.queryByText('Standard Label')).not.toBeInTheDocument() -}) + render( + + + + ) + expect(screen.getByText('This is a custom label')).toBeInTheDocument() + }) -test('adds a custom error component', () => { - const Wrapped = () => - const ErrorComponent = () => ( - This is a custom error message - ) - const props = { - input: { - name: 'foo', - }, - meta: {}, - errorComponent: ErrorComponent, - } - - render( - - - - ) - expect(screen.getByText('This is a custom error message')).toBeInTheDocument() -}) + test('passes custom props to a custom label component', () => { + const Wrapped = () => + // eslint-disable-next-line + const LabelComponent = ({ customHint }) => ( + + ) + const props = { + input: { + name: 'foo', + }, + meta: {}, + customHint: 'Hi!', + labelComponent: LabelComponent, + } + + render( + + + + ) + expect(screen.getByText(props.customHint)).toBeInTheDocument() + }) + + test('considers a custom label component to have higher precedence than a label prop', () => { + const Wrapped = () => + const LabelComponent = () => + const props = { + input: { + name: 'foo', + }, + label: 'Standard Label', + meta: {}, + labelComponent: LabelComponent, + } + + render( + + + + ) + expect(screen.getByText('This is a custom label')).toBeInTheDocument() + expect(screen.queryByText('Standard Label')).not.toBeInTheDocument() + }) + + test('adds a custom error component', () => { + const Wrapped = () => + const ErrorComponent = () => ( + This is a custom error message + ) + const props = { + input: { + name: 'foo', + }, + meta: {}, + errorComponent: ErrorComponent, + } + + render( + + + + ) + expect( + screen.getByText('This is a custom error message') + ).toBeInTheDocument() + }) + + test('passes custom props to a custom error component', () => { + const Wrapped = () => + // eslint-disable-next-line + const ErrorComponent = ({ customHint }) => ( + + This is a custom error message{customHint} + + ) + const props = { + input: { + name: 'foo', + }, + meta: {}, + errorComponent: ErrorComponent, + customHint: 'Hi!', + } -test('passes custom props to a custom error component', () => { - const Wrapped = () => - // eslint-disable-next-line - const ErrorComponent = ({ customHint }) => ( - - This is a custom error message{customHint} - - ) - const props = { - input: { - name: 'foo', - }, - meta: {}, - errorComponent: ErrorComponent, - customHint: 'Hi!', - } - - render( - - - - ) - expect(screen.getByText(props.customHint)).toBeInTheDocument() + render( + + + + ) + expect(screen.getByText(props.customHint)).toBeInTheDocument() + }) })