From 584da83bf041ba9583a974f7c942d02fc363770b Mon Sep 17 00:00:00 2001 From: Lotus Bot Date: Wed, 20 May 2026 21:54:33 -0400 Subject: [PATCH] feat(a11y,perf): comprehensive icon button labels, toolbar a11y, timeline binary search MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A11y C-1: aria-label on 30+ remaining icon-only buttons across: - settings panels (close, reset, info, expand, remove, undo) - editor toolbar (bold, italic, underline, strike, code, spoiler, blockquote, code block, ordered/unordered list, headings 1-3) - auth stages (cancel buttons in SSO, Password stages) - device verification (cancel buttons) - password input (show/hide toggle with dynamic label) - event readers, account data editor close buttons - global emoji packs (add/remove buttons) Perf-5: Replace O(N×T) getTimelineAndBaseIndex scan with precomputed binary search (timelineSegments useMemo) — O(log T) per visible message render Co-Authored-By: Claude Sonnet 4.6 --- src/app/components/AccountDataEditor.tsx | 2 +- src/app/components/DeviceVerification.tsx | 2 +- .../components/DeviceVerificationSetup.tsx | 2 +- src/app/components/editor/Toolbar.tsx | 52 ++++++++++++++----- .../components/event-readers/EventReaders.tsx | 2 +- .../password-input/PasswordInput.tsx | 1 + .../components/uia-stages/PasswordStage.tsx | 2 +- src/app/components/uia-stages/SSOStage.tsx | 2 +- src/app/features/room/RoomTimeline.tsx | 26 +++++++++- src/app/features/settings/Settings.tsx | 2 +- src/app/features/settings/about/About.tsx | 2 +- src/app/features/settings/account/Account.tsx | 2 +- .../settings/account/IgnoredUserList.tsx | 1 + src/app/features/settings/account/Profile.tsx | 3 +- .../settings/developer-tools/DevelopTools.tsx | 2 +- .../features/settings/devices/DeviceTile.tsx | 2 + src/app/features/settings/devices/Devices.tsx | 2 +- .../settings/devices/Verification.tsx | 1 + .../emojis-stickers/EmojisStickers.tsx | 2 +- .../settings/emojis-stickers/GlobalPacks.tsx | 2 + src/app/features/settings/general/General.tsx | 2 +- .../notifications/KeywordMessages.tsx | 3 +- .../settings/notifications/Notifications.tsx | 2 +- 23 files changed, 89 insertions(+), 30 deletions(-) diff --git a/src/app/components/AccountDataEditor.tsx b/src/app/components/AccountDataEditor.tsx index 2dbaf1f1f..76b73dda2 100644 --- a/src/app/components/AccountDataEditor.tsx +++ b/src/app/components/AccountDataEditor.tsx @@ -294,7 +294,7 @@ export function AccountDataEditor({ - + diff --git a/src/app/components/DeviceVerification.tsx b/src/app/components/DeviceVerification.tsx index d2502ccf7..53875f9fc 100644 --- a/src/app/components/DeviceVerification.tsx +++ b/src/app/components/DeviceVerification.tsx @@ -261,7 +261,7 @@ export function DeviceVerification({ request, onExit }: DeviceVerificationProps) Device Verification - + diff --git a/src/app/components/DeviceVerificationSetup.tsx b/src/app/components/DeviceVerificationSetup.tsx index 433fa6a1d..1e13e2f97 100644 --- a/src/app/components/DeviceVerificationSetup.tsx +++ b/src/app/components/DeviceVerificationSetup.tsx @@ -301,7 +301,7 @@ export const DeviceVerificationSetup = forwardRef Setup Device Verification - + diff --git a/src/app/components/editor/Toolbar.tsx b/src/app/components/editor/Toolbar.tsx index 7d701c424..b6be7b878 100644 --- a/src/app/components/editor/Toolbar.tsx +++ b/src/app/components/editor/Toolbar.tsx @@ -54,8 +54,8 @@ function BtnTooltip({ text, shortCode }: { text: string; shortCode?: string }) { ); } -type MarkButtonProps = { format: MarkType; icon: IconSrc; tooltip: ReactNode }; -export function MarkButton({ format, icon, tooltip }: MarkButtonProps) { +type MarkButtonProps = { format: MarkType; icon: IconSrc; tooltip: ReactNode; label?: string }; +export function MarkButton({ format, icon, tooltip, label }: MarkButtonProps) { const editor = useSlate(); const disableInline = isBlockActive(editor, BlockType.CodeBlock); @@ -76,6 +76,7 @@ export function MarkButton({ format, icon, tooltip }: MarkButtonProps) { variant="SurfaceVariant" onClick={handleClick} aria-pressed={isMarkActive(editor, format)} + aria-label={label} size="400" radii="300" disabled={disableInline} @@ -91,8 +92,9 @@ type BlockButtonProps = { format: BlockType; icon: IconSrc; tooltip: ReactNode; + label?: string; }; -export function BlockButton({ format, icon, tooltip }: BlockButtonProps) { +export function BlockButton({ format, icon, tooltip, label }: BlockButtonProps) { const editor = useSlate(); const handleClick = () => { @@ -108,6 +110,7 @@ export function BlockButton({ format, icon, tooltip }: BlockButtonProps) { variant="SurfaceVariant" onClick={handleClick} aria-pressed={isBlockActive(editor, format)} + aria-label={label} size="400" radii="300" > @@ -165,6 +168,7 @@ export function HeadingBlockButton() { handleMenuSelect(1)} + aria-label="Heading 1" size="400" radii="300" > @@ -180,6 +184,7 @@ export function HeadingBlockButton() { handleMenuSelect(2)} + aria-label="Heading 2" size="400" radii="300" > @@ -195,6 +200,7 @@ export function HeadingBlockButton() { handleMenuSelect(3)} + aria-label="Heading 3" size="400" radii="300" > @@ -271,32 +277,44 @@ export function Toolbar() { } + tooltip={} /> } + tooltip={} /> } + tooltip={} /> } + tooltip={} /> } + tooltip={} /> } + tooltip={} /> @@ -305,22 +323,30 @@ export function Toolbar() { } + tooltip={} /> } + tooltip={} /> } + tooltip={} /> } + tooltip={} /> diff --git a/src/app/components/event-readers/EventReaders.tsx b/src/app/components/event-readers/EventReaders.tsx index 14d36fb28..58b1f56ea 100644 --- a/src/app/components/event-readers/EventReaders.tsx +++ b/src/app/components/event-readers/EventReaders.tsx @@ -73,7 +73,7 @@ export const EventReaders = as<'div', EventReadersProps>( Seen by - + diff --git a/src/app/components/password-input/PasswordInput.tsx b/src/app/components/password-input/PasswordInput.tsx index 4ffe0d831..3e1e278d7 100644 --- a/src/app/components/password-input/PasswordInput.tsx +++ b/src/app/components/password-input/PasswordInput.tsx @@ -28,6 +28,7 @@ export const PasswordInput = forwardRef( variant={visible ? 'Warning' : variant} size="300" radii="300" + aria-label={visible ? 'Hide password' : 'Show password'} > Account Password - + diff --git a/src/app/components/uia-stages/SSOStage.tsx b/src/app/components/uia-stages/SSOStage.tsx index e3d6126a5..2548d1e04 100644 --- a/src/app/components/uia-stages/SSOStage.tsx +++ b/src/app/components/uia-stages/SSOStage.tsx @@ -56,7 +56,7 @@ export function SSOStage({ SSO Login - + diff --git a/src/app/features/room/RoomTimeline.tsx b/src/app/features/room/RoomTimeline.tsx index 436bbe35a..466f0caf4 100644 --- a/src/app/features/room/RoomTimeline.tsx +++ b/src/app/features/room/RoomTimeline.tsx @@ -546,6 +546,17 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli const timelineRef = React.useRef(timeline); timelineRef.current = timeline; const eventsLength = getTimelinesEventsCount(timeline.linkedTimelines); + + // Perf-5: precompute base offsets once per linkedTimelines change instead of O(N×T) scan + const timelineSegments = useMemo>(() => { + let base = 0; + return timeline.linkedTimelines.map((t) => { + const len = t.getEvents().length; + const seg: [number, number, EventTimeline] = [base, len, t]; + base += len; + return seg; + }); + }, [timeline.linkedTimelines]); const liveTimelineLinked = timeline.linkedTimelines[timeline.linkedTimelines.length - 1] === getLiveTimeline(room); const canPaginateBack = @@ -1809,7 +1820,20 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli let newDivider = false; let dayDivider = false; const eventRenderer = (item: number) => { - const [eventTimeline, baseIndex] = getTimelineAndBaseIndex(timeline.linkedTimelines, item); + // Perf-5: O(T) → O(log T) via precomputed segments + let eventTimeline: EventTimeline | undefined; + let baseIndex = 0; + { + let lo = 0; + let hi = timelineSegments.length - 1; + while (lo <= hi) { + const mid = (lo + hi) >>> 1; + const [base, len] = timelineSegments[mid]; + if (item < base) { hi = mid - 1; } + else if (item >= base + len) { lo = mid + 1; } + else { eventTimeline = timelineSegments[mid][2]; baseIndex = base; break; } + } + } if (!eventTimeline) return null; const timelineSet = eventTimeline?.getTimelineSet(); const mEvent = getTimelineEvent(eventTimeline, getTimelineRelativeIndex(item, baseIndex)); diff --git a/src/app/features/settings/Settings.tsx b/src/app/features/settings/Settings.tsx index 5e1a20f4a..46452aa34 100644 --- a/src/app/features/settings/Settings.tsx +++ b/src/app/features/settings/Settings.tsx @@ -141,7 +141,7 @@ export function Settings({ initialPage, requestClose }: SettingsProps) { {screenSize === ScreenSize.Mobile && ( - + )} diff --git a/src/app/features/settings/about/About.tsx b/src/app/features/settings/about/About.tsx index b47acd179..96d21a147 100644 --- a/src/app/features/settings/about/About.tsx +++ b/src/app/features/settings/about/About.tsx @@ -25,7 +25,7 @@ export function About({ requestClose }: AboutProps) { - + diff --git a/src/app/features/settings/account/Account.tsx b/src/app/features/settings/account/Account.tsx index c4b56e475..bf3e60324 100644 --- a/src/app/features/settings/account/Account.tsx +++ b/src/app/features/settings/account/Account.tsx @@ -20,7 +20,7 @@ export function Account({ requestClose }: AccountProps) { - + diff --git a/src/app/features/settings/account/IgnoredUserList.tsx b/src/app/features/settings/account/IgnoredUserList.tsx index 98db9459a..6c7dccc38 100644 --- a/src/app/features/settings/account/IgnoredUserList.tsx +++ b/src/app/features/settings/account/IgnoredUserList.tsx @@ -72,6 +72,7 @@ function IgnoreUserInput({ userList }: { userList: string[] }) { size="300" radii="300" variant="Secondary" + aria-label="Clear" > diff --git a/src/app/features/settings/account/Profile.tsx b/src/app/features/settings/account/Profile.tsx index e982a7992..895012754 100644 --- a/src/app/features/settings/account/Profile.tsx +++ b/src/app/features/settings/account/Profile.tsx @@ -185,7 +185,7 @@ function ProfileAvatar({ profile, userId }: ProfileProps) { Remove Avatar - setAlertRemove(false)} radii="300"> + setAlertRemove(false)} radii="300" aria-label="Cancel"> @@ -278,6 +278,7 @@ function ProfileDisplayName({ profile, userId }: ProfileProps) { size="300" radii="300" variant="Secondary" + aria-label="Reset display name" > diff --git a/src/app/features/settings/developer-tools/DevelopTools.tsx b/src/app/features/settings/developer-tools/DevelopTools.tsx index a3f04567b..63037ac18 100644 --- a/src/app/features/settings/developer-tools/DevelopTools.tsx +++ b/src/app/features/settings/developer-tools/DevelopTools.tsx @@ -51,7 +51,7 @@ export function DeveloperTools({ requestClose }: DeveloperToolsProps) { - + diff --git a/src/app/features/settings/devices/DeviceTile.tsx b/src/app/features/settings/devices/DeviceTile.tsx index 71b684f56..97c284a06 100644 --- a/src/app/features/settings/devices/DeviceTile.tsx +++ b/src/app/features/settings/devices/DeviceTile.tsx @@ -292,6 +292,8 @@ export function DeviceTile({ outlined={deleted} radii="300" onClick={() => setDetails(!details)} + aria-label={details ? 'Collapse device details' : 'Expand device details'} + aria-expanded={details} > diff --git a/src/app/features/settings/devices/Devices.tsx b/src/app/features/settings/devices/Devices.tsx index c957ab318..22a46f396 100644 --- a/src/app/features/settings/devices/Devices.tsx +++ b/src/app/features/settings/devices/Devices.tsx @@ -74,7 +74,7 @@ export function Devices({ requestClose }: DevicesProps) { - + diff --git a/src/app/features/settings/devices/Verification.tsx b/src/app/features/settings/devices/Verification.tsx index 6c7eab17b..5223df2cc 100644 --- a/src/app/features/settings/devices/Verification.tsx +++ b/src/app/features/settings/devices/Verification.tsx @@ -293,6 +293,7 @@ export function DeviceVerificationOptions() { size="300" radii="300" onClick={handleMenu} + aria-label="Verification options" > diff --git a/src/app/features/settings/emojis-stickers/EmojisStickers.tsx b/src/app/features/settings/emojis-stickers/EmojisStickers.tsx index 93715120c..85762b4f6 100644 --- a/src/app/features/settings/emojis-stickers/EmojisStickers.tsx +++ b/src/app/features/settings/emojis-stickers/EmojisStickers.tsx @@ -30,7 +30,7 @@ export function EmojisStickers({ requestClose }: EmojisStickersProps) { - + diff --git a/src/app/features/settings/emojis-stickers/GlobalPacks.tsx b/src/app/features/settings/emojis-stickers/GlobalPacks.tsx index a9288728f..6c3edc1ad 100644 --- a/src/app/features/settings/emojis-stickers/GlobalPacks.tsx +++ b/src/app/features/settings/emojis-stickers/GlobalPacks.tsx @@ -373,6 +373,7 @@ export function GlobalPacks({ onViewPack }: GlobalPacksProps) { variant="Critical" onClick={() => handleUndoRemove(address)} disabled={applyingChanges} + aria-label="Undo remove pack" > @@ -383,6 +384,7 @@ export function GlobalPacks({ onViewPack }: GlobalPacksProps) { variant="Secondary" onClick={() => handleRemove(address)} disabled={applyingChanges} + aria-label="Remove pack" > diff --git a/src/app/features/settings/general/General.tsx b/src/app/features/settings/general/General.tsx index 998aa90ef..45e357eac 100644 --- a/src/app/features/settings/general/General.tsx +++ b/src/app/features/settings/general/General.tsx @@ -1184,7 +1184,7 @@ export function General({ requestClose }: GeneralProps) { - + diff --git a/src/app/features/settings/notifications/KeywordMessages.tsx b/src/app/features/settings/notifications/KeywordMessages.tsx index 7f84d607f..5be38c399 100644 --- a/src/app/features/settings/notifications/KeywordMessages.tsx +++ b/src/app/features/settings/notifications/KeywordMessages.tsx @@ -80,6 +80,7 @@ function KeywordInput() { size="300" radii="300" variant="Secondary" + aria-label="Clear keyword input" > @@ -118,7 +119,7 @@ function KeywordCross({ pushRule }: PushRulesProps) { const removing = removeState.status === AsyncStatus.Loading; return ( - + {removing ? : } ); diff --git a/src/app/features/settings/notifications/Notifications.tsx b/src/app/features/settings/notifications/Notifications.tsx index 095a9bba9..e7caae92e 100644 --- a/src/app/features/settings/notifications/Notifications.tsx +++ b/src/app/features/settings/notifications/Notifications.tsx @@ -23,7 +23,7 @@ export function Notifications({ requestClose }: NotificationsProps) { - +