add scroll top container component

This commit is contained in:
Ajay Bura 2024-03-20 09:12:12 +05:30
parent bfb591f495
commit f5860dfd4a
5 changed files with 100 additions and 33 deletions

View file

@ -0,0 +1,35 @@
import React, { RefObject, useCallback, useState } from 'react';
import { Box, as } from 'folds';
import classNames from 'classnames';
import * as css from './style.css';
import {
getIntersectionObserverEntry,
useIntersectionObserver,
} from '../../hooks/useIntersectionObserver';
export const ScrollTopContainer = as<
'div',
{
scrollRef?: RefObject<HTMLElement>;
anchorRef: RefObject<HTMLElement>;
}
>(({ className, scrollRef, anchorRef, ...props }, ref) => {
const [onTop, setOnTop] = useState(true);
useIntersectionObserver(
useCallback(
(intersectionEntries) => {
if (!anchorRef.current) return;
const entry = getIntersectionObserverEntry(anchorRef.current, intersectionEntries);
if (entry) setOnTop(entry.isIntersecting);
},
[anchorRef]
),
useCallback(() => ({ root: scrollRef?.current }), [scrollRef]),
useCallback(() => anchorRef.current, [anchorRef])
);
if (onTop) return null;
return <Box className={classNames(css.ScrollTopContainer, className)} {...props} ref={ref} />;
});

View file

@ -0,0 +1 @@
export * from './ScrollTopContainer';

View file

@ -0,0 +1,20 @@
import { keyframes, style } from '@vanilla-extract/css';
import { config } from 'folds';
const ScrollContainerAnime = keyframes({
'0%': {
transform: `translate(-50%, -100%) scale(0)`,
},
'100%': {
transform: `translate(-50%, 0) scale(1)`,
},
});
export const ScrollTopContainer = style({
position: 'absolute',
top: config.space.S200,
left: '50%',
transform: 'translateX(-50%)',
zIndex: 1,
animation: `${ScrollContainerAnime} 100ms`,
});

View file

