chore: Adds RTL tests to DropdownContainer (#22041)

This commit is contained in:
Michael S. Molina 2022-11-07 10:11:28 -05:00 committed by GitHub
parent e33a08693b
commit aa48cae6fb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 193 additions and 30 deletions

View File

@ -31,7 +31,7 @@ export default {
const ITEMS_COUNT = 6;
const ITEM_OPTIONS = 10;
const MIN_WIDTH = 700;
const MAX_WIDTH = 1500;
const MAX_WIDTH = 1300;
const HEIGHT = 400;
const itemsOptions = Array.from({ length: ITEM_OPTIONS }).map((_, i) => ({
@ -47,10 +47,10 @@ const generateItems = (overflowingState?: OverflowingState) =>
Array.from({ length: ITEMS_COUNT }).map((_, i) => ({
id: `el-${i}`,
element: (
<div style={{ minWidth: 200 }}>
<div style={{ minWidth: 150 }}>
<Select
options={itemsOptions}
header={`Option ${i}`}
header={`Label ${i}`}
headerPosition={
overflowingState?.overflowed.includes(`el-${i}`) ? 'top' : 'left'
}

View File

@ -0,0 +1,143 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React from 'react';
import userEvent from '@testing-library/user-event';
import { screen, render } from 'spec/helpers/testing-library';
import Button from '../Button';
import Icons from '../Icons';
import DropdownContainer from '.';
const generateItems = (n: number) =>
Array.from({ length: n }).map((_, i) => ({
id: `el-${i + 1}`,
element: <Button>{`Element ${i + 1}`}</Button>,
}));
const ITEMS = generateItems(10);
const mockOverflowingIndex = async (
overflowingIndex: number,
func: Function,
) => {
const spy = jest.spyOn(React, 'useState');
spy.mockImplementation(() => [overflowingIndex, jest.fn()]);
await func();
spy.mockRestore();
};
test('renders children', () => {
render(<DropdownContainer items={generateItems(3)} />);
expect(screen.getByText('Element 1')).toBeInTheDocument();
expect(screen.getByText('Element 2')).toBeInTheDocument();
expect(screen.getByText('Element 3')).toBeInTheDocument();
});
test('renders children with custom horizontal spacing', () => {
render(<DropdownContainer items={ITEMS} style={{ gap: 20 }} />);
expect(screen.getByTestId('container')).toHaveStyle('gap: 20px');
});
test('renders a dropdown trigger when overflowing', async () => {
await mockOverflowingIndex(3, () => {
render(<DropdownContainer items={ITEMS} />);
expect(screen.getByText('More')).toBeInTheDocument();
});
});
test('renders a dropdown trigger with custom icon', async () => {
await mockOverflowingIndex(3, async () => {
render(
<DropdownContainer items={ITEMS} dropdownTriggerIcon={<Icons.Link />} />,
);
expect(
await screen.findByRole('img', { name: 'link' }),
).toBeInTheDocument();
});
});
test('renders a dropdown trigger with custom text', async () => {
await mockOverflowingIndex(3, () => {
const customText = 'Custom text';
render(
<DropdownContainer items={ITEMS} dropdownTriggerText={customText} />,
);
expect(screen.getByText(customText)).toBeInTheDocument();
});
});
test('renders a dropdown trigger with custom count', async () => {
await mockOverflowingIndex(3, () => {
const customCount = 99;
render(
<DropdownContainer items={ITEMS} dropdownTriggerCount={customCount} />,
);
expect(screen.getByTitle(customCount)).toBeInTheDocument();
});
});
test('does not render a dropdown button when not overflowing', () => {
render(<DropdownContainer items={generateItems(3)} />);
expect(screen.queryByText('More')).not.toBeInTheDocument();
});
test('renders a dropdown when overflowing', async () => {
await mockOverflowingIndex(3, () => {
render(<DropdownContainer items={ITEMS} />);
userEvent.click(screen.getByText('More'));
expect(screen.getByTestId('dropdown-content')).toBeInTheDocument();
});
});
test('renders children with custom vertical spacing', async () => {
await mockOverflowingIndex(3, () => {
render(<DropdownContainer items={ITEMS} dropdownStyle={{ gap: 20 }} />);
userEvent.click(screen.getByText('More'));
expect(screen.getByTestId('dropdown-content')).toHaveStyle('gap: 20px');
});
});
test('fires event when overflowing state changes', async () => {
await mockOverflowingIndex(3, () => {
const onOverflowingStateChange = jest.fn();
render(
<DropdownContainer
items={generateItems(5)}
onOverflowingStateChange={onOverflowingStateChange}
/>,
);
expect(onOverflowingStateChange).toHaveBeenCalledWith({
notOverflowed: ['el-1', 'el-2', 'el-3'],
overflowed: ['el-4', 'el-5'],
});
});
});
test('renders a dropdown with custom content', async () => {
await mockOverflowingIndex(3, () => {
const customDropdownContent = <div>Custom content</div>;
render(
<DropdownContainer
items={ITEMS}
dropdownContent={() => customDropdownContent}
/>,
);
userEvent.click(screen.getByText('More'));
expect(screen.getByText('Custom content')).toBeInTheDocument();
});
});

View File

@ -0,0 +1,17 @@
import { Meta, Source } from '@storybook/addon-docs';
<Meta title="DropdownContainer/Overview" />
# Usage
The dropdown container is used to display elements horizontally in a responsive way. If the elements don't fit in
the available width, they are displayed vertically in a dropdown. Some of the common applications in Superset are:
- Display chart filters in the CRUD views
- Horizontally display native filters in a dashboard
# Variations
The component accepts any React element which ensures a high level of variability in Superset.
To check the component in detail and its interactions, check the [DropdownContainer](/story/dropdowncontainer--component) page.

View File

@ -50,7 +50,7 @@ export interface Item {
}
/**
* Horizontal container that displays overflowed items in a popover.
* Horizontal container that displays overflowed items in a dropdown.
* It shows an indicator of how many items are currently overflowing.
*/
export interface DropdownContainerProps {
@ -61,36 +61,36 @@ export interface DropdownContainerProps {
items: Item[];
/**
* Event handler called every time an element moves between
* main container and popover.
* main container and dropdown.
*/
onOverflowingStateChange?: (overflowingState: {
notOverflowed: string[];
overflowed: string[];
}) => void;
/**
* Option to customize the content of the popover.
* Option to customize the content of the dropdown.
*/
popoverContent?: (overflowedItems: Item[]) => ReactElement;
dropdownContent?: (overflowedItems: Item[]) => ReactElement;
/**
* Popover ref.
* Dropdown ref.
*/
popoverRef?: RefObject<HTMLDivElement>;
dropdownRef?: RefObject<HTMLDivElement>;
/**
* Popover additional style properties.
* Dropdown additional style properties.
*/
popoverStyle?: CSSProperties;
dropdownStyle?: CSSProperties;
/**
* Displayed count in the popover trigger.
* Displayed count in the dropdown trigger.
*/
popoverTriggerCount?: number;
dropdownTriggerCount?: number;
/**
* Icon of the popover trigger.
* Icon of the dropdown trigger.
*/
popoverTriggerIcon?: ReactElement;
dropdownTriggerIcon?: ReactElement;
/**
* Text of the popover trigger.
* Text of the dropdown trigger.
*/
popoverTriggerText?: string;
dropdownTriggerText?: string;
/**
* Main container additional style properties.
*/
@ -104,12 +104,12 @@ const DropdownContainer = forwardRef(
{
items,
onOverflowingStateChange,
popoverContent,
popoverRef,
popoverStyle = {},
popoverTriggerCount,
popoverTriggerIcon,
popoverTriggerText = t('More'),
dropdownContent: popoverContent,
dropdownRef: popoverRef,
dropdownStyle: popoverStyle = {},
dropdownTriggerCount: popoverTriggerCount,
dropdownTriggerIcon: popoverTriggerIcon,
dropdownTriggerText: popoverTriggerText = t('More'),
style,
}: DropdownContainerProps,
outerRef: RefObject<Ref>,
@ -118,10 +118,12 @@ const DropdownContainer = forwardRef(
const { ref, width = 0 } = useResizeDetector<HTMLDivElement>();
const previousWidth = usePrevious(width) || 0;
const { current } = ref;
const [overflowingIndex, setOverflowingIndex] = useState<number>(-1);
const [itemsWidth, setItemsWidth] = useState<number[]>([]);
const [popoverVisible, setPopoverVisible] = useState(false);
// We use React.useState to be able to mock the state in Jest
const [overflowingIndex, setOverflowingIndex] = React.useState<number>(-1);
useLayoutEffect(() => {
const container = current?.children.item(0);
if (container) {
@ -149,10 +151,10 @@ const DropdownContainer = forwardRef(
const buttonRight = button?.getBoundingClientRect().right || 0;
const containerRight = current?.getBoundingClientRect().right || 0;
const remainingSpace = containerRight - buttonRight;
// Checks if the first element in the popover fits in the remaining space
// Checks if the first element in the dropdown fits in the remaining space
const fitsInRemainingSpace = remainingSpace >= itemsWidth[0];
if (fitsInRemainingSpace && overflowingIndex < items.length) {
// Moves element from popover to container
// Moves element from dropdown to container
setOverflowingIndex(overflowingIndex + 1);
}
}
@ -215,6 +217,7 @@ const DropdownContainer = forwardRef(
flex-direction: column;
gap: ${theme.gridUnit * 3}px;
`}
data-test="dropdown-content"
style={popoverStyle}
ref={popoverRef}
>
@ -260,6 +263,7 @@ const DropdownContainer = forwardRef(
margin-right: ${theme.gridUnit * 3}px;
min-width: 100px;
`}
data-test="container"
style={style}
>
{notOverflowedItems.map(item => item.element)}
@ -270,10 +274,6 @@ const DropdownContainer = forwardRef(
trigger="click"
visible={popoverVisible}
onVisibleChange={visible => setPopoverVisible(visible)}
overlayInnerStyle={{
maxHeight: 500,
overflowY: 'auto',
}}
>
<Button buttonStyle="secondary">
{popoverTriggerIcon}

View File

@ -23,6 +23,9 @@ import AntdSelect from 'antd/lib/select';
export const StyledHeader = styled.span<{ headerPosition: string }>`
${({ theme, headerPosition }) => `
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
margin-right: ${headerPosition === 'left' ? theme.gridUnit * 2 : 0}px;
`}
`;