diff --git a/app/src/renderer/apps/Account-old/components/NotificationList.tsx b/app/src/renderer/apps/Account-old/components/NotificationList.tsx index 5188e30c8f..f9748614e8 100644 --- a/app/src/renderer/apps/Account-old/components/NotificationList.tsx +++ b/app/src/renderer/apps/Account-old/components/NotificationList.tsx @@ -45,7 +45,7 @@ const NotificationListPresenter = ({ unseen, seen }: INotificationList) => { { + itemContent={(index, item) => { if (item.type === 'title') { const title = item.data as string; return ( diff --git a/app/src/renderer/apps/Account/components/NotificationList.tsx b/app/src/renderer/apps/Account/components/NotificationList.tsx index 5188e30c8f..f9748614e8 100644 --- a/app/src/renderer/apps/Account/components/NotificationList.tsx +++ b/app/src/renderer/apps/Account/components/NotificationList.tsx @@ -45,7 +45,7 @@ const NotificationListPresenter = ({ unseen, seen }: INotificationList) => { { + itemContent={(index, item) => { if (item.type === 'title') { const title = item.data as string; return ( diff --git a/app/src/renderer/apps/Courier/components/ChatMessage.tsx b/app/src/renderer/apps/Courier/components/ChatMessage.tsx index 3f75197cbd..02a9e3a9f6 100644 --- a/app/src/renderer/apps/Courier/components/ChatMessage.tsx +++ b/app/src/renderer/apps/Courier/components/ChatMessage.tsx @@ -17,7 +17,7 @@ type ChatMessageProps = { ourColor: string; isPrevGrouped: boolean; isNextGrouped: boolean; - measure: () => void; + onReplyClick?: (msgId: string) => void; }; export const ChatMessagePresenter = ({ @@ -26,7 +26,7 @@ export const ChatMessagePresenter = ({ ourColor, isPrevGrouped, isNextGrouped, - measure, + onReplyClick, }: ChatMessageProps) => { const { ship, friends, theme } = useServices(); const { selectedChat } = useChatStore(); @@ -195,7 +195,7 @@ export const ChatMessagePresenter = ({ return ( ); }; diff --git a/app/src/renderer/apps/Courier/views/ChatLog.tsx b/app/src/renderer/apps/Courier/views/ChatLog.tsx index 054c6d0ad3..d93ea00980 100644 --- a/app/src/renderer/apps/Courier/views/ChatLog.tsx +++ b/app/src/renderer/apps/Courier/views/ChatLog.tsx @@ -3,9 +3,7 @@ import { observer } from 'mobx-react'; import styled from 'styled-components'; import { AnimatePresence } from 'framer-motion'; import { - Box, Flex, - WindowedList, Text, Reply, measureImage, @@ -18,12 +16,11 @@ import { ChatInputBox } from '../components/ChatInputBox'; import { ChatLogHeader } from '../components/ChatLogHeader'; import { ChatAvatar } from '../components/ChatAvatar'; import { IuseStorage } from 'renderer/logic/lib/useStorage'; -import { ChatMessage } from '../components/ChatMessage'; import { PinnedContainer } from '../components/PinnedMessage'; import { useServices } from 'renderer/logic/store'; import { ChatMessageType, ChatModelType } from '../models'; import { useAccountStore } from 'renderer/apps/Account/store'; -import { displayDate } from 'os/lib/time'; +import { ChatLogList } from './ChatLogList'; const FullWidthAnimatePresence = styled(AnimatePresence)` width: 100%; @@ -198,66 +195,12 @@ export const ChatLogPresenter = ({ storage }: ChatLogProps) => { /> )} - { - const isLast = selectedChat - ? index === messages.length - 1 - : false; - - const isNextGrouped = - index < messages.length - 1 && - row.sender === messages[index + 1].sender; - - const isPrevGrouped = - index > 0 && - row.sender === messages[index - 1].sender && - Object.keys(messages[index - 1].contents[0])[0] !== - 'status'; - - const topSpacing = isPrevGrouped ? '3px' : 2; - const bottomSpacing = isNextGrouped ? '3px' : 2; - - const thisMsgDate = new Date(row.createdAt).toDateString(); - const prevMsgDate = - messages[index - 1] && - new Date(messages[index - 1].createdAt).toDateString(); - const showDate: boolean = - index === 0 || thisMsgDate !== prevMsgDate; - return ( - - {showDate && ( - - {displayDate(row.createdAt)} - - )} - - - ); - }} + ourColor={ourColor} /> )} diff --git a/app/src/renderer/apps/Courier/views/ChatLogList.tsx b/app/src/renderer/apps/Courier/views/ChatLogList.tsx new file mode 100644 index 0000000000..124c9dba11 --- /dev/null +++ b/app/src/renderer/apps/Courier/views/ChatLogList.tsx @@ -0,0 +1,105 @@ +import { useRef } from 'react'; +import { + Box, + Text, + WindowedList, + WindowedListRef, +} from '@holium/design-system'; +import { displayDate } from 'os/lib/time'; +import { ChatMessage } from '../components/ChatMessage'; +import { ChatMessageType, ChatModelType } from '../models'; + +type Props = { + width: number; + height: number; + messages: ChatMessageType[]; + selectedChat: ChatModelType; + ourColor: string; +}; + +export const ChatLogList = ({ + width, + height, + messages, + selectedChat, + ourColor, +}: Props) => { + const listRef = useRef(null); + + const scrollbarWidth = 12; + + const renderChatRow = (index: number, row: ChatMessageType) => { + const isLast = selectedChat ? index === messages.length - 1 : false; + + const isNextGrouped = + index < messages.length - 1 && row.sender === messages[index + 1].sender; + + const isPrevGrouped = + index > 0 && + row.sender === messages[index - 1].sender && + Object.keys(messages[index - 1].contents[0])[0] !== 'status'; + + const topSpacing = isPrevGrouped ? '3px' : 2; + const bottomSpacing = isNextGrouped ? '3px' : 2; + + const thisMsgDate = new Date(row.createdAt).toDateString(); + const prevMsgDate = + messages[index - 1] && + new Date(messages[index - 1].createdAt).toDateString(); + const showDate = index === 0 || thisMsgDate !== prevMsgDate; + + return ( + + {showDate && ( + + {displayDate(row.createdAt)} + + )} + { + const replyIndex = messages.findIndex((msg) => msg.id === replyId); + if (replyIndex === -1) return; + + console.log('reply index', replyIndex); + + listRef.current?.scrollToIndex({ + index: replyIndex, + align: 'start', + behavior: 'smooth', + }); + }} + /> + + ); + }; + + return ( + + ); +}; diff --git a/app/src/renderer/apps/Messages/DMs.tsx b/app/src/renderer/apps/Messages/DMs.tsx index 19f828e774..a806ddffbc 100644 --- a/app/src/renderer/apps/Messages/DMs.tsx +++ b/app/src/renderer/apps/Messages/DMs.tsx @@ -111,10 +111,8 @@ const DMsPresenter = (props: IProps) => { key={lastTimeSent} width={364} height={544} - rowHeight={57} - data={previews} - filter={searchFilter} - rowRenderer={(dm, index) => ( + data={previews.filter(searchFilter)} + itemContent={(index, dm) => ( ( + initialTopMostItemIndex={messages.length - 1} + itemContent={(index, message) => ( )} - startAtBottom /> ), diff --git a/app/src/renderer/apps/Messages/components/ChatMessage.tsx b/app/src/renderer/apps/Messages/components/ChatMessage.tsx index 23c77ed327..61a0a03711 100644 --- a/app/src/renderer/apps/Messages/components/ChatMessage.tsx +++ b/app/src/renderer/apps/Messages/components/ChatMessage.tsx @@ -16,7 +16,6 @@ interface IProps { ourColor: string; contents: GraphDMType['contents']; timeSent: number; - onImageLoad?: () => void; } export const ChatMessage = ({ @@ -29,7 +28,6 @@ export const ChatMessage = ({ isSending, primaryBubble, timeSent, - onImageLoad, }: IProps) => { const messageTypes = useMemo( () => @@ -111,7 +109,6 @@ export const ChatMessage = ({ textColor={theme.textColor} bgColor={referenceColor} content={content} - onImageLoad={onImageLoad} /> ); })} @@ -135,7 +132,6 @@ export const ChatMessage = ({ contents, isMention, isSending, - onImageLoad, ourColor, primaryBubble, referenceColor, diff --git a/app/src/renderer/apps/Rooms/Room/Chat.tsx b/app/src/renderer/apps/Rooms/Room/Chat.tsx index 110e640238..fa8910282f 100644 --- a/app/src/renderer/apps/Rooms/Room/Chat.tsx +++ b/app/src/renderer/apps/Rooms/Room/Chat.tsx @@ -78,9 +78,8 @@ const RoomChatPresenter = () => { a.timeReceived - b.timeReceived} - rowRenderer={(chat, index) => ( + data={chats.sort((a, b) => a.timeReceived - b.timeReceived)} + itemContent={(index, chat) => ( { } /> )} - startAtBottom /> ); }, [chats]); diff --git a/app/src/renderer/apps/Spaces/FeaturedList.tsx b/app/src/renderer/apps/Spaces/FeaturedList.tsx index e95d0416ce..e4bc7299fc 100644 --- a/app/src/renderer/apps/Spaces/FeaturedList.tsx +++ b/app/src/renderer/apps/Spaces/FeaturedList.tsx @@ -39,7 +39,7 @@ const FeaturedListPresenter = () => { key={`featured-spaces-${listData.length}`} width={354} data={listData} - rowRenderer={(data: any) => { + itemContent={(_, data) => { const onJoin = async () => { setJoining(true); SpacesActions.joinSpace(data.path.substring(1)) @@ -75,6 +75,7 @@ const FeaturedListPresenter = () => { height="32px" width="32px" src={data.picture} + alt={data.name} /> ) : ( diff --git a/app/src/renderer/apps/Spaces/SpacesList.tsx b/app/src/renderer/apps/Spaces/SpacesList.tsx index 2907462a30..4c4397df2b 100644 --- a/app/src/renderer/apps/Spaces/SpacesList.tsx +++ b/app/src/renderer/apps/Spaces/SpacesList.tsx @@ -1,14 +1,13 @@ import { useMemo } from 'react'; import { observer } from 'mobx-react'; +import { WindowedList } from '@holium/design-system'; import { SpaceModelType } from 'os/services/spaces/models/spaces'; - import { Flex, Text, ActionButton, Icons } from 'renderer/components'; import { SpaceRow } from './SpaceRow'; import { ShellActions } from 'renderer/logic/actions/shell'; import { useServices } from 'renderer/logic/store'; import { VisaRow } from './components/VisaRow'; import { rgba } from 'polished'; -import { WindowedList } from '@holium/design-system'; export interface Space { color?: string; @@ -108,11 +107,10 @@ const SpacesListPresenter = ({ return ( { + itemContent={(_, { space, visa }) => { if (space) { return ( + = observer( [results.length, search.length] ); - const RowRenderer = (contact: (typeof results)[number]) => { + const RowRenderer = (index: number, contact: (typeof results)[number]) => { const nickname = contact[1].nickname ?? ''; const sigilColor = contact[1].color ?? '#000000'; const avatar = contact[1].avatar; return ( { evt.stopPropagation(); @@ -112,38 +112,8 @@ export const ShipSearch: FC = observer( ); }; - // Todo, move the show logic in here - if (results.length === 0) { - return ( - - {/* - No DMs - */} - {/* - Type a valid ID - */} - - ); - } const resultList = ( - + ); if (isDropdown) { diff --git a/app/src/renderer/system/desktop/components/Home/Ship/FriendsList.tsx b/app/src/renderer/system/desktop/components/Home/Ship/FriendsList.tsx index baf8b97580..7788db98ba 100644 --- a/app/src/renderer/system/desktop/components/Home/Ship/FriendsList.tsx +++ b/app/src/renderer/system/desktop/components/Home/Ship/FriendsList.tsx @@ -127,9 +127,8 @@ const FriendsListPresenter = () => { { + itemContent={(_, rowData) => { if (rowData.type === 'friend') { const friend = rowData.data; return ; diff --git a/app/src/renderer/system/desktop/components/Home/Space/MembersList.tsx b/app/src/renderer/system/desktop/components/Home/Space/MembersList.tsx index 777ea16f33..bd7ff20375 100644 --- a/app/src/renderer/system/desktop/components/Home/Space/MembersList.tsx +++ b/app/src/renderer/system/desktop/components/Home/Space/MembersList.tsx @@ -153,7 +153,7 @@ const MembersListPresenter = (props: IMembersList) => { { + itemContent={(_, rowData) => { if (rowData.type === 'title') { const title = rowData.data as string; return ; diff --git a/lib/design-system/package.json b/lib/design-system/package.json index d17aad83b4..a8bdd70789 100644 --- a/lib/design-system/package.json +++ b/lib/design-system/package.json @@ -21,6 +21,7 @@ "react-player": "^2.11.0", "react-spotify-embed": "^1.0.4", "react-twitter-embed": "^4.0.4", + "react-virtuoso": "^4.2.0", "urbit-ob": "^5.0.1" }, "devDependencies": { diff --git a/lib/design-system/src/blocks/Bubble/Bubble.stories.tsx b/lib/design-system/src/blocks/Bubble/Bubble.stories.tsx index 428af22ee7..61ddf16ee4 100644 --- a/lib/design-system/src/blocks/Bubble/Bubble.stories.tsx +++ b/lib/design-system/src/blocks/Bubble/Bubble.stories.tsx @@ -25,7 +25,6 @@ export const Default: ComponentStory = () => { { ship: '~lomder-librun' }, ]} onReaction={() => {}} - onMeasure={() => {}} /> = () => { { italics: 'italics' }, ]} onReaction={() => {}} - onMeasure={() => {}} /> = () => { { plain: 'and then let me know whats up' }, ]} onReaction={() => {}} - onMeasure={() => {}} /> {}} - onMeasure={() => {}} /> {}} - onMeasure={() => {}} /> @@ -117,7 +112,6 @@ export const BlockQuote: ComponentStory = () => { { plain: 'woooohooo' }, ]} onReaction={() => {}} - onMeasure={() => {}} /> = () => { { plain: 'woooohooo' }, ]} onReaction={() => {}} - onMeasure={() => {}} /> ); @@ -160,7 +153,6 @@ export const InlineCode: ComponentStory = () => { { plain: 'before running' }, ]} onReaction={() => {}} - onMeasure={() => {}} /> = () => { { plain: 'and then let me know whats up' }, ]} onReaction={() => {}} - onMeasure={() => {}} /> ); @@ -198,7 +189,6 @@ export const Mentions: ComponentStory = () => { { ship: '~lomder-librun' }, ]} onReaction={() => {}} - onMeasure={() => {}} /> = () => { { ship: '~fasnut-famden' }, ]} onReaction={() => {}} - onMeasure={() => {}} /> ); @@ -234,7 +223,6 @@ export const CodeBlock: ComponentStory = () => { }, ]} onReaction={() => {}} - onMeasure={() => {}} /> = () => { }, ]} onReaction={() => {}} - onMeasure={() => {}} /> ); @@ -268,7 +255,6 @@ export const Link: ComponentStory = () => ( }, ]} onReaction={() => {}} - onMeasure={() => {}} /> = () => ( }, ]} onReaction={() => {}} - onMeasure={() => {}} /> = () => ( }, ]} onReaction={() => {}} - onMeasure={() => {}} /> ); @@ -314,7 +298,6 @@ export const Image: ComponentStory = () => ( }, ]} onReaction={() => {}} - onMeasure={() => {}} /> = () => ( }, ]} onReaction={() => {}} - onMeasure={() => {}} /> ); @@ -372,7 +354,6 @@ export const Reactions: ComponentStory = () => { ]} reactions={reacts} onReaction={onReaction} - onMeasure={() => {}} /> = () => { { msgId: '5', by: '~fes', emoji: '1f525' }, ]} onReaction={() => {}} - onMeasure={() => {}} /> ); @@ -421,7 +401,6 @@ export const ReplyTo: ComponentStory = () => ( }, ]} onReaction={() => {}} - onMeasure={() => {}} /> = () => ( }, ]} onReaction={() => {}} - onMeasure={() => {}} /> = () => ( }, ]} onReaction={() => {}} - onMeasure={() => {}} /> = () => ( }, ]} onReaction={() => {}} - onMeasure={() => {}} /> ); @@ -517,7 +493,6 @@ export const RelicTab: ComponentStory = () => ( }, ]} onReaction={() => {}} - onMeasure={() => {}} /> ); diff --git a/lib/design-system/src/blocks/Bubble/Bubble.tsx b/lib/design-system/src/blocks/Bubble/Bubble.tsx index 0a9b437f26..4a2a043fef 100644 --- a/lib/design-system/src/blocks/Bubble/Bubble.tsx +++ b/lib/design-system/src/blocks/Bubble/Bubble.tsx @@ -1,10 +1,4 @@ -import { - forwardRef, - useLayoutEffect, - useMemo, - useState, - useEffect, -} from 'react'; +import { useMemo, useState, useEffect, Ref } from 'react'; import { Flex, Text, BoxProps, Box, convertDarkText, Icon } from '../..'; import { BubbleStyle, BubbleAuthor, BubbleFooter } from './Bubble.styles'; import { FragmentBlock, LineBreak, renderFragment } from './fragment-lib'; @@ -19,7 +13,6 @@ import { InlineStatus } from './InlineStatus'; import { BUBBLE_HEIGHT, STATUS_HEIGHT } from './Bubble.constants'; export type BubbleProps = { - ref: any; id: string; author: string; authorColor?: string; @@ -38,261 +31,245 @@ export type BubbleProps = { containerWidth?: number; isPrevGrouped?: boolean; // should we show the author if multiple messages by same author? isNextGrouped?: boolean; // should we show the author if multiple messages by same author? + innerRef?: Ref; onReaction?: (payload: OnReactionPayload) => void; onReplyClick?: (msgId: string) => void; - onMeasure: () => void; } & BoxProps; -export const Bubble = forwardRef( - (props: BubbleProps, ref) => { - const { - id, - author, - authorNickname, - themeMode, - isOur, - ourColor, - ourShip, - sentAt, - authorColor, - message, - isEdited, - isEditing, - containerWidth, - reactions = [], - isPrevGrouped, - isNextGrouped, - updatedAt, - expiresAt, - onMeasure, - onReaction, - // onReplyClick = () => {}, - } = props; - - const [dateDisplay, setDateDisplay] = useState(chatDate(new Date(sentAt))); - useEffect(() => { - let timer: NodeJS.Timeout; - function initClock() { - clearTimeout(timer); - const sentDate = new Date(sentAt); - const interval: number = (60 - sentDate.getSeconds()) * 1000 + 5; - setDateDisplay(chatDate(sentDate)); - timer = setTimeout(initClock, interval); - } - initClock(); - return () => { - clearTimeout(timer); - }; - }, [sentAt]); - - const authorColorDisplay = useMemo( - () => - (authorColor && convertDarkText(authorColor, themeMode)) || - 'rgba(var(--rlm-text-rgba))', - [authorColor] - ); - - const [lastReactonLength, setLastReactionLength] = useState( - reactions.length - ); +export const Bubble = ({ + innerRef, + id, + author, + authorNickname, + themeMode, + isOur, + ourColor, + ourShip, + sentAt, + authorColor, + message, + isEdited, + isEditing, + containerWidth, + reactions = [], + isPrevGrouped, + isNextGrouped, + updatedAt, + expiresAt, + onReaction, + onReplyClick, +}: BubbleProps) => { + const [dateDisplay, setDateDisplay] = useState(chatDate(new Date(sentAt))); + useEffect(() => { + let timer: NodeJS.Timeout; + function initClock() { + clearTimeout(timer); + const sentDate = new Date(sentAt); + const interval: number = (60 - sentDate.getSeconds()) * 1000 + 5; + setDateDisplay(chatDate(sentDate)); + timer = setTimeout(initClock, interval); + } + initClock(); + return () => { + clearTimeout(timer); + }; + }, [sentAt]); - const innerWidth = useMemo( - () => (containerWidth ? containerWidth - 16 : undefined), - [containerWidth] - ); + const authorColorDisplay = useMemo( + () => + (authorColor && convertDarkText(authorColor, themeMode)) || + 'rgba(var(--rlm-text-rgba))', + [authorColor] + ); - // if the number of reactions changes, we need to re-measure - useLayoutEffect(() => { - if (lastReactonLength !== reactions.length) { - if ( - (lastReactonLength === 0 && reactions.length === 1) || - (reactions.length === 0 && lastReactonLength >= 1) - ) { - // only re-measure if we're going from 0 to 1 or 1 to 0 - onMeasure(); - setLastReactionLength(reactions.length); - } - } - }, [reactions.length, lastReactonLength, onMeasure]); + const innerWidth = useMemo( + () => (containerWidth ? containerWidth - 16 : undefined), + [containerWidth] + ); - const footerHeight = useMemo(() => { - if (reactions.length > 0) { - return BUBBLE_HEIGHT.rem.footerReactions; - } - return BUBBLE_HEIGHT.rem.footer; - }, [reactions.length]); + const footerHeight = useMemo(() => { + if (reactions.length > 0) { + return BUBBLE_HEIGHT.rem.footerReactions; + } + return BUBBLE_HEIGHT.rem.footer; + }, [reactions.length]); - const fragments = useMemo(() => { - if (!message) return []; - return message?.map((fragment, index) => { - let prevLineBreak, nextLineBreak; - if (index > 0) { - if (message[index - 1]) { - const previousType = Object.keys(message[index - 1])[0]; - if (previousType === 'image') { - prevLineBreak = ; - } - } else { - console.warn( - 'expected a non-null message at ', - index - 1, - message[index - 1] - ); + const fragments = useMemo(() => { + if (!message) return []; + return message?.map((fragment, index) => { + let prevLineBreak, nextLineBreak; + if (index > 0) { + if (message[index - 1]) { + const previousType = Object.keys(message[index - 1])[0]; + if (previousType === 'image') { + prevLineBreak = ; } + } else { + console.warn( + 'expected a non-null message at ', + index - 1, + message[index - 1] + ); } - if (index < message.length - 1) { - if (message[index + 1]) { - const nextType = Object.keys(message[index + 1])[0]; - if (nextType === 'image') { - nextLineBreak = ; - } - } else { - console.warn( - 'expected a non-null message at ', - index + 1, - message[index + 1] - ); + } + if (index < message.length - 1) { + if (message[index + 1]) { + const nextType = Object.keys(message[index + 1])[0]; + if (nextType === 'image') { + nextLineBreak = ; } + } else { + console.warn( + 'expected a non-null message at ', + index + 1, + message[index + 1] + ); } + } - return ( - - {prevLineBreak} - {renderFragment(id, fragment, index, author, innerWidth, onMeasure)} - {nextLineBreak} - - ); - }); - }, [message, updatedAt]); - - const minBubbleWidth = useMemo(() => (isEdited ? 164 : 114), [isEdited]); - - const reactionsDisplay = useMemo(() => { return ( - + + {prevLineBreak} + {renderFragment( + id, + fragment, + index, + author, + innerWidth, + onReplyClick + )} + {nextLineBreak} + ); - }, [reactions.length, isOur, ourShip, ourColor, onReaction]); + }); + }, [message, updatedAt]); - return useMemo(() => { - if (message?.length === 1) { - const contentType = Object.keys(message[0])[0]; - if (contentType === 'status') { - return ( - - - - ); - } + const minBubbleWidth = useMemo(() => (isEdited ? 164 : 114), [isEdited]); + + const reactionsDisplay = useMemo(() => { + return ( + + ); + }, [reactions.length, isOur, ourShip, ourColor, onReaction]); + + return useMemo(() => { + if (message?.length === 1) { + const contentType = Object.keys(message[0])[0]; + if (contentType === 'status') { + return ( + + + + ); } - return ( - + - - {!isOur && !isPrevGrouped && ( - - {authorNickname || author} - - )} - {fragments} - - - {reactionsDisplay} - - + {authorNickname || author} + + )} + {fragments} + + + {reactionsDisplay} + + + {expiresAt && ( + // TODO tooltip with time remaining + + )} + - {expiresAt && ( - // TODO tooltip with time remaining - - )} - - {isEditing && 'Editing... · '} - {isEdited && !isEditing && 'Edited · '} - {dateDisplay} - - - - - - ); - }, [ - id, - isPrevGrouped, - isNextGrouped, - isOur, - ourColor, - isEditing, - isEdited, - authorColorDisplay, - authorNickname, - author, - fragments, - reactionsDisplay, - dateDisplay, - minBubbleWidth, - footerHeight, - onMeasure, - ]); - } -); + {isEditing && 'Editing... · '} + {isEdited && !isEditing && 'Edited · '} + {dateDisplay} + + + + + + ); + }, [ + id, + isPrevGrouped, + isNextGrouped, + isOur, + ourColor, + isEditing, + isEdited, + authorColorDisplay, + authorNickname, + author, + fragments, + reactionsDisplay, + dateDisplay, + minBubbleWidth, + footerHeight, + ]); +}; diff --git a/lib/design-system/src/blocks/Bubble/fragment-lib.tsx b/lib/design-system/src/blocks/Bubble/fragment-lib.tsx index 6ab735c097..fb7f9f7b71 100644 --- a/lib/design-system/src/blocks/Bubble/fragment-lib.tsx +++ b/lib/design-system/src/blocks/Bubble/fragment-lib.tsx @@ -254,7 +254,7 @@ export const renderFragment = ( index: number, author: string, containerWidth?: number, - onMeasure?: () => void // used in the case where async data is loaded + onReplyClick?: (id: string) => void ) => { const key = Object.keys(fragment)[0] as FragmentKey; switch (key) { @@ -377,7 +377,6 @@ export const renderFragment = ( width={imageFrag.metadata?.width} height={imageFrag.metadata?.height} by={author} - onImageLoaded={onMeasure} /> ); @@ -385,6 +384,7 @@ export const renderFragment = ( case 'reply': const msg = (fragment as FragmentReplyType).reply.message[0]; const replyAuthor = (fragment as FragmentReplyType).reply.author; + const replyId = (fragment as FragmentReplyType).reply.msgId; const fragmentType: string = Object.keys(msg)[0]; let replyContent = null; if ( @@ -416,6 +416,7 @@ export const renderFragment = ( style={{ height: 42 }} id={id} key={`${author + index}-reply`} + onClick={() => onReplyClick?.(replyId)} > = () => { return ( - ( + initialItemCount={messages.length - 1} + itemContent={(index, row) => ( {}} - onMeasure={measure} /> )} diff --git a/lib/design-system/src/general/WindowedList/WindowedList.stories.tsx b/lib/design-system/src/general/WindowedList/WindowedList.stories.tsx deleted file mode 100644 index a27596097e..0000000000 --- a/lib/design-system/src/general/WindowedList/WindowedList.stories.tsx +++ /dev/null @@ -1,127 +0,0 @@ -import { ComponentMeta, Story } from '@storybook/react'; -import { WindowedList } from './WindowedList'; -import { Box } from '../Box/Box'; -import { Flex } from '../Flex/Flex'; -import { Skeleton } from '../Skeleton/Skeleton'; - -export default { - component: WindowedList, - argTypes: { - rows: { - control: { - type: 'range', - min: 0, - max: 10000, - step: 10, - }, - }, - containerHeight: { - control: { - type: 'range', - min: 100, - max: 1000, - step: 1, - }, - }, - itemHeight: { - control: { - type: 'range', - min: 10, - max: 100, - step: 1, - }, - }, - filter: { - control: { - type: 'select', - options: ['none', 'even', 'odd'], - }, - }, - hideScrollbar: { - control: { - type: 'boolean', - }, - }, - }, -} as ComponentMeta; - -interface DemoTemplateProps { - rows: number; - containerHeight: number; - itemHeight: number; - filter: 'none' | 'even' | 'odd'; - hideScrollbar: boolean; -} - -const DemoTemplate: Story = ({ - rows, - containerHeight, - itemHeight, - filter, - hideScrollbar, -}: DemoTemplateProps) => { - const data = Array.from({ length: rows }, (_, i) => i); - - const getFilterFunction = () => { - switch (filter) { - case 'even': - return (_: any, i: number) => i % 2 === 0; - case 'odd': - return (_: any, i: number) => i % 2 === 1; - default: - return () => true; - } - }; - - return ( - <> -

- This component fills the parent and makes sure only visible items are - kept in the DOM. You can experiment by changing the container's height - and the data size. -

- - ( - - {row} - - )} - hideScrollbar={hideScrollbar} - /> - - - ); -}; - -export const Demo = DemoTemplate.bind({}); -Demo.args = { - rows: 100, - containerHeight: 420, - itemHeight: 50, - filter: 'none', - hideScrollbar: false, -}; - -export const Loading = () => ( - - i)} - rowRenderer={() => ( - - - - )} - /> - -); diff --git a/lib/design-system/src/general/WindowedList/WindowedList.styles.tsx b/lib/design-system/src/general/WindowedList/WindowedList.styles.tsx deleted file mode 100644 index 4290b4c6de..0000000000 --- a/lib/design-system/src/general/WindowedList/WindowedList.styles.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import styled from 'styled-components'; -import { List } from './source/List/List'; - -export const StyledList = styled(List)<{ hideScrollbar: boolean }>` - ${({ hideScrollbar }) => - hideScrollbar && - ` - -ms-overflow-style: none; - scrollbar-width: none; - - &::-webkit-scrollbar { - display: none; - } - `} -`; diff --git a/lib/design-system/src/general/WindowedList/WindowedList.tsx b/lib/design-system/src/general/WindowedList/WindowedList.tsx index 5284c1dce7..ce0b8a3c80 100644 --- a/lib/design-system/src/general/WindowedList/WindowedList.tsx +++ b/lib/design-system/src/general/WindowedList/WindowedList.tsx @@ -1,104 +1,77 @@ -import { useMemo, useRef } from 'react'; -import { Box } from '../Box/Box'; -import { AutoSizer } from './source/AutoSizer/AutoSizer'; -import { CellMeasurer } from './source/CellMeasurer/CellMeasurer'; -import { CellMeasurerCache } from './source/CellMeasurer/CellMeasurerCache'; -import { Scroll } from './source/List/types'; -import { StyledList } from './WindowedList.styles'; +import { Ref } from 'react'; +import styled from 'styled-components'; +import { Virtuoso, VirtuosoProps, VirtuosoHandle } from 'react-virtuoso'; -type WindowedListProps = { - data: T[]; - rowRenderer: ( - rowData: T, - index: number, - measure: () => void, - sortedAndFilteredData: T[] - ) => JSX.Element; - /** - * The width of the list. If undefined, the list will auto-size to the width of its parent container. - * It is preferred to set this value if known ahead of time, to save render time. - */ - width?: number; - /** - * The height of the list. If undefined, the list will auto-size to the height of its parent container. - * It is preferred to set this value if known ahead of time, to save render time. - */ - height?: number; - /** - * The height of the row. If undefined, the row will auto-size to the height of its parent container. - * It is preferred to set this value if known ahead of time, to save render time. - */ - rowHeight?: number; - onScroll?: (params: Scroll) => void; - hideScrollbar?: boolean; - startAtBottom?: boolean; - sort?: (a: T, b: T) => number; - filter?: (rowData: T, index: number) => boolean; -}; +const Container = styled.div<{ hideScrollbar?: boolean }>` + position: relative; + width: 100%; + height: 100%; + + > :nth-child(1) { + overflow-x: hidden; + + /* custom scrollbar */ + ::-webkit-scrollbar { + width: 12px; + } + + ::-webkit-scrollbar-thumb { + border-radius: 20px; + border: 3px solid transparent; + background-clip: content-box; + background-color: transparent; + } + + ::-webkit-scrollbar-track { + border-radius: 20px; + border: 3px solid transparent; + background-clip: content-box; + background-color: transparent; + } + } -export const WindowedList = ({ - data: rawData, - rowRenderer, - width, - height, - rowHeight, - onScroll, - hideScrollbar = true, - startAtBottom = false, - sort = () => 0, - filter = () => true, -}: WindowedListProps) => { - const data = useMemo( - () => rawData.filter(filter).sort(sort), - [rawData, filter, sort] - ); - const cache = useRef( - new CellMeasurerCache({ - fixedWidth: true, - minWidth: width, - defaultWidth: width, - minHeight: rowHeight, - defaultHeight: rowHeight, - }) - ); + // On hover, show the scrollbar, unless hideScrollbar is true. + &:hover { + > :nth-child(1) { + ::-webkit-scrollbar-thumb { + background-color: rgba(var(--rlm-text-rgba), 0.5); + } - return ( - - - {({ width: maybeAutoWidth, height: maybeAutoHeight }) => ( - ( - - {({ measure, registerChild }) => ( -
- {rowRenderer(data[index], index, measure, data)} -
- )} -
- )} - onScroll={onScroll} - hideScrollbar={hideScrollbar} - startAtBottom={startAtBottom} - scrollToIndex={startAtBottom ? data.length - 1 : 0} - scrollToAlignment={startAtBottom ? 'end' : 'auto'} - /> - )} -
-
- ); + ::-webkit-scrollbar-thumb:hover { + background-color: rgba(var(--rlm-text-rgba), 1); + } + + ::-webkit-scrollbar-track:hover { + background-color: rgba(var(--rlm-input-rgba), 0.5); + } + } + } +`; + +export type WindowedListRef = VirtuosoHandle; + +type Props = VirtuosoProps & { + innerRef?: Ref; + hideScrollbar?: boolean; }; + +export const WindowedList = ({ + innerRef, + width = '100%', + height = '100%', + hideScrollbar = false, + style, + ...props +}: Props) => ( + + + +); diff --git a/lib/design-system/src/general/WindowedList/source/AutoSizer/AutoSizer.tsx b/lib/design-system/src/general/WindowedList/source/AutoSizer/AutoSizer.tsx deleted file mode 100644 index 30d6f4db88..0000000000 --- a/lib/design-system/src/general/WindowedList/source/AutoSizer/AutoSizer.tsx +++ /dev/null @@ -1,130 +0,0 @@ -import { Component, ReactElement } from 'react'; -import { createDetectElementResize } from './detectElementResize'; - -export type Size = { - width: number; - height: number; -}; - -type Props = { - children: (size: Size) => ReactElement; - disableWidth?: boolean; - disableHeight?: boolean; - defaultWidth?: number; - defaultHeight?: number; -}; - -type ResizeHandler = (element: HTMLElement, onResize: () => void) => void; - -type DetectElementResize = { - addResizeListener: ResizeHandler; - removeResizeListener: ResizeHandler; -}; - -export class AutoSizer extends Component { - static defaultProps = { - disableWidth: false, - disableHeight: false, - }; - state = { - width: this.props.defaultWidth ?? 0, - height: this.props.defaultHeight ?? 0, - }; - _parentNode: HTMLElement | null | undefined; - _autoSizer: HTMLElement | null | undefined; - _window: Window | null | undefined; - - _detectElementResize: DetectElementResize | undefined; - - componentDidMount() { - if ( - this._autoSizer && - this._autoSizer.parentNode && - this._autoSizer.parentNode.ownerDocument && - this._autoSizer.parentNode.ownerDocument.defaultView && - this._autoSizer.parentNode instanceof - this._autoSizer.parentNode.ownerDocument.defaultView.HTMLElement - ) { - // Delay access of parentNode until mount. - // This handles edge-cases where the component has already been unmounted before its ref has been set, - // As well as libraries like react-lite which have a slightly different lifecycle. - this._parentNode = this._autoSizer.parentNode; - this._window = this._autoSizer.parentNode.ownerDocument.defaultView; - // Defer requiring resize handler in order to support server-side rendering. - this._detectElementResize = createDetectElementResize(this._window); - - this._detectElementResize.addResizeListener( - this._parentNode, - this._onResize - ); - - this._onResize(); - } - } - - componentWillUnmount() { - if (this._detectElementResize && this._parentNode) { - this._detectElementResize.removeResizeListener( - this._parentNode, - this._onResize - ); - } - } - - render() { - const { children, disableWidth, disableHeight } = this.props; - const { width, height } = this.state; - - const outerStyle: Record = { - overflow: 'visible', - }; - const childParams: Record = {}; - - if (!disableHeight) { - outerStyle.height = 0; - childParams.height = height; - } - - if (!disableWidth) { - outerStyle.width = 0; - childParams.width = width; - } - - return ( -
- {children(childParams as Size)} -
- ); - } - - _onResize = () => { - const { disableWidth, disableHeight } = this.props; - - if (this._parentNode) { - const height = this._parentNode.offsetHeight || 0; - const width = this._parentNode.offsetWidth || 0; - const win = this._window || window; - const style = win.getComputedStyle(this._parentNode) || {}; - const paddingLeft = parseInt(style.paddingLeft, 10) || 0; - const paddingRight = parseInt(style.paddingRight, 10) || 0; - const paddingTop = parseInt(style.paddingTop, 10) || 0; - const paddingBottom = parseInt(style.paddingBottom, 10) || 0; - const newHeight = height - paddingTop - paddingBottom; - const newWidth = width - paddingLeft - paddingRight; - - if ( - (!disableHeight && this.state.height !== newHeight) || - (!disableWidth && this.state.width !== newWidth) - ) { - this.setState({ - height: height - paddingTop - paddingBottom, - width: width - paddingLeft - paddingRight, - }); - } - } - }; - - _setRef = (autoSizer: HTMLElement | null | undefined) => { - this._autoSizer = autoSizer; - }; -} diff --git a/lib/design-system/src/general/WindowedList/source/AutoSizer/detectElementResize.ts b/lib/design-system/src/general/WindowedList/source/AutoSizer/detectElementResize.ts deleted file mode 100644 index 6662d9db07..0000000000 --- a/lib/design-system/src/general/WindowedList/source/AutoSizer/detectElementResize.ts +++ /dev/null @@ -1,264 +0,0 @@ -/* eslint-disable no-restricted-globals */ -/** - * Detect Element Resize. - * https://github.com/sdecima/javascript-detect-element-resize - * Sebastian Decima - * - * Forked from version 0.5.3; includes the following modifications: - * 1) Guard against unsafe 'window' and 'document' references (to support SSR). - * 2) Defer initialization code via a top-level function wrapper (to support SSR). - * 3) Avoid unnecessary reflows by not measuring size for scroll events bubbling from children. - * 4) Add nonce for style element. - * 5) Added support for injecting custom window object - **/ -export function createDetectElementResize(hostWindow: Window) { - // Check `document` and `window` in case of server-side rendering - var _window: any; - - if (typeof hostWindow !== 'undefined') { - _window = hostWindow; - } else if (typeof window !== 'undefined') { - _window = window; - } else if (typeof self !== 'undefined') { - _window = self; - } else { - _window = global; - } - - var attachEvent = - typeof _window.document !== 'undefined' && _window.document.attachEvent; - - if (!attachEvent) { - var requestFrame = (function () { - var raf = - _window.requestAnimationFrame || - _window.mozRequestAnimationFrame || - _window.webkitRequestAnimationFrame || - function (fn: any) { - return _window.setTimeout(fn, 20); - }; - - return function (fn: any) { - return raf(fn); - }; - })(); - - var cancelFrame = (function () { - var cancel = - _window.cancelAnimationFrame || - _window.mozCancelAnimationFrame || - _window.webkitCancelAnimationFrame || - _window.clearTimeout; - return function (id: any) { - return cancel(id); - }; - })(); - - var resetTriggers = function (element: any) { - var triggers = element.__resizeTriggers__, - expand = triggers.firstElementChild, - contract = triggers.lastElementChild, - expandChild = expand.firstElementChild; - contract.scrollLeft = contract.scrollWidth; - contract.scrollTop = contract.scrollHeight; - expandChild.style.width = expand.offsetWidth + 1 + 'px'; - expandChild.style.height = expand.offsetHeight + 1 + 'px'; - expand.scrollLeft = expand.scrollWidth; - expand.scrollTop = expand.scrollHeight; - }; - - var checkTriggers = function (element: any) { - return ( - element.offsetWidth !== element.__resizeLast__.width || - element.offsetHeight !== element.__resizeLast__.height - ); - }; - - var scrollListener = function (e: any) { - // Don't measure (which forces) reflow for scrolls that happen inside of children! - if ( - e.target.className && - typeof e.target.className.indexOf === 'function' && - e.target.className.indexOf('contract-trigger') < 0 && - e.target.className.indexOf('expand-trigger') < 0 - ) { - return; - } - - // @ts-ignore - var element = this; - // @ts-ignore - resetTriggers(this); - - // @ts-ignore - if (this.__resizeRAF__) { - // @ts-ignore - cancelFrame(this.__resizeRAF__); - } - - // @ts-ignore - this.__resizeRAF__ = requestFrame(function () { - if (checkTriggers(element)) { - element.__resizeLast__.width = element.offsetWidth; - element.__resizeLast__.height = element.offsetHeight; - - element.__resizeListeners__.forEach(function (fn: any) { - fn.call(element, e); - }); - } - }); - }; - - /* Detect CSS Animations support to detect element display/re-attach */ - var animation = false, - keyframeprefix = '', - animationstartevent = 'animationstart', - domPrefixes = 'Webkit Moz O ms'.split(' '), - startEvents = - 'webkitAnimationStart animationstart oAnimationStart MSAnimationStart'.split( - ' ' - ), - pfx = ''; - { - var elm = _window.document.createElement('fakeelement'); - - if (elm.style.animationName !== undefined) { - animation = true; - } - - if (animation === false) { - for (var i = 0; i < domPrefixes.length; i++) { - if (elm.style[domPrefixes[i] + 'AnimationName'] !== undefined) { - pfx = domPrefixes[i]; - keyframeprefix = '-' + pfx.toLowerCase() + '-'; - animationstartevent = startEvents[i]; - animation = true; - break; - } - } - } - } - var animationName = 'resizeanim'; - var animationKeyframes = - '@' + - keyframeprefix + - 'keyframes ' + - animationName + - ' { from { opacity: 0; } to { opacity: 0; } } '; - var animationStyle = - keyframeprefix + 'animation: 1ms ' + animationName + '; '; - } - - var createStyles = function (doc: any) { - if (!doc.getElementById('detectElementResize')) { - //opacity:0 works around a chrome bug https://code.google.com/p/chromium/issues/detail?id=286360 - var css = - (animationKeyframes ? animationKeyframes : '') + - '.resize-triggers { ' + - (animationStyle ? animationStyle : '') + - 'visibility: hidden; opacity: 0; } ' + - '.resize-triggers, .resize-triggers > div, .contract-trigger:before { content: " "; display: block; position: absolute; top: 0; left: 0; height: 100%; width: 100%; overflow: hidden; z-index: -1; } .resize-triggers > div { background: #eee; overflow: auto; } .contract-trigger:before { width: 200%; height: 200%; }', - head = doc.head || doc.getElementsByTagName('head')[0], - style = doc.createElement('style'); - style.id = 'detectElementResize'; - style.type = 'text/css'; - - if (style.styleSheet) { - style.styleSheet.cssText = css; - } else { - style.appendChild(doc.createTextNode(css)); - } - - head.appendChild(style); - } - }; - - var addResizeListener = function (element: any, fn: any) { - if (attachEvent) { - element.attachEvent('onresize', fn); - } else { - if (!element.__resizeTriggers__) { - var doc = element.ownerDocument; - - var elementStyle = _window.getComputedStyle(element); - - if (elementStyle && elementStyle.position === 'static') { - element.style.position = 'relative'; - } - - createStyles(doc); - element.__resizeLast__ = {}; - element.__resizeListeners__ = []; - (element.__resizeTriggers__ = doc.createElement('div')).className = - 'resize-triggers'; - var expandTrigger = doc.createElement('div'); - expandTrigger.className = 'expand-trigger'; - expandTrigger.appendChild(doc.createElement('div')); - var contractTrigger = doc.createElement('div'); - contractTrigger.className = 'contract-trigger'; - - element.__resizeTriggers__.appendChild(expandTrigger); - - element.__resizeTriggers__.appendChild(contractTrigger); - - element.appendChild(element.__resizeTriggers__); - resetTriggers(element); - element.addEventListener('scroll', scrollListener, true); - - /* Listen for a css animation to detect element display/re-attach */ - if (animationstartevent) { - element.__resizeTriggers__.__animationListener__ = - function animationListener(e: any) { - if (e.animationName === animationName) { - resetTriggers(element); - } - }; - - element.__resizeTriggers__.addEventListener( - animationstartevent, - element.__resizeTriggers__.__animationListener__ - ); - } - } - - element.__resizeListeners__.push(fn); - } - }; - - var removeResizeListener = function (element: any, fn: any) { - if (attachEvent) { - element.detachEvent('onresize', fn); - } else { - element.__resizeListeners__.splice( - element.__resizeListeners__.indexOf(fn), - 1 - ); - - if (!element.__resizeListeners__.length) { - element.removeEventListener('scroll', scrollListener, true); - - if (element.__resizeTriggers__.__animationListener__) { - element.__resizeTriggers__.removeEventListener( - animationstartevent, - element.__resizeTriggers__.__animationListener__ - ); - - element.__resizeTriggers__.__animationListener__ = null; - } - - try { - element.__resizeTriggers__ = !element.removeChild( - element.__resizeTriggers__ - ); - } catch (e) { - // Preact compat; see developit/preact-compat/issues/228 - } - } - } - }; - - return { - addResizeListener, - removeResizeListener, - }; -} diff --git a/lib/design-system/src/general/WindowedList/source/CellMeasurer/CellMeasurer.ts b/lib/design-system/src/general/WindowedList/source/CellMeasurer/CellMeasurer.ts deleted file mode 100644 index 5b4a7a04f5..0000000000 --- a/lib/design-system/src/general/WindowedList/source/CellMeasurer/CellMeasurer.ts +++ /dev/null @@ -1,173 +0,0 @@ -import React from 'react'; -import { findDOMNode } from 'react-dom'; -import type { CellMeasureCache } from './types'; -type Children = (params: { - measure: () => void; - registerChild: (element: any) => void; -}) => React.ReactElement; -type Cell = { - columnIndex: number; - rowIndex: number; -}; -type Props = { - cache: CellMeasureCache; - children: Children | React.ReactElement; - columnIndex?: number; - index?: number; - parent: { - invalidateCellSizeAfterRender?: (cell: Cell) => void; - recomputeGridSize?: (cell: Cell) => void; - }; - rowIndex?: number; -}; -/** - * Wraps a cell and measures its rendered content. - * Measurements are stored in a per-cell cache. - * Cached-content is not be re-measured. - */ - -export class CellMeasurer extends React.PureComponent { - static __internalCellMeasurerFlag = false; - _child: Element | null | undefined; - - componentDidMount() { - this._maybeMeasureCell(); - } - - componentDidUpdate() { - this._maybeMeasureCell(); - } - - render() { - const { children } = this.props; - return typeof children === 'function' - ? children({ - measure: this._measure, - registerChild: this._registerChild, - }) - : children; - } - - _getCellMeasurements() { - const { cache } = this.props; - const node = this._child || findDOMNode(this); - - // TODO Check for a bad combination of fixedWidth and missing numeric width or vice versa with height - if ( - node && - node.ownerDocument && - node.ownerDocument.defaultView && - node instanceof node.ownerDocument.defaultView.HTMLElement - ) { - const styleWidth = node.style.width; - const styleHeight = node.style.height; - - // If we are re-measuring a cell that has already been measured, - // It will have a hard-coded width/height from the previous measurement. - // The fact that we are measuring indicates this measurement is probably stale, - // So explicitly clear it out (eg set to "auto") so we can recalculate. - // See issue #593 for more info. - // Even if we are measuring initially- if we're inside of a MultiGrid component, - // Explicitly clear width/height before measuring to avoid being tainted by another Grid. - // eg top/left Grid renders before bottom/right Grid - // Since the CellMeasurerCache is shared between them this taints derived cell size values. - if (!cache.hasFixedWidth()) { - node.style.width = 'auto'; - } - - if (!cache.hasFixedHeight()) { - node.style.height = 'auto'; - } - - const height = Math.ceil(node.offsetHeight); - const width = Math.ceil(node.offsetWidth); - - // Reset after measuring to avoid breaking styles; see #660 - if (styleWidth) { - node.style.width = styleWidth; - } - - if (styleHeight) { - node.style.height = styleHeight; - } - - return { - height, - width, - }; - } else { - return { - height: 0, - width: 0, - }; - } - } - - _maybeMeasureCell() { - const { - cache, - columnIndex = 0, - parent, - rowIndex = this.props.index || 0, - } = this.props; - - if (!cache.has(rowIndex, columnIndex)) { - const { height, width } = this._getCellMeasurements(); - - cache.set(rowIndex, columnIndex, width, height); - - // If size has changed, let Grid know to re-render. - if ( - parent && - typeof parent.invalidateCellSizeAfterRender === 'function' - ) { - parent.invalidateCellSizeAfterRender({ - columnIndex, - rowIndex, - }); - } - } - } - - _measure = () => { - const { - cache, - columnIndex = 0, - parent, - rowIndex = this.props.index || 0, - } = this.props; - - const { height, width } = this._getCellMeasurements(); - - if ( - height !== cache.getHeight(rowIndex, columnIndex) || - width !== cache.getWidth(rowIndex, columnIndex) - ) { - cache.set(rowIndex, columnIndex, width, height); - - if (parent && typeof parent.recomputeGridSize === 'function') { - parent.recomputeGridSize({ - columnIndex, - rowIndex, - }); - } - } - }; - _registerChild = (element: any) => { - if (element && !(element instanceof Element)) { - console.warn( - 'CellMeasurer registerChild expects to be passed Element or null' - ); - } - - this._child = element; - - if (element) { - this._maybeMeasureCell(); - } - }; -} // Used for DEV mode warning check - -if (process.env.NODE_ENV !== 'production') { - CellMeasurer.__internalCellMeasurerFlag = true; -} diff --git a/lib/design-system/src/general/WindowedList/source/CellMeasurer/CellMeasurerCache.ts b/lib/design-system/src/general/WindowedList/source/CellMeasurer/CellMeasurerCache.ts deleted file mode 100644 index f8d3132380..0000000000 --- a/lib/design-system/src/general/WindowedList/source/CellMeasurer/CellMeasurerCache.ts +++ /dev/null @@ -1,224 +0,0 @@ -import type { CellMeasureCache } from './types'; -export const DEFAULT_HEIGHT = 30; -export const DEFAULT_WIDTH = 100; -// Enables more intelligent mapping of a given column and row index to an item ID. -// This prevents a cell cache from being invalidated when its parent collection is modified. -type KeyMapper = (rowIndex: number, columnIndex: number) => any; -type CellMeasurerCacheParams = { - defaultHeight?: number; - defaultWidth?: number; - fixedHeight?: boolean; - fixedWidth?: boolean; - minHeight?: number; - minWidth?: number; - keyMapper?: KeyMapper; -}; -type Cache = { [key in any]?: number }; -type IndexParam = { - index: number; -}; -/** - * Caches measurements for a given cell. - */ - -export class CellMeasurerCache implements CellMeasureCache { - _cellHeightCache: Cache = {}; - _cellWidthCache: Cache = {}; - _columnWidthCache: Cache = {}; - _rowHeightCache: Cache = {}; - _defaultHeight: number; - _defaultWidth: number; - _minHeight: number; - _minWidth: number; - _keyMapper: KeyMapper; - _hasFixedHeight: boolean; - _hasFixedWidth: boolean; - _columnCount = 0; - _rowCount = 0; - - constructor(params: CellMeasurerCacheParams = {}) { - const { - defaultHeight, - defaultWidth, - fixedHeight, - fixedWidth, - keyMapper, - minHeight, - minWidth, - } = params; - this._hasFixedHeight = fixedHeight === true; - this._hasFixedWidth = fixedWidth === true; - this._minHeight = minHeight || 0; - this._minWidth = minWidth || 0; - this._keyMapper = keyMapper || defaultKeyMapper; - this._defaultHeight = Math.max( - this._minHeight, - typeof defaultHeight === 'number' ? defaultHeight : DEFAULT_HEIGHT - ); - this._defaultWidth = Math.max( - this._minWidth, - typeof defaultWidth === 'number' ? defaultWidth : DEFAULT_WIDTH - ); - - if (process.env.NODE_ENV !== 'production') { - if (this._hasFixedHeight === false && this._hasFixedWidth === false) { - console.warn( - "CellMeasurerCache should only measure a cell's width or height. " + - 'You have configured CellMeasurerCache to measure both. ' + - 'This will result in poor performance.' - ); - } - - if (this._hasFixedHeight === false && this._defaultHeight === 0) { - console.warn( - 'Fixed height CellMeasurerCache should specify a :defaultHeight greater than 0. ' + - 'Failing to do so will lead to unnecessary layout and poor performance.' - ); - } - - if (this._hasFixedWidth === false && this._defaultWidth === 0) { - console.warn( - 'Fixed width CellMeasurerCache should specify a :defaultWidth greater than 0. ' + - 'Failing to do so will lead to unnecessary layout and poor performance.' - ); - } - } - } - - clear(rowIndex: number, columnIndex: number = 0) { - const key = this._keyMapper(rowIndex, columnIndex); - - delete this._cellHeightCache[key]; - delete this._cellWidthCache[key]; - - this._updateCachedColumnAndRowSizes(rowIndex, columnIndex); - } - - clearAll() { - this._cellHeightCache = {}; - this._cellWidthCache = {}; - this._columnWidthCache = {}; - this._rowHeightCache = {}; - this._rowCount = 0; - this._columnCount = 0; - } - - columnWidth = ({ index }: IndexParam) => { - const key = this._keyMapper(0, index); - - return this._columnWidthCache[key] !== undefined - ? this._columnWidthCache[key] - : this._defaultWidth; - }; - - get defaultHeight(): number { - return this._defaultHeight; - } - - get defaultWidth(): number { - return this._defaultWidth; - } - - hasFixedHeight(): boolean { - return this._hasFixedHeight; - } - - hasFixedWidth(): boolean { - return this._hasFixedWidth; - } - - getHeight(rowIndex: number, columnIndex: number = 0): number { - if (this._hasFixedHeight) { - return this._defaultHeight; - } else { - const key = this._keyMapper(rowIndex, columnIndex); - - return this._cellHeightCache[key] !== undefined - ? Math.max(this._minHeight, this._cellHeightCache[key] as any) - : this._defaultHeight; - } - } - - getWidth(rowIndex: number, columnIndex: number = 0): number { - if (this._hasFixedWidth) { - return this._defaultWidth; - } else { - const key = this._keyMapper(rowIndex, columnIndex); - - return this._cellWidthCache[key] !== undefined - ? Math.max(this._minWidth, this._cellWidthCache[key] as any) - : this._defaultWidth; - } - } - - has(rowIndex: number, columnIndex: number = 0): boolean { - const key = this._keyMapper(rowIndex, columnIndex); - - return this._cellHeightCache[key] !== undefined; - } - - rowHeight = ({ index }: IndexParam) => { - const key = this._keyMapper(index, 0); - - return this._rowHeightCache[key] !== undefined - ? this._rowHeightCache[key] - : this._defaultHeight; - }; - - set( - rowIndex: number, - columnIndex: number, - width: number, - height: number - ): void { - const key = this._keyMapper(rowIndex, columnIndex); - - if (columnIndex >= this._columnCount) { - this._columnCount = columnIndex + 1; - } - - if (rowIndex >= this._rowCount) { - this._rowCount = rowIndex + 1; - } - - // Size is cached per cell so we don't have to re-measure if cells are re-ordered. - this._cellHeightCache[key] = height; - this._cellWidthCache[key] = width; - - this._updateCachedColumnAndRowSizes(rowIndex, columnIndex); - } - - _updateCachedColumnAndRowSizes(rowIndex: number, columnIndex: number) { - // :columnWidth and :rowHeight are derived based on all cells in a column/row. - // Pre-cache these derived values for faster lookup later. - // Reads are expected to occur more frequently than writes in this case. - // Only update non-fixed dimensions though to avoid doing unnecessary work. - if (!this._hasFixedWidth) { - let columnWidth = 0; - - for (let i = 0; i < this._rowCount; i++) { - columnWidth = Math.max(columnWidth, this.getWidth(i, columnIndex)); - } - - const columnKey = this._keyMapper(0, columnIndex); - - this._columnWidthCache[columnKey] = columnWidth; - } - - if (!this._hasFixedHeight) { - let rowHeight = 0; - - for (let i = 0; i < this._columnCount; i++) { - rowHeight = Math.max(rowHeight, this.getHeight(rowIndex, i)); - } - - const rowKey = this._keyMapper(rowIndex, 0); - - this._rowHeightCache[rowKey] = rowHeight; - } - } -} - -function defaultKeyMapper(rowIndex: number, columnIndex: number) { - return `${rowIndex}-${columnIndex}`; -} diff --git a/lib/design-system/src/general/WindowedList/source/CellMeasurer/types.ts b/lib/design-system/src/general/WindowedList/source/CellMeasurer/types.ts deleted file mode 100644 index f76dac13ca..0000000000 --- a/lib/design-system/src/general/WindowedList/source/CellMeasurer/types.ts +++ /dev/null @@ -1,13 +0,0 @@ -export interface CellMeasureCache { - hasFixedWidth(): boolean; - hasFixedHeight(): boolean; - has(rowIndex: number, columnIndex: number): boolean; - set( - rowIndex: number, - columnIndex: number, - width: number, - height: number - ): void; - getHeight(rowIndex: number, columnIndex?: number): number; - getWidth(rowIndex: number, columnIndex?: number): number; -} diff --git a/lib/design-system/src/general/WindowedList/source/Grid/Grid.tsx b/lib/design-system/src/general/WindowedList/source/Grid/Grid.tsx deleted file mode 100644 index c765e304cd..0000000000 --- a/lib/design-system/src/general/WindowedList/source/Grid/Grid.tsx +++ /dev/null @@ -1,1590 +0,0 @@ -import type { - CellRenderer, - CellRangeRenderer, - CellPosition, - CellSize, - CellSizeGetter, - NoContentRenderer, - Scroll, - ScrollbarPresenceChange, - RenderedSection, - OverscanIndicesGetter, - Alignment, - CellCache, - StyleCache, -} from './types'; -import type { AnimationTimeoutId } from '../utils/requestAnimationTimeout'; -import React, { UIEventHandler } from 'react'; -import { calculateSizeAndPositionDataAndUpdateScrollOffset } from './utils/calculateSizeAndPositionDataAndUpdateScrollOffset'; -import { ScalingCellSizeAndPositionManager } from './utils/ScalingCellSizeAndPositionManager'; -import { createCallbackMemoizer } from '../utils/createCallbackMemoizer'; -import { - defaultOverscanIndicesGetter, - SCROLL_DIRECTION_BACKWARD, - SCROLL_DIRECTION_FORWARD, -} from './defaultOverscanIndicesGetter'; -import { updateScrollIndexHelper } from './utils/updateScrollIndexHelper'; -import { defaultCellRangeRenderer } from './defaultCellRangeRenderer'; -import scrollbarSize from 'dom-helpers/scrollbarSize'; -import { polyfill } from 'react-lifecycles-compat'; -import { - requestAnimationTimeout, - cancelAnimationTimeout, -} from '../utils/requestAnimationTimeout'; - -type Shape = Partial; - -/** - * Specifies the number of milliseconds during which to disable pointer events while a scroll is in progress. - * This improves performance and makes scrolling smoother. - */ -export const DEFAULT_SCROLLING_RESET_TIME_INTERVAL = 150; - -/** - * Controls whether the Grid updates the DOM element's scrollLeft/scrollTop based on the current state or just observes it. - * This prevents Grid from interrupting mouse-wheel animations (see issue #2). - */ -const SCROLL_POSITION_CHANGE_REASONS = { - OBSERVED: 'observed', - REQUESTED: 'requested', -}; - -const renderNull: NoContentRenderer = () => null; - -type ScrollPosition = { - scrollTop?: number; - scrollLeft?: number; -}; -type Props = { - /** - * Set the width of the inner scrollable container to 'auto'. - * This is useful for single-column Grids to ensure that the column doesn't extend below a vertical scrollbar. - */ - autoContainerWidth: boolean; - /** Responsible for rendering a cell given an row and column index. */ - cellRenderer: CellRenderer; - - /** Responsible for rendering a group of cells given their index ranges. */ - cellRangeRenderer: CellRangeRenderer; - - /** Optional custom CSS class name to attach to root Grid element. */ - className?: string; - - /** Number of columns in grid. */ - columnCount: number; - - /** Either a fixed column width (number) or a function that returns the width of a column given its index. */ - columnWidth: CellSize; - - /** Unfiltered props for the Grid container. */ - containerProps?: Record; - - /** ARIA role for the cell-container. */ - containerRole: string; - - /** Optional inline style applied to inner cell-container */ - containerStyle: Record; - - /** - * If CellMeasurer is used to measure this Grid's children, this should be a pointer to its CellMeasurerCache. - * A shared CellMeasurerCache reference enables Grid and CellMeasurer to share measurement data. - */ - deferredMeasurementCache?: Record; - - /** - * Used to estimate the total width of a Grid before all of its columns have actually been measured. - * The estimated total width is adjusted as columns are rendered. - */ - estimatedColumnSize: number; - - /** - * Used to estimate the total height of a Grid before all of its rows have actually been measured. - * The estimated total height is adjusted as rows are rendered. - */ - estimatedRowSize: number; - - /** Exposed for testing purposes only. */ - getScrollbarSize: () => number; - - /** Height of Grid; this property determines the number of visible (vs virtualized) rows. */ - height: number; - - /** Optional custom id to attach to root Grid element. */ - id?: string; - - /** - * Override internal is-scrolling state tracking. - * This property is primarily intended for use with the WindowScroller component. - */ - isScrolling?: boolean; - - /** - * Opt-out of isScrolling param passed to cellRangeRenderer. - * To avoid the extra render when scroll stops. - */ - isScrollingOptOut: boolean; - - /** Optional renderer to be used in place of rows when either :rowCount or :columnCount is 0. */ - noContentRenderer: NoContentRenderer; - - /** - * Callback invoked whenever the scroll offset changes within the inner scrollable region. - * This callback can be used to sync scrolling between lists, tables, or grids. - */ - onScroll: (params: Scroll) => void; - - /** - * Called whenever a horizontal or vertical scrollbar is added or removed. - * This prop is not intended for end-user use; - * It is used by MultiGrid to support fixed-row/fixed-column scroll syncing. - */ - onScrollbarPresenceChange: (params: ScrollbarPresenceChange) => void; - - /** Callback invoked with information about the section of the Grid that was just rendered. */ - onSectionRendered: (params: RenderedSection) => void; - - /** - * Number of columns to render before/after the visible section of the grid. - * These columns can help for smoother scrolling on touch devices or browsers that send scroll events infrequently. - */ - overscanColumnCount: number; - - /** - * Calculates the number of cells to overscan before and after a specified range. - * This function ensures that overscanning doesn't exceed the available cells. - */ - overscanIndicesGetter: OverscanIndicesGetter; - - /** - * Number of rows to render above/below the visible section of the grid. - * These rows can help for smoother scrolling on touch devices or browsers that send scroll events infrequently. - */ - overscanRowCount: number; - - /** ARIA role for the grid element. */ - role: string; - - /** - * Either a fixed row height (number) or a function that returns the height of a row given its index. - * Should implement the following interface: ({ index: number }): number - */ - rowHeight: CellSize; - - /** Number of rows in grid. */ - rowCount: number; - - /** Wait this amount of time after the last scroll event before resetting Grid `pointer-events`. */ - scrollingResetTimeInterval: number; - - /** Horizontal offset. */ - scrollLeft?: number; - - /** - * Controls scroll-to-cell behavior of the Grid. - * The default ("auto") scrolls the least amount possible to ensure that the specified cell is fully visible. - * Use "start" to align cells to the top/left of the Grid and "end" to align bottom/right. - */ - scrollToAlignment: Alignment; - - /** Column index to ensure visible (by forcefully scrolling if necessary) */ - scrollToColumn: number; - - /** Vertical offset. */ - scrollTop?: number; - - /** Row index to ensure visible (by forcefully scrolling if necessary) */ - scrollToRow: number; - - /** Start adding elements from the bottom and scroll on mount/update. */ - startAtBottom?: boolean; - - /** Optional inline style */ - style: Record; - - /** Tab index for focus */ - tabIndex: number | undefined; - - /** Width of Grid; this property determines the number of visible (vs virtualized) columns. */ - width: number; -}; - -type InstanceProps = { - prevColumnWidth: CellSize; - prevRowHeight: CellSize; - prevColumnCount: number; - prevRowCount: number; - prevIsScrolling: boolean; - prevScrollToColumn: number; - prevScrollToRow: number; - columnSizeAndPositionManager: ScalingCellSizeAndPositionManager; - rowSizeAndPositionManager: ScalingCellSizeAndPositionManager; - scrollbarSize: number; - scrollbarSizeMeasured: boolean; -}; - -type State = { - instanceProps: InstanceProps; - isScrolling: boolean; - scrollDirectionHorizontal: -1 | 1; - scrollDirectionVertical: -1 | 1; - scrollLeft: number; - scrollTop: number; - scrollPositionChangeReason: - | (typeof SCROLL_POSITION_CHANGE_REASONS)[keyof typeof SCROLL_POSITION_CHANGE_REASONS] - | null; - needToResetStyleCache: boolean; -}; - -/** - * Renders tabular data with virtualization along the vertical and horizontal axes. - * Row heights and column widths must be known ahead of time and specified as properties. - */ -export class Grid extends React.PureComponent { - static defaultProps = { - autoContainerWidth: false, - cellRangeRenderer: defaultCellRangeRenderer, - containerRole: 'row', - containerStyle: {}, - estimatedColumnSize: 100, - estimatedRowSize: 30, - getScrollbarSize: scrollbarSize, - noContentRenderer: renderNull, - onScroll: () => {}, - onScrollbarPresenceChange: () => {}, - onSectionRendered: () => {}, - overscanColumnCount: 0, - overscanIndicesGetter: defaultOverscanIndicesGetter, - overscanRowCount: 10, - role: 'grid', - scrollingResetTimeInterval: DEFAULT_SCROLLING_RESET_TIME_INTERVAL, - scrollToAlignment: 'auto', - scrollToColumn: -1, - scrollToRow: -1, - style: {}, - tabIndex: 0, - isScrollingOptOut: false, - }; - // Invokes onSectionRendered callback only when start/stop row or column indices change - _onGridRenderedMemoizer = createCallbackMemoizer(); - _onScrollMemoizer = createCallbackMemoizer(false); - _deferredInvalidateColumnIndex: number | null = null; - _deferredInvalidateRowIndex: number | null = null; - _recomputeScrollLeftFlag = false; - _recomputeScrollTopFlag = false; - _horizontalScrollBarSize = 0; - _verticalScrollBarSize = 0; - _scrollbarPresenceChanged = false; - _scrollingContainer: Element | undefined; - _childrenToDisplay: React.ReactElement[] | undefined; - _columnStartIndex: number | undefined; - _columnStopIndex: number | undefined; - _rowStartIndex: number | undefined; - _rowStopIndex: number | undefined; - _renderedColumnStartIndex: number | undefined = 0; - _renderedColumnStopIndex: number | undefined = 0; - _renderedRowStartIndex: number | undefined = 0; - _renderedRowStopIndex: number | undefined = 0; - _initialScrollTop: number | undefined; - _initialScrollLeft: number | undefined; - _disablePointerEventsTimeoutId: AnimationTimeoutId | null | undefined; - _styleCache: StyleCache = {}; - _cellCache: CellCache = {}; - - constructor(props: Props) { - super(props); - const columnSizeAndPositionManager = new ScalingCellSizeAndPositionManager({ - cellCount: props.columnCount, - cellSizeGetter: (params) => - Grid._wrapSizeGetter(props.columnWidth)(params), - estimatedCellSize: Grid._getEstimatedColumnSize(props), - }); - const rowSizeAndPositionManager = new ScalingCellSizeAndPositionManager({ - cellCount: props.rowCount, - cellSizeGetter: (params) => Grid._wrapSizeGetter(props.rowHeight)(params), - estimatedCellSize: Grid._getEstimatedRowSize(props), - }); - this.state = { - instanceProps: { - columnSizeAndPositionManager, - rowSizeAndPositionManager, - prevColumnWidth: props.columnWidth, - prevRowHeight: props.rowHeight, - prevColumnCount: props.columnCount, - prevRowCount: props.rowCount, - prevIsScrolling: props.isScrolling === true, - prevScrollToColumn: props.scrollToColumn, - prevScrollToRow: props.scrollToRow, - scrollbarSize: 0, - scrollbarSizeMeasured: false, - }, - isScrolling: false, - scrollDirectionHorizontal: SCROLL_DIRECTION_FORWARD, - scrollDirectionVertical: SCROLL_DIRECTION_FORWARD, - scrollLeft: 0, - scrollTop: 0, - scrollPositionChangeReason: null, - needToResetStyleCache: false, - }; - - if (props.scrollToRow > 0) { - this._initialScrollTop = this._getCalculatedScrollTop(props, this.state); - } - - if (props.scrollToColumn > 0) { - this._initialScrollLeft = this._getCalculatedScrollLeft( - props, - this.state - ); - } - } - - /** - * Gets offsets for a given cell and alignment. - */ - getOffsetForCell({ - alignment = this.props.scrollToAlignment, - columnIndex = this.props.scrollToColumn, - rowIndex = this.props.scrollToRow, - }: { - alignment?: Alignment; - columnIndex?: number; - rowIndex?: number; - } = {}) { - const offsetProps = { - ...this.props, - scrollToAlignment: alignment, - scrollToColumn: columnIndex, - scrollToRow: rowIndex, - }; - return { - scrollLeft: this._getCalculatedScrollLeft(offsetProps), - scrollTop: this._getCalculatedScrollTop(offsetProps), - }; - } - - /** - * Gets estimated total rows' height. - */ - getTotalRowsHeight() { - return this.state.instanceProps.rowSizeAndPositionManager.getTotalSize(); - } - - /** - * Gets estimated total columns' width. - */ - getTotalColumnsWidth() { - return this.state.instanceProps.columnSizeAndPositionManager.getTotalSize(); - } - - /** - * This method handles a scroll event originating from an external scroll control. - * It's an advanced method and should probably not be used unless you're implementing a custom scroll-bar solution. - */ - handleScrollEvent({ - scrollLeft: scrollLeftParam = 0, - scrollTop: scrollTopParam = 0, - }: ScrollPosition) { - // On iOS, we can arrive at negative offsets by swiping past the start. - // To prevent flicker here, we make playing in the negative offset zone cause nothing to happen. - if (scrollTopParam < 0) { - return; - } - - // Prevent pointer events from interrupting a smooth scroll - this._debounceScrollEnded(); - - const { height, width } = this.props; - const { instanceProps } = this.state; - // When this component is shrunk drastically, React dispatches a series of back-to-back scroll events, - // Gradually converging on a scrollTop that is within the bounds of the new, smaller height. - // This causes a series of rapid renders that is slow for long lists. - // We can avoid that by doing some simple bounds checking to ensure that scroll offsets never exceed their bounds. - const scrollbarSize = instanceProps.scrollbarSize; - const totalRowsHeight = - instanceProps.rowSizeAndPositionManager.getTotalSize(); - const totalColumnsWidth = - instanceProps.columnSizeAndPositionManager.getTotalSize(); - const scrollLeft = Math.min( - Math.max(0, totalColumnsWidth - width + scrollbarSize), - scrollLeftParam - ); - const scrollTop = Math.min( - Math.max(0, totalRowsHeight - height + scrollbarSize), - scrollTopParam - ); - - // Certain devices (like Apple touchpad) rapid-fire duplicate events. - // Don't force a re-render if this is the case. - // The mouse may move faster then the animation frame does. - // Use requestAnimationFrame to avoid over-updating. - if ( - this.state.scrollLeft !== scrollLeft || - this.state.scrollTop !== scrollTop - ) { - // Track scrolling direction so we can more efficiently overscan rows to reduce empty space around the edges while scrolling. - // Don't change direction for an axis unless scroll offset has changed. - const scrollDirectionHorizontal = - scrollLeft !== this.state.scrollLeft - ? scrollLeft > this.state.scrollLeft - ? SCROLL_DIRECTION_FORWARD - : SCROLL_DIRECTION_BACKWARD - : this.state.scrollDirectionHorizontal; - const scrollDirectionVertical = - scrollTop !== this.state.scrollTop - ? scrollTop > this.state.scrollTop - ? SCROLL_DIRECTION_FORWARD - : SCROLL_DIRECTION_BACKWARD - : this.state.scrollDirectionVertical; - const newState: Shape = { - isScrolling: true, - scrollDirectionHorizontal, - scrollDirectionVertical, - scrollPositionChangeReason: SCROLL_POSITION_CHANGE_REASONS.OBSERVED, - }; - - newState.scrollTop = scrollTop; - newState.scrollLeft = scrollLeft; - - newState.needToResetStyleCache = false; - this.setState(newState as any); - } - - this._invokeOnScrollMemoizer({ - scrollLeft, - scrollTop, - totalColumnsWidth, - totalRowsHeight, - }); - } - - /** - * Invalidate Grid size and recompute visible cells. - * This is a deferred wrapper for recomputeGridSize(). - * It sets a flag to be evaluated on cDM/cDU to avoid unnecessary renders. - * This method is intended for advanced use-cases like CellMeasurer. - */ - // @TODO (bvaughn) Add automated test coverage for this. - invalidateCellSizeAfterRender({ columnIndex, rowIndex }: CellPosition) { - this._deferredInvalidateColumnIndex = - typeof this._deferredInvalidateColumnIndex === 'number' - ? Math.min(this._deferredInvalidateColumnIndex, columnIndex) - : columnIndex; - this._deferredInvalidateRowIndex = - typeof this._deferredInvalidateRowIndex === 'number' - ? Math.min(this._deferredInvalidateRowIndex, rowIndex) - : rowIndex; - } - - /** - * Pre-measure all columns and rows in a Grid. - * Typically cells are only measured as needed and estimated sizes are used for cells that have not yet been measured. - * This method ensures that the next call to getTotalSize() returns an exact size (as opposed to just an estimated one). - */ - measureAllCells() { - const { columnCount, rowCount } = this.props; - const { instanceProps } = this.state; - instanceProps.columnSizeAndPositionManager.getSizeAndPositionOfCell( - columnCount - 1 - ); - instanceProps.rowSizeAndPositionManager.getSizeAndPositionOfCell( - rowCount - 1 - ); - } - - /** - * Forced recompute of row heights and column widths. - * This function should be called if dynamic column or row sizes have changed but nothing else has. - * Since Grid only receives :columnCount and :rowCount it has no way of detecting when the underlying data changes. - */ - recomputeGridSize({ - columnIndex = 0, - rowIndex = 0, - }: Partial = {}) { - const { scrollToColumn, scrollToRow } = this.props; - const { instanceProps } = this.state; - instanceProps.columnSizeAndPositionManager.resetCell(columnIndex); - instanceProps.rowSizeAndPositionManager.resetCell(rowIndex); - // Cell sizes may be determined by a function property. - // In this case the cDU handler can't know if they changed. - // Store this flag to let the next cDU pass know it needs to recompute the scroll offset. - this._recomputeScrollLeftFlag = - scrollToColumn >= 0 && - (this.state.scrollDirectionHorizontal === SCROLL_DIRECTION_FORWARD - ? columnIndex <= scrollToColumn - : columnIndex >= scrollToColumn); - this._recomputeScrollTopFlag = - scrollToRow >= 0 && - (this.state.scrollDirectionVertical === SCROLL_DIRECTION_FORWARD - ? rowIndex <= scrollToRow - : rowIndex >= scrollToRow); - // Clear cell cache in case we are scrolling; - // Invalid row heights likely mean invalid cached content as well. - this._styleCache = {}; - this._cellCache = {}; - this.forceUpdate(); - } - - /** - * Ensure column and row are visible. - */ - scrollToCell({ columnIndex, rowIndex }: CellPosition) { - const { columnCount } = this.props; - const props = this.props; - - // Don't adjust scroll offset for single-column grids (eg List, Table). - // This can cause a funky scroll offset because of the vertical scrollbar width. - if (columnCount > 1 && columnIndex !== undefined) { - this._updateScrollLeftForScrollToColumn({ - ...props, - scrollToColumn: columnIndex, - }); - } - - if (rowIndex !== undefined) { - this._updateScrollTopForScrollToRow({ ...props, scrollToRow: rowIndex }); - } - } - - componentDidMount() { - const { - getScrollbarSize, - height, - scrollLeft, - scrollToColumn, - scrollTop, - scrollToRow, - width, - } = this.props; - const { instanceProps } = this.state; - // Reset initial offsets to be ignored in browser - this._initialScrollTop = 0; - this._initialScrollLeft = 0; - - // If cell sizes have been invalidated (eg we are using CellMeasurer) then reset cached positions. - // We must do this at the start of the method as we may calculate and update scroll position below. - this._handleInvalidatedGridSize(); - - // If this component was first rendered server-side, scrollbar size will be undefined. - // In that event we need to remeasure. - if (!instanceProps.scrollbarSizeMeasured) { - this.setState((prevState) => { - const stateUpdate = { ...prevState, needToResetStyleCache: false }; - stateUpdate.instanceProps.scrollbarSize = getScrollbarSize(); - stateUpdate.instanceProps.scrollbarSizeMeasured = true; - return stateUpdate; - }); - } - - if ( - (typeof scrollLeft === 'number' && scrollLeft >= 0) || - (typeof scrollTop === 'number' && scrollTop >= 0) - ) { - const stateUpdate = Grid._getScrollToPositionStateUpdate({ - prevState: this.state, - scrollLeft, - scrollTop, - }); - - if (stateUpdate) { - stateUpdate.needToResetStyleCache = false; - this.setState(stateUpdate as any); - } - } - - // refs don't work in `react-test-renderer` - if (this._scrollingContainer) { - // setting the ref's scrollLeft and scrollTop. - // Somehow in MultiGrid the main grid doesn't trigger a update on mount. - if (this._scrollingContainer.scrollLeft !== this.state.scrollLeft) { - this._scrollingContainer.scrollLeft = this.state.scrollLeft; - } - - if (this._scrollingContainer.scrollTop !== this.state.scrollTop) { - this._scrollingContainer.scrollTop = this.state.scrollTop; - } - } - - // Don't update scroll offset if the size is 0; we don't render any cells in this case. - // Setting a state may cause us to later thing we've updated the offce when we haven't. - const sizeIsBiggerThanZero = height > 0 && width > 0; - - if (scrollToColumn >= 0 && sizeIsBiggerThanZero) { - this._updateScrollLeftForScrollToColumn(); - } - - if (scrollToRow >= 0 && sizeIsBiggerThanZero) { - this._updateScrollTopForScrollToRow(); - } - - // Update onRowsRendered callback - this._invokeOnGridRenderedHelper(); - - // Initialize onScroll callback - this._invokeOnScrollMemoizer({ - scrollLeft: scrollLeft || 0, - scrollTop: scrollTop || 0, - totalColumnsWidth: - instanceProps.columnSizeAndPositionManager.getTotalSize(), - totalRowsHeight: instanceProps.rowSizeAndPositionManager.getTotalSize(), - }); - - this._maybeCallOnScrollbarPresenceChange(); - } - - /** - * @private - * This method updates scrollLeft/scrollTop in state for the following conditions: - * 1) New scroll-to-cell props have been set - */ - componentDidUpdate(prevProps: Props, prevState: State) { - const { - columnCount, - height, - rowCount, - scrollToAlignment, - scrollToColumn, - scrollToRow, - width, - } = this.props; - const { scrollLeft, scrollPositionChangeReason, scrollTop, instanceProps } = - this.state; - - // If cell sizes have been invalidated (eg we are using CellMeasurer) then reset cached positions. - // We must do this at the start of the method as we may calculate and update scroll position below. - this._handleInvalidatedGridSize(); - - // Handle edge case where column or row count has only just increased over 0. - // In this case we may have to restore a previously-specified scroll offset. - // For more info see bvaughn/react-virtualized/issues/218 - const columnOrRowCountJustIncreasedFromZero = - (columnCount > 0 && prevProps.columnCount === 0) || - (rowCount > 0 && prevProps.rowCount === 0); - - // Make sure requested changes to :scrollLeft or :scrollTop get applied. - // Assigning to scrollLeft/scrollTop tells the browser to interrupt any running scroll animations, - // And to discard any pending async changes to the scroll position that may have happened in the meantime (e.g. on a separate scrolling thread). - // So we only set these when we require an adjustment of the scroll position. - // See issue #2 for more information. - if ( - scrollPositionChangeReason === SCROLL_POSITION_CHANGE_REASONS.REQUESTED - ) { - if ( - scrollLeft >= 0 && - this._scrollingContainer && - (scrollLeft !== this._scrollingContainer.scrollLeft || - columnOrRowCountJustIncreasedFromZero) - ) { - this._scrollingContainer.scrollLeft = scrollLeft; - } - - if ( - scrollTop >= 0 && - this._scrollingContainer && - (scrollTop !== this._scrollingContainer.scrollTop || - columnOrRowCountJustIncreasedFromZero) - ) { - this._scrollingContainer.scrollTop = scrollTop; - } - } - - // Special case where the previous size was 0: - // In this case we don't show any windowed cells at all. - // So we should always recalculate offset afterwards. - const sizeJustIncreasedFromZero = - (prevProps.width === 0 || prevProps.height === 0) && - height > 0 && - width > 0; - - // Update scroll offsets if the current :scrollToColumn or :scrollToRow values requires it - // @TODO Do we also need this check or can the one in componentWillUpdate() suffice? - if (this._recomputeScrollLeftFlag) { - this._recomputeScrollLeftFlag = false; - - this._updateScrollLeftForScrollToColumn(this.props); - } else { - updateScrollIndexHelper({ - cellSizeAndPositionManager: instanceProps.columnSizeAndPositionManager, - previousCellsCount: prevProps.columnCount, - previousCellSize: prevProps.columnWidth, - previousScrollToAlignment: prevProps.scrollToAlignment, - previousScrollToIndex: prevProps.scrollToColumn, - previousSize: prevProps.width, - scrollOffset: scrollLeft, - scrollToAlignment, - scrollToIndex: scrollToColumn, - size: width, - sizeJustIncreasedFromZero, - updateScrollIndexCallback: () => - this._updateScrollLeftForScrollToColumn(this.props), - }); - } - - if (this._recomputeScrollTopFlag) { - this._recomputeScrollTopFlag = false; - - this._updateScrollTopForScrollToRow(this.props); - } else { - updateScrollIndexHelper({ - cellSizeAndPositionManager: instanceProps.rowSizeAndPositionManager, - previousCellsCount: prevProps.rowCount, - previousCellSize: prevProps.rowHeight, - previousScrollToAlignment: prevProps.scrollToAlignment, - previousScrollToIndex: prevProps.scrollToRow, - previousSize: prevProps.height, - scrollOffset: scrollTop, - scrollToAlignment, - scrollToIndex: scrollToRow, - size: height, - sizeJustIncreasedFromZero, - updateScrollIndexCallback: () => - this._updateScrollTopForScrollToRow(this.props), - }); - } - - // Update onRowsRendered callback if start/stop indices have changed - this._invokeOnGridRenderedHelper(); - - // Changes to :scrollLeft or :scrollTop should also notify :onScroll listeners - if ( - scrollLeft !== prevState.scrollLeft || - scrollTop !== prevState.scrollTop - ) { - const totalRowsHeight = - instanceProps.rowSizeAndPositionManager.getTotalSize(); - const totalColumnsWidth = - instanceProps.columnSizeAndPositionManager.getTotalSize(); - - this._invokeOnScrollMemoizer({ - scrollLeft, - scrollTop, - totalColumnsWidth, - totalRowsHeight, - }); - } - - this._maybeCallOnScrollbarPresenceChange(); - } - - componentWillUnmount() { - if (this._disablePointerEventsTimeoutId) { - cancelAnimationTimeout(this._disablePointerEventsTimeoutId); - } - } - - /** - * This method updates scrollLeft/scrollTop in state for the following conditions: - * 1) Empty content (0 rows or columns) - * 2) New scroll props overriding the current state - * 3) Cells-count or cells-size has changed, making previous scroll offsets invalid - */ - static getDerivedStateFromProps( - nextProps: Props, - prevState: State - ): Shape { - const newState: Partial = {}; - - if ( - (nextProps.columnCount === 0 && prevState.scrollLeft !== 0) || - (nextProps.rowCount === 0 && prevState.scrollTop !== 0) - ) { - newState.scrollLeft = 0; - newState.scrollTop = 0; // only use scroll{Left,Top} from props if scrollTo{Column,Row} isn't specified - // scrollTo{Column,Row} should override scroll{Left,Top} - } else if ( - (nextProps.scrollLeft !== prevState.scrollLeft && - nextProps.scrollToColumn < 0) || - (nextProps.scrollTop !== prevState.scrollTop && nextProps.scrollToRow < 0) - ) { - Object.assign( - newState, - Grid._getScrollToPositionStateUpdate({ - prevState, - scrollLeft: nextProps.scrollLeft, - scrollTop: nextProps.scrollTop, - }) - ); - } - - const { instanceProps } = prevState; - // Initially we should not clearStyleCache - newState.needToResetStyleCache = false; - - if ( - nextProps.columnWidth !== instanceProps.prevColumnWidth || - nextProps.rowHeight !== instanceProps.prevRowHeight - ) { - // Reset cache. set it to {} in render - newState.needToResetStyleCache = true; - } - - instanceProps.columnSizeAndPositionManager.configure({ - cellCount: nextProps.columnCount, - estimatedCellSize: Grid._getEstimatedColumnSize(nextProps), - cellSizeGetter: Grid._wrapSizeGetter(nextProps.columnWidth), - }); - instanceProps.rowSizeAndPositionManager.configure({ - cellCount: nextProps.rowCount, - estimatedCellSize: Grid._getEstimatedRowSize(nextProps), - cellSizeGetter: Grid._wrapSizeGetter(nextProps.rowHeight), - }); - - if ( - instanceProps.prevColumnCount === 0 || - instanceProps.prevRowCount === 0 - ) { - instanceProps.prevColumnCount = 0; - instanceProps.prevRowCount = 0; - } - - let maybeStateA: Partial = {}; - let maybeStateB: Partial = {}; - calculateSizeAndPositionDataAndUpdateScrollOffset({ - cellCount: instanceProps.prevColumnCount, - cellSize: - typeof instanceProps.prevColumnWidth === 'number' - ? instanceProps.prevColumnWidth - : null, - computeMetadataCallback: () => - instanceProps.columnSizeAndPositionManager.resetCell(0), - computeMetadataCallbackProps: nextProps, - nextCellsCount: nextProps.columnCount, - nextCellSize: - typeof nextProps.columnWidth === 'number' - ? nextProps.columnWidth - : null, - nextScrollToIndex: nextProps.scrollToColumn, - scrollToIndex: instanceProps.prevScrollToColumn, - updateScrollOffsetForScrollToIndex: () => { - maybeStateA = Grid._getScrollLeftForScrollToColumnStateUpdate( - nextProps, - prevState - ); - }, - }); - calculateSizeAndPositionDataAndUpdateScrollOffset({ - cellCount: instanceProps.prevRowCount, - cellSize: - typeof instanceProps.prevRowHeight === 'number' - ? instanceProps.prevRowHeight - : null, - computeMetadataCallback: () => - instanceProps.rowSizeAndPositionManager.resetCell(0), - computeMetadataCallbackProps: nextProps, - nextCellsCount: nextProps.rowCount, - nextCellSize: - typeof nextProps.rowHeight === 'number' ? nextProps.rowHeight : null, - nextScrollToIndex: nextProps.scrollToRow, - scrollToIndex: instanceProps.prevScrollToRow, - updateScrollOffsetForScrollToIndex: () => { - maybeStateB = Grid._getScrollTopForScrollToRowStateUpdate( - nextProps, - prevState - ); - }, - }); - instanceProps.prevColumnCount = nextProps.columnCount; - instanceProps.prevColumnWidth = nextProps.columnWidth; - instanceProps.prevIsScrolling = nextProps.isScrolling === true; - instanceProps.prevRowCount = nextProps.rowCount; - instanceProps.prevRowHeight = nextProps.rowHeight; - instanceProps.prevScrollToColumn = nextProps.scrollToColumn; - instanceProps.prevScrollToRow = nextProps.scrollToRow; - // getting scrollBarSize (moved from componentWillMount) - instanceProps.scrollbarSize = nextProps.getScrollbarSize(); - - if (instanceProps.scrollbarSize === undefined) { - instanceProps.scrollbarSizeMeasured = false; - instanceProps.scrollbarSize = 0; - } else { - instanceProps.scrollbarSizeMeasured = true; - } - - newState.instanceProps = instanceProps; - return { ...newState, ...maybeStateA, ...maybeStateB }; - } - - render() { - const { - autoContainerWidth, - className, - containerProps, - containerRole, - containerStyle, - height, - id, - noContentRenderer, - role, - style, - tabIndex, - width, - } = this.props; - const { instanceProps, needToResetStyleCache } = this.state; - - const isScrolling = this._isScrolling(); - - const gridStyle: Record = { - boxSizing: 'border-box', - direction: 'ltr', - height: height, - position: 'relative', - width: width, - WebkitOverflowScrolling: 'touch', - willChange: 'transform', - }; - - if (needToResetStyleCache) { - this._styleCache = {}; - } - - // calculate _styleCache here - // if state.isScrolling (not from _isScrolling) then reset - if (!this.state.isScrolling) { - this._resetStyleCache(); - } - - // calculate children to render here - this._calculateChildrenToRender(this.props, this.state); - - const totalColumnsWidth = - instanceProps.columnSizeAndPositionManager.getTotalSize(); - const totalRowsHeight = - instanceProps.rowSizeAndPositionManager.getTotalSize(); - // Force browser to hide scrollbars when we know they aren't necessary. - // Otherwise once scrollbars appear they may not disappear again. - // For more info see issue #116 - const verticalScrollBarSize = - totalRowsHeight > height ? instanceProps.scrollbarSize : 0; - const horizontalScrollBarSize = - totalColumnsWidth > width ? instanceProps.scrollbarSize : 0; - - if ( - horizontalScrollBarSize !== this._horizontalScrollBarSize || - verticalScrollBarSize !== this._verticalScrollBarSize - ) { - this._horizontalScrollBarSize = horizontalScrollBarSize; - this._verticalScrollBarSize = verticalScrollBarSize; - this._scrollbarPresenceChanged = true; - } - - // Also explicitly init styles to 'auto' if scrollbars are required. - // This works around an obscure edge case where external CSS styles have not yet been loaded, - // But an initial scroll index of offset is set as an external prop. - // Without this style, Grid would render the correct range of cells but would NOT update its internal offset. - // This was originally reported via clauderic/react-infinite-calendar/issues/23 - gridStyle.overflowX = - totalColumnsWidth + verticalScrollBarSize <= width ? 'hidden' : 'auto'; - gridStyle.overflowY = - totalRowsHeight + horizontalScrollBarSize <= height ? 'hidden' : 'auto'; - const childrenToDisplay = this._childrenToDisplay; - const showNoContentRenderer = - childrenToDisplay?.length === 0 && height > 0 && width > 0; - const whiteSpace = height - totalRowsHeight; - - return ( -
0 - ? { transform: `translateY(${whiteSpace}px)` } - : {}), - }} - tabIndex={tabIndex} - > - {childrenToDisplay && childrenToDisplay.length > 0 && ( -
- {childrenToDisplay} -
- )} - {showNoContentRenderer && noContentRenderer()} -
- ); - } - - /* ---------------------------- Helper methods ---------------------------- */ - _calculateChildrenToRender( - props: Props = this.props, - state: State = this.state - ) { - const { - cellRenderer, - cellRangeRenderer, - columnCount, - deferredMeasurementCache, - height, - overscanColumnCount, - overscanIndicesGetter, - overscanRowCount, - rowCount, - width, - isScrollingOptOut, - } = props; - const { - scrollDirectionHorizontal, - scrollDirectionVertical, - instanceProps, - } = state; - const scrollTop = - this._initialScrollTop && this._initialScrollTop > 0 - ? this._initialScrollTop - : state.scrollTop; - const scrollLeft = - this._initialScrollLeft && this._initialScrollLeft > 0 - ? this._initialScrollLeft - : state.scrollLeft; - - const isScrolling = this._isScrolling(props, state); - - this._childrenToDisplay = []; - - // Render only enough columns and rows to cover the visible area of the grid. - if (height > 0 && width > 0) { - const visibleColumnIndices = - instanceProps.columnSizeAndPositionManager.getVisibleCellRange({ - containerSize: width, - offset: scrollLeft, - }); - const visibleRowIndices = - instanceProps.rowSizeAndPositionManager.getVisibleCellRange({ - containerSize: height, - offset: scrollTop, - }); - const horizontalOffsetAdjustment = - instanceProps.columnSizeAndPositionManager.getOffsetAdjustment({ - containerSize: width, - offset: scrollLeft, - }); - const verticalOffsetAdjustment = - instanceProps.rowSizeAndPositionManager.getOffsetAdjustment({ - containerSize: height, - offset: scrollTop, - }); - // Store for _invokeOnGridRenderedHelper() - this._renderedColumnStartIndex = visibleColumnIndices.start; - this._renderedColumnStopIndex = visibleColumnIndices.stop; - this._renderedRowStartIndex = visibleRowIndices.start; - this._renderedRowStopIndex = visibleRowIndices.stop; - const overscanColumnIndices = overscanIndicesGetter({ - direction: 'horizontal', - cellCount: columnCount, - overscanCellsCount: overscanColumnCount, - scrollDirection: scrollDirectionHorizontal, - startIndex: - typeof visibleColumnIndices.start === 'number' - ? visibleColumnIndices.start - : 0, - stopIndex: - typeof visibleColumnIndices.stop === 'number' - ? visibleColumnIndices.stop - : -1, - }); - const overscanRowIndices = overscanIndicesGetter({ - direction: 'vertical', - cellCount: rowCount, - overscanCellsCount: overscanRowCount, - scrollDirection: scrollDirectionVertical, - startIndex: - typeof visibleRowIndices.start === 'number' - ? visibleRowIndices.start - : 0, - stopIndex: - typeof visibleRowIndices.stop === 'number' - ? visibleRowIndices.stop - : -1, - }); - // Store for _invokeOnGridRenderedHelper() - let columnStartIndex = overscanColumnIndices.overscanStartIndex; - let columnStopIndex = overscanColumnIndices.overscanStopIndex; - let rowStartIndex = overscanRowIndices.overscanStartIndex; - let rowStopIndex = overscanRowIndices.overscanStopIndex; - - // Advanced use-cases (eg CellMeasurer) require batched measurements to determine accurate sizes. - if (deferredMeasurementCache) { - // If rows have a dynamic height, scan the rows we are about to render. - // If any have not yet been measured, then we need to render all columns initially, - // Because the height of the row is equal to the tallest cell within that row, - // (And so we can't know the height without measuring all column-cells first). - if (!deferredMeasurementCache.hasFixedHeight()) { - for ( - let rowIndex = rowStartIndex; - rowIndex <= rowStopIndex; - rowIndex++ - ) { - if (!deferredMeasurementCache.has(rowIndex, 0)) { - columnStartIndex = 0; - columnStopIndex = columnCount - 1; - break; - } - } - } - - // If columns have a dynamic width, scan the columns we are about to render. - // If any have not yet been measured, then we need to render all rows initially, - // Because the width of the column is equal to the widest cell within that column, - // (And so we can't know the width without measuring all row-cells first). - if (!deferredMeasurementCache.hasFixedWidth()) { - for ( - let columnIndex = columnStartIndex; - columnIndex <= columnStopIndex; - columnIndex++ - ) { - if (!deferredMeasurementCache.has(0, columnIndex)) { - rowStartIndex = 0; - rowStopIndex = rowCount - 1; - break; - } - } - } - } - - this._childrenToDisplay = cellRangeRenderer({ - cellCache: this._cellCache, - cellRenderer, - columnSizeAndPositionManager: - instanceProps.columnSizeAndPositionManager, - columnStartIndex, - columnStopIndex, - deferredMeasurementCache, - horizontalOffsetAdjustment, - isScrolling, - isScrollingOptOut, - parent: this, - rowSizeAndPositionManager: instanceProps.rowSizeAndPositionManager, - rowStartIndex, - rowStopIndex, - scrollLeft, - scrollTop, - styleCache: this._styleCache, - verticalOffsetAdjustment, - visibleColumnIndices, - visibleRowIndices, - }); - // update the indices - this._columnStartIndex = columnStartIndex; - this._columnStopIndex = columnStopIndex; - this._rowStartIndex = rowStartIndex; - this._rowStopIndex = rowStopIndex; - } - } - - /** - * Sets an :isScrolling flag for a small window of time. - * This flag is used to disable pointer events on the scrollable portion of the Grid. - * This prevents jerky/stuttery mouse-wheel scrolling. - */ - _debounceScrollEnded() { - const { scrollingResetTimeInterval } = this.props; - - if (this._disablePointerEventsTimeoutId) { - cancelAnimationTimeout(this._disablePointerEventsTimeoutId); - } - - this._disablePointerEventsTimeoutId = requestAnimationTimeout( - this._debounceScrollEndedCallback, - scrollingResetTimeInterval - ); - } - - _debounceScrollEndedCallback = () => { - this._disablePointerEventsTimeoutId = null; - // isScrolling is used to determine if we reset styleCache - this.setState({ - isScrolling: false, - needToResetStyleCache: false, - }); - }; - - static _getEstimatedColumnSize(props: Props) { - return typeof props.columnWidth === 'number' - ? props.columnWidth - : props.estimatedColumnSize; - } - - static _getEstimatedRowSize(props: Props) { - return typeof props.rowHeight === 'number' - ? props.rowHeight - : props.estimatedRowSize; - } - - /** - * Check for batched CellMeasurer size invalidations. - * This will occur the first time one or more previously unmeasured cells are rendered. - */ - _handleInvalidatedGridSize() { - if ( - typeof this._deferredInvalidateColumnIndex === 'number' && - typeof this._deferredInvalidateRowIndex === 'number' - ) { - const columnIndex = this._deferredInvalidateColumnIndex; - const rowIndex = this._deferredInvalidateRowIndex; - this._deferredInvalidateColumnIndex = null; - this._deferredInvalidateRowIndex = null; - this.recomputeGridSize({ - columnIndex, - rowIndex, - }); - } - } - - _invokeOnGridRenderedHelper = () => { - const { onSectionRendered } = this.props; - - this._onGridRenderedMemoizer({ - callback: onSectionRendered, - indices: { - columnOverscanStartIndex: this._columnStartIndex, - columnOverscanStopIndex: this._columnStopIndex, - columnStartIndex: this._renderedColumnStartIndex, - columnStopIndex: this._renderedColumnStopIndex, - rowOverscanStartIndex: this._rowStartIndex, - rowOverscanStopIndex: this._rowStopIndex, - rowStartIndex: this._renderedRowStartIndex, - rowStopIndex: this._renderedRowStopIndex, - }, - }); - }; - - _invokeOnScrollMemoizer({ - scrollLeft, - scrollTop, - totalColumnsWidth, - totalRowsHeight, - }: { - scrollLeft: number; - scrollTop: number; - totalColumnsWidth: number; - totalRowsHeight: number; - }) { - this._onScrollMemoizer({ - callback: ({ scrollLeft, scrollTop }) => { - const { height, onScroll, width } = this.props; - onScroll({ - clientHeight: height, - clientWidth: width, - scrollHeight: totalRowsHeight, - scrollLeft, - scrollTop, - scrollWidth: totalColumnsWidth, - }); - }, - indices: { - scrollLeft, - scrollTop, - }, - }); - } - - _isScrolling(props: Props = this.props, state: State = this.state): boolean { - // If isScrolling is defined in props, use it to override the value in state - // This is a performance optimization for WindowScroller + Grid - return Object.hasOwnProperty.call(props, 'isScrolling') - ? Boolean(props.isScrolling) - : Boolean(state.isScrolling); - } - - _maybeCallOnScrollbarPresenceChange() { - if (this._scrollbarPresenceChanged) { - const { onScrollbarPresenceChange } = this.props; - this._scrollbarPresenceChanged = false; - onScrollbarPresenceChange({ - horizontal: this._horizontalScrollBarSize > 0, - size: this.state.instanceProps.scrollbarSize, - vertical: this._verticalScrollBarSize > 0, - }); - } - } - - _setScrollingContainerRef = (ref: HTMLDivElement) => { - this._scrollingContainer = ref; - }; - - /** - * Get the updated state after scrolling to - * scrollLeft and scrollTop - */ - static _getScrollToPositionStateUpdate({ - prevState, - scrollLeft, - scrollTop, - }: { - prevState: State; - scrollLeft?: number; - scrollTop?: number; - }): Shape { - const newState: Record = { - scrollPositionChangeReason: SCROLL_POSITION_CHANGE_REASONS.REQUESTED, - }; - - if (typeof scrollLeft === 'number' && scrollLeft >= 0) { - newState.scrollDirectionHorizontal = - scrollLeft > prevState.scrollLeft - ? SCROLL_DIRECTION_FORWARD - : SCROLL_DIRECTION_BACKWARD; - newState.scrollLeft = scrollLeft; - } - - if (typeof scrollTop === 'number' && scrollTop >= 0) { - newState.scrollDirectionVertical = - scrollTop > prevState.scrollTop - ? SCROLL_DIRECTION_FORWARD - : SCROLL_DIRECTION_BACKWARD; - newState.scrollTop = scrollTop; - } - - if ( - (typeof scrollLeft === 'number' && - scrollLeft >= 0 && - scrollLeft !== prevState.scrollLeft) || - (typeof scrollTop === 'number' && - scrollTop >= 0 && - scrollTop !== prevState.scrollTop) - ) { - return newState; - } - - return {}; - } - - /** - * Scroll to the specified offset(s). - * Useful for animating position changes. - */ - scrollToPosition({ scrollLeft, scrollTop }: ScrollPosition) { - const stateUpdate = Grid._getScrollToPositionStateUpdate({ - prevState: this.state, - scrollLeft, - scrollTop, - }); - - if (stateUpdate) { - stateUpdate.needToResetStyleCache = false; - this.setState(stateUpdate as any); - } - } - - static _wrapSizeGetter(value: CellSize): CellSizeGetter { - return typeof value === 'function' ? value : () => value as any; - } - - static _getCalculatedScrollLeft(nextProps: Props, prevState: State) { - const { columnCount, height, scrollToAlignment, scrollToColumn, width } = - nextProps; - const { scrollLeft, instanceProps } = prevState; - - if (columnCount > 0) { - const finalColumn = columnCount - 1; - const targetIndex = - scrollToColumn < 0 - ? finalColumn - : Math.min(finalColumn, scrollToColumn); - const totalRowsHeight = - instanceProps.rowSizeAndPositionManager.getTotalSize(); - const scrollBarSize = - instanceProps.scrollbarSizeMeasured && totalRowsHeight > height - ? instanceProps.scrollbarSize - : 0; - return instanceProps.columnSizeAndPositionManager.getUpdatedOffsetForIndex( - { - align: scrollToAlignment, - containerSize: width - scrollBarSize, - currentOffset: scrollLeft, - targetIndex, - } - ); - } - - return 0; - } - - _getCalculatedScrollLeft( - props: Props = this.props, - state: State = this.state - ) { - return Grid._getCalculatedScrollLeft(props, state); - } - - static _getScrollLeftForScrollToColumnStateUpdate( - nextProps: Props, - prevState: State - ): Shape { - const { scrollLeft } = prevState; - - const calculatedScrollLeft = Grid._getCalculatedScrollLeft( - nextProps, - prevState - ); - - if ( - typeof calculatedScrollLeft === 'number' && - calculatedScrollLeft >= 0 && - scrollLeft !== calculatedScrollLeft - ) { - return Grid._getScrollToPositionStateUpdate({ - prevState, - scrollLeft: calculatedScrollLeft, - scrollTop: -1, - }); - } - - return {}; - } - - _updateScrollLeftForScrollToColumn( - props: Props = this.props, - state: State = this.state - ) { - const stateUpdate = Grid._getScrollLeftForScrollToColumnStateUpdate( - props, - state - ); - - if (stateUpdate) { - stateUpdate.needToResetStyleCache = false; - this.setState(stateUpdate as any); - } - } - - static _getCalculatedScrollTop(nextProps: Props, prevState: State) { - const { height, rowCount, scrollToAlignment, scrollToRow, width } = - nextProps; - const { scrollTop, instanceProps } = prevState; - - if (rowCount > 0) { - const finalRow = rowCount - 1; - const targetIndex = - scrollToRow < 0 ? finalRow : Math.min(finalRow, scrollToRow); - const totalColumnsWidth = - instanceProps.columnSizeAndPositionManager.getTotalSize(); - const scrollBarSize = - instanceProps.scrollbarSizeMeasured && totalColumnsWidth > width - ? instanceProps.scrollbarSize - : 0; - return instanceProps.rowSizeAndPositionManager.getUpdatedOffsetForIndex({ - align: scrollToAlignment, - containerSize: height - scrollBarSize, - currentOffset: scrollTop, - targetIndex, - }); - } - - return 0; - } - - _getCalculatedScrollTop( - props: Props = this.props, - state: State = this.state - ) { - return Grid._getCalculatedScrollTop(props, state); - } - - _resetStyleCache() { - const styleCache = this._styleCache; - const cellCache = this._cellCache; - const { isScrollingOptOut } = this.props; - // Reset cell and style caches once scrolling stops. - // This makes Grid simpler to use (since cells commonly change). - // And it keeps the caches from growing too large. - // Performance is most sensitive when a user is scrolling. - // Don't clear visible cells from cellCache if isScrollingOptOut is specified. - // This keeps the cellCache to a resonable size. - this._cellCache = {}; - this._styleCache = {}; - - if ( - !this._rowStartIndex || - !this._rowStopIndex || - !this._columnStartIndex || - !this._columnStopIndex - ) - return; - // Copy over the visible cell styles so avoid unnecessary re-render. - for ( - let rowIndex = this._rowStartIndex; - rowIndex <= this._rowStopIndex; - rowIndex++ - ) { - for ( - let columnIndex: number = this._columnStartIndex; - columnIndex <= this._columnStopIndex; - columnIndex++ - ) { - const key = `${rowIndex}-${columnIndex}`; - this._styleCache[key] = styleCache[key]; - - if (isScrollingOptOut) { - this._cellCache[key] = cellCache[key]; - } - } - } - } - - static _getScrollTopForScrollToRowStateUpdate( - nextProps: Props, - prevState: State - ): Shape { - const { scrollTop } = prevState; - - const calculatedScrollTop = Grid._getCalculatedScrollTop( - nextProps, - prevState - ); - - if ( - typeof calculatedScrollTop === 'number' && - calculatedScrollTop >= 0 && - scrollTop !== calculatedScrollTop - ) { - return Grid._getScrollToPositionStateUpdate({ - prevState, - scrollLeft: -1, - scrollTop: calculatedScrollTop, - }); - } - - return {}; - } - - _updateScrollTopForScrollToRow( - props: Props = this.props, - state: State = this.state - ) { - const stateUpdate = Grid._getScrollTopForScrollToRowStateUpdate( - props, - state - ); - - if (stateUpdate) { - stateUpdate.needToResetStyleCache = false; - this.setState(stateUpdate as any); - } - } - - _onScroll: UIEventHandler = (event) => { - // In certain edge-cases React dispatches an onScroll event with an invalid target.scrollLeft / target.scrollTop. - // This invalid event can be detected by comparing event.target to this component's scrollable DOM element. - // See issue #404 for more information. - if (event.target === this._scrollingContainer) { - this.handleScrollEvent(event.target as any); - } - }; -} - -polyfill(Grid as any); diff --git a/lib/design-system/src/general/WindowedList/source/Grid/accessibilityOverscanIndicesGetter.ts b/lib/design-system/src/general/WindowedList/source/Grid/accessibilityOverscanIndicesGetter.ts deleted file mode 100644 index ec882bb384..0000000000 --- a/lib/design-system/src/general/WindowedList/source/Grid/accessibilityOverscanIndicesGetter.ts +++ /dev/null @@ -1,37 +0,0 @@ -import type { OverscanIndicesGetterParams, OverscanIndices } from './types'; - -export const SCROLL_DIRECTION_BACKWARD = -1; -export const SCROLL_DIRECTION_FORWARD = 1; -export const SCROLL_DIRECTION_HORIZONTAL = 'horizontal'; -export const SCROLL_DIRECTION_VERTICAL = 'vertical'; - -/** - * Calculates the number of cells to overscan before and after a specified range. - * This function ensures that overscanning doesn't exceed the available cells. - */ -export function accessibilityOverscanIndicesGetter({ - cellCount, - overscanCellsCount, - scrollDirection, - startIndex, - stopIndex, -}: OverscanIndicesGetterParams): OverscanIndices { - // Make sure we render at least 1 cell extra before and after (except near boundaries) - // This is necessary in order to support keyboard navigation (TAB/SHIFT+TAB) in some cases - overscanCellsCount = Math.max(1, overscanCellsCount); - - if (scrollDirection === SCROLL_DIRECTION_FORWARD) { - return { - overscanStartIndex: Math.max(0, startIndex - 1), - overscanStopIndex: Math.min( - cellCount - 1, - stopIndex + overscanCellsCount - ), - }; - } else { - return { - overscanStartIndex: Math.max(0, startIndex - overscanCellsCount), - overscanStopIndex: Math.min(cellCount - 1, stopIndex + 1), - }; - } -} diff --git a/lib/design-system/src/general/WindowedList/source/Grid/defaultCellRangeRenderer.ts b/lib/design-system/src/general/WindowedList/source/Grid/defaultCellRangeRenderer.ts deleted file mode 100644 index e3dc4299cb..0000000000 --- a/lib/design-system/src/general/WindowedList/source/Grid/defaultCellRangeRenderer.ts +++ /dev/null @@ -1,169 +0,0 @@ -import React from 'react'; -import type { CellRangeRendererParams } from './types'; -/** - * Default implementation of cellRangeRenderer used by Grid. - * This renderer supports cell-caching while the user is scrolling. - */ - -export function defaultCellRangeRenderer({ - cellCache, - cellRenderer, - columnSizeAndPositionManager, - columnStartIndex, - columnStopIndex, - deferredMeasurementCache, - horizontalOffsetAdjustment, - isScrolling, - isScrollingOptOut, - parent, - // Grid (or List or Table) - rowSizeAndPositionManager, - rowStartIndex, - rowStopIndex, - styleCache, - verticalOffsetAdjustment, - visibleColumnIndices, - visibleRowIndices, -}: CellRangeRendererParams) { - const renderedCells = []; - // Browsers have native size limits for elements (eg Chrome 33M pixels, IE 1.5M pixes). - // User cannot scroll beyond these size limitations. - // In order to work around this, ScalingCellSizeAndPositionManager compresses offsets. - // We should never cache styles for compressed offsets though as this can lead to bugs. - // See issue #576 for more. - const areOffsetsAdjusted = - columnSizeAndPositionManager.areOffsetsAdjusted() || - rowSizeAndPositionManager.areOffsetsAdjusted(); - const canCacheStyle = !isScrolling && !areOffsetsAdjusted; - - for (let rowIndex = rowStartIndex; rowIndex <= rowStopIndex; rowIndex++) { - const rowDatum = - rowSizeAndPositionManager.getSizeAndPositionOfCell(rowIndex); - - for ( - let columnIndex = columnStartIndex; - columnIndex <= columnStopIndex; - columnIndex++ - ) { - const columnDatum = - columnSizeAndPositionManager.getSizeAndPositionOfCell(columnIndex); - const isVisible = - columnIndex >= visibleColumnIndices.start && - columnIndex <= visibleColumnIndices.stop && - rowIndex >= visibleRowIndices.start && - rowIndex <= visibleRowIndices.stop; - const key = `${rowIndex}-${columnIndex}`; - let style; - - // Cache style objects so shallow-compare doesn't re-render unnecessarily. - if (canCacheStyle && styleCache[key]) { - style = styleCache[key]; - } else { - // In deferred mode, cells will be initially rendered before we know their size. - // Don't interfere with CellMeasurer's measurements by setting an invalid size. - if ( - deferredMeasurementCache && - !deferredMeasurementCache.has(rowIndex, columnIndex) - ) { - // Position not-yet-measured cells at top/left 0,0, - // And give them width/height of 'auto' so they can grow larger than the parent Grid if necessary. - // Positioning them further to the right/bottom influences their measured size. - style = { - height: 'auto', - left: 0, - position: 'absolute', - top: 0, - width: 'auto', - }; - } else { - style = { - height: rowDatum.size, - left: columnDatum.offset + horizontalOffsetAdjustment, - position: 'absolute', - top: rowDatum.offset + verticalOffsetAdjustment, - width: columnDatum.size, - }; - styleCache[key] = style; - } - } - - const cellRendererParams = { - columnIndex, - isScrolling, - isVisible, - key, - parent, - rowIndex, - style, - }; - let renderedCell; - - // Avoid re-creating cells while scrolling. - // This can lead to the same cell being created many times and can cause performance issues for "heavy" cells. - // If a scroll is in progress- cache and reuse cells. - // This cache will be thrown away once scrolling completes. - // However if we are scaling scroll positions and sizes, we should also avoid caching. - // This is because the offset changes slightly as scroll position changes and caching leads to stale values. - // For more info refer to issue #395 - // - // If isScrollingOptOut is specified, we always cache cells. - // For more info refer to issue #1028 - if ( - (isScrollingOptOut || isScrolling) && - !horizontalOffsetAdjustment && - !verticalOffsetAdjustment - ) { - if (!cellCache[key]) { - cellCache[key] = cellRenderer(cellRendererParams); - } - - renderedCell = cellCache[key]; // If the user is no longer scrolling, don't cache cells. - // This makes dynamic cell content difficult for users and would also lead to a heavier memory footprint. - } else { - renderedCell = cellRenderer(cellRendererParams); - } - - if (renderedCell === null) { - continue; - } - - if (process.env.NODE_ENV !== 'production') { - warnAboutMissingStyle(parent, renderedCell); - } - - if (!renderedCell.props.role) { - renderedCell = React.cloneElement(renderedCell, { - role: 'gridcell', - }); - } - - renderedCells.push(renderedCell); - } - } - - return renderedCells; -} - -function warnAboutMissingStyle(parent: any, renderedCell: any) { - if (process.env.NODE_ENV !== 'production') { - if (renderedCell) { - // If the direct child is a CellMeasurer, then we should check its child - // See issue #611 - if (renderedCell.type && renderedCell.type.__internalCellMeasurerFlag) { - renderedCell = renderedCell.props.children; - } - - if ( - renderedCell && - renderedCell.props && - renderedCell.props.style === undefined && - parent.__warnedAboutMissingStyle !== true - ) { - parent.__warnedAboutMissingStyle = true; - console.warn( - 'Rendered cell should include style property for positioning.' - ); - } - } - } -} diff --git a/lib/design-system/src/general/WindowedList/source/Grid/defaultOverscanIndicesGetter.ts b/lib/design-system/src/general/WindowedList/source/Grid/defaultOverscanIndicesGetter.ts deleted file mode 100644 index 5bae66c71a..0000000000 --- a/lib/design-system/src/general/WindowedList/source/Grid/defaultOverscanIndicesGetter.ts +++ /dev/null @@ -1,32 +0,0 @@ -import type { OverscanIndicesGetterParams, OverscanIndices } from './types'; -export const SCROLL_DIRECTION_BACKWARD = -1; -export const SCROLL_DIRECTION_FORWARD = 1; -export const SCROLL_DIRECTION_HORIZONTAL = 'horizontal'; -export const SCROLL_DIRECTION_VERTICAL = 'vertical'; -/** - * Calculates the number of cells to overscan before and after a specified range. - * This function ensures that overscanning doesn't exceed the available cells. - */ - -export function defaultOverscanIndicesGetter({ - cellCount, - overscanCellsCount, - scrollDirection, - startIndex, - stopIndex, -}: OverscanIndicesGetterParams): OverscanIndices { - if (scrollDirection === SCROLL_DIRECTION_FORWARD) { - return { - overscanStartIndex: Math.max(0, startIndex), - overscanStopIndex: Math.min( - cellCount - 1, - stopIndex + overscanCellsCount - ), - }; - } else { - return { - overscanStartIndex: Math.max(0, startIndex - overscanCellsCount), - overscanStopIndex: Math.min(cellCount - 1, stopIndex), - }; - } -} diff --git a/lib/design-system/src/general/WindowedList/source/Grid/types.ts b/lib/design-system/src/general/WindowedList/source/Grid/types.ts deleted file mode 100644 index 1689d836be..0000000000 --- a/lib/design-system/src/general/WindowedList/source/Grid/types.ts +++ /dev/null @@ -1,96 +0,0 @@ -import React from 'react'; -import { ScalingCellSizeAndPositionManager } from './utils/ScalingCellSizeAndPositionManager'; -export type CellPosition = { - columnIndex: number; - rowIndex: number; -}; -export type CellRendererParams = { - columnIndex: number; - isScrolling: boolean; - isVisible: boolean; - key: string; - parent: Record; - rowIndex: number; - style: Record; -}; -export type CellRenderer = ( - props: CellRendererParams -) => React.ReactElement; -export type CellCache = Record>; -export type StyleCache = Record>; -export type CellRangeRendererParams = { - cellCache: CellCache; - cellRenderer: CellRenderer; - columnSizeAndPositionManager: ScalingCellSizeAndPositionManager; - columnStartIndex: number; - columnStopIndex: number; - deferredMeasurementCache?: Record; - horizontalOffsetAdjustment: number; - isScrolling: boolean; - isScrollingOptOut: boolean; - parent: Record; - rowSizeAndPositionManager: ScalingCellSizeAndPositionManager; - rowStartIndex: number; - rowStopIndex: number; - scrollLeft: number; - scrollTop: number; - styleCache: StyleCache; - verticalOffsetAdjustment: number; - visibleColumnIndices: Record; - visibleRowIndices: Record; -}; -export type CellRangeRenderer = ( - params: CellRangeRendererParams -) => React.ReactElement[]; -export type CellSizeGetter = (params: { index: number }) => number; -export type CellSize = CellSizeGetter | number; -export type NoContentRenderer = () => React.ReactElement | null; -export type Scroll = { - clientHeight: number; - clientWidth: number; - scrollHeight: number; - scrollLeft: number; - scrollTop: number; - scrollWidth: number; -}; -export type ScrollbarPresenceChange = { - horizontal: boolean; - vertical: boolean; - size: number; -}; -export type RenderedSection = { - columnOverscanStartIndex: number; - columnOverscanStopIndex: number; - columnStartIndex: number; - columnStopIndex: number; - rowOverscanStartIndex: number; - rowOverscanStopIndex: number; - rowStartIndex: number; - rowStopIndex: number; -}; -export type OverscanIndicesGetterParams = { - // One of SCROLL_DIRECTION_HORIZONTAL or SCROLL_DIRECTION_VERTICAL - direction: 'horizontal' | 'vertical'; - // One of SCROLL_DIRECTION_BACKWARD or SCROLL_DIRECTION_FORWARD - scrollDirection: -1 | 1; - // Number of rows or columns in the current axis - cellCount: number; - // Maximum number of cells to over-render in either direction - overscanCellsCount: number; - // Begin of range of visible cells - startIndex: number; - // End of range of visible cells - stopIndex: number; -}; -export type OverscanIndices = { - overscanStartIndex: number; - overscanStopIndex: number; -}; -export type OverscanIndicesGetter = ( - params: OverscanIndicesGetterParams -) => OverscanIndices; -export type Alignment = 'auto' | 'end' | 'start' | 'center'; -export type VisibleCellRange = { - start?: number; - stop?: number; -}; diff --git a/lib/design-system/src/general/WindowedList/source/Grid/utils/CellSizeAndPositionManager.ts b/lib/design-system/src/general/WindowedList/source/Grid/utils/CellSizeAndPositionManager.ts deleted file mode 100644 index 96a5ef2de6..0000000000 --- a/lib/design-system/src/general/WindowedList/source/Grid/utils/CellSizeAndPositionManager.ts +++ /dev/null @@ -1,306 +0,0 @@ -import type { Alignment, CellSizeGetter, VisibleCellRange } from '../types'; -type CellSizeAndPositionManagerParams = { - cellCount: number; - cellSizeGetter: CellSizeGetter; - estimatedCellSize: number; -}; -type ConfigureParams = { - cellCount: number; - estimatedCellSize: number; - cellSizeGetter: CellSizeGetter; -}; -type GetUpdatedOffsetForIndex = { - align: Alignment; - containerSize: number; - currentOffset: number; - targetIndex: number; -}; -type GetVisibleCellRangeParams = { - containerSize: number; - offset: number; -}; -type SizeAndPositionData = { - offset: number; - size: number; -}; -/** - * Just-in-time calculates and caches size and position information for a collection of cells. - */ - -export class CellSizeAndPositionManager { - // Cache of size and position data for cells, mapped by cell index. - // Note that invalid values may exist in this map so only rely on cells up to this._lastMeasuredIndex - _cellSizeAndPositionData = {}; - // Measurements for cells up to this index can be trusted; cells afterward should be estimated. - _lastMeasuredIndex = -1; - // Used in deferred mode to track which cells have been queued for measurement. - _lastBatchedIndex = -1; - _cellCount: number; - _cellSizeGetter: CellSizeGetter; - _estimatedCellSize: number; - - constructor({ - cellCount, - cellSizeGetter, - estimatedCellSize, - }: CellSizeAndPositionManagerParams) { - this._cellSizeGetter = cellSizeGetter; - this._cellCount = cellCount; - this._estimatedCellSize = estimatedCellSize; - } - - areOffsetsAdjusted() { - return false; - } - - configure({ cellCount, estimatedCellSize, cellSizeGetter }: ConfigureParams) { - this._cellCount = cellCount; - this._estimatedCellSize = estimatedCellSize; - this._cellSizeGetter = cellSizeGetter; - } - - getCellCount(): number { - return this._cellCount; - } - - getEstimatedCellSize(): number { - return this._estimatedCellSize; - } - - getLastMeasuredIndex(): number { - return this._lastMeasuredIndex; - } - - getOffsetAdjustment() { - return 0; - } - - /** - * This method returns the size and position for the cell at the specified index. - * It just-in-time calculates (or used cached values) for cells leading up to the index. - */ - getSizeAndPositionOfCell(index: number): SizeAndPositionData { - if (index < 0 || index >= this._cellCount) { - throw Error( - `Requested index ${index} is outside of range 0..${this._cellCount}` - ); - } - - if (index > this._lastMeasuredIndex) { - const lastMeasuredCellSizeAndPosition = - this.getSizeAndPositionOfLastMeasuredCell(); - let offset = - lastMeasuredCellSizeAndPosition.offset + - lastMeasuredCellSizeAndPosition.size; - - for (var i = this._lastMeasuredIndex + 1; i <= index; i++) { - const size = this._cellSizeGetter({ - index: i, - }); - - // undefined or NaN probably means a logic error in the size getter. - // null means we're using CellMeasurer and haven't yet measured a given index. - if (size === undefined || isNaN(size)) { - throw Error(`Invalid size returned for cell ${i} of value ${size}`); - } else if (size === null) { - // @ts-ignore - this._cellSizeAndPositionData[i] = { - offset, - size: 0, - }; - this._lastBatchedIndex = index; - } else { - // @ts-ignore - this._cellSizeAndPositionData[i] = { - offset, - size, - }; - offset += size; - this._lastMeasuredIndex = index; - } - } - } - - // @ts-ignore - return this._cellSizeAndPositionData[index]; - } - - getSizeAndPositionOfLastMeasuredCell(): SizeAndPositionData { - return this._lastMeasuredIndex >= 0 - ? // @ts-ignore - this._cellSizeAndPositionData[this._lastMeasuredIndex] - : { - offset: 0, - size: 0, - }; - } - - /** - * Total size of all cells being measured. - * This value will be completely estimated initially. - * As cells are measured, the estimate will be updated. - */ - getTotalSize(): number { - const lastMeasuredCellSizeAndPosition = - this.getSizeAndPositionOfLastMeasuredCell(); - const totalSizeOfMeasuredCells = - lastMeasuredCellSizeAndPosition.offset + - lastMeasuredCellSizeAndPosition.size; - const numUnmeasuredCells = this._cellCount - this._lastMeasuredIndex - 1; - const totalSizeOfUnmeasuredCells = - numUnmeasuredCells * this._estimatedCellSize; - return totalSizeOfMeasuredCells + totalSizeOfUnmeasuredCells; - } - - /** - * Determines a new offset that ensures a certain cell is visible, given the current offset. - * If the cell is already visible then the current offset will be returned. - * If the current offset is too great or small, it will be adjusted just enough to ensure the specified index is visible. - * - * @param align Desired alignment within container; one of "auto" (default), "start", or "end" - * @param containerSize Size (width or height) of the container viewport - * @param currentOffset Container's current (x or y) offset - * @param totalSize Total size (width or height) of all cells - * @return Offset to use to ensure the specified cell is visible - */ - getUpdatedOffsetForIndex({ - align = 'auto', - containerSize, - currentOffset, - targetIndex, - }: GetUpdatedOffsetForIndex): number { - if (containerSize <= 0) { - return 0; - } - - const datum = this.getSizeAndPositionOfCell(targetIndex); - const maxOffset = datum.offset; - const minOffset = maxOffset - containerSize + datum.size; - let idealOffset; - - switch (align) { - case 'start': - idealOffset = maxOffset; - break; - - case 'end': - idealOffset = minOffset; - break; - - case 'center': - idealOffset = maxOffset - (containerSize - datum.size) / 2; - break; - - default: - idealOffset = Math.max(minOffset, Math.min(maxOffset, currentOffset)); - break; - } - - const totalSize = this.getTotalSize(); - return Math.max(0, Math.min(totalSize - containerSize, idealOffset)); - } - - getVisibleCellRange(params: GetVisibleCellRangeParams): VisibleCellRange { - let { containerSize, offset } = params; - const totalSize = this.getTotalSize(); - - if (totalSize === 0) { - return {}; - } - - const maxOffset = offset + containerSize; - - const start = this._findNearestCell(offset); - - const datum = this.getSizeAndPositionOfCell(start); - offset = datum.offset + datum.size; - let stop = start; - - while (offset < maxOffset && stop < this._cellCount - 1) { - stop++; - offset += this.getSizeAndPositionOfCell(stop).size; - } - - return { - start, - stop, - }; - } - - /** - * Clear all cached values for cells after the specified index. - * This method should be called for any cell that has changed its size. - * It will not immediately perform any calculations; they'll be performed the next time getSizeAndPositionOfCell() is called. - */ - resetCell(index: number): void { - this._lastMeasuredIndex = Math.min(this._lastMeasuredIndex, index - 1); - } - - _binarySearch(high: number, low: number, offset: number): number { - while (low <= high) { - const middle = low + Math.floor((high - low) / 2); - const currentOffset = this.getSizeAndPositionOfCell(middle).offset; - - if (currentOffset === offset) { - return middle; - } else if (currentOffset < offset) { - low = middle + 1; - } else if (currentOffset > offset) { - high = middle - 1; - } - } - - if (low > 0) { - return low - 1; - } else { - return 0; - } - } - - _exponentialSearch(index: number, offset: number): number { - let interval = 1; - - while ( - index < this._cellCount && - this.getSizeAndPositionOfCell(index).offset < offset - ) { - index += interval; - interval *= 2; - } - - return this._binarySearch( - Math.min(index, this._cellCount - 1), - Math.floor(index / 2), - offset - ); - } - - /** - * Searches for the cell (index) nearest the specified offset. - * - * If no exact match is found the next lowest cell index will be returned. - * This allows partially visible cells (with offsets just before/above the fold) to be visible. - */ - _findNearestCell(offset: number): number { - if (isNaN(offset)) { - throw Error(`Invalid offset ${offset} specified`); - } - - // Our search algorithms find the nearest match at or below the specified offset. - // So make sure the offset is at least 0 or no match will be found. - offset = Math.max(0, offset); - const lastMeasuredCellSizeAndPosition = - this.getSizeAndPositionOfLastMeasuredCell(); - const lastMeasuredIndex = Math.max(0, this._lastMeasuredIndex); - - if (lastMeasuredCellSizeAndPosition.offset >= offset) { - // If we've already measured cells within this range just use a binary search as it's faster. - return this._binarySearch(lastMeasuredIndex, 0, offset); - } else { - // If we haven't yet measured this high, fallback to an exponential search with an inner binary search. - // The exponential search avoids pre-computing sizes for the full set of cells as a binary search would. - // The overall complexity for this approach is O(log n). - return this._exponentialSearch(lastMeasuredIndex, offset); - } - } -} diff --git a/lib/design-system/src/general/WindowedList/source/Grid/utils/ScalingCellSizeAndPositionManager.ts b/lib/design-system/src/general/WindowedList/source/Grid/utils/ScalingCellSizeAndPositionManager.ts deleted file mode 100644 index 956bc9d8e8..0000000000 --- a/lib/design-system/src/general/WindowedList/source/Grid/utils/ScalingCellSizeAndPositionManager.ts +++ /dev/null @@ -1,204 +0,0 @@ -import type { Alignment, CellSizeGetter, VisibleCellRange } from '../types'; -import { CellSizeAndPositionManager } from './CellSizeAndPositionManager'; -import { getMaxElementSize } from './maxElementSize'; - -type ContainerSizeAndOffset = { - containerSize: number; - offset: number; -}; - -/** - * Browsers have scroll offset limitations (eg Chrome stops scrolling at ~33.5M pixels where as Edge tops out at ~1.5M pixels). - * After a certain position, the browser won't allow the user to scroll further (even via JavaScript scroll offset adjustments). - * This util picks a lower ceiling for max size and artificially adjusts positions within to make it transparent for users. - */ -type Params = { - maxScrollSize?: number; - cellCount: number; - cellSizeGetter: CellSizeGetter; - estimatedCellSize: number; -}; - -/** - * Extends CellSizeAndPositionManager and adds scaling behavior for lists that are too large to fit within a browser's native limits. - */ -export class ScalingCellSizeAndPositionManager { - _cellSizeAndPositionManager: CellSizeAndPositionManager; - _maxScrollSize: number; - - constructor({ maxScrollSize = getMaxElementSize(), ...params }: Params) { - // Favor composition over inheritance to simplify IE10 support - this._cellSizeAndPositionManager = new CellSizeAndPositionManager(params); - this._maxScrollSize = maxScrollSize; - } - - areOffsetsAdjusted(): boolean { - return ( - this._cellSizeAndPositionManager.getTotalSize() > this._maxScrollSize - ); - } - - configure(params: { - cellCount: number; - estimatedCellSize: number; - cellSizeGetter: CellSizeGetter; - }) { - this._cellSizeAndPositionManager.configure(params); - } - - getCellCount(): number { - return this._cellSizeAndPositionManager.getCellCount(); - } - - getEstimatedCellSize(): number { - return this._cellSizeAndPositionManager.getEstimatedCellSize(); - } - - getLastMeasuredIndex(): number { - return this._cellSizeAndPositionManager.getLastMeasuredIndex(); - } - - /** - * Number of pixels a cell at the given position (offset) should be shifted in order to fit within the scaled container. - * The offset passed to this function is scaled (safe) as well. - */ - getOffsetAdjustment({ - containerSize, - offset, // safe - }: ContainerSizeAndOffset): number { - const totalSize = this._cellSizeAndPositionManager.getTotalSize(); - - const safeTotalSize = this.getTotalSize(); - - const offsetPercentage = this._getOffsetPercentage({ - containerSize, - offset, - totalSize: safeTotalSize, - }); - - return Math.round(offsetPercentage * (safeTotalSize - totalSize)); - } - - getSizeAndPositionOfCell(index: number) { - return this._cellSizeAndPositionManager.getSizeAndPositionOfCell(index); - } - - getSizeAndPositionOfLastMeasuredCell() { - return this._cellSizeAndPositionManager.getSizeAndPositionOfLastMeasuredCell(); - } - - /** See CellSizeAndPositionManager#getTotalSize */ - getTotalSize(): number { - return Math.min( - this._maxScrollSize, - this._cellSizeAndPositionManager.getTotalSize() - ); - } - - /** See CellSizeAndPositionManager#getUpdatedOffsetForIndex */ - getUpdatedOffsetForIndex({ - align = 'auto', - containerSize, - currentOffset, - // safe - targetIndex, - }: { - align: Alignment; - containerSize: number; - currentOffset: number; - targetIndex: number; - }) { - currentOffset = this._safeOffsetToOffset({ - containerSize, - offset: currentOffset, - }); - - const offset = this._cellSizeAndPositionManager.getUpdatedOffsetForIndex({ - align, - containerSize, - currentOffset, - targetIndex, - }); - - return this._offsetToSafeOffset({ - containerSize, - offset, - }); - } - - /** See CellSizeAndPositionManager#getVisibleCellRange */ - getVisibleCellRange({ - containerSize, - offset, // safe - }: ContainerSizeAndOffset): VisibleCellRange { - offset = this._safeOffsetToOffset({ - containerSize, - offset, - }); - return this._cellSizeAndPositionManager.getVisibleCellRange({ - containerSize, - offset, - }); - } - - resetCell(index: number): void { - this._cellSizeAndPositionManager.resetCell(index); - } - - _getOffsetPercentage({ - containerSize, - offset, - // safe - totalSize, - }: { - containerSize: number; - offset: number; - totalSize: number; - }) { - return totalSize <= containerSize - ? 0 - : offset / (totalSize - containerSize); - } - - _offsetToSafeOffset({ - containerSize, - offset, // unsafe - }: ContainerSizeAndOffset): number { - const totalSize = this._cellSizeAndPositionManager.getTotalSize(); - - const safeTotalSize = this.getTotalSize(); - - if (totalSize === safeTotalSize) { - return offset; - } else { - const offsetPercentage = this._getOffsetPercentage({ - containerSize, - offset, - totalSize, - }); - - return Math.round(offsetPercentage * (safeTotalSize - containerSize)); - } - } - - _safeOffsetToOffset({ - containerSize, - offset, // safe - }: ContainerSizeAndOffset): number { - const totalSize = this._cellSizeAndPositionManager.getTotalSize(); - - const safeTotalSize = this.getTotalSize(); - - if (totalSize === safeTotalSize) { - return offset; - } else { - const offsetPercentage = this._getOffsetPercentage({ - containerSize, - offset, - totalSize: safeTotalSize, - }); - - return Math.round(offsetPercentage * (totalSize - containerSize)); - } - } -} diff --git a/lib/design-system/src/general/WindowedList/source/Grid/utils/calculateSizeAndPositionDataAndUpdateScrollOffset.ts b/lib/design-system/src/general/WindowedList/source/Grid/utils/calculateSizeAndPositionDataAndUpdateScrollOffset.ts deleted file mode 100644 index 164a2547fe..0000000000 --- a/lib/design-system/src/general/WindowedList/source/Grid/utils/calculateSizeAndPositionDataAndUpdateScrollOffset.ts +++ /dev/null @@ -1,51 +0,0 @@ -/** - * Helper method that determines when to recalculate row or column metadata. - */ -type Params = { - // Number of rows or columns in the current axis - cellCount: number; - // Width or height of cells for the current axis - cellSize: number | null | undefined; - // Method to invoke if cell metadata should be recalculated - computeMetadataCallback: (props: T) => void; - // Parameters to pass to :computeMetadataCallback - computeMetadataCallbackProps: T; - // Newly updated number of rows or columns in the current axis - nextCellsCount: number; - // Newly updated width or height of cells for the current axis - nextCellSize: number | null | undefined; - // Newly updated scroll-to-index - nextScrollToIndex: number; - // Scroll-to-index - scrollToIndex: number; - // Callback to invoke if the scroll position should be recalculated - updateScrollOffsetForScrollToIndex: () => void; -}; - -export function calculateSizeAndPositionDataAndUpdateScrollOffset({ - cellCount, - cellSize, - computeMetadataCallback, - computeMetadataCallbackProps, - nextCellsCount, - nextCellSize, - nextScrollToIndex, - scrollToIndex, - updateScrollOffsetForScrollToIndex, -}: Params) { - // Don't compare cell sizes if they are functions because inline functions would cause infinite loops. - // In that event users should use the manual recompute methods to inform of changes. - if ( - cellCount !== nextCellsCount || - ((typeof cellSize === 'number' || typeof nextCellSize === 'number') && - cellSize !== nextCellSize) - ) { - computeMetadataCallback(computeMetadataCallbackProps); - - // Updated cell metadata may have hidden the previous scrolled-to item. - // In this case we should also update the scrollTop to ensure it stays visible. - if (scrollToIndex >= 0 && scrollToIndex === nextScrollToIndex) { - updateScrollOffsetForScrollToIndex(); - } - } -} diff --git a/lib/design-system/src/general/WindowedList/source/Grid/utils/maxElementSize.ts b/lib/design-system/src/general/WindowedList/source/Grid/utils/maxElementSize.ts deleted file mode 100644 index c8fe4812fc..0000000000 --- a/lib/design-system/src/general/WindowedList/source/Grid/utils/maxElementSize.ts +++ /dev/null @@ -1,16 +0,0 @@ -const DEFAULT_MAX_ELEMENT_SIZE = 1500000; -const CHROME_MAX_ELEMENT_SIZE = 1.67771e7; - -const isBrowser = () => typeof window !== 'undefined'; - -const isChrome = () => !!window.chrome; - -export const getMaxElementSize = (): number => { - if (isBrowser()) { - if (isChrome()) { - return CHROME_MAX_ELEMENT_SIZE; - } - } - - return DEFAULT_MAX_ELEMENT_SIZE; -}; diff --git a/lib/design-system/src/general/WindowedList/source/Grid/utils/updateScrollIndexHelper.ts b/lib/design-system/src/general/WindowedList/source/Grid/utils/updateScrollIndexHelper.ts deleted file mode 100644 index f6aa570980..0000000000 --- a/lib/design-system/src/general/WindowedList/source/Grid/utils/updateScrollIndexHelper.ts +++ /dev/null @@ -1,79 +0,0 @@ -import type { Alignment, CellSize } from '../types'; -import { ScalingCellSizeAndPositionManager } from './ScalingCellSizeAndPositionManager'; - -/** - * Helper function that determines when to update scroll offsets to ensure that a scroll-to-index remains visible. - * This function also ensures that the scroll ofset isn't past the last column/row of cells. - */ -type Params = { - // Width or height of cells for the current axis - cellSize?: CellSize; - // Manages size and position metadata of cells - cellSizeAndPositionManager: ScalingCellSizeAndPositionManager; - // Previous number of rows or columns - previousCellsCount: number; - // Previous width or height of cells - previousCellSize: CellSize; - previousScrollToAlignment: Alignment; - // Previous scroll-to-index - previousScrollToIndex: number; - // Previous width or height of the virtualized container - previousSize: number; - // Current scrollLeft or scrollTop - scrollOffset: number; - scrollToAlignment: Alignment; - // Scroll-to-index - scrollToIndex: number; - // Width or height of the virtualized container - size: number; - sizeJustIncreasedFromZero: boolean; - // Callback to invoke with an scroll-to-index value - updateScrollIndexCallback: (index: number) => void; -}; -export function updateScrollIndexHelper({ - cellSize, - cellSizeAndPositionManager, - previousCellsCount, - previousCellSize, - previousScrollToAlignment, - previousScrollToIndex, - previousSize, - scrollOffset, - scrollToAlignment, - scrollToIndex, - size, - sizeJustIncreasedFromZero, - updateScrollIndexCallback, -}: Params) { - const cellCount = cellSizeAndPositionManager.getCellCount(); - const hasScrollToIndex = scrollToIndex >= 0 && scrollToIndex < cellCount; - const sizeHasChanged = - size !== previousSize || - sizeJustIncreasedFromZero || - !previousCellSize || - (typeof cellSize === 'number' && cellSize !== previousCellSize); - - // If we have a new scroll target OR if height/row-height has changed, - // We should ensure that the scroll target is visible. - if ( - hasScrollToIndex && - (sizeHasChanged || - scrollToAlignment !== previousScrollToAlignment || - scrollToIndex !== previousScrollToIndex) - ) { - updateScrollIndexCallback(scrollToIndex); // If we don't have a selected item but list size or number of children have decreased, - // Make sure we aren't scrolled too far past the current content. - } else if ( - !hasScrollToIndex && - cellCount > 0 && - (size < previousSize || cellCount < previousCellsCount) - ) { - // We need to ensure that the current scroll offset is still within the collection's range. - // To do this, we don't need to measure everything; CellMeasurer would perform poorly. - // Just check to make sure we're still okay. - // Only adjust the scroll position if we've scrolled below the last set of rows. - if (scrollOffset > cellSizeAndPositionManager.getTotalSize() - size) { - updateScrollIndexCallback(cellCount - 1); - } - } -} diff --git a/lib/design-system/src/general/WindowedList/source/List/List.tsx b/lib/design-system/src/general/WindowedList/source/List/List.tsx deleted file mode 100644 index 8f287abae6..0000000000 --- a/lib/design-system/src/general/WindowedList/source/List/List.tsx +++ /dev/null @@ -1,262 +0,0 @@ -import { ElementRef, PureComponent } from 'react'; -import type { RowRenderer, RenderedRows, Scroll } from './types'; -import type { - NoContentRenderer, - OverscanIndicesGetter, - CellSize, - Alignment, - CellPosition, - CellRendererParams, - RenderedSection, -} from '../Grid/types'; -import { accessibilityOverscanIndicesGetter } from '../Grid/accessibilityOverscanIndicesGetter'; -import { Grid } from '../Grid/Grid'; - -/** - * This component renders a virtualized list of elements with either fixed or dynamic heights. - */ -type Props = { - /** Optional CSS class name */ - className?: string; - - /** - * If CellMeasurer is used to measure this Grid's children, this should be a pointer to its CellMeasurerCache. - * A shared CellMeasurerCache reference enables Grid and CellMeasurer to share measurement data. - */ - deferredMeasurementCache?: Record; - - /** - * Used to estimate the total height of a List before all of its rows have actually been measured. - * The estimated total height is adjusted as rows are rendered. - */ - estimatedRowSize: number; - - /** Height constraint for list (determines how many actual rows are rendered) */ - height: number; - - /** Optional renderer to be used in place of rows when rowCount is 0 */ - noRowsRenderer: NoContentRenderer; - - /** Callback invoked with information about the slice of rows that were just rendered. */ - onRowsRendered: (params: RenderedRows) => void; - - /** - * Callback invoked whenever the scroll offset changes within the inner scrollable region. - * This callback can be used to sync scrolling between lists, tables, or grids. - */ - onScroll?: (params: Scroll) => void; - - /** See Grid#overscanIndicesGetter */ - overscanIndicesGetter: OverscanIndicesGetter; - - /** - * Number of rows to render above/below the visible bounds of the list. - * These rows can help for smoother scrolling on touch devices. - */ - overscanRowCount: number; - - /** Either a fixed row height (number) or a function that returns the height of a row given its index. */ - rowHeight: CellSize; - - /** Responsible for rendering a row given an index; ({ index: number }): node */ - rowRenderer: RowRenderer; - - /** Number of rows in list. */ - rowCount: number; - - /** Start adding elements from the bottom and scroll on mount/update. */ - startAtBottom?: boolean; - - /** See Grid#scrollToAlignment */ - scrollToAlignment: Alignment; - - /** Row index to ensure visible (by forcefully scrolling if necessary) */ - scrollToIndex: number; - - /** Vertical offset. */ - scrollTop?: number; - - /** Optional inline style */ - style: Record; - - /** Tab index for focus */ - tabIndex?: number; - - /** Width of list */ - width: number; -}; - -export class List extends PureComponent { - static defaultProps = { - estimatedRowSize: 30, - onScroll: () => {}, - noRowsRenderer: () => null, - onRowsRendered: () => {}, - overscanIndicesGetter: accessibilityOverscanIndicesGetter, - overscanRowCount: 10, - scrollToAlignment: 'auto', - scrollToIndex: -1, - style: {}, - }; - Grid: ElementRef | null | undefined; - - forceUpdateGrid() { - if (this.Grid) { - this.Grid.forceUpdate(); - } - } - - /** See Grid#getOffsetForCell */ - getOffsetForRow({ - alignment, - index, - }: { - alignment: Alignment; - index: number; - }) { - if (this.Grid) { - const { scrollTop } = this.Grid.getOffsetForCell({ - alignment, - rowIndex: index, - columnIndex: 0, - }); - return scrollTop; - } - - return 0; - } - - /** CellMeasurer compatibility */ - invalidateCellSizeAfterRender({ columnIndex, rowIndex }: CellPosition) { - if (this.Grid) { - this.Grid.invalidateCellSizeAfterRender({ - rowIndex, - columnIndex, - }); - } - } - - /** See Grid#measureAllCells */ - measureAllRows() { - if (this.Grid) { - this.Grid.measureAllCells(); - } - } - - /** CellMeasurer compatibility */ - recomputeGridSize({ - columnIndex = 0, - rowIndex = 0, - }: Partial = {}) { - if (this.Grid) { - this.Grid.recomputeGridSize({ - rowIndex, - columnIndex, - }); - } - } - - /** See Grid#recomputeGridSize */ - recomputeRowHeights(index: number = 0) { - if (this.Grid) { - this.Grid.recomputeGridSize({ - rowIndex: index, - columnIndex: 0, - }); - } - } - - /** See Grid#scrollToPosition */ - scrollToPosition(scrollTop: number = 0) { - if (this.Grid) { - this.Grid.scrollToPosition({ - scrollTop, - }); - } - } - - /** See Grid#scrollToCell */ - scrollToRow(index: number = 0) { - if (this.Grid) { - this.Grid.scrollToCell({ - columnIndex: 0, - rowIndex: index, - }); - } - } - - render() { - const { className, noRowsRenderer, scrollToIndex, width } = this.props; - return ( - - ); - } - - _cellRenderer = ({ - parent, - rowIndex, - style, - isScrolling, - isVisible, - key, - }: CellRendererParams) => { - const { rowRenderer } = this.props; - // TRICKY The style object is sometimes cached by Grid. - // This prevents new style objects from bypassing shallowCompare(). - // However as of React 16, style props are auto-frozen (at least in dev mode) - // Check to make sure we can still modify the style before proceeding. - // https://github.com/facebook/react/commit/977357765b44af8ff0cfea327866861073095c12#commitcomment-20648713 - const widthDescriptor = Object.getOwnPropertyDescriptor(style, 'width'); - - if (widthDescriptor && widthDescriptor.writable) { - // By default, List cells should be 100% width. - // This prevents them from flowing under a scrollbar (if present). - style.width = '100%'; - } - - return rowRenderer({ - index: rowIndex, - style, - isScrolling, - isVisible, - key, - parent, - }); - }; - _setRef = (ref: ElementRef | null | undefined) => { - this.Grid = ref; - }; - _onScroll = ({ clientHeight, scrollHeight, scrollTop }: any) => { - const { onScroll } = this.props; - onScroll?.({ - clientHeight, - scrollHeight, - scrollTop, - }); - }; - _onSectionRendered = ({ - rowOverscanStartIndex, - rowOverscanStopIndex, - rowStartIndex, - rowStopIndex, - }: RenderedSection) => { - const { onRowsRendered } = this.props; - onRowsRendered({ - overscanStartIndex: rowOverscanStartIndex, - overscanStopIndex: rowOverscanStopIndex, - startIndex: rowStartIndex, - stopIndex: rowStopIndex, - }); - }; -} diff --git a/lib/design-system/src/general/WindowedList/source/List/types.ts b/lib/design-system/src/general/WindowedList/source/List/types.ts deleted file mode 100644 index a0e026e175..0000000000 --- a/lib/design-system/src/general/WindowedList/source/List/types.ts +++ /dev/null @@ -1,23 +0,0 @@ -import React from 'react'; -export type RowRendererParams = { - index: number; - isScrolling: boolean; - isVisible: boolean; - key: string; - parent: Record; - style: Record; -}; -export type RowRenderer = ( - params: RowRendererParams -) => React.ReactElement; -export type RenderedRows = { - overscanStartIndex: number; - overscanStopIndex: number; - startIndex: number; - stopIndex: number; -}; -export type Scroll = { - clientHeight: number; - scrollHeight: number; - scrollTop: number; -}; diff --git a/lib/design-system/src/general/WindowedList/source/utils/animationFrame.ts b/lib/design-system/src/general/WindowedList/source/utils/animationFrame.ts deleted file mode 100644 index 952b205a71..0000000000 --- a/lib/design-system/src/general/WindowedList/source/utils/animationFrame.ts +++ /dev/null @@ -1,39 +0,0 @@ -/* eslint-disable no-restricted-globals */ -type Callback = (timestamp: number) => void; -type CancelAnimationFrame = (requestId: number) => void; -type RequestAnimationFrame = (callback: Callback) => number; -// Properly handle server-side rendering. -let win: any; - -if (typeof window !== 'undefined') { - win = window; -} else if (typeof self !== 'undefined') { - win = self; -} else { - win = {}; -} - -// requestAnimationFrame() shim by Paul Irish -// http://paulirish.com/2011/requestanimationframe-for-smart-animating/ -const request = - win.requestAnimationFrame || - win.webkitRequestAnimationFrame || - win.mozRequestAnimationFrame || - win.oRequestAnimationFrame || - win.msRequestAnimationFrame || - function (callback: Callback): RequestAnimationFrame { - return (win as any).setTimeout(callback, 1000 / 60); - }; - -const cancel = - win.cancelAnimationFrame || - win.webkitCancelAnimationFrame || - win.mozCancelAnimationFrame || - win.oCancelAnimationFrame || - win.msCancelAnimationFrame || - function (id: number) { - (win as any).clearTimeout(id); - }; - -export const raf: RequestAnimationFrame = request as any; -export const caf: CancelAnimationFrame = cancel as any; diff --git a/lib/design-system/src/general/WindowedList/source/utils/createCallbackMemoizer.ts b/lib/design-system/src/general/WindowedList/source/utils/createCallbackMemoizer.ts deleted file mode 100644 index 6ba204209e..0000000000 --- a/lib/design-system/src/general/WindowedList/source/utils/createCallbackMemoizer.ts +++ /dev/null @@ -1,34 +0,0 @@ -type CallBackProps = { - callback: (...args: any[]) => any; - indices: any; -}; - -/** - * Helper utility that updates the specified callback whenever any of the specified indices have changed. - */ -export function createCallbackMemoizer(requireAllKeys = true) { - let cachedIndices: Record = {}; - return ({ callback, indices }: CallBackProps) => { - const keys = Object.keys(indices); - const allInitialized = - !requireAllKeys || - keys.every((key) => { - const value = indices[key]; - return Array.isArray(value) ? value.length > 0 : value >= 0; - }); - const indexChanged = - keys.length !== Object.keys(cachedIndices).length || - keys.some((key) => { - const cachedValue = cachedIndices[key]; - const value = indices[key]; - return Array.isArray(value) - ? cachedValue.join(',') !== value.join(',') - : cachedValue !== value; - }); - cachedIndices = indices; - - if (allInitialized && indexChanged) { - callback(indices); - } - }; -} diff --git a/lib/design-system/src/general/WindowedList/source/utils/getUpdatedOffsetForIndex.ts b/lib/design-system/src/general/WindowedList/source/utils/getUpdatedOffsetForIndex.ts deleted file mode 100644 index 4490cf238c..0000000000 --- a/lib/design-system/src/general/WindowedList/source/utils/getUpdatedOffsetForIndex.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { Alignment } from '../Grid/types'; - -/** - * Determines a new offset that ensures a certain cell is visible, given the current offset. - * If the cell is already visible then the current offset will be returned. - * If the current offset is too great or small, it will be adjusted just enough to ensure the specified index is visible. - * - * @param align Desired alignment within container; one of "auto" (default), "start", or "end" - * @param cellOffset Offset (x or y) position for cell - * @param cellSize Size (width or height) of cell - * @param containerSize Total size (width or height) of the container - * @param currentOffset Container's current (x or y) offset - * @return Offset to use to ensure the specified cell is visible - */ -export function getUpdatedOffsetForIndex({ - align = 'auto', - cellOffset, - cellSize, - containerSize, - currentOffset, -}: { - align?: Alignment; - cellOffset: number; - cellSize: number; - containerSize: number; - currentOffset: number; -}): number { - const maxOffset = cellOffset; - const minOffset = maxOffset - containerSize + cellSize; - - switch (align) { - case 'start': - return maxOffset; - - case 'end': - return minOffset; - - case 'center': - return maxOffset - (containerSize - cellSize) / 2; - - default: - return Math.max(minOffset, Math.min(maxOffset, currentOffset)); - } -} diff --git a/lib/design-system/src/general/WindowedList/source/utils/initCellMetadata.ts b/lib/design-system/src/general/WindowedList/source/utils/initCellMetadata.ts deleted file mode 100644 index 2b2dd344c5..0000000000 --- a/lib/design-system/src/general/WindowedList/source/utils/initCellMetadata.ts +++ /dev/null @@ -1,35 +0,0 @@ -/** - * Initializes metadata for an axis and its cells. - * This data is used to determine which cells are visible given a container size and scroll position. - * - * @param cellCount Total number of cells. - * @param size Either a fixed size or a function that returns the size for a given given an index. - * @return Object mapping cell index to cell metadata (size, offset) - */ -export function initCellMetadata({ - cellCount, - size, -}: { - cellCount: number; - size: number | (() => number); -}) { - const sizeGetter = typeof size === 'function' ? size : () => size; - const cellMetadata = []; - let offset = 0; - - for (var i = 0; i < cellCount; i++) { - const size = sizeGetter(); - - if (size == null || isNaN(size)) { - throw Error(`Invalid size returned for cell ${i} of value ${size}`); - } - - cellMetadata[i] = { - size, - offset, - }; - offset += size; - } - - return cellMetadata; -} diff --git a/lib/design-system/src/general/WindowedList/source/utils/requestAnimationTimeout.ts b/lib/design-system/src/general/WindowedList/source/utils/requestAnimationTimeout.ts deleted file mode 100644 index ce1914a528..0000000000 --- a/lib/design-system/src/general/WindowedList/source/utils/requestAnimationTimeout.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { caf, raf } from './animationFrame'; -export type AnimationTimeoutId = { - id: number; -}; -export const cancelAnimationTimeout = (frame: AnimationTimeoutId) => - caf(frame.id); - -/** - * Recursively calls requestAnimationFrame until a specified delay has been met or exceeded. - * When the delay time has been reached the function you're timing out will be called. - */ -export const requestAnimationTimeout = ( - callback: (...args: Array) => any, - delay: number -): AnimationTimeoutId => { - let start: number; - // wait for end of processing current event handler, because event handler may be long - Promise.resolve().then(() => { - start = Date.now(); - }); - - const timeout = () => { - if (Date.now() - start >= delay) { - // @ts-ignore - callback.call(); - } else { - frame.id = raf(timeout); - } - }; - - const frame: AnimationTimeoutId = { - id: raf(timeout), - }; - return frame; -}; diff --git a/lib/design-system/src/general/index.ts b/lib/design-system/src/general/index.ts index 7e3a40d005..5e12823930 100644 --- a/lib/design-system/src/general/index.ts +++ b/lib/design-system/src/general/index.ts @@ -1,4 +1,3 @@ -export { CellMeasurer } from './WindowedList/source/CellMeasurer/CellMeasurer'; export * from './Anchor/Anchor'; export * from './Avatar/Avatar'; export * from './Avatar/AvatarRow'; diff --git a/lib/design-system/src/os/Notifications/NotificationList.tsx b/lib/design-system/src/os/Notifications/NotificationList.tsx index 2defec4dbe..244d36d615 100644 --- a/lib/design-system/src/os/Notifications/NotificationList.tsx +++ b/lib/design-system/src/os/Notifications/NotificationList.tsx @@ -1,6 +1,5 @@ import { Flex, BoxProps, Text } from '../../'; import { NotificationType } from './Notifications.types'; -// import { WindowedList } from '../../'; import { AppGroup } from './AppGroup'; import { Notification } from './Notification'; diff --git a/yarn.lock b/yarn.lock index ed913db77b..232eabcbf1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -14705,6 +14705,11 @@ react-twitter-embed@^4.0.4: dependencies: scriptjs "^2.5.9" +react-virtuoso@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/react-virtuoso/-/react-virtuoso-4.2.0.tgz#29d47c1075793ea5cbc6b9fc3f2345cf17749e2b" + integrity sha512-lO1akVyALlDMp+eIo4E99HjSQ8Cn2AKXBVfq7GaBjdlnlJaRvou8az6tVYGHFD6Az5EcPUc7OfzHvAyojOhgqw== + react@^18.0.0, react@^18.2.0: version "18.2.0" resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5"