Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

improvement: arrow-key nav in sticker picker #4372

Draft
wants to merge 11 commits into
base: main
Choose a base branch
from
48 changes: 38 additions & 10 deletions packages/frontend/src/components/composer/EmojiAndStickerPicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import React, {
useEffect,
forwardRef,
PropsWithChildren,
useRef,
} from 'react'
import classNames from 'classnames'

Expand All @@ -17,6 +18,10 @@ import useMessage from '../../hooks/chat/useMessage'
import styles from './styles.module.scss'

import type { EmojiData } from 'emoji-mart/index'
import {
RovingTabindexProvider,
useRovingTabindex,
} from '../../contexts/RovingTabindex'

type Props = {
stickerPackName: string
Expand All @@ -34,6 +39,8 @@ const DisplayedStickerPack = ({
const { jumpToMessage } = useMessage()
const accountId = selectedAccountId()

const listRef = useRef<HTMLDivElement>(null)

const onClickSticker = (fileName: string) => {
const stickerPath = fileName.replace('file://', '')
BackendRemote.rpc
Expand All @@ -47,21 +54,42 @@ const DisplayedStickerPack = ({
return (
<div className='sticker-pack'>
<div className='title'>{stickerPackName}</div>
<div className='container'>
{stickerPackImages.map((filePath, index) => (
<button
className='sticker'
key={index}
onClick={() => onClickSticker(filePath)}
>
<img src={filePath} />
</button>
))}
<div ref={listRef} className='container'>
{/* Yes, we have separate `RovingTabindexProvider` for each
sticker pack, instead of having one for all stickers.
Users probably want to switch between sticker packs with Tab. */}
<RovingTabindexProvider wrapperElementRef={listRef}>
{stickerPackImages.map((filePath, index) => (
<StickersListItem
key={index}
filePath={filePath}
onClick={() => onClickSticker(filePath)}
/>
))}
</RovingTabindexProvider>
</div>
</div>
)
}

function StickersListItem(props: { filePath: string; onClick: () => void }) {
const { filePath, onClick } = props
const ref = useRef<HTMLButtonElement>(null)
const rovingTabindex = useRovingTabindex(ref)
return (
<button
ref={ref}
className={'sticker ' + rovingTabindex.className}
onClick={onClick}
tabIndex={rovingTabindex.tabIndex}
onKeyDown={rovingTabindex.onKeydown}
onFocus={rovingTabindex.setAsActiveElement}
>
<img src={filePath} />
</button>
)
}

export const StickerPicker = ({
stickers,
chatId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import InfiniteLoader from 'react-window-infinite-loader'
import { AddMemberChip } from './AddMemberDialog'
import styles from './styles.module.scss'
import classNames from 'classnames'
import { RovingTabindexProvider } from '../../../contexts/RovingTabindex'

export function AddMemberInnerDialog({
onCancel,
Expand Down Expand Up @@ -275,58 +276,61 @@ export function AddMemberInnerDialog({
// minimumBatchSize={100}
>
{({ onItemsRendered, ref }) => (
// Not using 'react-window' results in ~5 second rendering time
// if the user has 5000 contacts.
// (see https://github.com/deltachat/deltachat-desktop/issues/1830)
<FixedSizeList
itemData={contactIds}
itemCount={itemCount}
itemKey={(index, contactIds) => {
const isExtraItem = index >= contactIds.length
return isExtraItem ? 'addContact' : contactIds[index]
}}
onItemsRendered={onItemsRendered}
ref={ref}
height={height}
width='100%'
// TODO fix: The size of each item is determined
// by `--local-avatar-size` and `--local-avatar-vertical-margin`,
// which might be different, e.g. currently they're smaller for
// "Rocket Theme", which results in gaps between the elements.
itemSize={64}
>
{({ index, style, data: contactIds }) => {
const isExtraItem = index >= contactIds.length
if (isExtraItem) {
return renderAddContact()
}
<RovingTabindexProvider wrapperElementRef={contactListRef}>
{/* Not using 'react-window' results in ~5 second rendering time
if the user has 5000 contacts.
(see https://github.com/deltachat/deltachat-desktop/issues/1830) */}
<FixedSizeList
itemData={contactIds}
itemCount={itemCount}
itemKey={(index, contactIds) => {
const isExtraItem = index >= contactIds.length
return isExtraItem ? 'addContact' : contactIds[index]
}}
onItemsRendered={onItemsRendered}
ref={ref}
height={height}
width='100%'
// TODO fix: The size of each item is determined
// by `--local-avatar-size` and `--local-avatar-vertical-margin`,
// which might be different, e.g. currently they're smaller for
// "Rocket Theme", which results in gaps between the elements.
itemSize={64}
>
{({ index, style, data: contactIds }) => {
const isExtraItem = index >= contactIds.length
if (isExtraItem) {
return renderAddContact()
}

const contact = contactCache[contactIds[index]]
if (!contact) {
// Not loaded yet
return <div style={style}></div>
}
const contact = contactCache[contactIds[index]]
if (!contact) {
// Not loaded yet
return <div style={style}></div>
}

return (
<div style={style}>
<ContactListItem
contact={contact}
showCheckbox
checked={
contactIdsToAdd.some(c => c.id === contact.id) ||
contactIdsInGroup.includes(contact.id)
}
disabled={
contactIdsInGroup.includes(contact.id) ||
contact.id === C.DC_CONTACT_ID_SELF
}
onCheckboxClick={toggleMember}
showRemove={false}
/>
</div>
)
}}
</FixedSizeList>
return (
<div style={style}>
<ContactListItem
contact={contact}
showCheckbox
checked={
contactIdsToAdd.some(
c => c.id === contact.id
) || contactIdsInGroup.includes(contact.id)
}
disabled={
contactIdsInGroup.includes(contact.id) ||
contact.id === C.DC_CONTACT_ID_SELF
}
onCheckboxClick={toggleMember}
showRemove={false}
/>
</div>
)
}}
</FixedSizeList>
</RovingTabindexProvider>
)}
</InfiniteLoader>
)}
Expand Down
66 changes: 42 additions & 24 deletions packages/frontend/src/components/dialogs/CreateChat/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -419,6 +419,8 @@ export function CreateGroup(props: CreateGroupProps) {
const [errorMissingGroupName, setErrorMissingGroupName] = useState(false)
const [groupContacts, setGroupContacts] = useState<Type.Contact[]>([])

const groupMemberContactListWrapperRef = useRef<HTMLDivElement>(null)

useMemo(() => {
BackendRemote.rpc
.getContactsByIds(accountId, groupMembers)
Expand Down Expand Up @@ -459,18 +461,25 @@ export function CreateGroup(props: CreateGroupProps) {
quantity: groupMembers.length,
})}
</div>
<div className='group-member-contact-list-wrapper'>
<PseudoListItemAddMember
onClick={showAddMemberDialog}
isBroadcast={false}
/>
<ContactList
contacts={groupContacts}
showRemove
onRemoveClick={c => {
removeGroupMember(c)
}}
/>
<div
className='group-member-contact-list-wrapper'
ref={groupMemberContactListWrapperRef}
>
<RovingTabindexProvider
wrapperElementRef={groupMemberContactListWrapperRef}
>
<PseudoListItemAddMember
onClick={showAddMemberDialog}
isBroadcast={false}
/>
<ContactList
contacts={groupContacts}
showRemove
onRemoveClick={c => {
removeGroupMember(c)
}}
/>
</RovingTabindexProvider>
</div>
</DialogBody>
<DialogFooter>
Expand Down Expand Up @@ -525,6 +534,8 @@ function CreateBroadcastList(props: CreateBroadcastListProps) {

const [broadcastContacts, setBroadcastContacts] = useState<Type.Contact[]>([])

const groupMemberContactListWrapperRef = useRef<HTMLDivElement>(null)

useMemo(() => {
BackendRemote.rpc
.getContactsByIds(accountId, broadcastRecipients)
Expand Down Expand Up @@ -585,18 +596,25 @@ function CreateBroadcastList(props: CreateBroadcastListProps) {
})}
</div>
)}
<div className='group-member-contact-list-wrapper'>
<PseudoListItemAddMember
onClick={showAddMemberDialog}
isBroadcast
/>
<ContactList
contacts={broadcastContacts}
showRemove
onRemoveClick={c => {
removeBroadcastRecipient(c)
}}
/>
<div
className='group-member-contact-list-wrapper'
ref={groupMemberContactListWrapperRef}
>
<RovingTabindexProvider
wrapperElementRef={groupMemberContactListWrapperRef}
>
<PseudoListItemAddMember
onClick={showAddMemberDialog}
isBroadcast
/>
<ContactList
contacts={broadcastContacts}
showRemove
onRemoveClick={c => {
removeBroadcastRecipient(c)
}}
/>
</RovingTabindexProvider>
</div>
</DialogContent>
</DialogBody>
Expand Down
69 changes: 37 additions & 32 deletions packages/frontend/src/components/dialogs/ForwardMessage/index.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import AutoSizer from 'react-virtualized-auto-sizer'
import React, { useState } from 'react'
import React, { useRef, useState } from 'react'
import { C } from '@deltachat/jsonrpc-client'

import ChatListItem from '../../chat/ChatListItem'
Expand All @@ -20,6 +20,7 @@ import styles from './styles.module.scss'

import type { T } from '@deltachat/jsonrpc-client'
import type { DialogProps } from '../../../contexts/DialogContext'
import { RovingTabindexProvider } from '../../../contexts/RovingTabindex'

type Props = {
message: T.Message
Expand All @@ -42,6 +43,8 @@ export default function ForwardMessage(props: Props) {
const { isChatLoaded, loadChats, chatCache } =
useLogicVirtualChatList(chatListIds)

const chatListRef = useRef<HTMLDivElement>(null)

const onChatClick = async (chatId: number) => {
const chat = await BackendRemote.rpc.getFullChatById(accountId, chatId)
onClose()
Expand Down Expand Up @@ -96,37 +99,39 @@ export default function ForwardMessage(props: Props) {
spellCheck={false}
/>
</div>
<div className='forward-message-list-chat-list'>
{noResults && queryStr && (
<PseudoListItemNoSearchResults queryStr={queryStr} />
)}
<div style={{ height: noResults ? '0px' : '100%' }}>
<AutoSizer>
{({ width, height }) => (
<ChatListPart
isRowLoaded={isChatLoaded}
loadMoreRows={loadChats}
rowCount={chatListIds.length}
width={width}
height={height}
itemKey={index => 'key' + chatListIds[index]}
itemHeight={CHATLISTITEM_CHAT_HEIGHT}
>
{({ index, style }) => {
const chatId = chatListIds[index]
return (
<div style={style}>
<ChatListItem
chatListItem={chatCache[chatId] || undefined}
onClick={onChatClick.bind(null, chatId)}
/>
</div>
)
}}
</ChatListPart>
)}
</AutoSizer>
</div>
<div className='forward-message-list-chat-list' ref={chatListRef}>
<RovingTabindexProvider wrapperElementRef={chatListRef}>
{noResults && queryStr && (
<PseudoListItemNoSearchResults queryStr={queryStr} />
)}
<div style={{ height: noResults ? '0px' : '100%' }}>
<AutoSizer>
{({ width, height }) => (
<ChatListPart
isRowLoaded={isChatLoaded}
loadMoreRows={loadChats}
rowCount={chatListIds.length}
width={width}
height={height}
itemKey={index => 'key' + chatListIds[index]}
itemHeight={CHATLISTITEM_CHAT_HEIGHT}
>
{({ index, style }) => {
const chatId = chatListIds[index]
return (
<div style={style}>
<ChatListItem
chatListItem={chatCache[chatId] || undefined}
onClick={onChatClick.bind(null, chatId)}
/>
</div>
)
}}
</ChatListPart>
)}
</AutoSizer>
</div>
</RovingTabindexProvider>
</div>
</DialogBody>
</Dialog>
Expand Down
Loading
Loading