fix(costs): row buttons, settle-up rows and share toggles say what they are about to a screen reader

A screen reader lists a page's buttons together, and the costs views gave
it twenty "Edit" and "Delete" buttons with nothing to tell them apart.

- Edit and delete on an expense name the expense ("Edit: Dinner"), edit
  and undo on a payment name who paid whom, on desktop and phone.
- A settle-up row shows only initials in its avatars, its names lived in a
  tooltip; the row now carries them for assistive tech too, and its Settle
  button says who pays whom and how much.
- The search field of every searchable select (currency, time zone,
  category) was read out as "..."; it now says Search in the user's
  language.
- The public link toggles on the phone share sheet and the journey share
  toggles say whether they are on (aria-pressed), not only by colour.
- Japanese still said "you" in the settle-up lines.

Refs #2604
This commit is contained in:
Maurice
2026-10-12 15:45:56 +02:00
parent dbbbd07b1c
commit 35eba21128
14 changed files with 111 additions and 63 deletions
@@ -71,7 +71,7 @@ describe('CustomSelect', () => {
render(<CustomSelect value="" onChange={onChange} options={OPTIONS} searchable={true} />);
await user.click(screen.getByRole('button')); // open
const searchInput = screen.getByPlaceholderText('...');
const searchInput = screen.getByPlaceholderText('common.search');
await user.type(searchInput, 'ban');
// Only Banana should remain, Apple and Cherry should be filtered out
@@ -257,8 +257,8 @@ describe('CustomSelect', () => {
render(<CustomSelect ariaLabel="Choose fruit" value="" onChange={onChange} options={OPTIONS} searchable />);
const trigger = screen.getByRole('button', { name: 'Choose fruit' });
await user.click(trigger);
expect(screen.getByPlaceholderText('...')).toHaveFocus();
await user.type(screen.getByPlaceholderText('...'), 'ban');
expect(screen.getByPlaceholderText('common.search')).toHaveFocus();
await user.type(screen.getByPlaceholderText('common.search'), 'ban');
await user.keyboard('{ArrowDown}{Enter}');
expect(onChange).toHaveBeenCalledWith('banana');
expect(trigger).toHaveFocus();
@@ -2,6 +2,7 @@ import React, { useState, useRef, useEffect, useId } from 'react'
import { createPortal } from 'react-dom'
import { ChevronDown, Check } from 'lucide-react'
import { useAnchoredPosition, scrollAnchorIntoView } from '../../hooks/useAnchoredPosition'
import { useTranslation } from '../../i18n'
interface SelectOption {
// Callers use both string keys and numeric ids (e.g. day/place ids) as values, and
@@ -91,6 +92,7 @@ export default function CustomSelect({
const searchRef = useRef<HTMLInputElement>(null)
const keyboardDirection = useRef<'first' | 'last' | null>(null)
const menuId = useId()
const { t } = useTranslation()
// Follows the trigger while the sheet scrolls and while the on-screen keyboard
// resizes the viewport, instead of freezing at the rect measured on open (#1999).
@@ -258,7 +260,8 @@ export default function CustomSelect({
type="text"
value={search}
onChange={e => setSearch(e.target.value)}
placeholder="..."
placeholder={t('common.search')}
aria-label={t('common.search')}
style={{
width: '100%', border: '1px solid var(--border-secondary)', borderRadius: 6,
padding: '5px 8px', fontSize: 'calc(12px * var(--fs-scale-body, 1))', outline: 'none', fontFamily: 'inherit',