feat: Adds more customization properties to DropdownContainer (#22031)

This commit is contained in:
Michael S. Molina 2022-11-04 10:20:49 -04:00 committed by GitHub
parent 9b6322b640
commit b040211970
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 223 additions and 180 deletions

View File

@ -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<ItemsType>([]);
const [overflowingState, setOverflowingState] = useState<OverflowingState>();
const containerRef = React.useRef<Ref>(null);
useEffect(() => {
setItems(generateItems(overflowingState));
}, [overflowingState]);
return (
<div
css={css`
position: relative;
overflow: auto;
min-width: ${MIN_WIDTH}px;
width: ${MIN_WIDTH}px;
max-width: ${MAX_WIDTH}px;
height: 80vh;
border: 1px solid lightgray;
resize: horizontal;
padding: 24px;
`}
>
<DropdownContainer
{...props}
items={items}
onOverflowingStateChange={value => {
if (!isEqual(overflowingState, value)) {
setOverflowingState(value);
}
}}
/>
<div>
<div
css={css`
overflow: auto;
min-width: ${MIN_WIDTH}px;
width: ${MIN_WIDTH}px;
max-width: ${MAX_WIDTH}px;
height: ${HEIGHT}px;
border: 1px solid lightgray;
resize: horizontal;
padding: 24px;
margin-bottom: 12px;
`}
>
<DropdownContainer
{...props}
items={items}
onOverflowingStateChange={value => {
if (!isEqual(overflowingState, value)) {
setOverflowingState(value);
}
}}
ref={containerRef}
/>
</div>
<Button onClick={() => containerRef.current?.open()}>Open</Button>
<span
css={css`
position: absolute;
right: 20px;
bottom: 8px;
margin-left: ${MIN_WIDTH - 340}px;
color: gray;
`}
>

View File

@ -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<HTMLDivElement>;
/**
* 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<HTMLDivElement>();
const previousWidth = usePrevious(width) || 0;
const { current } = ref;
const [overflowingIndex, setOverflowingIndex] = useState<number>(-1);
const [itemsWidth, setItemsWidth] = useState<number[]>([]);
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<Ref>,
) => {
const theme = useTheme();
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);
// 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);
// Stores items width once
if (itemsWidth.length === 0) {
setItemsWidth(
childrenArray.map(child => child.getBoundingClientRect().width),
);
}
// 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);
// 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);
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,
),
),
[items, overflowingIndex],
);
const [notOverflowedItems, notOverflowedIds] = useMemo(
() =>
reduceItems(
items.slice(
0,
overflowingIndex !== -1 ? overflowingIndex : items.length,
),
),
[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(
() => (
<div
css={css`
display: flex;
flex-direction: column;
gap: ${theme.gridUnit * 3}px;
width: 200px;
`}
style={popoverStyle}
>
{popoverContent
? popoverContent(overflowedItems)
: overflowedItems.map(item => item.element)}
</div>
),
[overflowedItems, popoverContent, popoverStyle, theme.gridUnit],
);
const overflowingCount =
overflowingIndex !== -1 ? items.length - overflowingIndex : 0;
return (
<div
ref={ref}
css={css`
display: flex;
align-items: flex-end;
`}
>
<div
css={css`
display: flex;
align-items: center;
gap: ${theme.gridUnit * 3}px;
margin-right: ${theme.gridUnit * 3}px;
min-width: 100px;
`}
style={style}
>
{notOverflowedItems.map(item => item.element)}
</div>
{overflowingCount > 0 && (
<Popover
content={content}
trigger="click"
overlayInnerStyle={{
overflow: 'auto',
maxHeight: 500,
}}
const content = useMemo(
() => (
<div
css={css`
display: flex;
flex-direction: column;
gap: ${theme.gridUnit * 3}px;
`}
style={popoverStyle}
ref={popoverRef}
>
<Button buttonStyle="secondary">
{popoverTriggerIcon}
{popoverTriggerText}
<Badge count={overflowingCount} />
<Icons.DownOutlined
iconSize="m"
iconColor={theme.colors.grayscale.base}
/>
</Button>
</Popover>
)}
</div>
);
};
{popoverContent
? popoverContent(overflowedItems)
: overflowedItems.map(item => item.element)}
</div>
),
[
overflowedItems,
popoverContent,
popoverRef,
popoverStyle,
theme.gridUnit,
],
);
useImperativeHandle(
outerRef,
() => ({
...(ref.current as HTMLDivElement),
open: () => setPopoverVisible(true),
}),
[ref],
);
const overflowingCount =
overflowingIndex !== -1 ? items.length - overflowingIndex : 0;
return (
<div
ref={ref}
css={css`
display: flex;
align-items: flex-end;
`}
>
<div
css={css`
display: flex;
align-items: center;
gap: ${theme.gridUnit * 3}px;
margin-right: ${theme.gridUnit * 3}px;
min-width: 100px;
`}
style={style}
>
{notOverflowedItems.map(item => item.element)}
</div>
{overflowingCount > 0 && (
<Popover
content={content}
trigger="click"
visible={popoverVisible}
onVisibleChange={visible => setPopoverVisible(visible)}
overlayInnerStyle={{
maxHeight: 500,
overflowY: 'auto',
}}
>
<Button buttonStyle="secondary">
{popoverTriggerIcon}
{popoverTriggerText}
<Badge count={popoverTriggerCount || overflowingCount} />
<Icons.DownOutlined
iconSize="m"
iconColor={theme.colors.grayscale.base}
/>
</Button>
</Popover>
)}
</div>
);
},
);
export default DropdownContainer;