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

fix: a11y: arrow-key navigation stopping working after ~10 keypresses #4441

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
- dev: upgrade react to v18 and react pinch pan zoom to v3

## Fixed
- accessibility: fix arrow-key navigation stopping working after ~10 key presses #4441

## [1.50.1] - 2024-12-18

Expand Down
125 changes: 85 additions & 40 deletions packages/frontend/src/components/Gallery.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -379,30 +379,16 @@ export default class Gallery extends Component<
}
this.updateFirstVisibleMessage(message)
}}
>
{({ columnIndex, rowIndex, style }) => {
const msgId =
mediaMessageIds[rowIndex * itemsPerRow + columnIndex]
const message = mediaLoadResult[msgId]
if (!message) {
return null
}
return (
<div
style={{ ...style }}
className='item'
key={msgId}
>
<this.state.element
messageId={msgId}
loadResult={message}
openFullscreenMedia={this.openFullscreenMedia.bind(
this
)}
/>
</div>
)
itemData={{
Component: this.state.element,
mediaMessageIds,
mediaLoadResult,
openFullscreenMedia:
this.openFullscreenMedia.bind(this),
itemsPerRow,
}}
>
{GalleryGridCell}
</FixedSizeGrid>
)
}}
Expand All @@ -414,6 +400,46 @@ export default class Gallery extends Component<
)
}
}
function GalleryGridCell({
columnIndex,
rowIndex,
style,
data,
}: {
columnIndex: number
rowIndex: number
style: React.CSSProperties
data: {
Component: GalleryElement
mediaMessageIds: number[]
mediaLoadResult: Record<number, Type.MessageLoadResult>
openFullscreenMedia: (message: Type.Message) => void
itemsPerRow: number
}
}) {
const {
Component,
mediaMessageIds,
mediaLoadResult,
openFullscreenMedia,
itemsPerRow,
} = data

const msgId = mediaMessageIds[rowIndex * itemsPerRow + columnIndex]
const message = mediaLoadResult[msgId]
if (!message) {
return null
}
return (
<div style={{ ...style }} className='item' key={msgId}>
<Component
messageId={msgId}
loadResult={message}
openFullscreenMedia={openFullscreenMedia}
/>
</div>
)
}

