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
6 changes: 3 additions & 3 deletions src/components/filters/FilterDropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ export function FilterDropdown(props: Readonly<FilterDropdownProps>) {
setActiveCategoryIndex(0);
onOpen?.();
if (categories.length > 0) {
onCategorySelect?.(categories[0].label);
onCategorySelect?.(categories[0].id);
}
} else {
onClose?.();
Expand All @@ -108,7 +108,7 @@ export function FilterDropdown(props: Readonly<FilterDropdownProps>) {
(index: number) => {
if (index !== activeCategoryIndex) {
setActiveCategoryIndex(index);
Comment thread
kimphanh marked this conversation as resolved.
onCategorySelect?.(categories[index].label);
onCategorySelect?.(categories[index].id);
}
},
[activeCategoryIndex, categories, onCategorySelect],
Expand Down Expand Up @@ -237,7 +237,7 @@ export function FilterDropdown(props: Readonly<FilterDropdownProps>) {
{categories.map((category, index) => (
<FilterDropdownCategoryItem
isActive={index === activeCategoryIndex}
key={category.label}
key={category.id}
label={category.label}
onClick={() => handleCategoryClick(index)}
ref={categoryRegister(index)}
Expand Down
12 changes: 11 additions & 1 deletion src/components/filters/FilterDropdownTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,11 @@ export interface FilterDropdownCategoryWithItems {
* Use {@link FilterDropdownCategoryWithContent} to render custom content.
*/
content?: never;
/**
* Unique identifier for this category.
* Used as the React key for the category list and passed to `onCategorySelect`.
*/
id: string;
/**
* Whether items in this category support multi-selection (checkboxes).
* When false, only one item can be selected at a time (radio buttons).
Expand Down Expand Up @@ -107,6 +112,11 @@ export interface FilterDropdownCategoryWithContent {
* and `onSearch` must not be provided.
*/
content: ReactNode;
/**
* Unique identifier for this category.
* Used as the React key for the category list and passed to `onCategorySelect`.
*/
id: string;
/**
* Not applicable when using custom content.
* @see FilterDropdownCategoryWithItems
Expand Down Expand Up @@ -218,7 +228,7 @@ export interface FilterDropdownProps {
* Called every time the active category changes, including on initial open.
* Use this to load or refresh items for the newly active category.
*/
onCategorySelect?: (categoryLabel: string) => void;
onCategorySelect?: (categoryId: string) => void;
/**
* Called when the user clicks Clear.
*/
Expand Down
21 changes: 14 additions & 7 deletions src/components/filters/__tests__/FilterDropdown-test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { FilterDropdown, FilterDropdownCategory, FilterDropdownProps } from '..'

const CATEGORIES: FilterDropdownCategory[] = [
{
id: 'severity',
isMultiSelect: true,
label: 'Severity',
items: [
Expand All @@ -33,6 +34,7 @@ const CATEGORIES: FilterDropdownCategory[] = [
],
},
{
id: 'type',
isMultiSelect: true,
label: 'Type',
items: [
Expand Down Expand Up @@ -73,10 +75,10 @@ describe('FilterDropdown', () => {
const { user } = renderFilterDropdown({ onCategorySelect });

await user.click(screen.getByRole('button', { name: 'Filters' }));
expect(onCategorySelect).toHaveBeenCalledWith('Severity');
expect(onCategorySelect).toHaveBeenCalledWith('severity');

await user.click(screen.getByRole('option', { name: /type/i }));
expect(onCategorySelect).toHaveBeenCalledWith('Type');
expect(onCategorySelect).toHaveBeenCalledWith('type');

await user.click(screen.getByRole('option', { name: /type/i }));
expect(onCategorySelect).toHaveBeenCalledTimes(2);
Expand Down Expand Up @@ -143,7 +145,7 @@ describe('FilterDropdown', () => {

it('shows a loading spinner when category items have never been loaded', async () => {
const { user } = renderFilterDropdown({
categories: [{ label: 'Severity', items: undefined }],
categories: [{ id: 'severity', label: 'Severity', items: undefined }],
});

await user.click(screen.getByRole('button', { name: 'Filters' }));
Expand All @@ -164,6 +166,7 @@ describe('FilterDropdown', () => {
const { user } = renderFilterDropdown({
categories: [
{
id: 'severity',
isMultiSelect: false,
label: 'Severity',
items: [
Expand Down Expand Up @@ -199,12 +202,12 @@ describe('FilterDropdown', () => {

onCategorySelect.mockClear();
await user.keyboard('{ArrowDown}');
expect(onCategorySelect).toHaveBeenCalledWith('Type');
expect(onCategorySelect).toHaveBeenCalledWith('type');
expect(screen.getByRole('option', { name: /type/i })).toHaveFocus();

onCategorySelect.mockClear();
await user.keyboard('{ArrowUp}');
expect(onCategorySelect).toHaveBeenCalledWith('Severity');
expect(onCategorySelect).toHaveBeenCalledWith('severity');
expect(screen.getByRole('option', { name: /severity/i })).toHaveFocus();
});

Expand Down Expand Up @@ -244,6 +247,7 @@ describe('FilterDropdown', () => {
const { user } = renderFilterDropdown({
categories: [
{
id: 'severity',
isMultiSelect,
label: 'Severity',
items: [
Expand All @@ -266,8 +270,9 @@ describe('FilterDropdown', () => {
it('renders custom content in the right panel for a content category', async () => {
const { user } = renderFilterDropdown({
categories: [
{ label: 'Severity', items: [{ label: 'High', value: 'high' }] },
{ id: 'severity', label: 'Severity', items: [{ label: 'High', value: 'high' }] },
{
id: 'date-range',
label: 'Date Range',
content: <button type="button">Pick date</button>,
onFocusContent: jest.fn(),
Expand All @@ -286,8 +291,9 @@ describe('FilterDropdown', () => {
const buttonRef = { current: null as HTMLButtonElement | null };
const { user } = renderFilterDropdown({
categories: [
{ label: 'Severity', items: [{ label: 'High', value: 'high' }] },
{ id: 'severity', label: 'Severity', items: [{ label: 'High', value: 'high' }] },
{
id: 'date-range',
label: 'Date Range',
content: (
<button ref={buttonRef} type="button">
Expand All @@ -314,6 +320,7 @@ describe('FilterDropdown', () => {
const { user } = renderFilterDropdown({
categories: [
{
id: 'severity',
isMultiSelect: true,
isSearchable: true,
label: 'Severity',
Expand Down
12 changes: 7 additions & 5 deletions stories/filters/filter-dropdown-helpers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -197,8 +197,8 @@ export function useFilterDropdownCategories() {
const [assigneeItems, setAssigneeItems] = useState<FilterDropdownCategory['items']>(undefined);

const handleCategorySelect = useCallback(
(label: string) => {
if (label === 'Assignee' && assigneeItems === undefined) {
(id: string) => {
if (id === 'assignee' && assigneeItems === undefined) {
setTimeout(
() => setAssigneeItems(withRandomCounts(ALL_ASSIGNEE_ITEMS)),
randomDelay(1000, 3000),
Expand All @@ -219,10 +219,11 @@ export function useFilterDropdownCategories() {

const categories = useMemo<FilterDropdownCategory[]>(
() => [
{ isMultiSelect: true, label: 'Security', items: SECURITY_ITEMS },
{ isMultiSelect: true, label: 'Duplications', items: DUPLICATION_ITEMS },
{ isMultiSelect: false, label: 'Type', items: TYPE_ITEMS },
{ id: 'security', isMultiSelect: true, label: 'Security', items: SECURITY_ITEMS },
{ id: 'duplications', isMultiSelect: true, label: 'Duplications', items: DUPLICATION_ITEMS },
{ id: 'type', isMultiSelect: false, label: 'Type', items: TYPE_ITEMS },
{
id: 'assignee',
isMultiSelect: true,
isSearchable: true,
items: assigneeItems,
Expand All @@ -231,6 +232,7 @@ export function useFilterDropdownCategories() {
onSearch: handleAssigneeSearch,
},
{
id: 'date-range',
content: <DateRangeContent ref={dateInputRef} />,
onFocusContent: () => {
dateInputRef.current?.focus();
Expand Down
Loading