-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(filter): create dumb filter component
- Loading branch information
Zachary Rose
authored and
Zachary Rose
committed
Nov 6, 2024
1 parent
ecd7048
commit ce8be65
Showing
14 changed files
with
700 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,71 @@ | ||
import { Meta } from '@storybook/react'; | ||
import Filter from './Filter'; | ||
import FilterHeader from './FilterHeader'; | ||
import FilterValue from './FilterValue'; | ||
|
||
// 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<typeof Filter>; | ||
|
||
export default meta; | ||
|
||
type FilterDimension = { label: string; disabled?: boolean | undefined }; | ||
|
||
type FilterType = { | ||
label: string; | ||
filterDimensions: FilterDimension[]; | ||
}; | ||
|
||
type PropTypes = { | ||
filter: FilterType; | ||
viewAllLimit: number; | ||
isLast: boolean; | ||
isViewingAll: boolean; | ||
}; | ||
|
||
const filter: FilterType = { | ||
label: 'Artists & Makers', | ||
filterDimensions: [ | ||
{ label: 'Jimmy' }, | ||
{ label: 'Bob' }, | ||
{ label: 'Alan' }, | ||
{ label: 'Nick' }, | ||
{ label: 'Joe' }, | ||
{ label: 'Fred' }, | ||
{ label: 'Rob' }, | ||
{ label: 'Roy' }, | ||
{ label: 'disabled', disabled: true }, | ||
], | ||
}; | ||
|
||
export const Playground = (props: PropTypes) => { | ||
const { filter, viewAllLimit, isLast, isViewingAll } = props; | ||
return ( | ||
<Filter key={filter.label} isLast={isLast} viewAllLimit={viewAllLimit} viewingAll={isViewingAll}> | ||
<FilterHeader label={filter.label} /> | ||
{filter.filterDimensions.map((value: FilterDimension) => ( | ||
<FilterValue | ||
key={value.label} | ||
label={value.label} | ||
onChange={(e) => { | ||
e; | ||
}} | ||
inputType="checkbox" | ||
disabled={value?.disabled} | ||
/> | ||
))} | ||
</Filter> | ||
); | ||
}; | ||
|
||
// More on writing stories with args: https://storybook.js.org/docs/react/writing-stories/args | ||
Playground.args = { | ||
filter, | ||
viewAllLimit: 10, | ||
isLast: true, | ||
isViewingAll: false, | ||
}; | ||
|
||
Playground.argTypes = {}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
import Filter from './Filter'; | ||
import { runCommonTests } from '../../utils/testUtils'; | ||
import { render, screen } from '@testing-library/react'; | ||
import FilterHeader from './FilterHeader'; | ||
import FilterValue from './FilterValue'; | ||
|
||
describe('Filter', () => { | ||
runCommonTests(Filter, 'Filter'); | ||
|
||
it('renders the different input types of filter values', () => { | ||
const handleChange = vi.fn(); | ||
|
||
render( | ||
<Filter> | ||
<FilterValue label="Filter 1" inputType="checkbox" onChange={handleChange} /> | ||
<FilterValue label="Filter 2" inputType="radio" onChange={handleChange} /> | ||
</Filter>, | ||
); | ||
|
||
expect(screen.getByText('Filter 1')).toBeInTheDocument(); | ||
expect(screen.getByText('Filter 2')).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( | ||
<Filter> | ||
<FilterHeader label="Filter Header 1" /> | ||
</Filter>, | ||
); | ||
|
||
expect(screen.getByText('Filter Header 1')).toBeInTheDocument(); | ||
}); | ||
|
||
it('should disable filters when disabled prop is passed', () => { | ||
const handleChange = vi.fn(); | ||
|
||
render( | ||
<Filter> | ||
<FilterValue label="Filter 1" inputType="checkbox" onChange={handleChange} disabled /> | ||
</Filter>, | ||
); | ||
|
||
const checkbox = screen.getByRole('checkbox'); | ||
expect(screen.getByText('Filter 1')).toHaveClass('disabled-label'); | ||
expect(checkbox).toBeDisabled(); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,85 @@ | ||
import { | ||
ComponentProps, | ||
forwardRef, | ||
Children, | ||
cloneElement, | ||
isValidElement, | ||
ReactNode, | ||
Dispatch, | ||
SetStateAction, | ||
} from 'react'; | ||
import { getCommonProps, px } from '../../utils'; | ||
import classnames from 'classnames'; | ||
import FilterHeader, { FilterHeaderProps } from './FilterHeader'; | ||
import { FilterValueProps } from './FilterValue'; | ||
import Button from '../Button/Button'; | ||
import { ButtonVariants } from '../Button/types'; | ||
import ChevronNextIcon from '../../assets/chevronNext.svg?react'; | ||
|
||
// You'll need to change the ComponentProps<"htmlelementname"> to match the top-level element of your component | ||
export interface FilterProps extends ComponentProps<'div'> { | ||
children: ReactNode; | ||
|
||
// If true, do not include bottom border. True by default, do not define if you do not wish to use separators. | ||
isLast?: boolean; | ||
|
||
// Setter for values to display in view all | ||
setViewAllFilter?: Dispatch<SetStateAction<ReactNode[]>>; | ||
|
||
// Number of values to display before truncating with view all button | ||
viewAllLimit?: number; | ||
|
||
// Whether this is a view all filter or not | ||
viewingAll?: boolean; | ||
} | ||
/** | ||
* ## 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<HTMLDivElement, FilterProps>( | ||
({ className, children, isLast = true, viewAllLimit = 10, viewingAll = false, setViewAllFilter, ...props }, ref) => { | ||
const { className: baseClassName, ...commonProps } = getCommonProps(props, 'Filter'); | ||
const childrenArray = Children.toArray(children); | ||
|
||
return ( | ||
<div | ||
{...commonProps} | ||
className={classnames(baseClassName, className, { | ||
[`${px}-has-separators`]: !isLast, | ||
})} | ||
{...props} | ||
ref={ref} | ||
> | ||
{childrenArray.map((child, index) => | ||
isValidElement(child) | ||
? child.type === FilterHeader | ||
? cloneElement(child, { showBack: viewingAll, setViewAllFilter } as FilterHeaderProps) | ||
: cloneElement(child, { isHidden: !viewingAll && index > viewAllLimit } as FilterValueProps) | ||
: child, | ||
)} | ||
{childrenArray.length > viewAllLimit && !viewingAll ? ( | ||
<Button | ||
variant={ButtonVariants.tertiary} | ||
onClick={() => { | ||
setViewAllFilter && setViewAllFilter(childrenArray); | ||
}} | ||
> | ||
{`View All`} | ||
<ChevronNextIcon className={`${baseClassName}__chevron`} /> | ||
</Button> | ||
) : null} | ||
</div> | ||
); | ||
}, | ||
); | ||
|
||
Filter.displayName = 'Filter'; | ||
|
||
export type FilterComponent = ReturnType<typeof Filter>; | ||
|
||
export default Filter; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
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 | ||
label: string; | ||
|
||
// Whether the show all back button should be displayed (when viewing all filters) | ||
showBack?: boolean; | ||
|
||
// Setter function for info to be displayed in View All | ||
setViewAllFilter?: Dispatch<SetStateAction<[]>>; | ||
} | ||
/** | ||
* ## Overview | ||
* | ||
* The header of a filter | ||
*/ | ||
const FilterHeader = forwardRef<HTMLDivElement, FilterHeaderProps>( | ||
({ className, label, showBack = false, setViewAllFilter, ...props }, ref) => { | ||
const { className: baseClassName, ...commonProps } = getCommonProps(props, 'FilterHeader'); | ||
|
||
return ( | ||
<div {...commonProps} className={classnames(baseClassName, className)} {...props} ref={ref}> | ||
<Text variant={TextVariants.heading4} className={`${baseClassName}__label`}> | ||
{label} | ||
</Text> | ||
{showBack ? ( | ||
<Button | ||
variant={ButtonVariants.tertiary} | ||
onClick={() => { | ||
setViewAllFilter && setViewAllFilter([]); | ||
}} | ||
> | ||
<ChevronNextIcon className={`${baseClassName}__chevron`} /> | ||
{`Back to all`} | ||
</Button> | ||
) : null} | ||
</div> | ||
); | ||
}, | ||
); | ||
|
||
FilterHeader.displayName = 'FilterHeader'; | ||
|
||
export default FilterHeader; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
import { ComponentProps, forwardRef } from 'react'; | ||
import { getCommonProps } from '../../utils'; | ||
import classnames from 'classnames'; | ||
import { Text, TextVariants } from '../Text'; | ||
import Input from '../Input/Input'; | ||
|
||
// You'll need to change the ComponentProps<"htmlelementname"> to match the top-level element of your component | ||
export interface FilterValueProps extends Omit<ComponentProps<'div'>, 'onChange'> { | ||
// Text to be displayed as a label for the neighboring input | ||
label: string; | ||
|
||
// Type of input for this filter | ||
inputType: 'checkbox' | 'radio'; | ||
|
||
// Should this filter be hidden, most common use will be for view all truncation | ||
isHidden?: boolean; | ||
|
||
// Whether or not this filter is disabled | ||
disabled?: boolean; | ||
|
||
// Function to trigger when the state of this filter is changed | ||
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void; | ||
} | ||
/** | ||
* ## Overview | ||
* | ||
* A label & input for filtering criteria | ||
* | ||
*/ | ||
const FilterValue = forwardRef<HTMLDivElement, FilterValueProps>( | ||
({ className, label, inputType, isHidden = false, onChange, disabled, ...props }, ref) => { | ||
const { className: baseClassName, ...commonProps } = getCommonProps(props, 'FilterValue'); | ||
const disabledLabel = disabled ? 'disabled-label' : ''; | ||
return ( | ||
<> | ||
{isHidden ? null : ( | ||
<div {...commonProps} className={classnames(baseClassName, className)} {...props} ref={ref}> | ||
<Text variant={TextVariants.body2} className={`${baseClassName}__label ${disabledLabel}`}> | ||
{label} | ||
</Text> | ||
<Input | ||
disabled={disabled} | ||
type={inputType} | ||
className={`${baseClassName}__input`} | ||
onChange={onChange} | ||
hideLabel={true} | ||
size="lg" | ||
value={label} | ||
/> | ||
</div> | ||
)} | ||
</> | ||
); | ||
}, | ||
); | ||
|
||
FilterValue.displayName = 'FilterValue'; | ||
|
||
export default FilterValue; |
Oops, something went wrong.