mirror of
https://github.com/cinnyapp/cinny.git
synced 2025-02-23 21:53:05 +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 * as css from './MembersDrawer.css';
|
||||||
import { useRoomMembers } from '../../hooks/useRoomMembers';
|
import { useRoomMembers } from '../../hooks/useRoomMembers';
|
||||||
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
||||||
import {
|
|
||||||
getIntersectionObserverEntry,
|
|
||||||
useIntersectionObserver,
|
|
||||||
} from '../../hooks/useIntersectionObserver';
|
|
||||||
import { Membership } from '../../../types/matrix/room';
|
import { Membership } from '../../../types/matrix/room';
|
||||||
import { UseStateProvider } from '../../components/UseStateProvider';
|
import { UseStateProvider } from '../../components/UseStateProvider';
|
||||||
import {
|
import {
|
||||||
|
@ -60,6 +56,7 @@ import { getMxIdLocalPart } from '../../utils/matrix';
|
||||||
import { useSetting } from '../../state/hooks/settings';
|
import { useSetting } from '../../state/hooks/settings';
|
||||||
import { settingsAtom } from '../../state/settings';
|
import { settingsAtom } from '../../state/settings';
|
||||||
import { millify } from '../../plugins/millify';
|
import { millify } from '../../plugins/millify';
|
||||||
|
import { ScrollTopContainer } from '../../components/scroll-top-container';
|
||||||
|
|
||||||
export const MembershipFilters = {
|
export const MembershipFilters = {
|
||||||
filterJoined: (m: RoomMember) => m.membership === Membership.Join,
|
filterJoined: (m: RoomMember) => m.membership === Membership.Join,
|
||||||
|
@ -190,8 +187,6 @@ export function MembersDrawer({ room }: MembersDrawerProps) {
|
||||||
const membershipFilter = membershipFilterMenu[membershipFilterIndex] ?? membershipFilterMenu[0];
|
const membershipFilter = membershipFilterMenu[membershipFilterIndex] ?? membershipFilterMenu[0];
|
||||||
const sortFilter = sortFilterMenu[sortFilterIndex] ?? sortFilterMenu[0];
|
const sortFilter = sortFilterMenu[sortFilterIndex] ?? sortFilterMenu[0];
|
||||||
|
|
||||||
const [onTop, setOnTop] = useState(true);
|
|
||||||
|
|
||||||
const typingMembers = useAtomValue(
|
const typingMembers = useAtomValue(
|
||||||
useMemo(() => selectRoomTypingMembersAtom(room.roomId, roomIdToTypingMembersAtom), [room])
|
useMemo(() => selectRoomTypingMembersAtom(room.roomId, roomIdToTypingMembersAtom), [room])
|
||||||
);
|
);
|
||||||
|
@ -235,16 +230,6 @@ export function MembersDrawer({ room }: MembersDrawerProps) {
|
||||||
overscan: 10,
|
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(
|
const handleSearchChange: ChangeEventHandler<HTMLInputElement> = useDebounce(
|
||||||
useCallback(
|
useCallback(
|
||||||
(evt) => {
|
(evt) => {
|
||||||
|
@ -446,8 +431,7 @@ export function MembersDrawer({ room }: MembersDrawerProps) {
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{!onTop && (
|
<ScrollTopContainer scrollRef={scrollRef} anchorRef={scrollTopAnchorRef}>
|
||||||
<Box className={css.DrawerScrollTop}>
|
|
||||||
<IconButton
|
<IconButton
|
||||||
onClick={() => virtualizer.scrollToOffset(0)}
|
onClick={() => virtualizer.scrollToOffset(0)}
|
||||||
variant="Surface"
|
variant="Surface"
|
||||||
|
@ -458,8 +442,7 @@ export function MembersDrawer({ room }: MembersDrawerProps) {
|
||||||
>
|
>
|
||||||
<Icon src={Icons.ChevronTop} size="300" />
|
<Icon src={Icons.ChevronTop} size="300" />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Box>
|
</ScrollTopContainer>
|
||||||
)}
|
|
||||||
|
|
||||||
{!fetchingMembers && !result && processMembers.length === 0 && (
|
{!fetchingMembers && !result && processMembers.length === 0 && (
|
||||||
<Text style={{ padding: config.space.S300 }} align="Center">
|
<Text style={{ padding: config.space.S300 }} align="Center">
|
||||||
|
|
|
@ -1,5 +1,17 @@
|
||||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
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 { useSearchParams } from 'react-router-dom';
|
||||||
import { INotification, INotificationsResponse, Method } from 'matrix-js-sdk';
|
import { INotification, INotificationsResponse, Method } from 'matrix-js-sdk';
|
||||||
import { useVirtualizer } from '@tanstack/react-virtual';
|
import { useVirtualizer } from '@tanstack/react-virtual';
|
||||||
|
@ -12,6 +24,7 @@ import { SequenceCard } from '../../../components/sequence-card';
|
||||||
import { RoomAvatar } from '../../../components/room-avatar';
|
import { RoomAvatar } from '../../../components/room-avatar';
|
||||||
import { nameInitials } from '../../../utils/common';
|
import { nameInitials } from '../../../utils/common';
|
||||||
import { getRoomAvatarUrl } from '../../../utils/room';
|
import { getRoomAvatarUrl } from '../../../utils/room';
|
||||||
|
import { ScrollTopContainer } from '../../../components/scroll-top-container';
|
||||||
|
|
||||||
type RoomNotificationsGroup = {
|
type RoomNotificationsGroup = {
|
||||||
roomId: string;
|
roomId: string;
|
||||||
|
@ -94,6 +107,7 @@ export function Notifications() {
|
||||||
const [searchParams, setSearchParams] = useSearchParams();
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
const notificationsSearchParams = getNotificationsSearchParams(searchParams);
|
const notificationsSearchParams = getNotificationsSearchParams(searchParams);
|
||||||
const scrollRef = useRef<HTMLDivElement>(null);
|
const scrollRef = useRef<HTMLDivElement>(null);
|
||||||
|
const scrollTopAnchorRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
const onlyHighlight = notificationsSearchParams.only === 'highlight';
|
const onlyHighlight = notificationsSearchParams.only === 'highlight';
|
||||||
const setOnlyHighlighted = (highlight: boolean) => {
|
const setOnlyHighlighted = (highlight: boolean) => {
|
||||||
|
@ -146,12 +160,12 @@ export function Notifications() {
|
||||||
</Box>
|
</Box>
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
|
|
||||||
<Box grow="Yes">
|
<Box style={{ position: 'relative' }} grow="Yes">
|
||||||
<Scroll ref={scrollRef} hideTrack visibility="Hover">
|
<Scroll ref={scrollRef} hideTrack visibility="Hover">
|
||||||
<PageContent>
|
<PageContent>
|
||||||
<PageContentCenter>
|
<PageContentCenter>
|
||||||
<Box direction="Column" gap="200">
|
<Box direction="Column" gap="200">
|
||||||
<Box direction="Column" gap="100">
|
<Box ref={scrollTopAnchorRef} direction="Column" gap="100">
|
||||||
<span data-spacing-node />
|
<span data-spacing-node />
|
||||||
<Text size="L400">Filter</Text>
|
<Text size="L400">Filter</Text>
|
||||||
<Box gap="200">
|
<Box gap="200">
|
||||||
|
@ -187,6 +201,18 @@ export function Notifications() {
|
||||||
</Chip>
|
</Chip>
|
||||||
</Box>
|
</Box>
|
||||||
</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
|
<div
|
||||||
style={{
|
style={{
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
|
@ -198,6 +224,8 @@ export function Notifications() {
|
||||||
if (!group) return null;
|
if (!group) return null;
|
||||||
const groupRoom = mx.getRoom(group.roomId);
|
const groupRoom = mx.getRoom(group.roomId);
|
||||||
if (!groupRoom) return null;
|
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 (
|
return (
|
||||||
<Box
|
<Box
|
||||||
|
|
Loading…
Reference in a new issue