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

TD-2308 Add support to show helper icon and tooltip #770

Merged
merged 13 commits into from
Nov 16, 2023
46 changes: 29 additions & 17 deletions src/Autocomplete/Autocomplete.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { AutocompleteProps, KeyValueOption } from "./Autocomplete.types";
import { Meta, StoryFn, StoryObj } from "@storybook/react";

import Autocomplete from "./Autocomplete";
import { AutocompleteProps } from "./Autocomplete.types";
import { KeyValueOption } from "../Common.types";
import React from "react";
import { action } from "@storybook/addon-actions";
import { useArgs } from "@storybook/preview-api";
Expand Down Expand Up @@ -50,21 +51,7 @@ const Template: StoryFn<
<Autocomplete
{...args}
onChange={(event, newValue) => {
if (newValue !== null) {
if (Array.isArray(newValue)) {
updateArgs({
value: newValue.map(val =>
typeof val === "string" ? val : val.value
)
});
} else {
updateArgs({
value: typeof newValue === "string" ? newValue : newValue.value
});
}
} else {
updateArgs({ value: multiple ? [] : "" });
}
updateArgs({ value: newValue });
action("onChange")(newValue);
}}
value={theValue}
Expand Down Expand Up @@ -119,7 +106,32 @@ export const KeyValueOptions: StoryObj<typeof Autocomplete> = {
],
required: false,
size: "medium",
value: "Option 1",
value: { key: 1, value: "Option 1" },
variant: "outlined"
},
render: Template
};

