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; `} `;