From 001477125684f081511315ab7c031d1d2efce9e4 Mon Sep 17 00:00:00 2001 From: Zach Rose Date: Fri, 22 Nov 2024 13:56:44 -0500 Subject: [PATCH] feat(filter): L3-4114 - create dumb filter component (#418) Co-authored-by: Zachary Rose Co-authored-by: Scott Dickerson <6663002+scottdickerson@users.noreply.github.com> --- package-lock.json | 81 +++--- package.json | 1 + src/componentStyles.scss | 16 +- src/components/DatePicker/DatePicker.tsx | 6 +- .../ErrorBoundary/ErrorBoundary.test.tsx | 2 +- src/components/Filter/Filter.stories.tsx | 99 +++++++ src/components/Filter/Filter.test.tsx | 74 +++++ src/components/Filter/Filter.tsx | 117 ++++++++ src/components/Filter/FilterHeader.tsx | 60 ++++ src/components/Filter/FilterInput.tsx | 38 +++ src/components/Filter/_filter.scss | 112 ++++++++ src/components/Filter/index.ts | 3 + src/components/Input/Input.stories.tsx | 44 ++- src/components/Input/Input.test.tsx | 2 +- src/components/Input/Input.tsx | 34 ++- src/components/Input/_input.scss | 2 +- src/components/Pagination/Pagination.tsx | 3 +- src/components/Select/Select.stories.tsx | 4 +- src/components/Select/Select.test.tsx | 3 +- src/components/Select/Select.tsx | 24 +- src/components/Toggle/Toggle.stories.tsx | 2 +- src/index.ts | 5 + .../FilterMenu/FilterMenu.stories.tsx | 264 ++++++++++++++++++ src/patterns/FilterMenu/FilterMenu.test.tsx | 137 +++++++++ src/patterns/FilterMenu/FilterMenu.tsx | 55 ++++ src/patterns/FilterMenu/_filterMenu.scss | 15 + src/patterns/FilterMenu/index.ts | 1 + src/patterns/FilterMenu/utils.tsx | 138 +++++++++ .../ViewingsList/ViewingsList.test.tsx | 8 +- .../ViewingsList/ViewingsListCard.tsx | 2 +- .../ViewingsList/ViewingsListCardForm.tsx | 38 ++- tsconfig.json | 1 + 32 files changed, 1278 insertions(+), 113 deletions(-) create mode 100644 src/components/Filter/Filter.stories.tsx create mode 100644 src/components/Filter/Filter.test.tsx create mode 100644 src/components/Filter/Filter.tsx create mode 100644 src/components/Filter/FilterHeader.tsx create mode 100644 src/components/Filter/FilterInput.tsx create mode 100644 src/components/Filter/_filter.scss create mode 100644 src/components/Filter/index.ts create mode 100644 src/patterns/FilterMenu/FilterMenu.stories.tsx create mode 100644 src/patterns/FilterMenu/FilterMenu.test.tsx create mode 100644 src/patterns/FilterMenu/FilterMenu.tsx create mode 100644 src/patterns/FilterMenu/_filterMenu.scss create mode 100644 src/patterns/FilterMenu/index.ts create mode 100644 src/patterns/FilterMenu/utils.tsx diff --git a/package-lock.json b/package-lock.json index 31e394a1..a4fa99b5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -83,6 +83,7 @@ "stylelint-order": "^6.0.4", "stylelint-scss": "^6.4.1", "ts-node": "^10.9.2", + "type-fest": "^4.27.0", "typescript": "^5.5.4", "vite": "^5.4.6", "vite-plugin-dts": "^4.2.4", @@ -3554,6 +3555,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@eslint/eslintrc/node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@eslint/js": { "version": "8.57.0", "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz", @@ -7131,18 +7144,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@semantic-release/npm/node_modules/type-fest": { - "version": "4.18.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.18.2.tgz", - "integrity": "sha512-+suCYpfJLAe4OXS6+PPXjW3urOS4IoP9waSiLuXfLgqZODKw/aWwASvzqE886wA0kQgGy0mIWyhd87VpqIy6Xg==", - "dev": true, - "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/@semantic-release/npm/node_modules/unique-string": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-3.0.0.tgz", @@ -7284,18 +7285,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@semantic-release/release-notes-generator/node_modules/type-fest": { - "version": "4.18.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.18.2.tgz", - "integrity": "sha512-+suCYpfJLAe4OXS6+PPXjW3urOS4IoP9waSiLuXfLgqZODKw/aWwASvzqE886wA0kQgGy0mIWyhd87VpqIy6Xg==", - "dev": true, - "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/@sinclair/typebox": { "version": "0.27.8", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", @@ -11562,6 +11551,18 @@ "node": ">=8" } }, + "node_modules/boxen/node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -14288,6 +14289,18 @@ "node": ">=8" } }, + "node_modules/eslint/node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/espree": { "version": "9.6.1", "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", @@ -25764,18 +25777,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/read-package-up/node_modules/type-fest": { - "version": "4.18.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.18.2.tgz", - "integrity": "sha512-+suCYpfJLAe4OXS6+PPXjW3urOS4IoP9waSiLuXfLgqZODKw/aWwASvzqE886wA0kQgGy0mIWyhd87VpqIy6Xg==", - "dev": true, - "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/readable-stream": { "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", @@ -28832,12 +28833,12 @@ } }, "node_modules/type-fest": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "version": "4.27.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.27.0.tgz", + "integrity": "sha512-3IMSWgP7C5KSQqmo1wjhKrwsvXAtF33jO3QY+Uy++ia7hqvgSK6iXbbg5PbDBc1P2ZbNEDgejOrN4YooXvhwCw==", "dev": true, "engines": { - "node": ">=10" + "node": ">=16" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" diff --git a/package.json b/package.json index 91426230..fabc6f7e 100644 --- a/package.json +++ b/package.json @@ -115,6 +115,7 @@ "stylelint-order": "^6.0.4", "stylelint-scss": "^6.4.1", "ts-node": "^10.9.2", + "type-fest": "^4.27.0", "typescript": "^5.5.4", "vite": "^5.4.6", "vite-plugin-dts": "^4.2.4", diff --git a/src/componentStyles.scss b/src/componentStyles.scss index a13cf570..fcc2d052 100644 --- a/src/componentStyles.scss +++ b/src/componentStyles.scss @@ -40,6 +40,11 @@ @use 'components/Tags/tags'; @use 'components/Countdown/countdown'; @use 'components/Countdown/duration'; +@use 'components/Detail/detail'; +@use 'components/PinchZoom/pinchZoom'; +@use 'components/Tabs/tabs'; +@use 'components/SeldonImage/seldonImage'; +@use 'components/Filter/filter'; // Patterns @use 'patterns/HeroBanner/heroBanner'; @@ -49,14 +54,11 @@ @use 'patterns/Subscribe/subscribe'; @use 'patterns/Social/social'; @use 'patterns/SaleHeaderBanner/saleHeaderBanner'; +@use 'patterns/ObjectTile/objectTile'; +@use 'patterns/BidSnapshot/bidSnapshot'; +@use 'patterns/FilterMenu/filterMenu'; +@use 'patterns/DetailList/detailList'; // Site Furniture @use 'site-furniture/Header/header'; @use 'site-furniture/Footer/footer'; -@use 'components/Detail/detail'; -@use 'patterns/DetailList/detailList'; -@use 'components/PinchZoom/pinchZoom'; -@use 'components/Tabs/tabs'; -@use 'components/SeldonImage/seldonImage'; -@use 'patterns/ObjectTile/objectTile'; -@use 'patterns/BidSnapshot/bidSnapshot'; diff --git a/src/components/DatePicker/DatePicker.tsx b/src/components/DatePicker/DatePicker.tsx index 3e7920bf..7a652078 100644 --- a/src/components/DatePicker/DatePicker.tsx +++ b/src/components/DatePicker/DatePicker.tsx @@ -6,7 +6,7 @@ import classnames from 'classnames'; import { noOp, useNormalizedInputProps } from '../../utils'; import Input, { InputProps } from '../Input/Input'; -export interface DatePickerProps extends Omit, Record { +export interface DatePickerProps extends Omit, Record { /** * Optionally allow manual entry to the date input */ @@ -181,12 +181,12 @@ const DatePicker = React.forwardRef( }; }, [allowInput, defaultValue, inputProps.disabled, enableTime, id, locale, onChange, ref, type]); - const handleKeyPress = (e: KeyboardEvent) => { + const handleKeyPress: React.KeyboardEventHandler = (e) => { if (allowInput && /[0-9-/:]+/g.test(e.key)) { manualValue.current = fp?.current?.selectedDates; } }; - const handOnBlur = (e: FocusEvent) => { + const handOnBlur: React.FocusEventHandler = (e) => { if ( !allowInput || (e.relatedTarget instanceof HTMLElement && e.relatedTarget?.classList.value.includes('flatpickr')) diff --git a/src/components/ErrorBoundary/ErrorBoundary.test.tsx b/src/components/ErrorBoundary/ErrorBoundary.test.tsx index 2c1af861..d2df7d37 100644 --- a/src/components/ErrorBoundary/ErrorBoundary.test.tsx +++ b/src/components/ErrorBoundary/ErrorBoundary.test.tsx @@ -13,7 +13,7 @@ const VolatileComponent = ({ throwError }: WrappedProps) => { throwError(); } }, [throwError]); - return ; + return ; }; describe('An ErrorBoundary', () => { diff --git a/src/components/Filter/Filter.stories.tsx b/src/components/Filter/Filter.stories.tsx new file mode 100644 index 00000000..c1be4423 --- /dev/null +++ b/src/components/Filter/Filter.stories.tsx @@ -0,0 +1,99 @@ +import { Meta } from '@storybook/react'; +import Filter from './Filter'; +import FilterHeader from './FilterHeader'; +import FilterInput from './FilterInput'; +import { useState } from 'react'; + +// More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction +const meta = { + title: 'Components/Filter', + component: Filter, +} satisfies Meta; + +export default meta; + +type FilterDimension = { label: string; disabled?: boolean | undefined; checked: boolean }; + +type FilterType = { + label: string; + id: string; + filterDimensions: FilterDimension[]; +}; + +type PropTypes = { + filter: FilterType; + viewAllLimit: number; + isViewingAll: boolean; +}; + +const filter: FilterType = { + label: 'Artists & Makers', + id: 'makers', + filterDimensions: [ + { label: 'Jimmy', checked: true }, + { label: 'Bob', checked: false }, + { label: 'Alan', checked: false }, + { label: 'Nick', checked: false }, + { label: 'Joe', checked: false }, + { label: 'Fred', checked: false }, + { label: 'Rob', checked: false }, + { label: 'Roy', checked: false }, + { label: 'disabled', disabled: true, checked: false }, + ], +}; + +const FilterComponent = (props: PropTypes) => { + const { filter: intialFilters, viewAllLimit, isViewingAll } = props; + const [filter, setFilter] = useState(intialFilters); + + const handleChange = (e: React.ChangeEvent) => { + const { checked, name } = e.target as HTMLInputElement; + const updatedFilterDimensions = filter.filterDimensions.map((dimension) => { + if (dimension.label === name) { + return { + ...dimension, + checked, + }; + } + return dimension; + }); + + const updatedFilter = { + ...filter, + filterDimensions: updatedFilterDimensions, + }; + + setFilter(updatedFilter); + }; + + return ( + + + {filter.filterDimensions.map((value: FilterDimension) => ( + + ))} + + ); +}; + +export const Playground = (props: PropTypes) => { + return ; +}; + +// More on writing stories with args: https://storybook.js.org/docs/react/writing-stories/args +Playground.args = { + filter, + viewAllLimit: 10, + isViewingAll: false, +}; + +Playground.argTypes = {}; diff --git a/src/components/Filter/Filter.test.tsx b/src/components/Filter/Filter.test.tsx new file mode 100644 index 00000000..413ecc46 --- /dev/null +++ b/src/components/Filter/Filter.test.tsx @@ -0,0 +1,74 @@ +import Filter from './Filter'; +import { runCommonTests } from '../../utils/testUtils'; +import { render, screen } from '@testing-library/react'; +import FilterHeader from './FilterHeader'; +import FilterInput from './FilterInput'; + +describe('Filter', () => { + runCommonTests(Filter, 'Filter'); + + it('renders the different input types of filter values', () => { + const handleChange = vi.fn(); + + render( + + + + , + ); + + expect(screen.getAllByText('Filter 1')[0]).toBeInTheDocument(); + expect(screen.getAllByText('Filter 2')[0]).toBeInTheDocument(); + + const checkbox = screen.getByRole('checkbox'); + const radio = screen.getByRole('radio'); + + expect(checkbox).toHaveAttribute('type', 'checkbox'); + expect(radio).toHaveAttribute('type', 'radio'); + }); + + it('renders a filter header', () => { + render( + + + , + ); + + expect(screen.getByText('Filter Header 1')).toBeInTheDocument(); + }); + + it('should disable filters when disabled prop is passed', () => { + const handleChange = vi.fn(); + + render( + + + , + ); + + const checkbox = screen.getByRole('checkbox'); + expect(checkbox).toBeDisabled(); + }); +}); diff --git a/src/components/Filter/Filter.tsx b/src/components/Filter/Filter.tsx new file mode 100644 index 00000000..42f5b84c --- /dev/null +++ b/src/components/Filter/Filter.tsx @@ -0,0 +1,117 @@ +import { + ComponentProps, + forwardRef, + Children, + cloneElement, + isValidElement, + useState, + Dispatch, + SetStateAction, +} from 'react'; +import { findChildrenExcludingTypes, findChildrenOfType, getCommonProps, px } from '../../utils'; +import classnames from 'classnames'; +import FilterHeader from './FilterHeader'; +import { FilterInputProps } from './FilterInput'; +import Button from '../Button/Button'; +import { ButtonVariants } from '../Button/types'; +import ChevronNextIcon from '../../assets/chevronNext.svg?react'; + +export interface FilterProps extends ComponentProps<'div'> { + /** Logical name of this filter */ + name: string; + + // Setter for values to display in view all + setViewAllFilter?: Dispatch>; + + // Number of values to display before truncating with view all button + viewAllLimit?: number; + + // Whether this is a view all filter or not + isViewingAll?: boolean; + + // Whether this filter is being hidden. Use when sibling is in view all state. + isHidden?: boolean; + /** + * translatable string for view all button + */ + viewAllLabel?: string; +} +/** + * ## Overview + * + * Component to display filtering dimensions + * + * [Figma Link](https://www.figma.com/design/OvBXAq48blO1r4qYbeBPjW/RW---Sale-Page-(PLP)?node-id=892-71019&node-type=frame&t=AsBDn4UgUEjNUnf7-0) + * + * [Storybook Link](https://phillips-seldon.netlify.app/?path=/docs/components-filter--overview) + */ +const Filter = forwardRef( + ( + { + className, + children, + name, + viewAllLimit = 10, + isViewingAll = false, + isHidden = false, + setViewAllFilter, + viewAllLabel = 'View All', + ...props + }, + ref, + ) => { + // filter "child" (view all state) is closing or not + const [isClosing, setIsClosing] = useState(false); + + const { className: baseClassName, ...commonProps } = getCommonProps(props, 'Filter'); + const childrenArray = Children.toArray(children); + + const headerProps = { isViewingAll, setViewAllFilter, setIsClosing }; + + const parsedFilterHeader = findChildrenOfType(childrenArray, FilterHeader)?.[0]; + const filterHeader = isValidElement(parsedFilterHeader) ? cloneElement(parsedFilterHeader, headerProps) : null; + + // this allows the component to be composable, while still passing down props from parent to child + // taking the children composed in the filter and constructing custom props based on state + const parsedFilterChildren = findChildrenExcludingTypes(childrenArray, [FilterHeader])?.map((child, index) => + isValidElement(child) + ? cloneElement(child, { hidden: !isViewingAll && index + 1 > viewAllLimit } as FilterInputProps) + : child, + ); + + return ( +
+
+ {filterHeader} +
{parsedFilterChildren}
+
+ {childrenArray.length > viewAllLimit && !isViewingAll ? ( + + ) : null} +
+ ); + }, +); + +Filter.displayName = 'Filter'; + +export type FilterComponent = ReturnType; + +export default Filter; diff --git a/src/components/Filter/FilterHeader.tsx b/src/components/Filter/FilterHeader.tsx new file mode 100644 index 00000000..aaa69be4 --- /dev/null +++ b/src/components/Filter/FilterHeader.tsx @@ -0,0 +1,60 @@ +import { ComponentProps, Dispatch, SetStateAction, forwardRef } from 'react'; +import { getCommonProps } from '../../utils'; +import classnames from 'classnames'; +import { Text, TextVariants } from '../Text'; +import Button from '../Button/Button'; +import { ButtonVariants } from '../Button/types'; +import ChevronNextIcon from '../../assets/chevronNext.svg?react'; + +export interface FilterHeaderProps extends ComponentProps<'div'> { + // Text to display as the header + heading: string; + + // Whether the show all back button should be displayed (when viewing all filters) + isViewingAll?: boolean; + + // Setter function for name of Filter to be displayed in View All + setViewAllFilter?: Dispatch>; + + // Setter to apply closing transition to view all filter + setIsClosing?: Dispatch>; +} +/** + * ## Overview + * + * The header of a filter + */ +const FilterHeader = forwardRef( + ({ className, heading, isViewingAll = false, setViewAllFilter, setIsClosing, ...props }, ref) => { + const { className: baseClassName, ...commonProps } = getCommonProps(props, 'FilterHeader'); + + const handleClose = () => { + setIsClosing?.(true); + setTimeout(() => { + setViewAllFilter?.(null); + setIsClosing?.(false); + // if this timeout changes, be sure to change $default-transition-duration in _filter.scss + }, 300); + }; + + return ( +
+ {heading}} + /> + {isViewingAll ? ( + + ) : null} +
+ ); + }, +); + +FilterHeader.displayName = 'FilterHeader'; + +export default FilterHeader; diff --git a/src/components/Filter/FilterInput.tsx b/src/components/Filter/FilterInput.tsx new file mode 100644 index 00000000..e12b8aaf --- /dev/null +++ b/src/components/Filter/FilterInput.tsx @@ -0,0 +1,38 @@ +import { forwardRef } from 'react'; +import { getCommonProps } from '../../utils'; +import Input, { InputProps } from '../Input/Input'; + +export interface FilterInputProps extends InputProps { + /** + * Type of input for this filter only radios or checkboxes allowed + */ + type: 'checkbox' | 'radio'; +} +/** + * ## Overview + * + * A label & input for filtering criteria + * + */ +const FilterInput = forwardRef( + ({ className, type = 'checkbox', name, hidden, onChange, ...props }, ref) => { + const { className: baseClassName } = getCommonProps(props, 'FilterInput'); + return ( + + ); + }, +); + +FilterInput.displayName = 'FilterValue'; + +export default FilterInput; diff --git a/src/components/Filter/_filter.scss b/src/components/Filter/_filter.scss new file mode 100644 index 00000000..5320275f --- /dev/null +++ b/src/components/Filter/_filter.scss @@ -0,0 +1,112 @@ +@use '#scss/allPartials' as *; + +$default-transition-duration: 0.3s; +$default-easing: ease-in-out; +$chevron-scale: 0.8; + +@keyframes slide-in { + from { + opacity: 0; + transform: translateX(100%); + } + + to { + opacity: 1; + transform: translateX(0); + } +} + +@keyframes slide-out { + from { + opacity: 1; + transform: translateX(0); + } + + to { + opacity: 0; + transform: translateX(100%); + } +} + +.#{$px}-filter { + &-header { + margin-bottom: $spacing-md; + + legend { + padding-inline: 0; // needed for default browser styles + } + + &__back { + @include text($body2); // workaround where the tertiary button text is not scaling at different breakpoints + + margin-top: $spacing-sm; + } + + &__chevron { + transform: rotateY(180deg) scale($chevron-scale); + } + } + + &__chevron { + transform: scale($chevron-scale); + } + + &__fieldset { + border: 0; + margin: 0 3px 0 0; // save room for input borders + padding: 0; + } + + &__filters { + display: flex; + flex-direction: column; + gap: $spacing-sm; + padding-bottom: $spacing-sm; + + .#{$px}-input { + align-items: center; + height: unset; + + &__label { + @include text($body2); + + color: inherit; + margin-bottom: 0; + } + + &__input { + align-self: center; + height: $body-line-height-size3; + margin-bottom: 0; + width: $body-line-height-size3; + } + + &__validation { + display: none; + } + } + } + + &__view-all { + @include text($body2); // workaround where the tertiary button text is not scaling at different breakpoints + } + + &.is-opening { + animation: slide-in $default-transition-duration $default-easing forwards; + } + + &.is-closing { + animation: slide-out $default-transition-duration $default-easing forwards; + } +} + +.#{$px}-filter-input { + &__input { + &__wrapper { + display: flex; + flex-direction: row; + height: 40px; + justify-content: space-between; + } + } +} diff --git a/src/components/Filter/index.ts b/src/components/Filter/index.ts new file mode 100644 index 00000000..f2fd90bd --- /dev/null +++ b/src/components/Filter/index.ts @@ -0,0 +1,3 @@ +export { default as Filter, type FilterProps } from './Filter'; +export { default as FilterInput, type FilterInputProps as FilterValueProps } from './FilterInput'; +export { default as FilterHeader, type FilterHeaderProps } from './FilterHeader'; diff --git a/src/components/Input/Input.stories.tsx b/src/components/Input/Input.stories.tsx index bc6f2beb..0aee24eb 100644 --- a/src/components/Input/Input.stories.tsx +++ b/src/components/Input/Input.stories.tsx @@ -1,6 +1,7 @@ import type { Meta } from '@storybook/react'; import Input, { InputProps } from './Input'; import { useState } from 'react'; +import Button from '../Button/Button'; // More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction const meta = { title: 'Components/Input', @@ -88,7 +89,7 @@ const argTypes = { export const DateTimeInput = ({ playgroundWidth, ...args }: StoryProps) => (
- +
); @@ -109,14 +110,14 @@ DateTimeInput.args = { export const RadioInput = ({ playgroundWidth, ...args }: StoryProps) => (
- +
); @@ -136,8 +137,8 @@ RadioInput.args = { export const CheckboxInput = ({ playgroundWidth, ...args }: StoryProps) => (
- - + +
); @@ -159,7 +160,7 @@ CheckboxInput.argTypes = argTypes; export const RangeInput = ({ playgroundWidth, ...args }: StoryProps) => (
- +
); @@ -188,7 +189,7 @@ export const ControlledInput = ({ playgroundWidth, ...args }: StoryProps) => { return (
( -
- -
-); +export const Playground = ({ playgroundWidth, ...args }: StoryProps) => { + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + const formData = new FormData(e.currentTarget); + const entries: string[] = []; + for (const entry of formData.entries()) { + entries.push(`${entry[0]}: ${entry[1]}`); + } + alert(`Form submitted ${entries.join('\n')}`); + }; + + return ( +
+
+ + + + + +
+
+ ); +}; Playground.args = { playgroundWidth: 300, @@ -227,7 +246,6 @@ Playground.args = { invalid: false, invalidText: 'Error message', disabled: false, - defaultValue: 'My values', labelText: 'Label text', warn: false, warnText: 'Warning message that is really long can wrap to more lines.', diff --git a/src/components/Input/Input.test.tsx b/src/components/Input/Input.test.tsx index d9834339..64b88d17 100644 --- a/src/components/Input/Input.test.tsx +++ b/src/components/Input/Input.test.tsx @@ -7,7 +7,7 @@ import Input, { InputProps } from './Input'; // NOTE: When using default value on controlled input we pass to the state hook and not the input const TestInput = React.forwardRef( ({ defaultValue, ...props }: InputProps, ref: React.ForwardedRef) => { - const [value, setValue] = React.useState((defaultValue as string) ?? ''); + const [value, setValue] = React.useState((defaultValue as string) ?? ''); const handleOnChange = function (e: React.ChangeEvent) { setValue(e?.currentTarget?.value); }; diff --git a/src/components/Input/Input.tsx b/src/components/Input/Input.tsx index 4674d6af..2b5641fc 100644 --- a/src/components/Input/Input.tsx +++ b/src/components/Input/Input.tsx @@ -3,7 +3,7 @@ import classnames from 'classnames'; import { px, useNormalizedInputProps } from '../../utils'; -export interface InputProps extends Record { +export interface InputProps extends Omit, 'size'> { /** * Optional className to be applied to the `` node */ @@ -12,7 +12,7 @@ export interface InputProps extends Record { /** * Optionally provide the default value of the ``. Should not be passed into controlled input! */ - defaultValue?: string | number; + defaultValue?: string | number | readonly string[]; /** * Booolean to specify whether the `` should be disabled @@ -27,7 +27,7 @@ export interface InputProps extends Record { /** * A custom `id` for the `` */ - id: string; + id?: string; /** * Boolean to dictate whether input is inline with the label or not. `true` to use the inline version. @@ -52,7 +52,7 @@ export interface InputProps extends Record { /** * Optional `onChange` handler that is called whenever `` is updated */ - onChange?: (event: React.ChangeEvent | React.ChangeEvent) => void; + onChange?: (event: React.ChangeEvent) => void; /** * Optional `onClick` handler that is called whenever the `` is clicked @@ -82,7 +82,7 @@ export interface InputProps extends Record { /** * Specify the value of the `` for controlled inputs */ - value?: string | number | undefined; + value?: string | number | undefined | readonly string[]; /** * Boolean to specify whether the control is currently in warning state @@ -134,7 +134,17 @@ const Input = React.forwardRef( }: InputProps, ref: React.ForwardedRef, ) => { - const inputProps = useNormalizedInputProps({ disabled, id, invalid, invalidText, readOnly, type, warn, warnText }); + const generatedId = React.useId(); + const inputProps = useNormalizedInputProps({ + disabled, + id: id || generatedId, + invalid, + invalidText, + readOnly, + type, + warn, + warnText, + }); const wrapperClassnames = classnames(`${px}-${type}-input`, `${px}-input`, `${px}-input--${size}`, { [`${px}-input--inline`]: inline, @@ -147,23 +157,25 @@ const Input = React.forwardRef( }); return (
-