Skip to content
Snippets Groups Projects
Unverified Commit 72ef155e authored by Ryan Laurie's avatar Ryan Laurie Committed by GitHub
Browse files

Convert SelectList to Typescript (#27910)

* convert SelectList to typescript

* add SelectList unit tests

* add SelectList storybook file

* update tests
parent 17cf2c82
No related branches found
No related tags found
No related merge requests found
import React from "react";
import PropTypes from "prop-types";
import { useScrollOnMount } from "metabase/hooks/use-scroll-on-mount";
import { BaseItemRoot } from "./SelectListItem.styled";
const propTypes = {
id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
name: PropTypes.string.isRequired,
onSelect: PropTypes.func.isRequired,
isSelected: PropTypes.bool,
size: PropTypes.oneOf(["small", "medium"]),
className: PropTypes.string,
children: PropTypes.node.isRequired,
as: PropTypes.any,
};
export interface BaseSelectListItemProps {
id: string | number;
name: string;
onSelect: (id: string | number) => void;
children: React.ReactNode;
isSelected?: boolean;
size?: "small" | "medium";
className?: string;
hasLeftIcon?: boolean;
hasRightIcon?: boolean;
as?: any;
}
export function BaseSelectListItem({
id,
......@@ -25,7 +26,7 @@ export function BaseSelectListItem({
as = BaseItemRoot,
children,
...rest
}) {
}: BaseSelectListItemProps) {
const ref = useScrollOnMount();
const Root = as;
return (
......@@ -37,7 +38,7 @@ export function BaseSelectListItem({
tabIndex={0}
size={size}
onClick={() => onSelect(id)}
onKeyDown={e => e.key === "Enter" && onSelect(id)}
onKeyDown={(e: KeyboardEvent) => e.key === "Enter" && onSelect(id)}
className={className}
{...rest}
>
......@@ -46,5 +47,4 @@ export function BaseSelectListItem({
);
}
BaseSelectListItem.propTypes = propTypes;
BaseSelectListItem.Root = BaseItemRoot;
import React from "react";
import _ from "underscore";
import { screen, render } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { getIcon } from "__support__/ui";
import SelectList from "./index";
describe("Components > SelectList", () => {
it("renders a list of items", () => {
render(
<SelectList color="brand">
<SelectList.Item id="1" name="Item 1" icon="check" onSelect={_.noop} />
<SelectList.Item id="2" name="Item 2" icon="check" onSelect={_.noop} />
</SelectList>,
);
expect(screen.getByText("Item 1")).toBeInTheDocument();
expect(screen.getByText("Item 2")).toBeInTheDocument();
});
it("shows the currently selected item", () => {
render(
<SelectList>
<SelectList.Item id="1" name="Item 1" icon="check" onSelect={_.noop} />
<SelectList.Item
id="2"
name="Item 2"
icon="check"
isSelected
onSelect={_.noop}
/>
</SelectList>,
);
expect(screen.getByLabelText("Item 2")).toHaveAttribute(
"aria-selected",
"true",
);
});
it("allows the user to select an item on click", () => {
const selectSpy = jest.fn();
render(
<SelectList color="brand">
<SelectList.Item id="1" name="Item 1" icon="check" onSelect={_.noop} />
<SelectList.Item
id="2"
name="Item 2"
icon="check"
onSelect={selectSpy}
/>
</SelectList>,
);
userEvent.click(screen.getByText("Item 2"));
expect(selectSpy).toHaveBeenCalledWith("2");
});
describe("SelectList.Item", () => {
it("renders the name of the item", () => {
render(
<SelectList.Item id="1" name="Item 1" icon="check" onSelect={_.noop} />,
);
expect(screen.getByText("Item 1")).toBeInTheDocument();
});
it("renders the icon of the item", () => {
render(
<SelectList.Item id="1" name="Item 1" icon="check" onSelect={_.noop} />,
);
expect(getIcon("check")).toBeInTheDocument();
});
it("renders the right icon of the item", () => {
render(
<SelectList.Item
id="1"
name="Item 1"
icon="check"
onSelect={_.noop}
rightIcon="warning"
/>,
);
expect(getIcon("warning")).toBeInTheDocument();
});
it("renders the item as selected", () => {
render(
<SelectList.Item
id="1"
name="Item 1"
icon="check"
onSelect={_.noop}
rightIcon="warning"
isSelected
/>,
);
expect(screen.getByLabelText("Item 1")).toHaveAttribute(
"aria-selected",
"true",
);
});
});
});
import React, { useState } from "react";
import type { ComponentStory } from "@storybook/react";
import SelectList from "./SelectList";
export default {
title: "Core/SelectList",
component: SelectList,
};
const items = ["alert", "all", "archive", "dyno", "history"];
const Template: ComponentStory<any> = args => {
const [value, setValue] = useState("dyno");
return (
<SelectList style={{ maxWidth: 200 }}>
{args.items.filter(Boolean).map((item: string) => (
<SelectList.Item
key={item}
id={item}
name={item}
icon={item}
rightIcon={args.rightIcon}
isSelected={value === item}
onSelect={() => setValue(item)}
/>
))}
</SelectList>
);
};
export const Default = Template.bind({});
Default.args = {
items: items,
rightIcon: "check",
};
......@@ -3,15 +3,15 @@ import { css } from "@emotion/react";
import Label from "metabase/components/type/Label";
import { color } from "metabase/lib/colors";
import Icon from "metabase/components/Icon";
import Icon, { IconProps } from "metabase/components/Icon";
export const ItemTitle = styled(Label)`
margin: 0;
word-break: break-word;
`;
export const ItemIcon = styled(Icon)`
color: ${props => color(props.color) || color("text-light")};
export const ItemIcon = styled(Icon)<{ color?: string | null }>`
color: ${props => color(props.color ?? "text-light")};
justify-self: end;
`;
......@@ -29,7 +29,10 @@ const VERTICAL_PADDING_BY_SIZE = {
medium: "0.75rem",
};
export const BaseItemRoot = styled.li`
export const BaseItemRoot = styled.li<{
size: "small" | "medium";
isSelected: boolean;
}>`
display: grid;
align-items: center;
cursor: pointer;
......@@ -48,12 +51,15 @@ export const BaseItemRoot = styled.li`
}
`;
const getGridTemplateColumns = (hasLeftIcon, hasRightIcon) =>
const getGridTemplateColumns = (hasLeftIcon: boolean, hasRightIcon: boolean) =>
`${hasLeftIcon ? "min-content" : ""} 1fr ${
hasRightIcon ? "min-content" : ""
}`;
export const ItemRoot = styled(BaseItemRoot)`
export const ItemRoot = styled(BaseItemRoot)<{
hasLeftIcon: boolean;
hasRightIcon: boolean;
}>`
display: grid;
grid-template-columns: ${props =>
getGridTemplateColumns(props.hasLeftIcon, props.hasRightIcon)};
......
import React from "react";
import PropTypes from "prop-types";
import _ from "underscore";
import { iconPropTypes } from "metabase/components/Icon";
import type { IconProps } from "metabase/components/Icon";
import { BaseSelectListItem } from "./BaseSelectListItem";
import {
BaseSelectListItem,
BaseSelectListItemProps,
} from "./BaseSelectListItem";
import { ItemRoot, ItemIcon, ItemTitle } from "./SelectListItem.styled";
const iconPropType = PropTypes.oneOfType([
PropTypes.string,
PropTypes.shape(iconPropTypes),
]);
const propTypes = {
...BaseSelectListItem.propTypes,
icon: iconPropType,
rightIcon: iconPropType,
};
export interface SelectListItemProps
extends Omit<BaseSelectListItemProps, "children"> {
name: string;
icon?: string | IconProps;
rightIcon?: string | IconProps;
children?: React.ReactNode;
}
export function SelectListItem(props) {
const { name, icon, rightIcon } = props;
const getIconProps = (icon?: string | IconProps): IconProps =>
_.isObject(icon) ? icon : ({ name: icon } as IconProps);
const iconProps = _.isObject(icon) ? icon : { name: icon };
const rightIconProps = _.isObject(rightIcon)
? rightIcon
: { name: rightIcon };
export function SelectListItem({
name,
icon,
rightIcon,
...otherProps
}: SelectListItemProps) {
const iconProps = getIconProps(icon);
const rightIconProps = getIconProps(rightIcon);
return (
<BaseSelectListItem
as={ItemRoot}
{...props}
{...otherProps}
name={name}
aria-label={name}
hasLeftIcon={!!icon}
hasRightIcon={!!rightIcon}
......@@ -40,5 +44,3 @@ export function SelectListItem(props) {
</BaseSelectListItem>
);
}
SelectListItem.propTypes = propTypes;
......@@ -104,7 +104,7 @@ const AutocompleteInput = ({
id={item}
name={item}
onSelect={item => {
handleOptionSelect(item);
handleOptionSelect(String(item));
closePopover();
}}
>
......
......@@ -5,7 +5,7 @@ export const useScrollOnMount = () => {
useEffect(() => {
if (ref.current) {
ref.current.scrollIntoView({ block: "center" });
ref.current.scrollIntoView?.({ block: "center" });
}
}, []);
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment