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 <70410625+michael-s-mol...@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;
   `}
 `;

Reply via email to