From b040211970e456337455e44ba17bf78f11650097 Mon Sep 17 00:00:00 2001 From: "Michael S. Molina" <70410625+michael-s-molina@users.noreply.github.com> Date: Fri, 4 Nov 2022 10:20:49 -0400 Subject: [PATCH] feat: Adds more customization properties to DropdownContainer (#22031) --- .../DropdownContainer.stories.tsx | 57 +-- .../components/DropdownContainer/index.tsx | 330 ++++++++++-------- 2 files changed, 215 insertions(+), 172 deletions(-) diff --git a/superset-frontend/src/components/DropdownContainer/DropdownContainer.stories.tsx b/superset-frontend/src/components/DropdownContainer/DropdownContainer.stories.tsx index eed8f166356d..e2fe280dd470 100644 --- a/superset-frontend/src/components/DropdownContainer/DropdownContainer.stories.tsx +++ b/superset-frontend/src/components/DropdownContainer/DropdownContainer.stories.tsx @@ -20,7 +20,8 @@ import React, { useEffect, useState } from 'react'; import { isEqual } from 'lodash'; import { css } from '@superset-ui/core'; import Select from '../Select/Select'; -import DropdownContainer, { DropdownContainerProps } from '.'; +import Button from '../Button'; +import DropdownContainer, { DropdownContainerProps, Ref } from '.'; export default { title: 'DropdownContainer', @@ -31,6 +32,7 @@ const ITEMS_COUNT = 6; const ITEM_OPTIONS = 10; const MIN_WIDTH = 700; const MAX_WIDTH = 1500; +const HEIGHT = 400; const itemsOptions = Array.from({ length: ITEM_OPTIONS }).map((_, i) => ({ label: `Option ${i}`, @@ -60,39 +62,42 @@ const generateItems = (overflowingState?: OverflowingState) => export const Component = (props: DropdownContainerProps) => { const [items, setItems] = useState([]); const [overflowingState, setOverflowingState] = useState(); + const containerRef = React.useRef(null); useEffect(() => { setItems(generateItems(overflowingState)); }, [overflowingState]); return ( -
- { - if (!isEqual(overflowingState, value)) { - setOverflowingState(value); - } - }} - /> +
+
+ { + if (!isEqual(overflowingState, value)) { + setOverflowingState(value); + } + }} + ref={containerRef} + /> +
+ diff --git a/superset-frontend/src/components/DropdownContainer/index.tsx b/superset-frontend/src/components/DropdownContainer/index.tsx index 36f909bb1679..34da7019f0e2 100644 --- a/superset-frontend/src/components/DropdownContainer/index.tsx +++ b/superset-frontend/src/components/DropdownContainer/index.tsx @@ -18,8 +18,11 @@ */ import React, { CSSProperties, + forwardRef, ReactElement, + RefObject, useEffect, + useImperativeHandle, useLayoutEffect, useMemo, useState, @@ -68,10 +71,18 @@ export interface DropdownContainerProps { * Option to customize the content of the popover. */ popoverContent?: (overflowedItems: Item[]) => ReactElement; + /** + * Popover ref. + */ + popoverRef?: RefObject; /** * Popover additional style properties. */ popoverStyle?: CSSProperties; + /** + * Displayed count in the popover trigger. + */ + popoverTriggerCount?: number; /** * Icon of the popover trigger. */ @@ -86,171 +97,198 @@ export interface DropdownContainerProps { style?: CSSProperties; } -const DropdownContainer = ({ - items, - onOverflowingStateChange, - popoverContent, - popoverStyle = {}, - popoverTriggerIcon, - popoverTriggerText = t('More'), - style, -}: DropdownContainerProps) => { - const theme = useTheme(); - const { ref, width = 0 } = useResizeDetector(); - const previousWidth = usePrevious(width) || 0; - const { current } = ref; - const [overflowingIndex, setOverflowingIndex] = useState(-1); - const [itemsWidth, setItemsWidth] = useState([]); +export type Ref = HTMLDivElement & { open: () => void }; - useLayoutEffect(() => { - const container = current?.children.item(0); - if (container) { - const { children } = container; - const childrenArray = Array.from(children); +const DropdownContainer = forwardRef( + ( + { + items, + onOverflowingStateChange, + popoverContent, + popoverRef, + popoverStyle = {}, + popoverTriggerCount, + popoverTriggerIcon, + popoverTriggerText = t('More'), + style, + }: DropdownContainerProps, + outerRef: RefObject, + ) => { + const theme = useTheme(); + const { ref, width = 0 } = useResizeDetector(); + const previousWidth = usePrevious(width) || 0; + const { current } = ref; + const [overflowingIndex, setOverflowingIndex] = useState(-1); + const [itemsWidth, setItemsWidth] = useState([]); + const [popoverVisible, setPopoverVisible] = useState(false); - // Stores items width once - if (itemsWidth.length === 0) { - setItemsWidth( - childrenArray.map(child => child.getBoundingClientRect().width), - ); - } + useLayoutEffect(() => { + const container = current?.children.item(0); + if (container) { + const { children } = container; + const childrenArray = Array.from(children); - // Calculates the index of the first overflowed element - const index = childrenArray.findIndex( - child => - child.getBoundingClientRect().right > - container.getBoundingClientRect().right, - ); - setOverflowingIndex(index === -1 ? children.length : index); + // Stores items width once + if (itemsWidth.length === 0) { + setItemsWidth( + childrenArray.map(child => child.getBoundingClientRect().width), + ); + } - if (width > previousWidth && overflowingIndex !== -1) { - // Calculates remaining space in the container - const button = current?.children.item(1); - 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 - const fitsInRemainingSpace = remainingSpace >= itemsWidth[0]; - if (fitsInRemainingSpace && overflowingIndex < items.length) { - // Moves element from popover to container - setOverflowingIndex(overflowingIndex + 1); + // Calculates the index of the first overflowed element + const index = childrenArray.findIndex( + child => + child.getBoundingClientRect().right > + container.getBoundingClientRect().right, + ); + setOverflowingIndex(index === -1 ? children.length : index); + + if (width > previousWidth && overflowingIndex !== -1) { + // Calculates remaining space in the container + const button = current?.children.item(1); + 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 + const fitsInRemainingSpace = remainingSpace >= itemsWidth[0]; + if (fitsInRemainingSpace && overflowingIndex < items.length) { + // Moves element from popover to container + setOverflowingIndex(overflowingIndex + 1); + } } } - } - }, [ - current, - items.length, - itemsWidth, - overflowingIndex, - previousWidth, - width, - ]); + }, [ + current, + items.length, + itemsWidth, + overflowingIndex, + previousWidth, + width, + ]); - const reduceItems = (items: Item[]): [Item[], string[]] => - items.reduce( - ([items, ids], item) => { - items.push({ - id: item.id, - element: React.cloneElement(item.element, { key: item.id }), - }); - ids.push(item.id); - return [items, ids]; - }, - [[], []] as [Item[], string[]], - ); + const reduceItems = (items: Item[]): [Item[], string[]] => + items.reduce( + ([items, ids], item) => { + items.push({ + id: item.id, + element: React.cloneElement(item.element, { key: item.id }), + }); + ids.push(item.id); + return [items, ids]; + }, + [[], []] as [Item[], string[]], + ); - const [notOverflowedItems, notOverflowedIds] = useMemo( - () => - reduceItems( - items.slice( - 0, - overflowingIndex !== -1 ? overflowingIndex : items.length, + const [notOverflowedItems, notOverflowedIds] = useMemo( + () => + reduceItems( + items.slice( + 0, + overflowingIndex !== -1 ? overflowingIndex : items.length, + ), ), - ), - [items, overflowingIndex], - ); + [items, overflowingIndex], + ); + + const [overflowedItems, overflowedIds] = useMemo( + () => + overflowingIndex !== -1 + ? reduceItems(items.slice(overflowingIndex, items.length)) + : [[], []], + [items, overflowingIndex], + ); - const [overflowedItems, overflowedIds] = useMemo( - () => - overflowingIndex !== -1 - ? reduceItems(items.slice(overflowingIndex, items.length)) - : [[], []], - [items, overflowingIndex], - ); + useEffect(() => { + if (onOverflowingStateChange) { + onOverflowingStateChange({ + notOverflowed: notOverflowedIds, + overflowed: overflowedIds, + }); + } + }, [notOverflowedIds, onOverflowingStateChange, overflowedIds]); - useEffect(() => { - if (onOverflowingStateChange) { - onOverflowingStateChange({ - notOverflowed: notOverflowedIds, - overflowed: overflowedIds, - }); - } - }, [notOverflowedIds, onOverflowingStateChange, overflowedIds]); + const content = useMemo( + () => ( +
+ {popoverContent + ? popoverContent(overflowedItems) + : overflowedItems.map(item => item.element)} +
+ ), + [ + overflowedItems, + popoverContent, + popoverRef, + popoverStyle, + theme.gridUnit, + ], + ); - const content = useMemo( - () => ( -
- {popoverContent - ? popoverContent(overflowedItems) - : overflowedItems.map(item => item.element)} -
- ), - [overflowedItems, popoverContent, popoverStyle, theme.gridUnit], - ); + useImperativeHandle( + outerRef, + () => ({ + ...(ref.current as HTMLDivElement), + open: () => setPopoverVisible(true), + }), + [ref], + ); - const overflowingCount = - overflowingIndex !== -1 ? items.length - overflowingIndex : 0; + const overflowingCount = + overflowingIndex !== -1 ? items.length - overflowingIndex : 0; - return ( -
+ return (
- {notOverflowedItems.map(item => item.element)} -
- {overflowingCount > 0 && ( - - - - )} -
- ); -}; + {notOverflowedItems.map(item => item.element)} +
+ {overflowingCount > 0 && ( + setPopoverVisible(visible)} + overlayInnerStyle={{ + maxHeight: 500, + overflowY: 'auto', + }} + > + + + )} +
+ ); + }, +); export default DropdownContainer;