You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by mi...@apache.org on 2022/11/07 15:11:39 UTC
[superset] branch master updated: chore: Adds RTL tests to DropdownContainer (#22041)
This is an automated email from the ASF dual-hosted git repository.
michaelsmolina pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/superset.git
The following commit(s) were added to refs/heads/master by this push:
new aa48cae6fb chore: Adds RTL tests to DropdownContainer (#22041)
aa48cae6fb is described below
commit aa48cae6fbbf9f319a3956052df56b51dd01683c
Author: Michael S. Molina <70...@users.noreply.github.com>
AuthorDate: Mon Nov 7 10:11:28 2022 -0500
chore: Adds RTL tests to DropdownContainer (#22041)
---
.../DropdownContainer.stories.tsx | 6 +-
.../DropdownContainer/DropdownContainer.test.tsx | 143 +++++++++++++++++++++
.../DropdownContainer/Overview.stories.mdx | 17 +++
.../src/components/DropdownContainer/index.tsx | 54 ++++----
superset-frontend/src/components/Select/styles.tsx | 3 +
5 files changed, 193 insertions(+), 30 deletions(-)
diff --git a/superset-frontend/src/components/DropdownContainer/DropdownContainer.stories.tsx b/superset-frontend/src/components/DropdownContainer/DropdownContainer.stories.tsx
index e2fe280dd4..d72b1bdd39 100644
--- a/superset-frontend/src/components/DropdownContainer/DropdownContainer.stories.tsx
+++ b/superset-frontend/src/components/DropdownContainer/DropdownContainer.stories.tsx
@@ -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'
}
diff --git a/superset-frontend/src/components/DropdownContainer/DropdownContainer.test.tsx b/superset-frontend/src/components/DropdownContainer/DropdownContainer.test.tsx
new file mode 100644
index 0000000000..de0c27bc70
--- /dev/null
+++ b/superset-frontend/src/components/DropdownContainer/DropdownContainer.test.tsx
@@ -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();
+ });
+});
diff --git a/superset-frontend/src/components/DropdownContainer/Overview.stories.mdx b/superset-frontend/src/components/DropdownContainer/Overview.stories.mdx
new file mode 100644
index 0000000000..5d3792cc5f
--- /dev/null
+++ b/superset-frontend/src/components/DropdownContainer/Overview.stories.mdx
@@ -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.
diff --git a/superset-frontend/src/components/DropdownContainer/index.tsx b/superset-frontend/src/components/DropdownContainer/index.tsx
index 34da7019f0..6111698f05 100644
--- a/superset-frontend/src/components/DropdownContainer/index.tsx
+++ b/superset-frontend/src/components/DropdownContainer/index.tsx
@@ -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}
diff --git a/superset-frontend/src/components/Select/styles.tsx b/superset-frontend/src/components/Select/styles.tsx
index 84a7d54977..2da49d7d6e 100644
--- a/superset-frontend/src/components/Select/styles.tsx
+++ b/superset-frontend/src/components/Select/styles.tsx
@@ -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;
`}
`;