mirror of
https://github.com/apache/superset.git
synced 2024-09-16 02:29:39 -04:00
chore: Adds RTL tests to DropdownContainer (#22041)
This commit is contained in:
parent
e33a08693b
commit
aa48cae6fb
@ -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'
|
||||
}
|
||||
|
@ -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();
|
||||
});
|
||||
});
|
@ -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.
|
@ -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}
|
||||
|
@ -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;
|
||||
`}
|
||||
`;
|
||||
|
Loading…
Reference in New Issue
Block a user