Skip to content

Commit 79e8959

Browse files
author
Mark Berger
authored
Replaced react-select menu with headless ui menu source and destination buttons (#17664)
* Replaced react-select menu with headless ui menu - Code refactoring - Added keyboard interaction
1 parent 9ccc7db commit 79e8959

File tree

16 files changed

+333
-262
lines changed

16 files changed

+333
-262
lines changed

airbyte-webapp-e2e-tests/cypress/pages/destinationPage.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
const newDestination = "button[data-id='new-destination'";
2-
const addSourceButton = "div[data-testid='select-source']";
2+
const addSourceButton = "button[data-id='select-source']";
33

44
export const goToDestinationPage = () => {
55
cy.intercept("/api/v1/destinations/list").as("getDestinationsList");

airbyte-webapp/src/components/ConnectorBlocks/TableItemTitle.module.scss

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,3 +30,7 @@
3030
height: 40px;
3131
width: 40px;
3232
}
33+
34+
.primary p {
35+
color: colors.$blue;
36+
}

airbyte-webapp/src/components/ConnectorBlocks/TableItemTitle.tsx

Lines changed: 21 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,19 @@ import React from "react";
22
import { FormattedMessage, useIntl } from "react-intl";
33

44
import { ReleaseStageBadge } from "components/ReleaseStageBadge";
5-
import { Button } from "components/ui/Button";
6-
import { DropDownOptionDataItem } from "components/ui/DropDown";
5+
import { DropdownMenu, DropdownMenuOptionType } from "components/ui/DropdownMenu";
76
import { Heading } from "components/ui/Heading";
8-
import { Popout } from "components/ui/Popout";
97
import { Text } from "components/ui/Text";
108

119
import { ReleaseStage } from "core/request/AirbyteClient";
1210

11+
import { Button } from "../ui/Button";
1312
import styles from "./TableItemTitle.module.scss";
1413

1514
interface TableItemTitleProps {
1615
type: "source" | "destination";
17-
dropDownData: DropDownOptionDataItem[];
18-
onSelect: (item: DropDownOptionDataItem) => void;
16+
dropdownOptions: DropdownMenuOptionType[];
17+
onSelect: (data: DropdownMenuOptionType) => void;
1918
entity: string;
2019
entityName: string;
2120
entityIcon?: React.ReactNode;
@@ -24,24 +23,14 @@ interface TableItemTitleProps {
2423

2524
const TableItemTitle: React.FC<TableItemTitleProps> = ({
2625
type,
27-
dropDownData,
26+
dropdownOptions,
2827
onSelect,
2928
entity,
3029
entityName,
3130
entityIcon,
3231
releaseStage,
3332
}) => {
3433
const { formatMessage } = useIntl();
35-
const options = [
36-
{
37-
label: formatMessage({
38-
id: `tables.${type}AddNew`,
39-
}),
40-
value: "create-new-item",
41-
primary: true,
42-
},
43-
...dropDownData,
44-
];
4534

4635
return (
4736
<>
@@ -59,25 +48,26 @@ const TableItemTitle: React.FC<TableItemTitleProps> = ({
5948
<Heading as="h3" size="sm">
6049
<FormattedMessage id="tables.connections" />
6150
</Heading>
62-
<Popout
63-
data-testid={`select-${type}`}
64-
options={options}
65-
isSearchable={false}
66-
styles={{
67-
// TODO: hack to position select. Should be refactored with Headless UI Menu
68-
menuPortal: (base) => ({
69-
...base,
70-
marginLeft: -130,
71-
}),
72-
}}
73-
menuShouldBlockScroll={false}
51+
<DropdownMenu
52+
placement="bottom-end"
53+
options={[
54+
{
55+
as: "button",
56+
className: styles.primary,
57+
displayName: formatMessage({
58+
id: `tables.${type}AddNew`,
59+
}),
60+
},
61+
...dropdownOptions,
62+
]}
7463
onChange={onSelect}
75-
targetComponent={({ onOpen }) => (
76-
<Button onClick={onOpen}>
64+
>
65+
{() => (
66+
<Button data-id={`select-${type}`}>
7767
<FormattedMessage id={`tables.${type}Add`} />
7868
</Button>
7969
)}
80-
/>
70+
</DropdownMenu>
8171
</div>
8272
</>
8373
);
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
@use "scss/colors";
2+
@use "scss/variables";
3+
@use "scss/z-indices";
4+
5+
.dropdownMenu {
6+
position: relative;
7+
}
8+
9+
.items {
10+
z-index: z-indices.$dropdownMenu;
11+
overflow: auto;
12+
width: 260px;
13+
max-height: 300px;
14+
padding: variables.$spacing-sm 0;
15+
outline: none;
16+
border-radius: variables.$border-radius-xs;
17+
background-color: colors.$white;
18+
box-shadow: 0 8px 10px 0 rgb(11 10 26 / 4%), 0 3px 14px 0 rgb(11 10 26 / 8%), 0 5px 5px 0 rgb(11 10 26 / 12%);
19+
20+
&:focus-within {
21+
outline: none;
22+
}
23+
}
24+
25+
.item {
26+
cursor: pointer;
27+
display: flex;
28+
align-items: center;
29+
height: 42px;
30+
width: 100%;
31+
padding: 0 variables.$spacing-lg;
32+
border: 0;
33+
background-color: transparent;
34+
text-decoration: none;
35+
36+
.icon {
37+
display: flex;
38+
align-items: center;
39+
font-size: 22px;
40+
color: colors.$dark-blue;
41+
}
42+
43+
.text {
44+
width: 100%;
45+
white-space: nowrap;
46+
overflow: hidden;
47+
text-overflow: ellipsis;
48+
text-align: left;
49+
}
50+
51+
&.active {
52+
background-color: colors.$grey-100;
53+
}
54+
55+
&.iconPositionRight {
56+
flex-direction: row-reverse;
57+
58+
.icon {
59+
margin-left: variables.$spacing-md;
60+
}
61+
}
62+
63+
&.iconPositionLeft {
64+
flex-direction: row;
65+
66+
.icon {
67+
margin-right: variables.$spacing-md;
68+
}
69+
}
70+
}
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import { autoUpdate, useFloating, offset } from "@floating-ui/react-dom";
2+
import { Menu } from "@headlessui/react";
3+
import classNames from "classnames";
4+
import React from "react";
5+
6+
import { Text } from "components/ui/Text";
7+
8+
import styles from "./DropdownMenu.module.scss";
9+
import { DropdownMenuProps, MenuItemContentProps, DropdownMenuOptionType } from "./types";
10+
11+
const MenuItemContent: React.FC<React.PropsWithChildren<MenuItemContentProps>> = ({ data }) => {
12+
return (
13+
<>
14+
{data?.icon && <span className={styles.icon}>{data.icon}</span>}
15+
<Text className={styles.text} size="lg">
16+
{data.displayName}
17+
</Text>
18+
</>
19+
);
20+
};
21+
22+
export const DropdownMenu: React.FC<React.PropsWithChildren<DropdownMenuProps>> = ({
23+
options,
24+
children,
25+
onChange,
26+
placement = "bottom",
27+
displacement = 5,
28+
}) => {
29+
const { x, y, reference, floating, strategy } = useFloating({
30+
middleware: [offset(displacement)],
31+
whileElementsMounted: autoUpdate,
32+
placement,
33+
});
34+
35+
const elementProps = (item: DropdownMenuOptionType, active: boolean) => {
36+
const anchorProps =
37+
item.as === "a"
38+
? {
39+
target: "_blank",
40+
rel: "noreferrer",
41+
href: item?.href,
42+
}
43+
: {};
44+
45+
return {
46+
...anchorProps,
47+
"data-id": item.displayName,
48+
className: classNames(styles.item, item?.className, {
49+
[styles.iconPositionLeft]: (item?.iconPosition === "left" && item.icon) || !item?.iconPosition,
50+
[styles.iconPositionRight]: item?.iconPosition === "right",
51+
[styles.active]: active,
52+
}),
53+
title: item.displayName,
54+
onClick: () => onChange && onChange(item),
55+
};
56+
};
57+
58+
return (
59+
<Menu ref={reference} className={styles.dropdownMenu} as="div">
60+
{({ open }) => (
61+
<>
62+
<Menu.Button as={React.Fragment}>{children({ open })}</Menu.Button>
63+
<Menu.Items
64+
ref={floating}
65+
className={styles.items}
66+
style={{
67+
position: strategy,
68+
top: y ?? 0,
69+
left: x ?? 0,
70+
}}
71+
>
72+
{options.map((item, index) => (
73+
<Menu.Item key={index}>
74+
{({ active }) =>
75+
React.createElement(
76+
item.as ?? "button",
77+
{ ...elementProps(item, active) },
78+
<MenuItemContent data={item} />
79+
)
80+
}
81+
</Menu.Item>
82+
))}
83+
</Menu.Items>
84+
</>
85+
)}
86+
</Menu>
87+
);
88+
};
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export * from "./DropdownMenu";
2+
export * from "./types";
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { Placement } from "@floating-ui/react-dom";
2+
import React from "react";
3+
4+
export type DisplacementType = 5 | 10; // $spacing-sm, $spacing-md
5+
6+
export type DropdownMenuItemElementType = "a" | "button";
7+
8+
export type DropdownMenuItemIconPositionType = "left" | "right";
9+
10+
export interface DropdownMenuOptionType {
11+
as?: DropdownMenuItemElementType;
12+
icon?: React.ReactNode;
13+
iconPosition?: DropdownMenuItemIconPositionType;
14+
displayName: string;
15+
value?: unknown;
16+
href?: string;
17+
className?: string;
18+
}
19+
20+
export interface MenuItemContentProps {
21+
data: DropdownMenuOptionType;
22+
active?: boolean;
23+
}
24+
25+
export interface DropdownMenuProps {
26+
options: DropdownMenuOptionType[];
27+
children: ({ open }: { open: boolean }) => React.ReactNode;
28+
onChange?: (data: DropdownMenuOptionType) => void;
29+
placement?: Placement;
30+
displacement?: DisplacementType;
31+
}

airbyte-webapp/src/components/ui/Modal/Modal.module.scss

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232

3333
.card {
3434
margin-left: variables.$width-size-menu;
35-
max-width: calc(100vw - variables.$width-size-menu - variables.$spacing-lg * 2);
35+
max-width: calc(100vw - #{variables.$width-size-menu} - #{variables.$spacing-lg} * 2);
3636

3737
&.sm {
3838
width: variables.$width-modal-sm;

0 commit comments

Comments
 (0)