Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 101 additions & 0 deletions packages/ui/src/components/Filters/Filters.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -279,3 +279,104 @@ export const WithSearchLeading: Story = {
hasLeadingItem: true,
},
};

export const InlineVariant: Story = {
args: {
filters: basicFilters,
initialValues: {
category: '',
price: '',
rating: [],
sort: 'relevance',
search: '',
},
hasLeadingItem: false,
},
render: ({ filters, initialValues }) => {
// eslint-disable-next-line react-hooks/rules-of-hooks
const [currentFilters, setCurrentFilters] = useState<typeof initialValues>(initialValues);

const handleSubmit = (values: Partial<typeof initialValues>) => {
console.log('Filters submitted:', values);
setCurrentFilters({
...currentFilters,
...values,
});
};

const handleReset = () => {
console.log('Filters reset');
setCurrentFilters(initialValues);
};

return (
<div className="space-y-4">
<FiltersContextProvider initialFilters={initialValues}>
<div className="p-4 border rounded-md mb-4">
<h3 className="text-sm font-semibold mb-2">Current Filters:</h3>
<pre className="text-xs bg-gray-100 p-2 rounded">{JSON.stringify(currentFilters, null, 2)}</pre>
</div>

<Filters
filters={filters}
initialValues={initialValues}
onSubmit={handleSubmit}
onReset={handleReset}
hasLeadingItem={false}
variant="inline"
Comment thread
marcinkrasowski marked this conversation as resolved.
labels={{ clickToSelect: 'Select an option' }}
/>
</FiltersContextProvider>
</div>
);
},
};

export const InlineVariantWithSection: Story = {
args: {
filters: basicFilters,
initialValues: {
category: '',
price: '',
rating: [],
sort: 'relevance',
search: '',
},
hasLeadingItem: false,
},
render: ({ filters, initialValues }) => {
// eslint-disable-next-line react-hooks/rules-of-hooks
const [currentFilters, setCurrentFilters] = useState<typeof initialValues>(initialValues);

const handleSubmit = (values: Partial<typeof initialValues>) => {
console.log('Filters submitted:', values);
setCurrentFilters({
...currentFilters,
...values,
});
};

const handleReset = () => {
console.log('Filters reset');
setCurrentFilters(initialValues);
};

return (
<div className="space-y-4">
<FiltersSection
title="Filter Products (Inline)"
initialFilters={initialValues}
filters={filters}
initialValues={initialValues}
onSubmit={handleSubmit}
onReset={handleReset}
variant="inline"
/>
<div className="p-4 border rounded-md mb-4">
<h3 className="text-sm font-semibold mb-2">Current Filters:</h3>
<pre className="text-xs bg-gray-100 p-2 rounded">{JSON.stringify(currentFilters, null, 2)}</pre>
</div>
</div>
);
},
};
Comment thread
marcinkrasowski marked this conversation as resolved.
103 changes: 72 additions & 31 deletions packages/ui/src/components/Filters/Filters.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,27 +16,26 @@ import { useFiltersContext } from './FiltersContext';

function separateLeadingItem<T>(items: Models.Filters.FilterItem<T>[]) {
let leadingItem: Models.Filters.FilterItem<T> | undefined;
let viewModeToggle: Models.Filters.FilterViewModeToggle | undefined;
const filteredItems: Models.Filters.FilterItem<T>[] = [];

for (const item of items) {
if (item.__typename === 'FilterViewModeToggle') {
viewModeToggle = item;
} else if (item.isLeading === true && leadingItem === undefined) {
if ('isLeading' in item && item.isLeading === true && leadingItem === undefined) {
leadingItem = item;
} else {
filteredItems.push(item);
}
}

return { leadingItem, viewModeToggle, filteredItems };
return { leadingItem, filteredItems };
Comment thread
marcinkrasowski marked this conversation as resolved.
}

export const Filters = <T, S extends FormikValues>({
filters,
initialValues,
onSubmit,
onReset,
hasLeadingItem,
variant = 'drawer',
labels,
}: Readonly<FiltersProps<T, S>>) => {
const [filtersOpen, setFiltersOpen] = useState(false);
Expand All @@ -48,14 +47,67 @@ export const Filters = <T, S extends FormikValues>({

const { label, title, description, submit, reset, items, removeFilters } = filters;

const { leadingItem, viewModeToggle, filteredItems } = separateLeadingItem(items);
const { leadingItem, filteredItems } = hasLeadingItem
? separateLeadingItem(items)
: { leadingItem: undefined, filteredItems: items };

const handleReset = (e: React.MouseEvent) => {
e.preventDefault();
countActiveFilters(initialFilters);
onReset();
};

// Inline variant: render filters directly without drawer
if (variant === 'inline') {
return (
<div className="w-full">
<Formik<S>
initialValues={initialValues}
enableReinitialize={true}
onSubmit={(values) => {
countActiveFilters(values);
onSubmit(values);
}}
>
{({ submitForm, setFieldValue }) => (
<Form>
<div className="flex flex-col gap-4">
Comment thread
marcinkrasowski marked this conversation as resolved.
{/* Filters container - grid layout for desktop, full-width rows for mobile */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
{items.map((item) => (
Copy link

Copilot AI Dec 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The inline variant is using items directly instead of filteredItems. This means:

  1. When hasLeadingItem is true, the leading item will be rendered twice (once in the inline grid and potentially elsewhere)
  2. The separation logic in separateLeadingItem is being bypassed

The drawer variant correctly uses filteredItems (line 170) to exclude the leading item from the drawer content. The inline variant should use filteredItems here to maintain consistent behavior.

Suggested change
{items.map((item) => (
{filteredItems.map((item) => (

Copilot uses AI. Check for mistakes.
<div key={String(item.id)} className="w-full">
<FilterItem
item={item}
setFieldValue={setFieldValue}
submitForm={submitForm}
labels={labels}
/>
</div>
))}
</div>
{/* Action buttons - right-aligned on desktop, stretched on mobile */}
<div className="flex flex-col sm:flex-row gap-4 sm:justify-end">
<Button
type="button"
variant="secondary"
onClick={handleReset}
className="w-full sm:w-auto sm:max-w-[50%]"
>
{reset}
</Button>
<Button type="submit" className="w-full sm:w-auto sm:max-w-[50%]">
{submit}
</Button>
</div>
</div>
</Form>
)}
</Formik>
</div>
);
}
Comment thread
marcinkrasowski marked this conversation as resolved.

// Drawer variant: original implementation
return (
<div className={cn(leadingItem ? 'w-full' : 'w-full sm:w-auto')}>
<Formik<S>
Expand All @@ -70,31 +122,20 @@ export const Filters = <T, S extends FormikValues>({
{({ submitForm, setFieldValue }) => (
<>
<div className="flex flex-col justify-between items-center w-full gap-6 md:flex-row">
<div className="flex items-center gap-4 w-full md:w-auto">
{leadingItem && (
<div className="overflow-hidden rounded-md">
<ScrollContainer className="scroll-container flex whitespace-nowrap items-center gap-4">
<FilterItem
key={String(leadingItem.id)}
item={leadingItem}
submitForm={submitForm}
setFieldValue={setFieldValue}
isLeading={true}
labels={labels}
/>
</ScrollContainer>
</div>
)}
{viewModeToggle && (
<FilterItem
item={viewModeToggle}
submitForm={submitForm}
setFieldValue={setFieldValue}
labels={labels}
/>
)}
</div>
<div className="flex gap-4 flex-col-reverse w-full sm:flex-row md:w-auto">
{leadingItem !== undefined && (
<div className="w-full md:w-auto overflow-hidden rounded-md">
<ScrollContainer className="scroll-container flex whitespace-nowrap w-full items-center gap-4">
<FilterItem
item={leadingItem}
submitForm={submitForm}
setFieldValue={setFieldValue}
isLeading={true}
labels={labels}
/>
</ScrollContainer>
</div>
)}
<div className="flex gap-4 flex-col w-full sm:flex-row md:w-auto">
Comment thread
marcinkrasowski marked this conversation as resolved.
{activeFilters > 0 && (
<Button variant="outline" onClick={handleReset} className="gap-0">
<X className="h-4 w-4 mr-2" />
Expand Down
1 change: 1 addition & 0 deletions packages/ui/src/components/Filters/Filters.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export interface FiltersProps<T, S> {
onSubmit: (filters: Partial<S>) => void;
onReset: () => void;
hasLeadingItem?: boolean;
variant?: 'drawer' | 'inline';
labels?: {
clickToSelect?: string;
};
Expand Down
3 changes: 3 additions & 0 deletions packages/ui/src/components/Filters/FiltersSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export const FiltersSection = <T, S extends FormikValues>({
initialValues,
onSubmit,
onReset,
variant,
labels,
}: Readonly<FiltersSectionProps<T, S>>) => {
const hasLeadingItem = filters?.items.some((item) => 'isLeading' in item && item.isLeading === true);
Expand All @@ -41,6 +42,8 @@ export const FiltersSection = <T, S extends FormikValues>({
initialValues={initialValues}
onSubmit={onSubmit}
onReset={onReset}
hasLeadingItem={hasLeadingItem}
variant={variant}
labels={labels}
/>
</FiltersContextProvider>
Expand Down
Loading