4810 display participants in the right drawer of the calendar event (#4896)
Closes #4810 - Introduces a new component `ExpandableList` which uses intersection observers to display the maximum number of elements possible
This commit is contained in:
@ -0,0 +1,104 @@
|
||||
import React, { ReactElement, useRef, useState } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { Chip, ChipVariant } from '@/ui/display/chip/components/Chip';
|
||||
import { IntersectionObserverWrapper } from '@/ui/display/expandable-list/IntersectionObserverWrapper';
|
||||
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex: 1;
|
||||
gap: ${({ theme }) => theme.spacing(1)};
|
||||
box-sizing: border-box;
|
||||
white-space: nowrap;
|
||||
overflow-x: hidden;
|
||||
`;
|
||||
|
||||
const StyledExpendableCell = styled.div`
|
||||
align-content: center;
|
||||
align-items: center;
|
||||
backdrop-filter: ${({ theme }) => theme.blur.strong};
|
||||
background: ${({ theme }) => theme.background.secondary};
|
||||
border: 1px solid ${({ theme }) => theme.border.color.medium};
|
||||
border-radius: ${({ theme }) => theme.border.radius.sm};
|
||||
box-shadow: ${({ theme }) => theme.boxShadow.light};
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: ${({ theme }) => theme.spacing(1)};
|
||||
padding: ${({ theme }) => theme.spacing(2)};
|
||||
`;
|
||||
|
||||
export const ExpandableList = ({
|
||||
listItems,
|
||||
rootRef,
|
||||
id,
|
||||
}: {
|
||||
listItems: ReactElement[];
|
||||
rootRef: React.RefObject<HTMLElement>;
|
||||
id: string;
|
||||
}) => {
|
||||
const [listItemsInView, setListItemsInView] = useState(new Set<number>());
|
||||
|
||||
const firstListItem = listItems[0];
|
||||
|
||||
const dropdownId = `expandable-list-dropdown-${id}`;
|
||||
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const divRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
return (
|
||||
<StyledContainer ref={containerRef}>
|
||||
{firstListItem}
|
||||
{listItems.slice(1).map((listItem, index) => (
|
||||
<React.Fragment key={index}>
|
||||
<IntersectionObserverWrapper
|
||||
set={setListItemsInView}
|
||||
id={index}
|
||||
rootRef={rootRef}
|
||||
>
|
||||
{listItem}
|
||||
</IntersectionObserverWrapper>
|
||||
{index === listItemsInView.size - 1 &&
|
||||
listItems.length - listItemsInView.size - 1 !== 0 && (
|
||||
<Dropdown
|
||||
dropdownId={dropdownId}
|
||||
dropdownHotkeyScope={{
|
||||
scope: dropdownId,
|
||||
}}
|
||||
clickableComponent={
|
||||
<Chip
|
||||
label={`+${listItems.length - listItemsInView.size - 1}`}
|
||||
variant={ChipVariant.Highlighted}
|
||||
/>
|
||||
}
|
||||
dropdownComponents={
|
||||
<>
|
||||
{divRef.current &&
|
||||
createPortal(
|
||||
<StyledExpendableCell>
|
||||
{listItems}
|
||||
</StyledExpendableCell>,
|
||||
divRef.current as HTMLElement,
|
||||
)}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</React.Fragment>
|
||||
))}
|
||||
<div
|
||||
ref={divRef}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: '100%',
|
||||
zIndex: 1,
|
||||
boxSizing: 'border-box',
|
||||
}}
|
||||
></div>
|
||||
</StyledContainer>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,47 @@
|
||||
import React from 'react';
|
||||
import { useInView } from 'react-intersection-observer';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
const StyledDiv = styled.div<{ inView?: boolean }>`
|
||||
opacity: ${({ inView }) => (inView === undefined || inView ? 1 : 0)};
|
||||
`;
|
||||
|
||||
export const IntersectionObserverWrapper = ({
|
||||
set,
|
||||
id,
|
||||
rootRef,
|
||||
children,
|
||||
}: {
|
||||
set: React.Dispatch<React.SetStateAction<Set<number>>>;
|
||||
id: number;
|
||||
rootRef?: React.RefObject<HTMLElement>;
|
||||
children: React.ReactNode;
|
||||
}) => {
|
||||
const { ref, inView } = useInView({
|
||||
threshold: 1,
|
||||
onChange: (inView) => {
|
||||
if (inView) {
|
||||
set((prev: Set<number>) => {
|
||||
const newSet = new Set(prev);
|
||||
newSet.add(id);
|
||||
return newSet;
|
||||
});
|
||||
}
|
||||
if (!inView) {
|
||||
set((prev: Set<number>) => {
|
||||
const newSet = new Set(prev);
|
||||
newSet.delete(id);
|
||||
return newSet;
|
||||
});
|
||||
}
|
||||
},
|
||||
root: rootRef?.current,
|
||||
rootMargin: '0px 0px -50px 0px',
|
||||
});
|
||||
|
||||
return (
|
||||
<StyledDiv ref={ref} inView={inView}>
|
||||
{children}
|
||||
</StyledDiv>
|
||||
);
|
||||
};
|
||||
@ -2,6 +2,7 @@ import styled from '@emotion/styled';
|
||||
|
||||
const StyledDropdownMenu = styled.div<{
|
||||
disableBlur?: boolean;
|
||||
disableBorder?: boolean;
|
||||
width?: `${string}px` | `${number}%` | 'auto' | number;
|
||||
}>`
|
||||
backdrop-filter: ${({ disableBlur }) =>
|
||||
@ -14,7 +15,8 @@ const StyledDropdownMenu = styled.div<{
|
||||
? theme.background.primary
|
||||
: theme.background.transparent.secondary};
|
||||
|
||||
border: 1px solid ${({ theme }) => theme.border.color.medium};
|
||||
border: ${({ disableBorder, theme }) =>
|
||||
disableBorder ? 'none' : `1px solid ${theme.border.color.medium}`};
|
||||
border-radius: ${({ theme }) => theme.border.radius.md};
|
||||
|
||||
box-shadow: ${({ theme }) => theme.boxShadow.strong};
|
||||
|
||||
@ -23,7 +23,7 @@ const StyledRightDrawerBody = styled.div`
|
||||
height: calc(
|
||||
100vh - ${({ theme }) => theme.spacing(14)} - 1px
|
||||
); // (-1 for border)
|
||||
overflow: auto;
|
||||
//overflow: auto;
|
||||
position: relative;
|
||||
`;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user