New Dropdown component (#2355)

* New Dropdown component

* Cleanup

* Styling

* Minireset
This commit is contained in:
Oleg Shur 2022-07-31 15:38:44 +03:00 committed by GitHub
parent bff27f1b5b
commit 70414d2279
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
24 changed files with 219 additions and 278 deletions

View file

@ -11,7 +11,7 @@
"@hookform/error-message": "^2.0.0",
"@hookform/resolvers": "^2.7.1",
"@reduxjs/toolkit": "^1.8.3",
"@rooks/use-outside-click-ref": "^4.10.1",
"@szhsin/react-menu": "^3.1.1",
"@tanstack/react-query": "^4.0.5",
"@testing-library/react": "^13.2.0",
"@types/testing-library__jest-dom": "^5.14.5",

View file

@ -13,7 +13,7 @@ specifiers:
'@jest/types': ^28.1.1
'@openapitools/openapi-generator-cli': ^2.5.1
'@reduxjs/toolkit': ^1.8.3
'@rooks/use-outside-click-ref': ^4.10.1
'@szhsin/react-menu': ^3.1.1
'@tanstack/react-query': ^4.0.5
'@testing-library/dom': ^8.11.1
'@testing-library/jest-dom': ^5.16.4
@ -97,7 +97,7 @@ dependencies:
'@hookform/error-message': 2.0.0_l2dcsysovzdujulgxvsen7vbsm
'@hookform/resolvers': 2.8.9_react-hook-form@7.6.9
'@reduxjs/toolkit': 1.8.3_ctm756ikdwcjcvyfxxwskzbr6q
'@rooks/use-outside-click-ref': 4.11.2_react@18.1.0
'@szhsin/react-menu': 3.1.1_ef5jwxihqo6n7gxfmzogljlgcm
'@tanstack/react-query': 4.0.5_ef5jwxihqo6n7gxfmzogljlgcm
'@testing-library/react': 13.2.0_ef5jwxihqo6n7gxfmzogljlgcm
'@types/testing-library__jest-dom': 5.14.5
@ -2352,14 +2352,6 @@ packages:
reselect: 4.1.5
dev: false
/@rooks/use-outside-click-ref/4.11.2_react@18.1.0:
resolution: {integrity: sha512-w2bCW69zcpLh0KmN/odAuBsQ3sps+73KEu7zMOi0o4YMfDo+tXcqwlTJiLYysd0BEoQC9pNIklzZmI9zZep69g==}
peerDependencies:
react: '>=16.8.0'
dependencies:
react: 18.1.0
dev: false
/@rushstack/eslint-patch/1.1.3:
resolution: {integrity: sha512-WiBSI6JBIhC6LRIsB2Kwh8DsGTlbBU+mLRxJmAe3LjHTdkDpwIbEOZgoXBbZilk/vlfjK8i6nKRAvIRn1XaIMw==}
dev: true
@ -2377,6 +2369,18 @@ packages:
dependencies:
'@sinonjs/commons': 1.8.3
/@szhsin/react-menu/3.1.1_ef5jwxihqo6n7gxfmzogljlgcm:
resolution: {integrity: sha512-IdHLyH61M+KqjTrvqglKo7JnbC0GIkg4OCtlXBxQPEjx/ecR5g0Iycqm+SG3rObEoniLZEz32iJkefve/LAHMA==}
peerDependencies:
react: '>=16.14.0'
react-dom: '>=16.14.0'
dependencies:
prop-types: 15.8.1
react: 18.1.0
react-dom: 18.1.0_react@18.1.0
react-transition-state: 1.1.4_ef5jwxihqo6n7gxfmzogljlgcm
dev: false
/@tanstack/query-core/4.0.5:
resolution: {integrity: sha512-QOJ2gLbwlf8p0487pMey6vv8EF5X2ib1zINayaD7mb9/LibUtXmZ12uJgTqcnjgNY/4tWZn5qJnEk2ePG5AVGA==}
dev: false
@ -6752,6 +6756,16 @@ packages:
react: 18.1.0
dev: false
/react-transition-state/1.1.4_ef5jwxihqo6n7gxfmzogljlgcm:
resolution: {integrity: sha512-6nQLWWx95gYazCm6OdtD1zGbRiirvVXPrDtHAGsYb4xs9spMM7bA8Vx77KCpjL8PJ8qz1lXFGz2PTboCSvt7iw==}
peerDependencies:
react: '>=16.8.0'
react-dom: '>=16.8.0'
dependencies:
react: 18.1.0
react-dom: 18.1.0_react@18.1.0
dev: false
/react/18.1.0:
resolution: {integrity: sha512-4oL8ivCz5ZEPyclFQXaNksK3adutVS8l2xzZU0cqEFrE9Sb7fC0EFK5uEk74wIreL1DERyjvsU915j1pcT2uEQ==}
engines: {node: '>=0.10.0'}

View file

@ -7,11 +7,9 @@ import {
} from 'lib/hooks/api/kafkaConnect';
import useAppParams from 'lib/hooks/useAppParams';
import { RouterParamsClusterConnectConnector } from 'lib/paths';
import Dropdown from 'components/common/Dropdown/Dropdown';
import DropdownItem from 'components/common/Dropdown/DropdownItem';
import VerticalElipsisIcon from 'components/common/Icons/VerticalElipsisIcon';
import getTagColor from 'components/common/Tag/getTagColor';
import { Tag } from 'components/common/Tag/Tag.styled';
import { Dropdown, DropdownItem } from 'components/common/Dropdown';
const Tasks: React.FC = () => {
const routerProps = useAppParams<RouterParamsClusterConnectConnector>();
@ -50,7 +48,7 @@ const Tasks: React.FC = () => {
<td>{task.status.trace || 'null'}</td>
<td style={{ width: '5%' }}>
<div>
<Dropdown label={<VerticalElipsisIcon />} right>
<Dropdown>
<DropdownItem
onClick={() => restartTaskHandler(task.id?.task)}
danger

View file

@ -3,15 +3,13 @@ import { FullConnectorInfo } from 'generated-sources';
import { clusterConnectConnectorPath, clusterTopicPath } from 'lib/paths';
import { ClusterName } from 'redux/interfaces';
import { Link, NavLink } from 'react-router-dom';
import Dropdown from 'components/common/Dropdown/Dropdown';
import DropdownItem from 'components/common/Dropdown/DropdownItem';
import ConfirmationModal from 'components/common/ConfirmationModal/ConfirmationModal';
import { Tag } from 'components/common/Tag/Tag.styled';
import { TableKeyLink } from 'components/common/table/Table/TableKeyLink.styled';
import VerticalElipsisIcon from 'components/common/Icons/VerticalElipsisIcon';
import getTagColor from 'components/common/Tag/getTagColor';
import useModal from 'lib/hooks/useModal';
import { useDeleteConnector } from 'lib/hooks/api/kafkaConnect';
import { Dropdown, DropdownItem } from 'components/common/Dropdown';
import * as S from './List.styled';
@ -79,7 +77,7 @@ const ListItem: React.FC<ListItemProps> = ({
</td>
<td>
<div>
<Dropdown label={<VerticalElipsisIcon />} right up>
<Dropdown>
<DropdownItem onClick={setOpen} danger>
Remove Connector
</DropdownItem>

View file

@ -9,11 +9,8 @@ import PageLoader from 'components/common/PageLoader/PageLoader';
import ConfirmationModal from 'components/common/ConfirmationModal/ConfirmationModal';
import ClusterContext from 'components/contexts/ClusterContext';
import PageHeading from 'components/common/PageHeading/PageHeading';
import VerticalElipsisIcon from 'components/common/Icons/VerticalElipsisIcon';
import * as Metrics from 'components/common/Metrics';
import { Tag } from 'components/common/Tag/Tag.styled';
import Dropdown from 'components/common/Dropdown/Dropdown';
import DropdownItem from 'components/common/Dropdown/DropdownItem';
import groupBy from 'lodash/groupBy';
import { Table } from 'components/common/table/Table/Table.styled';
import TableHeaderCell from 'components/common/table/TableHeaderCell/TableHeaderCell';
@ -26,6 +23,7 @@ import {
getAreConsumerGroupDetailsFulfilled,
} from 'redux/reducers/consumerGroups/consumerGroupsSlice';
import getTagColor from 'components/common/Tag/getTagColor';
import { Dropdown, DropdownItem } from 'components/common/Dropdown';
import ListItem from './ListItem';
@ -72,7 +70,7 @@ const Details: React.FC = () => {
<div>
<PageHeading text={consumerGroupID}>
{!isReadOnly && (
<Dropdown label={<VerticalElipsisIcon />} right>
<Dropdown>
<DropdownItem onClick={onResetOffsets}>Reset offset</DropdownItem>
<DropdownItem
onClick={() => setIsConfirmationModalVisible(true)}

View file

@ -10,9 +10,6 @@ import ConfirmationModal from 'components/common/ConfirmationModal/ConfirmationM
import PageLoader from 'components/common/PageLoader/PageLoader';
import PageHeading from 'components/common/PageHeading/PageHeading';
import { Button } from 'components/common/Button/Button';
import Dropdown from 'components/common/Dropdown/Dropdown';
import DropdownItem from 'components/common/Dropdown/DropdownItem';
import VerticalElipsisIcon from 'components/common/Icons/VerticalElipsisIcon';
import { Table } from 'components/common/table/Table/Table.styled';
import TableHeaderCell from 'components/common/table/TableHeaderCell/TableHeaderCell';
import { useAppDispatch, useAppSelector } from 'lib/hooks/redux';
@ -31,6 +28,7 @@ import { resetLoaderById } from 'redux/reducers/loader/loaderSlice';
import { TableTitle } from 'components/common/table/TableTitle/TableTitle.styled';
import useAppParams from 'lib/hooks/useAppParams';
import { schemasApiClient } from 'lib/api';
import { Dropdown, DropdownItem } from 'components/common/Dropdown';
import LatestVersionItem from './LatestVersion/LatestVersionItem';
import SchemaVersion from './SchemaVersion/SchemaVersion';
@ -101,7 +99,7 @@ const Details: React.FC = () => {
>
Edit Schema
</Button>
<Dropdown label={<VerticalElipsisIcon />} right>
<Dropdown>
<DropdownItem
onClick={() => setDeleteSchemaConfirmationVisible(true)}
danger

View file

@ -6,11 +6,8 @@ import {
} from 'generated-sources';
import { useAppDispatch } from 'lib/hooks/redux';
import ConfirmationModal from 'components/common/ConfirmationModal/ConfirmationModal';
import DropdownItem from 'components/common/Dropdown/DropdownItem';
import { TableCellProps } from 'components/common/SmartTable/TableColumn';
import { TopicWithDetailedInfo } from 'redux/interfaces';
import VerticalElipsisIcon from 'components/common/Icons/VerticalElipsisIcon';
import Dropdown from 'components/common/Dropdown/Dropdown';
import ClusterContext from 'components/contexts/ClusterContext';
import * as S from 'components/Topics/List/List.styled';
import { ClusterNameRoute } from 'lib/paths';
@ -22,6 +19,7 @@ import {
recreateTopic,
} from 'redux/reducers/topics/topicsSlice';
import { clearTopicMessages } from 'redux/reducers/topicMessages/topicMessagesSlice';
import { Dropdown, DropdownItem } from 'components/common/Dropdown';
interface TopicsListParams {
clusterName: string;
@ -68,8 +66,10 @@ const ActionsCell: React.FC<
const isHidden = internal || isReadOnly || !hovered;
const deleteTopicHandler = () =>
const deleteTopicHandler = () => {
dispatch(deleteTopic({ clusterName, topicName: name }));
closeDeleteTopicModal();
};
const clearTopicMessagesHandler = () => {
dispatch(clearTopicMessages({ clusterName, topicName: name }));
@ -86,20 +86,20 @@ const ActionsCell: React.FC<
<>
<S.ActionsContainer>
{!isHidden && (
<Dropdown label={<VerticalElipsisIcon />} right>
<Dropdown>
{cleanUpPolicy === CleanUpPolicy.DELETE && (
<DropdownItem onClick={openClearMessagesModal} danger>
Clear Messages
</DropdownItem>
)}
<DropdownItem onClick={openRecreateTopicModal} danger>
Recreate Topic
</DropdownItem>
{isTopicDeletionAllowed && (
<DropdownItem onClick={openDeleteTopicModal} danger>
Remove Topic
</DropdownItem>
)}
<DropdownItem onClick={openRecreateTopicModal} danger>
Recreate Topic
</DropdownItem>
</Dropdown>
)}
</S.ActionsContainer>

View file

@ -1,12 +1,5 @@
import styled from 'styled-components';
export const DropdownExtraMessage = styled.div`
color: ${({ theme }) => theme.topicMetaData.color.label};
font-size: 14px;
width: 100%;
margin-top: 10px;
`;
export const ReplicaCell = styled.span.attrs({ 'aria-label': 'replica-info' })<{
leader?: boolean;
}>`

View file

@ -13,18 +13,19 @@ import ClusterContext from 'components/contexts/ClusterContext';
import ConfirmationModal from 'components/common/ConfirmationModal/ConfirmationModal';
import PageHeading from 'components/common/PageHeading/PageHeading';
import { Button } from 'components/common/Button/Button';
import Dropdown from 'components/common/Dropdown/Dropdown';
import VerticalElipsisIcon from 'components/common/Icons/VerticalElipsisIcon';
import DropdownItem from 'components/common/Dropdown/DropdownItem';
import styled from 'styled-components';
import Navbar from 'components/common/Navigation/Navbar.styled';
import * as S from 'components/Topics/Topic/Details/Details.styled';
import { useAppDispatch, useAppSelector } from 'lib/hooks/redux';
import { useAppSelector } from 'lib/hooks/redux';
import {
getIsTopicDeletePolicy,
getIsTopicInternal,
} from 'redux/reducers/topics/selectors';
import useAppParams from 'lib/hooks/useAppParams';
import {
Dropdown,
DropdownItem,
DropdownItemHint,
} from 'components/common/Dropdown';
import OverviewContainer from './Overview/OverviewContainer';
import TopicConsumerGroupsContainer from './ConsumerGroups/TopicConsumerGroupsContainer';
@ -32,7 +33,6 @@ import SettingsContainer from './Settings/SettingsContainer';
import Messages from './Messages/Messages';
interface Props {
isDeleted: boolean;
deleteTopic: (payload: {
clusterName: ClusterName;
topicName: TopicName;
@ -55,7 +55,6 @@ const HeaderControlsWrapper = styled.div`
`;
const Details: React.FC<Props> = ({
isDeleted,
deleteTopic,
recreateTopic,
clearTopicMessages,
@ -65,15 +64,14 @@ const Details: React.FC<Props> = ({
const isInternal = useAppSelector((state) =>
getIsTopicInternal(state, topicName)
);
const isDeletePolicy = useAppSelector((state) =>
getIsTopicDeletePolicy(state, topicName)
);
const navigate = useNavigate();
const dispatch = useAppDispatch();
const { isReadOnly, isTopicDeletionAllowed } =
React.useContext(ClusterContext);
const [isDeleteTopicConfirmationVisible, setDeleteTopicConfirmationVisible] =
React.useState(false);
const [isClearTopicConfirmationVisible, setClearTopicConfirmationVisible] =
@ -82,13 +80,11 @@ const Details: React.FC<Props> = ({
isRecreateTopicConfirmationVisible,
setRecreateTopicConfirmationVisible,
] = React.useState(false);
const deleteTopicHandler = () => deleteTopic({ clusterName, topicName });
React.useEffect(() => {
if (isDeleted) {
navigate('../..');
}
}, [isDeleted, clusterName, dispatch, navigate]);
const deleteTopicHandler = () => {
deleteTopic({ clusterName, topicName });
setDeleteTopicConfirmationVisible(false);
navigate('../..');
};
const clearTopicMessagesHandler = () => {
clearTopicMessages({ clusterName, topicName });
@ -124,39 +120,37 @@ const Details: React.FC<Props> = ({
<Route
index
element={
<Dropdown label={<VerticalElipsisIcon />} right>
<Dropdown>
<DropdownItem
onClick={() => navigate(clusterTopicEditRelativePath)}
>
Edit settings
<S.DropdownExtraMessage>
<DropdownItemHint>
Pay attention! This operation has
<br />
especially important consequences.
</S.DropdownExtraMessage>
</DropdownItemHint>
</DropdownItem>
<DropdownItem
disabled={!isDeletePolicy}
onClick={() => setClearTopicConfirmationVisible(true)}
danger
>
Clear messages
</DropdownItem>
{isDeletePolicy && (
<DropdownItem
onClick={() => setClearTopicConfirmationVisible(true)}
danger
>
Clear messages
</DropdownItem>
)}
<DropdownItem
onClick={() => setRecreateTopicConfirmationVisible(true)}
danger
>
Recreate Topic
</DropdownItem>
{isTopicDeletionAllowed && (
<DropdownItem
onClick={() => setDeleteTopicConfirmationVisible(true)}
danger
>
Remove topic
</DropdownItem>
)}
<DropdownItem
disabled={!isTopicDeletionAllowed}
onClick={() => setDeleteTopicConfirmationVisible(true)}
danger
>
Remove Topic
</DropdownItem>
</Dropdown>
}
/>
@ -214,14 +208,11 @@ const Details: React.FC<Props> = ({
</Navbar>
<Routes>
<Route index element={<OverviewContainer />} />
<Route path={clusterTopicMessagesRelativePath} element={<Messages />} />
<Route
path={clusterTopicSettingsRelativePath}
element={<SettingsContainer />}
/>
<Route
path={clusterTopicConsumerGroupsRelativePath}
element={<TopicConsumerGroupsContainer />}

View file

@ -1,13 +1,11 @@
import React from 'react';
import dayjs from 'dayjs';
import { TopicMessage } from 'generated-sources';
import Dropdown from 'components/common/Dropdown/Dropdown';
import DropdownItem from 'components/common/Dropdown/DropdownItem';
import useDataSaver from 'lib/hooks/useDataSaver';
import VerticalElipsisIcon from 'components/common/Icons/VerticalElipsisIcon';
import MessageToggleIcon from 'components/common/Icons/MessageToggleIcon';
import IconButtonWrapper from 'components/common/Icons/IconButtonWrapper';
import styled from 'styled-components';
import { Dropdown, DropdownItem } from 'components/common/Dropdown';
import MessageContent from './MessageContent/MessageContent';
import * as S from './MessageContent/MessageContent.styled';
@ -76,7 +74,7 @@ const Message: React.FC<Props> = ({
</StyledDataCell>
<td style={{ width: '5%' }}>
{vEllipsisOpen && (
<Dropdown label={<VerticalElipsisIcon />} right>
<Dropdown>
<DropdownItem onClick={copyToClipboard}>
Copy to clipboard
</DropdownItem>

View file

@ -1,13 +1,10 @@
import React from 'react';
import { Partition, Replica } from 'generated-sources';
import { ClusterName, TopicName } from 'redux/interfaces';
import Dropdown from 'components/common/Dropdown/Dropdown';
import DropdownItem from 'components/common/Dropdown/DropdownItem';
import ClusterContext from 'components/contexts/ClusterContext';
import BytesFormatted from 'components/common/BytesFormatted/BytesFormatted';
import { Table } from 'components/common/table/Table/Table.styled';
import TableHeaderCell from 'components/common/table/TableHeaderCell/TableHeaderCell';
import VerticalElipsisIcon from 'components/common/Icons/VerticalElipsisIcon';
import * as Metrics from 'components/common/Metrics';
import { Tag } from 'components/common/Tag/Tag.styled';
import { useAppSelector } from 'lib/hooks/redux';
@ -15,6 +12,7 @@ import { getTopicByName } from 'redux/reducers/topics/selectors';
import { ReplicaCell } from 'components/Topics/Topic/Details/Details.styled';
import { RouteParamsClusterTopic } from 'lib/paths';
import useAppParams from 'lib/hooks/useAppParams';
import { Dropdown, DropdownItem } from 'components/common/Dropdown';
export interface Props {
clearTopicMessages(params: {
@ -135,7 +133,7 @@ const Overview: React.FC<Props> = ({ clearTopicMessages }) => {
<td>{partition.offsetMax - partition.offsetMin}</td>
<td style={{ width: '5%' }}>
{!internal && !isReadOnly && cleanUpPolicy === 'DELETE' ? (
<Dropdown label={<VerticalElipsisIcon />} right>
<Dropdown>
<DropdownItem
onClick={() =>
clearTopicMessages({

View file

@ -80,7 +80,7 @@ describe('Overview', () => {
});
describe('when it has internal flag', () => {
it('does not render the Action button a Topic', () => {
it('renders the Action button for Topic', () => {
setupComponent(
{},
{
@ -90,7 +90,7 @@ describe('Overview', () => {
cleanUpPolicy: CleanUpPolicy.DELETE,
}
);
expect(screen.getAllByRole('menu')[0]).toBeInTheDocument();
expect(screen.getAllByLabelText('Dropdown Toggle').length).toEqual(1);
});
it('does not render Partitions', () => {

View file

@ -46,7 +46,6 @@ describe('Details', () => {
deleteTopic={mockDelete}
recreateTopic={mockRecreateTopic}
clearTopicMessages={mockClearTopicMessages}
isDeleted={false}
{...props}
/>
</WithRoute>
@ -83,7 +82,6 @@ describe('Details', () => {
deleteTopic={mockDelete}
recreateTopic={mockRecreateTopic}
clearTopicMessages={mockClearTopicMessages}
isDeleted={false}
/>
</ClusterContext.Provider>
);
@ -95,8 +93,7 @@ describe('Details', () => {
describe('when remove topic modal is open', () => {
beforeEach(() => {
setupComponent();
const openModalButton = screen.getAllByText('Remove topic')[0];
const openModalButton = screen.getAllByText('Remove Topic')[0];
userEvent.click(openModalButton);
});
@ -156,7 +153,13 @@ describe('Details', () => {
});
it('redirects to the correct route if topic is deleted', () => {
setupComponent({ isDeleted: true });
setupComponent();
const deleteTopicButton = screen.getByText(/Remove topic/i);
userEvent.click(deleteTopicButton);
const submitDeleteButton = screen.getByText(/Submit/i);
userEvent.click(submitDeleteButton);
expect(mockNavigate).toHaveBeenCalledWith('../..');
});

View file

@ -170,7 +170,7 @@ const SendMessage: React.FC = () => {
aria-labelledby="selectPartitionOptions"
name={name}
onChange={onChange}
minWidth="100%"
minWidth="100px"
options={selectPartitionOptions}
value={selectPartitionOptions[0].value}
/>

View file

@ -1,30 +1,73 @@
import styled from 'styled-components';
import styled, { css, keyframes } from 'styled-components';
import { ControlledMenu } from '@szhsin/react-menu';
import { menuSelector, menuItemSelector } from '@szhsin/react-menu/style-utils';
export const TriggerWrapper = styled.div`
display: flex;
align-self: center;
import '@szhsin/react-menu/dist/core.css';
const menuShow = keyframes`
from {
opacity: 0;
}
`;
export const Trigger = styled.button.attrs({
type: 'button',
ariaHaspopup: 'true',
ariaControls: 'dropdown-menu',
})`
background: transparent;
border: none;
display: flex;
align-items: 'center';
justify-content: 'center';
&:hover {
cursor: pointer;
const menuHide = keyframes`
to {
opacity: 0;
}
`;
export const Item = styled.a.attrs({
href: '#end',
role: 'menuitem',
type: 'button',
})<{ $isDanger: boolean }>`
color: ${({ $isDanger, theme }) =>
$isDanger ? theme.dropdown.color : 'initial'};
export const Dropdown = styled(ControlledMenu)(
({ theme: { dropdown } }) => css`
// container for the menu items
${menuSelector.name} {
border: 1px solid ${dropdown.borderColor};
box-shadow: 0px 4px 16px ${dropdown.shadow};
padding: 8px 0;
border-radius: 4px;
font-size: 14px;
background-color: ${dropdown.backgroundColor};
text-align: left;
}
${menuSelector.stateOpening} {
animation: ${menuShow} 0.15s ease-out;
}
// NOTE: animation-fill-mode: forwards is required to
// prevent flickering with React 18 createRoot()
${menuSelector.stateClosing} {
animation: ${menuHide} 0.2s ease-out forwards;
}
${menuItemSelector.name} {
padding: 6px 16px;
min-width: 150px;
background-color: ${dropdown.item.backgroundColor.default};
white-space: nowrap;
}
${menuItemSelector.hover} {
background-color: ${dropdown.item.backgroundColor.hover};
}
${menuItemSelector.disabled} {
cursor: not-allowed;
}
`
);
export const DropdownButton = styled.button`
background-color: transparent;
border: none;
display: flex;
cursor: pointer;
`;
export const DangerItem = styled.div`
color: ${({ theme: { dropdown } }) => dropdown.item.color.danger};
`;
export const DropdownItemHint = styled.div`
color: ${({ theme }) => theme.topicMetaData.color.label};
font-size: 12px;
margin-top: 5px;
`;

View file

@ -1,46 +1,46 @@
import useOutsideClickRef from '@rooks/use-outside-click-ref';
import cx from 'classnames';
import React, { PropsWithChildren, useMemo, useState } from 'react';
import { MenuProps } from '@szhsin/react-menu';
import React, { PropsWithChildren, useRef } from 'react';
import VerticalElipsisIcon from 'components/common/Icons/VerticalElipsisIcon';
import useModal from 'lib/hooks/useModal';
import * as S from './Dropdown.styled';
export interface DropdownProps {
label: React.ReactNode;
right?: boolean;
up?: boolean;
interface DropdownProps extends PropsWithChildren<Partial<MenuProps>> {
label?: React.ReactNode;
}
const Dropdown: React.FC<PropsWithChildren<DropdownProps>> = ({
label,
right,
up,
children,
}) => {
const [active, setActive] = useState<boolean>(false);
const [wrapperRef] = useOutsideClickRef(() => setActive(false));
const onClick = (e: React.MouseEvent) => {
const Dropdown: React.FC<DropdownProps> = ({ label, children }) => {
const ref = useRef(null);
const { isOpen, setClose, setOpen } = useModal(false);
const handleClick: React.MouseEventHandler<HTMLButtonElement> = (e) => {
e.preventDefault();
e.stopPropagation();
setActive(!active);
setOpen();
};
const classNames = useMemo(
() =>
cx('dropdown', {
'is-active': active,
'is-right': right,
'is-up': up,
}),
[active, right, up]
);
return (
<div className={classNames} ref={wrapperRef}>
<S.TriggerWrapper>
<S.Trigger onClick={onClick}>{label}</S.Trigger>
</S.TriggerWrapper>
<div className="dropdown-menu" id="dropdown-menu" role="menu">
<div className="dropdown-content has-text-left">{children}</div>
</div>
</div>
<>
<S.DropdownButton
onClick={handleClick}
ref={ref}
aria-label="Dropdown Toggle"
>
{label || <VerticalElipsisIcon />}
</S.DropdownButton>
<S.Dropdown
anchorRef={ref}
state={isOpen ? 'open' : 'closed'}
onMouseLeave={setClose}
onClose={setClose}
align="end"
direction="bottom"
offsetY={10}
viewScroll="auto"
>
{children}
</S.Dropdown>
</>
);
};

View file

@ -1,5 +0,0 @@
import React from 'react';
const DropdownDivider: React.FC = () => <hr className="dropdown-divider" />;
export default DropdownDivider;

View file

@ -1,31 +1,32 @@
import React, { PropsWithChildren } from 'react';
import { ClickEvent, MenuItem, MenuItemProps } from '@szhsin/react-menu';
import * as S from './Dropdown.styled';
interface DropdownItemProps {
onClick(): void;
interface DropdownItemProps extends PropsWithChildren<MenuItemProps> {
danger?: boolean;
onClick?(): void;
}
const DropdownItem: React.FC<PropsWithChildren<DropdownItemProps>> = ({
const DropdownItem: React.FC<DropdownItemProps> = ({
onClick,
danger,
children,
...rest
}) => {
const onClickHandler = (e: React.MouseEvent) => {
e.preventDefault();
e.stopPropagation();
const handleClick = (e: ClickEvent) => {
if (!onClick) return;
// eslint-disable-next-line no-param-reassign
e.stopPropagation = true;
e.syntheticEvent.stopPropagation();
onClick();
};
return (
<S.Item
$isDanger={!!danger}
onClick={onClickHandler}
className="dropdown-item is-link"
>
{children}
</S.Item>
<MenuItem onClick={handleClick} {...rest}>
{danger ? <S.DangerItem>{children}</S.DangerItem> : children}
</MenuItem>
);
};

View file

@ -1,81 +0,0 @@
import React from 'react';
import Dropdown, { DropdownProps } from 'components/common/Dropdown/Dropdown';
import DropdownItem from 'components/common/Dropdown/DropdownItem';
import DropdownDivider from 'components/common/Dropdown/DropdownDivider';
import userEvent from '@testing-library/user-event';
import { render } from 'lib/testHelpers';
import { screen } from '@testing-library/react';
const dummyLable = 'My Test Label';
const dummyChildren = (
<>
<DropdownItem onClick={jest.fn()}>Child 1</DropdownItem>
<DropdownItem onClick={jest.fn()}>Child 2</DropdownItem>
<DropdownDivider />
<DropdownItem onClick={jest.fn()}>Child 3</DropdownItem>
</>
);
describe('Dropdown', () => {
const setupWrapper = (
props: Partial<DropdownProps> = {},
children: React.ReactNode = undefined
) => (
<Dropdown label={dummyLable} {...props}>
{children}
</Dropdown>
);
it('renders Dropdown with initial props', () => {
const wrapper = render(setupWrapper()).baseElement;
expect(wrapper.querySelector('.dropdown')).toBeTruthy();
expect(wrapper.querySelector('.dropdown.is-active')).toBeFalsy();
expect(wrapper.querySelector('.dropdown.is-right')).toBeFalsy();
expect(wrapper.querySelector('.dropdown.is-up')).toBeFalsy();
expect(wrapper.querySelector('.dropdown-content')).toBeTruthy();
expect(wrapper.querySelector('.dropdown-content')).toHaveTextContent('');
});
it('renders custom children', () => {
const wrapper = render(setupWrapper({}, dummyChildren)).baseElement;
expect(wrapper.querySelector('.dropdown-content')).toBeTruthy();
expect(wrapper.querySelectorAll('.dropdown-item').length).toEqual(3);
expect(wrapper.querySelectorAll('.dropdown-divider').length).toEqual(1);
});
it('renders dropdown with a right-aligned menu', () => {
const wrapper = render(setupWrapper({ right: true })).baseElement;
expect(wrapper.querySelector('.dropdown.is-right')).toBeTruthy();
});
it('renders dropdown with a popup menu', () => {
const wrapper = render(setupWrapper({ up: true })).baseElement;
expect(wrapper.querySelector('.dropdown.is-up')).toBeTruthy();
});
it('handles click', () => {
const wrapper = render(setupWrapper()).baseElement;
const button = screen.getByText('My Test Label');
expect(button).toBeInTheDocument();
expect(wrapper.querySelector('.dropdown.is-active')).toBeFalsy();
userEvent.click(button);
expect(wrapper.querySelector('.dropdown.is-active')).toBeTruthy();
});
it('to be in the document', () => {
render(
setupWrapper(
{
right: true,
up: true,
},
dummyChildren
)
);
expect(screen.getByRole('menu')).toBeInTheDocument();
});
});

View file

@ -1,20 +0,0 @@
import React from 'react';
import DropdownItem from 'components/common/Dropdown/DropdownItem';
import { render } from 'lib/testHelpers';
import userEvent from '@testing-library/user-event';
import { screen } from '@testing-library/react';
const onClick = jest.fn();
describe('DropdownItem', () => {
it('to be in the document', () => {
render(<DropdownItem onClick={jest.fn()}>Item 1</DropdownItem>);
expect(screen.getByText('Item 1')).toBeInTheDocument();
});
it('handles Click', () => {
render(<DropdownItem onClick={onClick}>Item 1</DropdownItem>);
userEvent.click(screen.getByText('Item 1'));
expect(onClick).toHaveBeenCalled();
});
});

View file

@ -0,0 +1,5 @@
import { DropdownItemHint } from './Dropdown.styled';
import Dropdown from './Dropdown';
import DropdownItem from './DropdownItem';
export { Dropdown, DropdownItem, DropdownItemHint };

View file

@ -1,12 +1,9 @@
@import '@fortawesome/fontawesome-free/css/all.min.css';
// Base
@import "bulma/sass/base/minireset";
@import "./minireset";
@import "bulma/sass/base/generic";
// Components
@import "bulma/sass/components/dropdown";
@import url('https://fonts.googleapis.com/css2?family=Roboto+Mono:wght@400;500&display=swap');
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500&display=swap');

View file

@ -0,0 +1 @@
/*! minireset.css v0.0.6 | MIT License | github.com/jgthms/minireset.css */html,body,p,ol,ul,li,dl,dt,dd,blockquote,figure,fieldset,legend,textarea,pre,iframe,hr,h1,h2,h3,h4,h5,h6{margin:0;padding:0}h1,h2,h3,h4,h5,h6{font-size:100%;font-weight:normal}ul{list-style:none}button,input,select{margin:0}html{box-sizing:border-box}*,*::before,*::after{box-sizing:inherit}img,video{height:auto;max-width:100%}iframe{border:0}table{border-collapse:collapse;border-spacing:0}td,th{padding:0}

View file

@ -77,7 +77,18 @@ const theme = {
breadcrumb: Colors.neutral[30],
connectEditWarning: Colors.yellow[10],
dropdown: {
color: Colors.red[50],
backgroundColor: Colors.neutral[0],
borderColor: Colors.neutral[5],
shadow: Colors.transparency[20],
item: {
color: {
danger: Colors.red[60],
},
backgroundColor: {
default: Colors.neutral[0],
hover: Colors.neutral[5],
},
},
},
ksqlDb: {
query: {