// Define the story for options with tooltips
export const KeyValueOptionsTooltip: StoryObj<typeof Autocomplete> = {
args: {
// Define the args for key-value options
disabled: false,
error: false,
helperText: "Helper Text",
label: "Select options",
limitTags: -1,
margin: "normal",
multiple: false,
options: [
{ key: 1, tooltip: "Tooltip 1", value: "Option 1" },
{ key: 2, tooltip: "Tooltip 2", value: "Option 2" },
{ key: 3, tooltip: "Tooltip 3", value: "Option 3" },
{ key: 4, tooltip: "Tooltip 4", value: "Option 4" }
],
required: false,
size: "medium",
value: { key: 1, tooltip: "Tooltip 1", value: "Option 1" },
variant: "outlined"
},
render: Template
Expand Down
53 changes: 52 additions & 1 deletion src/Autocomplete/Autocomplete.test.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { render, screen } from "@testing-library/react";
import { render, screen, waitFor } from "@testing-library/react";

import Autocomplete from ".";
import React from "react";
Expand All @@ -12,6 +12,12 @@ const keyValueOptions = [
{ key: 3, value: "Option 3" },
{ key: 4, value: "Option 4" }
];
const keyValueOptionsWithTooltip = [
{ key: 1, tooltip: "Tooltip 1", value: "Option 1" },
{ key: 2, tooltip: "Tooltip 2", value: "Option 2" },
{ key: 3, tooltip: "Tooltip 3", value: "Option 3" },
{ key: 4, tooltip: "Tooltip 4", value: "Option 4" }
];

/**
* Tests
Expand Down Expand Up @@ -137,4 +143,49 @@ describe("Select", () => {
expect.anything()
);
});

it("check helper icon is visible and tooltip shown on hover", async () => {
const onChange = jest.fn();

render(
<Autocomplete
multiple={false}
options={keyValueOptionsWithTooltip}
onChange={onChange}
label="Select an option"
value={{ key: 2, tooltip: "Tooltip 2", value: "Option 2" }}
/>
);

// click the label selector down arrow
await userEvent.click(screen.getByRole("button", { name: /open/i }));

// check that the options are rendered
keyValueOptionsWithTooltip.forEach(opt =>
expect(screen.getByText(opt.value)).toBeInTheDocument()
);

// check that helper icons is rendered
keyValueOptionsWithTooltip.forEach(opt =>
expect(screen.getByTestId(`tooltip-${opt.key}`)).toBeInTheDocument()
);

// check tooltip is shown on hover of helper icon
await userEvent.hover(screen.getByTestId("tooltip-1"));
waitFor(() => {
expect(screen.getByText("Tooltip 1")).toBeInTheDocument();
});

// click the first option and check callback is called
await userEvent.click(screen.getByText("Option 1"));
expect(onChange).toHaveBeenCalledTimes(1);

// check that the onChange event is fired with the expected value
expect(onChange).toHaveBeenCalledWith(
expect.anything(),
{ key: 1, tooltip: "Tooltip 1", value: "Option 1" },
expect.anything(),
expect.anything()
);
});
});
103 changes: 73 additions & 30 deletions src/Autocomplete/Autocomplete.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
import * as React from "react";

import {
AutocompleteRenderOptionState,
Box,
Checkbox,
Autocomplete as MuiAutocomplete,
TextField,
Tooltip,
Typography
} from "@mui/material";
import { CheckBox, CheckBoxOutlineBlank } from "@mui/icons-material";

import { AutocompleteProps } from "./Autocomplete.types";
import ErrorOutlineIcon from "@mui/icons-material/ErrorOutline";
import { KeyValueOption } from "../Common.types";
import { isKeyValueOption } from "../utils/common";

Expand Down Expand Up @@ -39,8 +42,8 @@ export default function Autocomplete<
multiple={multiple}
onChange={onChange}
options={options}
getOptionLabel={(option: KeyValueOption | string) =>
typeof option === "string" ? option : option.value
getOptionLabel={option =>
isKeyValueOption(option) ? option.value : String(option)
}
renderInput={params => (
<TextField
Expand All @@ -53,48 +56,88 @@ export default function Autocomplete<
variant={variant}
/>
)}
renderOption={multiple ? Option : undefined}
renderOption={Option}
value={value}
clearIcon={multiple ? null : undefined}
disabled={disabled}
size={size}
isOptionEqualToValue={(option, value) => {
// if the option is a key value pair, compare option.value to value
if (isKeyValueOption(option)) {
if (option.value === value) {
return true;
}
} else {
// if the option is not a key value then compare option to value
if (option === value) {
return true;
}
}

// if the option is not a match, return false
return false;
}}
isOptionEqualToValue={(option, value) =>
isKeyValueOption(option) && isKeyValueOption(value)
? option.key === value.key
: option === value
}
/>
);
}

// renderer for a checkbox option
function Option(
function Option<
Value extends KeyValueOption | string,
Multiple extends boolean | undefined
>(
props: React.HTMLAttributes<HTMLLIElement>,
option: KeyValueOption | string,
{ selected }: { selected: boolean }
{ selected }: AutocompleteRenderOptionState,
{ multiple }: AutocompleteProps<Value, Multiple>
) {
// handle key value options rendering
if (isKeyValueOption(option)) {
return (
<Box component="li" {...props}>
<Box
sx={{
alignItems: "center",
display: "flex",
flexGrow: 1,
justifyContent: "space-between"
}}
>
<Box sx={{ alignItems: "center", display: "flex" }}>
{multiple ? (
<Checkbox
icon={<CheckBoxOutlineBlank fontSize="small" />}
checkedIcon={<CheckBox fontSize="small" />}
checked={selected}
value={option.key}
/>
) : null}
<Typography>{option.value}</Typography>
</Box>
<Box sx={{ alignItems: "center", display: "flex" }}>
{option.tooltip && (
<Tooltip title={option.tooltip} placement="right" arrow>
<ErrorOutlineIcon
sx={{ height: 20, pl: 1, width: 20 }}
color="primary"
data-testid={`tooltip-${option.key}`}
/>
</Tooltip>
)}
</Box>
</Box>
</Box>
);
}

// handle string options rendering
return (
<Box component="li" {...props}>
<Checkbox
icon={<CheckBoxOutlineBlank fontSize="small" />}
checkedIcon={<CheckBox fontSize="small" />}
checked={selected}
value={typeof option === "string" ? option : option.key}
/>
<Typography>
{typeof option === "string" ? option : option.value}
</Typography>
<Box
sx={{
alignItems: "center",
display: "flex"
}}
>
{multiple ? (
<Checkbox
icon={<CheckBoxOutlineBlank fontSize="small" />}
checkedIcon={<CheckBox fontSize="small" />}
checked={selected}
value={option}
/>
) : null}
<Typography>{option}</Typography>
</Box>
</Box>
);
}
6 changes: 5 additions & 1 deletion src/Common.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,8 @@ export type Label = {
};

// KeyValueOption type
export type KeyValueOption = { key: string | number; value: string };
export type KeyValueOption = {
key: string | number;
value: string;
tooltip?: string;
};
4 changes: 2 additions & 2 deletions src/utils/common.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { KeyValueOption } from "../Common.types";

// This function is used to check if an object is of type KeyValueOption
export function isKeyValueOption<T>(obj: T): obj is T & KeyValueOption {
return obj && typeof obj === "object" && "key" in obj && "value" in obj;
export function isKeyValueOption(obj: unknown): obj is KeyValueOption {
return !!obj && typeof obj === "object" && "key" in obj && "value" in obj;
}