-
Notifications
You must be signed in to change notification settings - Fork 0
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(filter): L3-4114 - create dumb filter component #418
Changes from 1 commit
ce8be65
f48ad97
54aae48
ab7b24c
011799d
bf98d83
7b36d20
466c61d
5a7e104
545cbdf
76ff63a
611a423
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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 = {}; |
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(); | ||
}); | ||
}); |
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; | ||
Zach-OfAllTrades marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
/** | ||
* ## 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 | ||
Zach-OfAllTrades marked this conversation as resolved.
Show resolved
Hide resolved
|
||
{...commonProps} | ||
className={classnames(baseClassName, className, { | ||
[`${px}-has-separators`]: !isLast, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. not really clear, why do we need this separator? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. okay I see now, maybe we should There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When there are multiple filters, the design shows there is a separator between them. This Filter component, for the majority of cases I would think, would be implemented alongside other Filters. I created the FilterControl component to wrap and manage these. |
||
})} | ||
{...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, | ||
)} | ||
Zach-OfAllTrades marked this conversation as resolved.
Show resolved
Hide resolved
|
||
{childrenArray.length > viewAllLimit && !viewingAll ? ( | ||
Zach-OfAllTrades marked this conversation as resolved.
Show resolved
Hide resolved
|
||
<Button | ||
variant={ButtonVariants.tertiary} | ||
onClick={() => { | ||
setViewAllFilter && setViewAllFilter(childrenArray); | ||
Zach-OfAllTrades marked this conversation as resolved.
Show resolved
Hide resolved
|
||
}} | ||
> | ||
{`View All`} | ||
Zach-OfAllTrades marked this conversation as resolved.
Show resolved
Hide resolved
|
||
<ChevronNextIcon className={`${baseClassName}__chevron`} /> | ||
</Button> | ||
) : null} | ||
</div> | ||
); | ||
}, | ||
); | ||
|
||
Filter.displayName = 'Filter'; | ||
|
||
export type FilterComponent = ReturnType<typeof Filter>; | ||
|
||
export default Filter; |
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}> | ||
Zach-OfAllTrades marked this conversation as resolved.
Show resolved
Hide resolved
|
||
<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; |
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'> { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just an FYI this would be a good use of a Type Generic where the |
||
// 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}`}> | ||
Zach-OfAllTrades marked this conversation as resolved.
Show resolved
Hide resolved
|
||
{label} | ||
</Text> | ||
<Input | ||
disabled={disabled} | ||
type={inputType} | ||
className={`${baseClassName}__input`} | ||
onChange={onChange} | ||
hideLabel={true} | ||
size="lg" | ||
value={label} | ||
Zach-OfAllTrades marked this conversation as resolved.
Show resolved
Hide resolved
|
||
/> | ||
</div> | ||
)} | ||
</> | ||
); | ||
}, | ||
); | ||
|
||
FilterValue.displayName = 'FilterValue'; | ||
|
||
export default FilterValue; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
not sure why we need this
e
here