function GalleryTab(props: {
tabId: MediaTabKey
Expand Down Expand Up @@ -466,24 +492,43 @@ function FileTable({
itemSize={60}
itemCount={mediaMessageIds.length}
overscanCount={10}
itemData={mediaMessageIds}
>
{({ index, style, data }) => {
const msgId = data[index]
const message = mediaLoadResult[msgId]
if (!message) {
return null
}
return (
<div style={style} className='item' key={msgId}>
<FileAttachmentRow
messageId={msgId}
loadResult={message}
queryText={queryText}
/>
</div>
)
itemData={{
mediaMessageIds,
mediaLoadResult,
queryText,
}}
>
{FileAttachmentRowWrapper}
</FixedSizeList>
)
}

function FileAttachmentRowWrapper({
index,
style,
data,
}: {
index: number
style: React.CSSProperties
data: {
mediaMessageIds: number[]
mediaLoadResult: Record<number, Type.MessageLoadResult>
queryText: string
}
}) {
const { mediaMessageIds, mediaLoadResult, queryText } = data
const msgId = mediaMessageIds[index]
const message = mediaLoadResult[msgId]
if (!message) {
return null
}
return (
<div style={style} className='item' key={msgId}>
<FileAttachmentRow
messageId={msgId}
loadResult={message}
queryText={queryText}
/>
</div>
)
}
5 changes: 1 addition & 4 deletions packages/frontend/src/components/chat/ChatList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,10 +71,7 @@ export function ChatListPart({
height: number
itemKey: ListItemKeySelector<any>
setListRef?: (ref: List<any> | null) => void
itemData?:
| ChatListItemData
| ContactChatListItemData
| MessageChatListItemData
itemData: ChatListItemData | ContactChatListItemData | MessageChatListItemData
itemHeight: number
}) {
const infiniteLoaderRef = useRef<InfiniteLoader | null>(null)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -157,46 +157,6 @@ export function AddMemberInnerDialog({

const needToRenderAddContact = queryStr !== '' && contactIds.length === 0
const itemCount = contactIds.length + (needToRenderAddContact ? 1 : 0)
const renderAddContact = () => {
if (queryStrIsValidEmail) {
const pseudoContact: Type.Contact = {
address: queryStr,
color: 'lightgrey',
authName: '',
status: '',
displayName: queryStr,
id: -1,
lastSeen: -1,
name: queryStr,
profileImage: '',
nameAndAddr: '',
isBlocked: false,
isVerified: false,
verifierId: null,
wasSeenRecently: false,
isProfileVerified: false,
isBot: false,
e2eeAvail: false,
}
return (
<ContactListItem
contact={pseudoContact}
showCheckbox={true}
checked={false}
showRemove={false}
onCheckboxClick={createNewContact}
/>
)
} else {
return (
<PseudoListItemAddContact
queryStr={queryStr}
queryStrIsEmail={false}
onClick={undefined}
/>
)
}
}

const addContactOnKeyDown = (ev: React.KeyboardEvent<HTMLInputElement>) => {
if (ev.code == 'Enter') {
Expand Down Expand Up @@ -281,9 +241,18 @@ export function AddMemberInnerDialog({
if the user has 5000 contacts.
(see https://github.com/deltachat/deltachat-desktop/issues/1830) */}
<FixedSizeList
itemData={contactIds}
itemData={{
contactIds,
contactIdsInGroup,
contactIdsToAdd,
contactCache,
onCheckboxClick: toggleMember,
onCreateContactCheckboxClick: createNewContact,
queryStr,
queryStrIsValidEmail,
}}
itemCount={itemCount}
itemKey={(index, contactIds) => {
itemKey={(index, { contactIds }) => {
const isExtraItem = index >= contactIds.length
return isExtraItem ? 'addContact' : contactIds[index]
}}
Expand All @@ -297,38 +266,13 @@ export function AddMemberInnerDialog({
// "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>
}

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>
)
}}
{/* Remember that the renderer function
must not be defined _inline_.
Otherwise when the component re-renders,
item elements get replaces with fresh ones,
and we lose focus.
See https://github.com/bvaughn/react-window/issues/420#issuecomment-585813335 */}
{AddMemberInnerDialogRow}
</FixedSizeList>
</RovingTabindexProvider>
)}
Expand All @@ -345,3 +289,103 @@ export function AddMemberInnerDialog({
</>
)
}

function AddMemberInnerDialogRow({
index,
style,
data,
}: {
index: number
style: React.CSSProperties
data: {
contactIds: Array<T.Contact['id']>
contactIdsInGroup: Array<T.Contact['id']>
contactIdsToAdd: Type.Contact[]
contactCache: Parameters<typeof AddMemberInnerDialog>[0]['contactCache']
onCheckboxClick: (contact: T.Contact) => void
onCreateContactCheckboxClick: (contact: T.Contact) => void
queryStr: string
queryStrIsValidEmail: boolean
}
}) {
const {
contactIds,
contactIdsInGroup,
contactIdsToAdd,
contactCache,
onCheckboxClick,
onCreateContactCheckboxClick,
queryStr,
queryStrIsValidEmail,
} = data

const renderAddContact = () => {
if (queryStrIsValidEmail) {
const pseudoContact: Type.Contact = {
address: queryStr,
color: 'lightgrey',
authName: '',
status: '',
displayName: queryStr,
id: -1,
lastSeen: -1,
name: queryStr,
profileImage: '',
nameAndAddr: '',
isBlocked: false,
isVerified: false,
verifierId: null,
wasSeenRecently: false,
isProfileVerified: false,
isBot: false,
e2eeAvail: false,
}
return (
<ContactListItem
contact={pseudoContact}
showCheckbox={true}
checked={false}
showRemove={false}
onCheckboxClick={onCreateContactCheckboxClick}
/>
)
} else {
return (
<PseudoListItemAddContact
queryStr={queryStr}
queryStrIsEmail={false}
onClick={undefined}
/>
)
}
}
const isExtraItem = index >= contactIds.length
if (isExtraItem) {
return renderAddContact()
}

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={onCheckboxClick}
showRemove={false}
/>
</div>
)
}
Loading
Loading