Skip to content
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

VE-204 Virtualised Autocomplete For Label Filter #1013

Merged
Show file tree
Hide file tree
Changes from 3 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
19 changes: 0 additions & 19 deletions src/Filter/AlwaysOpenAutocomplete.tsx

This file was deleted.

8 changes: 0 additions & 8 deletions src/Filter/AlwaysOpenAutocomplete.types.tsx

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ export default {
// example options
const options = [
{ _id: 1, color: "#005FA8", description: "first label", name: "label 1" },
{ _id: 2, color: "#f542e0", description: "second label", name: "label 2" }
{ _id: 2, color: "#f542e0", description: "second label", name: "label 2" },
{ _id: 3, color: "#ffa500", description: "third label", name: "label 3" }
];

// story template with state for selection
Expand All @@ -19,7 +20,7 @@ const Template = args => {
React.useEffect(() => {
setValue(args.value);
}, [args.value]);
const onChange = value => {
const onChange = (value: typeof options) => {
Kaustubh9031 marked this conversation as resolved.
Show resolved Hide resolved
setValue(value);
action("onChange")(value);
};
Expand All @@ -32,17 +33,7 @@ export const Default = {
limitTags: -1,
name: "label-filter",
options,
value: [options[0]],
variant: "popper"
},

render: Template
};

export const AlwaysOpen = {
args: {
...Default.args,
variant: "always-open"
value: [options[0]]
},

render: Template
Expand Down
Original file line number Diff line number Diff line change
@@ -1,41 +1,55 @@
import * as React from "react";

import { describe, expect, it, vi } from "vitest";
import { render, screen } from "@testing-library/react";

import LabelFilter from "./LabelFilter";
import { LabelFilterProps } from "./LabelFilter.types";
import userEvent from "@testing-library/user-event";

/**
* Test wrapper for LabelFilter
*
* Provides state for value to avoid errors changing from uncontrolled to controlled.
*/
const LabelFilterWithState = ({ onChange, value: valueIn = [], ...rest }) => {
const LabelFilterWithState = ({
onChange = () => {},
value: valueIn = [],
options,
...rest
}: LabelFilterProps) => {
const [value, setValue] = React.useState(valueIn);
const handleChange = selectedValues => {
setValue(selectedValues);
onChange(selectedValues);
};

return <LabelFilter {...rest} onChange={handleChange} value={value} />;
return (
<LabelFilter
options={options}
{...rest}
onChange={handleChange}
value={value}
/>
);
};

// sample options
const options = [
{
_id: 1,
_id: "1",
color: "#FF0000",
description: "option 1 description",
name: "option 1"
},
{
_id: 2,
_id: "2",
color: "#00FF00",
description: "option 2 description",
name: "option 2"
},
{
_id: 3,
_id: "3",
color: "#0000FF",
description: "option 3 description",
name: "option 3"
Expand Down Expand Up @@ -80,51 +94,6 @@ describe("LabelFilter", () => {
await userEvent.click(screen.getByRole("button", { name: /open/i }));
await userEvent.click(screen.getByText("option 2"));

// check that the onChange event is fired
expect(onChange).toHaveBeenLastCalledWith(options.slice(0, 2));
});
});
describe("variant=always-open", () => {
it("can single select", async () => {
const onChange = vi.fn();
render(
<LabelFilterWithState
options={options}
onChange={onChange}
variant="always-open"
/>
);

// check that the options are rendered
expect(screen.getByText("option 1")).toBeInTheDocument();
expect(screen.getByText("option 2")).toBeInTheDocument();
expect(screen.getByText("option 3")).toBeInTheDocument();

// click the first option
await userEvent.click(screen.getByText("option 1"));

// check that the onChange event is fired
expect(onChange).toHaveBeenLastCalledWith([options[0]]);
});
it("can single select", async () => {
const onChange = vi.fn();
render(
<LabelFilterWithState
options={options}
onChange={onChange}
variant="always-open"
/>
);

// check that the options are rendered
expect(screen.getByText("option 1")).toBeInTheDocument();
expect(screen.getByText("option 2")).toBeInTheDocument();
expect(screen.getByText("option 3")).toBeInTheDocument();

// click the first option
await userEvent.click(screen.getByText("option 1"));
await userEvent.click(screen.getByText("option 2"));

// check that the onChange event is fired
expect(onChange).toHaveBeenLastCalledWith(options.slice(0, 2));
});
Expand Down
Original file line number Diff line number Diff line change
@@ -1,32 +1,29 @@
import * as React from "react";

import {
Autocomplete,
AutocompleteOwnerState,
AutocompleteRenderGetTagProps,
Box,
Checkbox,
TextField,
Typography,
autocompleteClasses
Typography
} from "@mui/material";

import AlwaysOpenAutocomplete from "../AlwaysOpenAutocomplete";
import { Label } from "../../LabelSelector/Label.types";
import LabelChip from "../../LabelSelector/LabelChip/LabelChip";
import { LabelFilterProps } from "./LabelFilter.types";
import { VirtualizedAutocomplete } from "../../Autocomplete/Autocomplete";

/**
* A label filter allows the user to select multiple labels from a list.
*/
export default function LabelFilter({
Kaustubh9031 marked this conversation as resolved.
Show resolved Hide resolved
variant = "popper",
value = [],
limitTags = -1,
...props
}) {
}: LabelFilterProps) {
const defaults = { limitTags, value };
return variant === "popper" ? (
<LabelFilterPopper {...props} {...defaults} />
) : (
<LabelFilterAlwaysOpen {...props} {...defaults} />
);
return <LabelFilterPopper {...props} {...defaults} />;
}

/**
Expand All @@ -39,15 +36,15 @@ function LabelFilterPopper({
options,
value,
limitTags
}) {
}: LabelFilterProps) {
return (
<Autocomplete
<VirtualizedAutocomplete
getOptionLabel={option => option.name}
isOptionEqualToValue={(option, value) => option._id === value._id}
limitTags={limitTags}
multiple
noOptionsText="No labels"
onChange={(e, newValue) => onChange(newValue)}
onChange={(e, newValue) => onChange?.(newValue)}
options={options}
value={value}
renderInput={params => (
Expand All @@ -59,68 +56,41 @@ function LabelFilterPopper({
);
}

/**
* An inline label filter is always open and the popper does not sit above other elements.
*/
function LabelFilterAlwaysOpen({
name,
label,
options,
value,
limitTags,
onChange
}) {
return (
<AlwaysOpenAutocomplete
getOptionLabel={option => option.name}
isOptionEqualToValue={(option, value) => option._id === value._id}
limitTags={limitTags}
multiple
noOptionsText="No labels"
onChange={(e, newValue) => onChange(newValue)}
options={options}
renderInput={params => {
return (
<TextField
{...params}
label={label}
name={name}
sx={{
[`& .${autocompleteClasses.popupIndicator}`]: { display: "none" }
}}
/>
);
}}
renderOption={Option}
renderTags={Tags}
value={value}
/>
);
}

// render for label chips
function Tags(labels, getTagProps, { onChange, value }) {
return labels.map(label => (
function Tags(
labels: Label[],
getTagProps: AutocompleteRenderGetTagProps,
Kaustubh9031 marked this conversation as resolved.
Show resolved Hide resolved
ownerState: AutocompleteOwnerState<Label, true, undefined, undefined, "div">
) {
const value = ownerState.value || []; // Current selected labels
const onChange = ownerState.onChange || (() => {}); // No-op if onChange is not provided
Kaustubh9031 marked this conversation as resolved.
Show resolved Hide resolved
return labels.map((label, index) => (
<LabelChip
Kaustubh9031 marked this conversation as resolved.
Show resolved Hide resolved
key={label._id}
label={label.name}
color={label.color}
style={{ marginLeft: 2 }}
Kaustubh9031 marked this conversation as resolved.
Show resolved Hide resolved
onDelete={e =>
onChange(
e,
value.filter(l => l._id !== label._id)
)
}
onDelete={e => {
const updatedValue = value.filter(l => l._id !== label._id); // Remove label from the value
// Trigger onChange with event, updated value, reason, and details
onChange(e as React.SyntheticEvent, updatedValue, "removeOption", {
Kaustubh9031 marked this conversation as resolved.
Show resolved Hide resolved
option: label
});
}}
clickable={false}
/>
));
}

// render for label options
function Option(props, option, { selected }) {
// Render label options
Kaustubh9031 marked this conversation as resolved.
Show resolved Hide resolved
Kaustubh9031 marked this conversation as resolved.
Show resolved Hide resolved
function Option(
props: React.HTMLAttributes<HTMLLIElement>,
option: Label,
{ selected }: { selected: boolean }
) {
const { key, ...restProps } = props as { key?: React.Key }; // Extract key explicitly
Kaustubh9031 marked this conversation as resolved.
Show resolved Hide resolved
return (
<li {...props}>
<li key={key} {...restProps}>
<Checkbox
checked={selected}
sx={{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@ import LabelFilter from "./LabelFilter";

export type LabelFilterProps = {
Kaustubh9031 marked this conversation as resolved.
Show resolved Hide resolved
label?: string;
limitTags?: number;
name?: string;
onChange?: (value: Label[]) => void;
options: Label[];
value?: Label[];
variant?: "popper" | "always-open";
};

export default LabelFilter as React.FC<LabelFilterProps>;
Kaustubh9031 marked this conversation as resolved.
Show resolved Hide resolved
5 changes: 4 additions & 1 deletion src/Filter/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@ export {
default as CheckboxFilter,
type CheckboxFilterProps
} from "./CheckboxFilter";
export { default as LabelFilter, type LabelFilterProps } from "./LabelFilter";
export {
default as LabelFilter,
type LabelFilterProps
} from "./LabelFilter/LabelFilter.types";
export { default as RangeFilter, type RangeFilterProps } from "./RangeFilter";

export { SidebarFilter, type SidebarFilterProps } from "./SidebarFilter";
Expand Down
4 changes: 3 additions & 1 deletion src/FontPicker/FontPicker.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { Autocomplete, Box, TextField, Typography } from "@mui/material";
import { Box, TextField, Typography } from "@mui/material";
import React, { useEffect, useState } from "react";

import Autocomplete from "../Autocomplete";
import PropTypes from "prop-types";

// default font options list
Expand Down
5 changes: 3 additions & 2 deletions src/LabelSelector/LabelSelector.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import Autocomplete, { createFilterOptions } from "@mui/material/Autocomplete";
import {
Box,
Button,
Expand All @@ -17,6 +16,8 @@ import EditLabelDialog from "../EditLabelDialog/EditLabelDialog";
import LabelChip from "./LabelChip/LabelChip";
import NoWrapTypography from "../NoWrapTypography/NoWrapTypography";
import PropTypes from "prop-types";
import { VirtualizedAutocomplete } from "../Autocomplete/Autocomplete";
import { createFilterOptions } from "@mui/material/Autocomplete";

// custom styling
const styles = {
Expand Down Expand Up @@ -166,7 +167,7 @@ export default function LabelSelector({

return (
<>
<Autocomplete
<VirtualizedAutocomplete
size={size}
disableCloseOnSelect
limitTags={limitTags}
Expand Down
Loading