mirror of https://github.com/apache/superset.git
feat: Adds more customization properties to DropdownContainer (#22031)
This commit is contained in:
parent
9b6322b640
commit
b040211970
|
@ -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;
|
||||
`}
|
||||
>
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Reference in New Issue