Skip to content

Commit

Permalink
WIP: improvement: a11y: arrow-key navigation for messages
Browse files Browse the repository at this point in the history
Closes #2141.

Basically what this commit comes down to:
1. Apply `useRovingTabindex` for message items
2. Set `tabindex="-1"` on all the interactive items
    inside every message that is currently not the active one,
    so that they do no have tab stops.
  • Loading branch information
WofWca committed Dec 19, 2024
1 parent a858eeb commit 69a0780
Show file tree
Hide file tree
Showing 10 changed files with 340 additions and 100 deletions.
18 changes: 17 additions & 1 deletion packages/frontend/scss/message/_message-list.scss
Original file line number Diff line number Diff line change
Expand Up @@ -123,10 +123,26 @@
scroll-margin-bottom: $margin-bottom;
// And this is purely cosmetic. For scrolling to a message
// that is above the visible area.
scroll-margin-top: math.div($margin-bottom, 2);
$scroll-margin-top: math.div($margin-bottom, 2);
scroll-margin-top: $scroll-margin-top;

min-width: 200px;

.roving-tabindex {
// The contents of the `<li>` are focusable (see `useRovingTabindex`).
// Focusing an element inside a scrollable region makes the browser
// scroll the focused element into view, so let's also apply the same
// margins for it.
// This is not as important though, but still nice.
//
// But ideally we'd probably want to refactor this in such a way that
// the focusabe element and the element that we `scrollIntoView()`
// when `jumpToMessage` is the same element, so only one
// needs `scroll-margin`.
scroll-margin-bottom: $margin-bottom;
scroll-margin-top: $scroll-margin-top;
}

&::after {
visibility: hidden;
display: block;
Expand Down
12 changes: 10 additions & 2 deletions packages/frontend/src/components/Avatar/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ export function Avatar(props: {
wasSeenRecently?: boolean
style?: htmlDivProps['style']
onClick?: () => void
tabIndex?: -1 | 0
className?: string
}) {
const {
Expand All @@ -54,6 +55,7 @@ export function Avatar(props: {
small,
wasSeenRecently,
onClick,
tabIndex,
className,
} = props

Expand All @@ -73,18 +75,23 @@ export function Avatar(props: {
className
)}
onClick={onClick}
tabIndex={tabIndex}
>
{content}
</div>
)
}

export function AvatarFromContact(
props: { contact: Type.Contact; onClick?: (contact: Type.Contact) => void },
props: {
contact: Type.Contact
onClick?: (contact: Type.Contact) => void
tabIndex?: -1 | 0
},
large?: boolean,
small?: boolean
) {
const { contact, onClick } = props
const { contact, onClick, tabIndex } = props
return (
<Avatar
avatarPath={contact.profileImage || undefined}
Expand All @@ -94,6 +101,7 @@ export function AvatarFromContact(
large={large === true}
small={small === true}
onClick={() => onClick && onClick(contact)}
tabIndex={tabIndex}
/>
)
}
Expand Down
2 changes: 1 addition & 1 deletion packages/frontend/src/components/chat/ChatListItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ const Message = React.memo<
}}
/>
)}
{message2React(summaryText2 || '', true)}
{message2React(summaryText2 || '', true, -1)}
</div>
{isContactRequest && (
<div className='label'>
Expand Down
2 changes: 1 addition & 1 deletion packages/frontend/src/components/composer/Composer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -339,7 +339,7 @@ const Composer = forwardRef<
<div className='upper-bar'>
{draftState.quote !== null && (
<div className='attachment-quote-section is-quote'>
<Quote quote={draftState.quote} />
<Quote quote={draftState.quote} tabIndex={0} />
<CloseButton onClick={removeQuote} />
</div>
)}
Expand Down
14 changes: 12 additions & 2 deletions packages/frontend/src/components/message/EmptyChatMessage.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import React from 'react'
import React, { useRef } from 'react'
import { C } from '@deltachat/jsonrpc-client'

import useTranslationFunction from '../../hooks/useTranslationFunction'

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

type Props = {
chat: T.FullChat
Expand All @@ -12,6 +13,9 @@ type Props = {
export default function EmptyChatMessage({ chat }: Props) {
const tx = useTranslationFunction()

const ref = useRef<HTMLLIElement>(null)
const rovingTabindex = useRovingTabindex(ref)

let emptyChatMessage = tx('chat_new_one_to_one_hint', [chat.name, chat.name])

if (chat.chatType === C.DC_CHAT_TYPE_BROADCAST) {
Expand All @@ -29,7 +33,13 @@ export default function EmptyChatMessage({ chat }: Props) {
}

return (
<li>
<li
ref={ref}
className={rovingTabindex.className}
tabIndex={rovingTabindex.tabIndex}
onKeyDown={rovingTabindex.onKeydown}
onFocus={rovingTabindex.setAsActiveElement}
>
<div className='info-message big'>
<div className='bubble'>{emptyChatMessage}</div>
</div>
Expand Down
19 changes: 17 additions & 2 deletions packages/frontend/src/components/message/Link.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,11 @@ function isDomainTrusted(domain: string): boolean {
export const LabeledLink = ({
label,
destination,
tabIndex,
}: {
label: string | JSX.Element | JSX.Element[]
destination: LinkDestination
tabIndex: -1 | 0
}) => {
const { openDialog } = useDialog()
const openLinkSafely = useOpenLinkSafely()
Expand Down Expand Up @@ -84,6 +86,7 @@ export const LabeledLink = ({
x-target-url={target}
title={realUrl}
onClick={onClick}
tabIndex={tabIndex}
onContextMenu={ev => ((ev as any).t = ev.currentTarget)}
>
{label}
Expand Down Expand Up @@ -164,7 +167,13 @@ function LabeledLinkConfirmationDialog(
)
}

export const Link = ({ destination }: { destination: LinkDestination }) => {
export const Link = ({
destination,
tabIndex,
}: {
destination: LinkDestination
tabIndex: -1 | 0
}) => {
const { openDialog } = useDialog()
const openLinkSafely = useOpenLinkSafely()
const accountId = selectedAccountId()
Expand Down Expand Up @@ -193,7 +202,13 @@ export const Link = ({ destination }: { destination: LinkDestination }) => {
}

return (
<a href='#' x-target-url={asciiUrl} title={asciiUrl} onClick={onClick}>
<a
href='#'
x-target-url={asciiUrl}
title={asciiUrl}
onClick={onClick}
tabIndex={tabIndex}
>
{target}
</a>
)
Expand Down
Loading

0 comments on commit 69a0780

Please sign in to comment.