@ -39,10 +39,6 @@ import { openInviteUser, openProfileViewer } from '../../../client/action/naviga
import * as css from './MembersDrawer.css';
import { useRoomMembers } from '../../hooks/useRoomMembers';
import { useMatrixClient } from '../../hooks/useMatrixClient';
import {
getIntersectionObserverEntry,
useIntersectionObserver,
} from '../../hooks/useIntersectionObserver';
import { Membership } from '../../../types/matrix/room';
import { UseStateProvider } from '../../components/UseStateProvider';
import {
@ -60,6 +56,7 @@ import { getMxIdLocalPart } from '../../utils/matrix';
import { useSetting } from '../../state/hooks/settings';
import { settingsAtom } from '../../state/settings';
import { millify } from '../../plugins/millify';
import { ScrollTopContainer } from '../../components/scroll-top-container';
export const MembershipFilters = {
filterJoined: (m: RoomMember) => m.membership === Membership.Join,
@ -190,8 +187,6 @@ export function MembersDrawer({ room }: MembersDrawerProps) {
const membershipFilter = membershipFilterMenu[membershipFilterIndex] ?? membershipFilterMenu[0];
const sortFilter = sortFilterMenu[sortFilterIndex] ?? sortFilterMenu[0];
const [onTop, setOnTop] = useState(true);
const typingMembers = useAtomValue(
useMemo(() => selectRoomTypingMembersAtom(room.roomId, roomIdToTypingMembersAtom), [room])
);
@ -235,16 +230,6 @@ export function MembersDrawer({ room }: MembersDrawerProps) {
overscan: 10,
});
useIntersectionObserver(
useCallback((intersectionEntries) => {
if (!scrollTopAnchorRef.current) return;
const entry = getIntersectionObserverEntry(scrollTopAnchorRef.current, intersectionEntries);
if (entry) setOnTop(entry.isIntersecting);
}, []),
useCallback(() => ({ root: scrollRef.current }), []),
useCallback(() => scrollTopAnchorRef.current, [])
);
const handleSearchChange: ChangeEventHandler<HTMLInputElement> = useDebounce(
useCallback(
(evt) => {
@ -446,20 +431,18 @@ export function MembersDrawer({ room }: MembersDrawerProps) {
</Box>
</Box>
{!onTop && (
<Box className={css.DrawerScrollTop}>
<IconButton
onClick={() => virtualizer.scrollToOffset(0)}
variant="Surface"
radii="Pill"
outlined
size="300"
aria-label="Scroll to Top"
>
<Icon src={Icons.ChevronTop} size="300" />
</IconButton>
</Box>
)}
<ScrollTopContainer scrollRef={scrollRef} anchorRef={scrollTopAnchorRef}>
<IconButton
onClick={() => virtualizer.scrollToOffset(0)}
variant="Surface"
radii="Pill"
outlined
size="300"
aria-label="Scroll to Top"
>
<Icon src={Icons.ChevronTop} size="300" />
</IconButton>
</ScrollTopContainer>
{!fetchingMembers && !result && processMembers.length === 0 && (
<Text style={{ padding: config.space.S300 }} align="Center">

View file

@ -1,5 +1,17 @@
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { Avatar, Box, Chip, Header, Icon, Icons, Scroll, Text, config, toRem } from 'folds';
import {
Avatar,
Box,
Chip,
Header,
Icon,
IconButton,
Icons,
Scroll,
Text,
config,
toRem,
} from 'folds';
import { useSearchParams } from 'react-router-dom';
import { INotification, INotificationsResponse, Method } from 'matrix-js-sdk';
import { useVirtualizer } from '@tanstack/react-virtual';
@ -12,6 +24,7 @@ import { SequenceCard } from '../../../components/sequence-card';
import { RoomAvatar } from '../../../components/room-avatar';
import { nameInitials } from '../../../utils/common';
import { getRoomAvatarUrl } from '../../../utils/room';
import { ScrollTopContainer } from '../../../components/scroll-top-container';
type RoomNotificationsGroup = {
roomId: string;
@ -94,6 +107,7 @@ export function Notifications() {
const [searchParams, setSearchParams] = useSearchParams();
const notificationsSearchParams = getNotificationsSearchParams(searchParams);
const scrollRef = useRef<HTMLDivElement>(null);
const scrollTopAnchorRef = useRef<HTMLDivElement>(null);
const onlyHighlight = notificationsSearchParams.only === 'highlight';
const setOnlyHighlighted = (highlight: boolean) => {
@ -146,12 +160,12 @@ export function Notifications() {
</Box>
</PageHeader>
<Box grow="Yes">
<Box style={{ position: 'relative' }} grow="Yes">
<Scroll ref={scrollRef} hideTrack visibility="Hover">
<PageContent>
<PageContentCenter>
<Box direction="Column" gap="200">
<Box direction="Column" gap="100">
<Box ref={scrollTopAnchorRef} direction="Column" gap="100">
<span data-spacing-node />
<Text size="L400">Filter</Text>
<Box gap="200">
@ -187,6 +201,18 @@ export function Notifications() {
</Chip>
</Box>
</Box>
<ScrollTopContainer scrollRef={scrollRef} anchorRef={scrollTopAnchorRef}>
<IconButton
onClick={() => virtualizer.scrollToOffset(0)}
variant="Surface"
radii="Pill"
outlined
size="300"
aria-label="Scroll to Top"
>
<Icon src={Icons.ChevronTop} size="300" />
</IconButton>
</ScrollTopContainer>
<div
style={{
position: 'relative',
@ -198,6 +224,8 @@ export function Notifications() {
if (!group) return null;
const groupRoom = mx.getRoom(group.roomId);
if (!groupRoom) return null;
// TODO: instead of null return empty div to measure element
// extract scroll to top floating btn component from MemberDrawer component
return (
<Box