mirror of
https://github.com/cinnyapp/cinny.git
synced 2025-02-23 13:43:07 +01:00
add scroll top container component
This commit is contained in:
parent
bfb591f495
commit
f5860dfd4a
5 changed files with 100 additions and 33 deletions
|
@ -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} />;
|
||||
});
|
1
src/app/components/scroll-top-container/index.ts
Normal file
1
src/app/components/scroll-top-container/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export * from './ScrollTopContainer';
|
20
src/app/components/scroll-top-container/style.css.ts
Normal file
20
src/app/components/scroll-top-container/style.css.ts
Normal 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`,
|
||||
});
|
|
@ -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">
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue