Skip to content

Commit

Permalink
feat(filter): L3-4114 updates per code review; working filter in story
Browse files Browse the repository at this point in the history
  • Loading branch information
Zachary Rose authored and Zachary Rose committed Nov 18, 2024
1 parent ce8be65 commit f48ad97
Show file tree
Hide file tree
Showing 10 changed files with 649 additions and 180 deletions.
68 changes: 51 additions & 17 deletions src/components/Filter/Filter.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Meta } from '@storybook/react';
import Filter from './Filter';
import FilterHeader from './FilterHeader';
import FilterValue from './FilterValue';
import { useState } from 'react';

// More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction
const meta = {
Expand All @@ -11,10 +12,11 @@ const meta = {

export default meta;

type FilterDimension = { label: string; disabled?: boolean | undefined };
type FilterDimension = { label: string; disabled?: boolean | undefined; active: boolean };

type FilterType = {
label: string;
id: string;
filterDimensions: FilterDimension[];
};

Expand All @@ -27,39 +29,71 @@ type PropTypes = {

const filter: FilterType = {
label: 'Artists & Makers',
id: 'makers',
filterDimensions: [
{ label: 'Jimmy' },
{ label: 'Bob' },
{ label: 'Alan' },
{ label: 'Nick' },
{ label: 'Joe' },
{ label: 'Fred' },
{ label: 'Rob' },
{ label: 'Roy' },
{ label: 'disabled', disabled: true },
{ label: 'Jimmy', active: true },
{ label: 'Bob', active: false },
{ label: 'Alan', active: false },
{ label: 'Nick', active: false },
{ label: 'Joe', active: false },
{ label: 'Fred', active: false },
{ label: 'Rob', active: false },
{ label: 'Roy', active: false },
{ label: 'disabled', disabled: false, active: false },
],
};

export const Playground = (props: PropTypes) => {
const { filter, viewAllLimit, isLast, isViewingAll } = props;
const FilterComponent = (props: PropTypes) => {
const { filter: intialFilters, viewAllLimit, isLast, isViewingAll } = props;
const [filter, setFilter] = useState(intialFilters);

const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { checked, name } = e.target;
const updatedFilterDimensions = filter.filterDimensions.map((dimension) => {
if (dimension.label === name) {
return {
...dimension,
active: checked,
};
}
return dimension;
});

const updatedFilter = {
...filter,
filterDimensions: updatedFilterDimensions,
};

setFilter(updatedFilter);
};
return (
<Filter key={filter.label} isLast={isLast} viewAllLimit={viewAllLimit} viewingAll={isViewingAll}>
<FilterHeader label={filter.label} />
<Filter
key={filter.label}
name={filter.label}
isLast={isLast}
viewAllLimit={viewAllLimit}
isViewingAll={isViewingAll}
>
<FilterHeader heading={filter.label} />
{filter.filterDimensions.map((value: FilterDimension) => (
<FilterValue
key={value.label}
label={value.label}
onChange={(e) => {
e;
}}
onChange={handleChange}
inputType="checkbox"
disabled={value?.disabled}
name={value.label}
isActive={value.active}
/>
))}
</Filter>
);
};

export const Playground = (props: PropTypes) => {
return <FilterComponent {...props} />;
};

// More on writing stories with args: https://storybook.js.org/docs/react/writing-stories/args
Playground.args = {
filter,
Expand Down
24 changes: 16 additions & 8 deletions src/components/Filter/Filter.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { runCommonTests } from '../../utils/testUtils';
import { render, screen } from '@testing-library/react';
import FilterHeader from './FilterHeader';
import FilterValue from './FilterValue';
import { px } from '../../utils';

describe('Filter', () => {
runCommonTests(Filter, 'Filter');
Expand All @@ -11,9 +12,9 @@ describe('Filter', () => {
const handleChange = vi.fn();

render(
<Filter>
<FilterValue label="Filter 1" inputType="checkbox" onChange={handleChange} />
<FilterValue label="Filter 2" inputType="radio" onChange={handleChange} />
<Filter name="test">
<FilterValue name="test[test1]" label="Filter 1" inputType="checkbox" onChange={handleChange} isActive={true} />
<FilterValue name="test[test2]" label="Filter 2" inputType="radio" onChange={handleChange} isActive={true} />
</Filter>,
);

Expand All @@ -29,8 +30,8 @@ describe('Filter', () => {

it('renders a filter header', () => {
render(
<Filter>
<FilterHeader label="Filter Header 1" />
<Filter name="test">
<FilterHeader heading="Filter Header 1" />
</Filter>,
);

Expand All @@ -41,13 +42,20 @@ describe('Filter', () => {
const handleChange = vi.fn();

render(
<Filter>
<FilterValue label="Filter 1" inputType="checkbox" onChange={handleChange} disabled />
<Filter name="test">
<FilterValue
name="test[test1]"
label="Filter 1"
inputType="checkbox"
onChange={handleChange}
disabled
isActive={true}
/>
</Filter>,
);

const checkbox = screen.getByRole('checkbox');
expect(screen.getByText('Filter 1')).toHaveClass('disabled-label');
expect(screen.getByText('Filter 1')).toHaveClass(`${px}-filter-value-disabled-label`);
expect(checkbox).toBeDisabled();
});
});
63 changes: 48 additions & 15 deletions src/components/Filter/Filter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
ReactNode,
Dispatch,
SetStateAction,
useState,
} from 'react';
import { getCommonProps, px } from '../../utils';
import classnames from 'classnames';
Expand All @@ -20,17 +21,22 @@ import ChevronNextIcon from '../../assets/chevronNext.svg?react';
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.
name: string;

// If true, do not include bottom border. True by default, do not define if you do not wish to use separators or rendering alone.
isLast?: boolean;

// Setter for values to display in view all
setViewAllFilter?: Dispatch<SetStateAction<ReactNode[]>>;
setViewAllFilter?: Dispatch<SetStateAction<string>>;

// Number of values to display before truncating with view all button
viewAllLimit?: number;

// Whether this is a view all filter or not
viewingAll?: boolean;
isViewingAll?: boolean;

// Whether this filter is being hidden. Use when sibling is in view all state.
isHidden?: boolean;
}
/**
* ## Overview
Expand All @@ -42,34 +48,61 @@ export interface FilterProps extends ComponentProps<'div'> {
* [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) => {
(
{
className,
children,
name,
isLast = true,
viewAllLimit = 10,
isViewingAll = false,
isHidden = false,
setViewAllFilter,
...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 };

// 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 = childrenArray.map((child, index) =>
isValidElement(child)
? child.type === FilterHeader
? cloneElement(child, headerProps as FilterHeaderProps)
: cloneElement(child, { isHidden: !isViewingAll && index > viewAllLimit } as FilterValueProps)
: child,
);

return (
<div
{...commonProps}
className={classnames(baseClassName, className, {
[`${px}-has-separators`]: !isLast,
[`${px}-has-separators`]: !isLast && !isViewingAll,
[`${px}-filter--hidden`]: isHidden,
'is-closing': isClosing,
})}
{...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 ? (
{/* TODO: REMOVE INLINE STYLING */}
<fieldset name={name} style={{ border: 'none', padding: 0 }}>
{parsedFilterChildren}
</fieldset>
{childrenArray.length > viewAllLimit && !isViewingAll ? (
<Button
variant={ButtonVariants.tertiary}
onClick={() => {
setViewAllFilter && setViewAllFilter(childrenArray);
setViewAllFilter?.(name);
}}
>
{`View All`}
View All
<ChevronNextIcon className={`${baseClassName}__chevron`} />
</Button>
) : null}
Expand Down
41 changes: 25 additions & 16 deletions src/components/Filter/FilterHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,37 +8,46 @@ import ChevronNextIcon from '../../assets/chevronNext.svg?react';

export interface FilterHeaderProps extends ComponentProps<'div'> {
// Text to display as the header
label: string;
heading: string;

// Whether the show all back button should be displayed (when viewing all filters)
showBack?: boolean;
isViewingAll?: boolean;

// Setter function for info to be displayed in View All
setViewAllFilter?: Dispatch<SetStateAction<[]>>;
// Setter function for name of Filter to be displayed in View All
setViewAllFilter?: Dispatch<SetStateAction<null>>;

// Setter to apply closing transition to view all filter
setIsClosing?: Dispatch<SetStateAction<boolean>>;
}
/**
* ## Overview
*
* The header of a filter
*/
const FilterHeader = forwardRef<HTMLDivElement, FilterHeaderProps>(
({ className, label, showBack = false, setViewAllFilter, ...props }, ref) => {
({ 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 (
<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([]);
}}
>
<Text
variant={TextVariants.heading4}
className={`${baseClassName}__heading`}
element={(props) => <legend {...props}>{heading}</legend>}
/>
{isViewingAll ? (
<Button variant={ButtonVariants.tertiary} onClick={handleClose} className={`${baseClassName}__back`}>
<ChevronNextIcon className={`${baseClassName}__chevron`} />
{`Back to all`}
Back to all
</Button>
) : null}
</div>
Expand Down
Loading

0 comments on commit f48ad97

Please sign in to comment.