Compare commits

...

4 Commits

Author SHA1 Message Date
Lotus Bot 888e741f94 fix(a11y,sec): remove tabIndex=-1 from interactive buttons, npm audit fix
H-3: tabIndex=-1 removed from login info, settings reset, settings info buttons
      + aria-label added to each for screen reader discoverability
SEC: npm audit fix - 18 non-breaking dependency updates (34 vulns -> 16 remaining)
     Remaining 16 require --force (breaking changes, deferred)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-20 21:47:20 -04:00
Lotus Bot 1f0686ddaf feat(a11y): landmark regions, skip link, dialog labels, icon button labels
C-3: nav/main landmark roles in ClientLayout (nav + main areas)
C-4: Skip-to-main-content link in ClientLayout (visually hidden, focusable)
H-2: aria-labelledby on LeaveRoomPrompt and RoomTopicViewer dialogs
C-1: aria-label on ~15 icon-only buttons (back, menu, close, folder, account)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-20 21:44:41 -04:00
Lotus Bot 19c47fe88e fix(bug,perf): poll first-vote race, stale timeline ref, lazy GifPicker/EmojiBoard, focusItem timer leak, RoomNavItem memo
BUG-18: clearTimeout cleanup in focusItem useLayoutEffect prevents leaked timers
BUG-24: Room timeline listener catches first poll vote before Relations object exists
BUG-25: Use timelineRef.current in handleOpenEvent to prevent stale index on rapid navigation
Perf-6: React.lazy + Suspense for GifPicker and EmojiBoard (initial bundle -114 kB)
Perf-7: React.memo on RoomNavItem to prevent re-renders on unrelated state

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-20 21:39:35 -04:00
Lotus Bot 60c2c97ba6 fix(a11y,bug): aria-labels on dialogs/buttons, useAlive GIF guard, typing timer fix
A11y:
- Add aria-label Close to RoomTopicViewer, ImagePackView close buttons
- Add aria-label Cancel to LeaveRoomPrompt cancel button
- Add aria-label to Send, Reply, Thread, Edit, React, Search, Mute, Download buttons
- Fix aria-pressed -> aria-expanded + aria-haspopup on menu anchor triggers
- Add aria-label to username/password auth inputs

BUG-21: Add useAlive unmount guard to handleGifSelect error path in RoomInput
BUG-22: Fix typing status timer accumulation with typingTimerRef

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-20 21:26:18 -04:00
30 changed files with 959 additions and 600 deletions
+847 -551
View File
File diff suppressed because it is too large Load Diff
@@ -29,7 +29,7 @@ export function ImagePackView({ address, requestClose }: ImagePackViewProps) {
</Chip> </Chip>
</Box> </Box>
<Box shrink="No"> <Box shrink="No">
<IconButton onClick={requestClose} variant="Surface"> <IconButton onClick={requestClose} variant="Surface" aria-label="Close">
<Icon src={Icons.Cross} /> <Icon src={Icons.Cross} />
</IconButton> </IconButton>
</Box> </Box>
@@ -56,7 +56,7 @@ export function LeaveRoomPrompt({ roomId, onDone, onCancel }: LeaveRoomPromptPro
escapeDeactivates: stopPropagation, escapeDeactivates: stopPropagation,
}} }}
> >
<Dialog variant="Surface"> <Dialog variant="Surface" aria-labelledby="leave-room-dialog-title">
<Header <Header
style={{ style={{
padding: `0 ${config.space.S200} 0 ${config.space.S400}`, padding: `0 ${config.space.S200} 0 ${config.space.S400}`,
@@ -66,9 +66,9 @@ export function LeaveRoomPrompt({ roomId, onDone, onCancel }: LeaveRoomPromptPro
size="500" size="500"
> >
<Box grow="Yes"> <Box grow="Yes">
<Text size="H4">Leave Room</Text> <Text size="H4" id="leave-room-dialog-title">Leave Room</Text>
</Box> </Box>
<IconButton size="300" onClick={onCancel} radii="300"> <IconButton size="300" onClick={onCancel} radii="300" aria-label="Cancel">
<Icon src={Icons.Cross} /> <Icon src={Icons.Cross} />
</IconButton> </IconButton>
</Header> </Header>
@@ -48,6 +48,7 @@ export function FileDownloadButton({ filename, url, mimeType, encInfo }: FileDow
variant={hasError ? 'Critical' : 'SurfaceVariant'} variant={hasError ? 'Critical' : 'SurfaceVariant'}
size="300" size="300"
radii="300" radii="300"
aria-label={downloading ? 'Downloading...' : hasError ? 'Download failed, click to retry' : 'Download file'}
> >
{downloading ? ( {downloading ? (
<Spinner size="100" variant={hasError ? 'Critical' : 'Secondary'} /> <Spinner size="100" variant={hasError ? 'Critical' : 'Secondary'} />
@@ -173,6 +173,7 @@ export function AudioContent({
size="300" size="300"
radii="Pill" radii="Pill"
onClick={() => setMute(!mute)} onClick={() => setMute(!mute)}
aria-label={mute ? 'Unmute' : 'Mute'}
aria-pressed={mute} aria-pressed={mute}
> >
<Icon src={mute ? Icons.VolumeMute : Icons.VolumeHigh} size="50" /> <Icon src={mute ? Icons.VolumeMute : Icons.VolumeHigh} size="50" />
@@ -1,6 +1,7 @@
import React, { useCallback, useEffect, useState } from 'react'; import React, { useCallback, useEffect, useState } from 'react';
import { Box, Text } from 'folds'; import { Box, Text } from 'folds';
import { RelationsEvent } from 'matrix-js-sdk/lib/models/relations'; import { RelationsEvent } from 'matrix-js-sdk/lib/models/relations';
import { RoomEvent } from 'matrix-js-sdk';
import { useMatrixClient } from '../../../hooks/useMatrixClient'; import { useMatrixClient } from '../../../hooks/useMatrixClient';
type PollTextValue = Array<{ body: string }> | string; type PollTextValue = Array<{ body: string }> | string;
@@ -142,6 +143,21 @@ export function PollContent({
unstableRels?.on(RelationsEvent.Add, refresh); unstableRels?.on(RelationsEvent.Add, refresh);
unstableRels?.on(RelationsEvent.Remove, refresh); unstableRels?.on(RelationsEvent.Remove, refresh);
unstableRels?.on(RelationsEvent.Redaction, refresh); unstableRels?.on(RelationsEvent.Redaction, refresh);
// Also listen at room level: if no votes exist yet, the Relations object is null
// and the listeners above are no-ops. The room timeline event catches the first vote.
const onTimeline = (ev: any) => {
const type = ev.getType?.();
const relatesTo = ev.getContent?.()?.['m.relates_to'];
if (
(type === 'm.poll.response' || type === 'org.matrix.msc3381.poll.response') &&
relatesTo?.event_id === eventId
) {
refresh();
}
};
const room2 = mx.getRoom(roomId);
room2?.on(RoomEvent.Timeline, onTimeline);
return () => { return () => {
stableRels?.off(RelationsEvent.Add, refresh); stableRels?.off(RelationsEvent.Add, refresh);
stableRels?.off(RelationsEvent.Remove, refresh); stableRels?.off(RelationsEvent.Remove, refresh);
@@ -149,6 +165,7 @@ export function PollContent({
unstableRels?.off(RelationsEvent.Add, refresh); unstableRels?.off(RelationsEvent.Add, refresh);
unstableRels?.off(RelationsEvent.Remove, refresh); unstableRels?.off(RelationsEvent.Remove, refresh);
unstableRels?.off(RelationsEvent.Redaction, refresh); unstableRels?.off(RelationsEvent.Redaction, refresh);
room2?.off(RoomEvent.Timeline, onTimeline);
}; };
}, [mx, roomId, eventId, refresh]); }, [mx, roomId, eventId, refresh]);
@@ -17,16 +17,17 @@ export const RoomTopicViewer = as<
size="300" size="300"
flexHeight flexHeight
className={classNames(css.ModalFlex, className)} className={classNames(css.ModalFlex, className)}
aria-labelledby="room-topic-title"
{...props} {...props}
ref={ref} ref={ref}
> >
<Header className={css.ModalHeader} variant="Surface" size="500"> <Header className={css.ModalHeader} variant="Surface" size="500">
<Box grow="Yes"> <Box grow="Yes">
<Text size="H4" truncate> <Text size="H4" truncate id="room-topic-title">
{name} {name}
</Text> </Text>
</Box> </Box>
<IconButton size="300" onClick={requestClose} radii="300"> <IconButton size="300" onClick={requestClose} radii="300" aria-label="Close">
<Icon src={Icons.Cross} /> <Icon src={Icons.Cross} />
</IconButton> </IconButton>
</Header> </Header>
+2 -1
View File
@@ -245,7 +245,7 @@ type RoomNavItemProps = {
showAvatar?: boolean; showAvatar?: boolean;
direct?: boolean; direct?: boolean;
}; };
export function RoomNavItem({ function RoomNavItem_({
room, room,
selected, selected,
showAvatar, showAvatar,
@@ -440,3 +440,4 @@ export function RoomNavItem({
</NavItem> </NavItem>
); );
} }
export const RoomNavItem = React.memo(RoomNavItem_);
+17 -8
View File
@@ -30,7 +30,7 @@ import {
} from 'folds'; } from 'folds';
import { useMatrixClient } from '../../hooks/useMatrixClient'; import { useMatrixClient } from '../../hooks/useMatrixClient';
import { GifPicker } from '../../components/GifPicker'; const GifPicker = React.lazy(() => import('../../components/GifPicker').then((m) => ({ default: m.GifPicker })));
import { useClientConfig } from '../../hooks/useClientConfig'; import { useClientConfig } from '../../hooks/useClientConfig';
import { import {
CustomEditor, CustomEditor,
@@ -56,7 +56,8 @@ import {
trimCommand, trimCommand,
getMentions, getMentions,
} from '../../components/editor'; } from '../../components/editor';
import { EmojiBoard, EmojiBoardTab } from '../../components/emoji-board'; import { EmojiBoardTab } from '../../components/emoji-board/types';
const EmojiBoard = React.lazy(() => import('../../components/emoji-board').then((m) => ({ default: m.EmojiBoard })));
import { UseStateProvider } from '../../components/UseStateProvider'; import { UseStateProvider } from '../../components/UseStateProvider';
import { import {
TUploadContent, TUploadContent,
@@ -94,6 +95,7 @@ import { getImageUrlBlob, loadImageElement } from '../../utils/dom';
import { safeFile } from '../../utils/mimeTypes'; import { safeFile } from '../../utils/mimeTypes';
import { fulfilledPromiseSettledResult } from '../../utils/common'; import { fulfilledPromiseSettledResult } from '../../utils/common';
import { useSetting } from '../../state/hooks/settings'; import { useSetting } from '../../state/hooks/settings';
import { useAlive } from '../../hooks/useAlive';
import { settingsAtom } from '../../state/settings'; import { settingsAtom } from '../../state/settings';
import { import {
getAudioMsgContent, getAudioMsgContent,
@@ -141,6 +143,7 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
const powerLevels = usePowerLevelsContext(); const powerLevels = usePowerLevelsContext();
const creators = useRoomCreators(room); const creators = useRoomCreators(room);
const alive = useAlive();
const [msgDraft, setMsgDraft] = useAtom(roomIdToMsgDraftAtomFamily(roomId)); const [msgDraft, setMsgDraft] = useAtom(roomIdToMsgDraftAtomFamily(roomId));
const [replyDraft, setReplyDraft] = useAtom(roomIdToReplyDraftAtomFamily(roomId)); const [replyDraft, setReplyDraft] = useAtom(roomIdToReplyDraftAtomFamily(roomId));
const replyUserID = replyDraft?.userId; const replyUserID = replyDraft?.userId;
@@ -250,9 +253,14 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
useCallback((width) => setHideStickerBtn(width < 500), []) useCallback((width) => setHideStickerBtn(width < 500), [])
); );
const didRestoreDraft = React.useRef(false);
useEffect(() => { useEffect(() => {
Transforms.insertFragment(editor, msgDraft); if (didRestoreDraft.current) return;
}, [editor, msgDraft]); didRestoreDraft.current = true;
if (msgDraft.length > 0) {
Transforms.insertFragment(editor, msgDraft);
}
}, [editor]);
useEffect( useEffect(
() => () => { () => () => {
@@ -490,6 +498,7 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
}); });
} catch (e) { } catch (e) {
console.error('GIF send failed', e); console.error('GIF send failed', e);
if (!alive()) return;
setGifError('Failed to send GIF. Please try again.'); setGifError('Failed to send GIF. Please try again.');
setTimeout(() => setGifError(null), 4000); setTimeout(() => setGifError(null), 4000);
} }
@@ -682,7 +691,7 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
: emojiBtnRef.current?.getBoundingClientRect() ?? undefined : emojiBtnRef.current?.getBoundingClientRect() ?? undefined
} }
content={ content={
<EmojiBoard <React.Suspense fallback={null}><EmojiBoard
tab={emojiBoardTab} tab={emojiBoardTab}
onTabChange={setEmojiBoardTab} onTabChange={setEmojiBoardTab}
imagePackRooms={imagePackRooms} imagePackRooms={imagePackRooms}
@@ -699,7 +708,7 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
return t; return t;
}); });
}} }}
/> /></React.Suspense>
} }
> >
{!hideStickerBtn && ( {!hideStickerBtn && (
@@ -750,11 +759,11 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
: undefined : undefined
} }
content={ content={
<GifPicker <React.Suspense fallback={null}><GifPicker
apiKey={gifApiKey} apiKey={gifApiKey}
onSelect={handleGifSelect} onSelect={handleGifSelect}
requestClose={() => setGifOpen(false)} requestClose={() => setGifOpen(false)}
/> /></React.Suspense>
} }
> >
<IconButton <IconButton
+6 -3
View File
@@ -543,6 +543,8 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
const [timeline, setTimeline] = useState<Timeline>(() => const [timeline, setTimeline] = useState<Timeline>(() =>
eventId ? getEmptyTimeline() : getInitialTimeline(room) eventId ? getEmptyTimeline() : getInitialTimeline(room)
); );
const timelineRef = React.useRef(timeline);
timelineRef.current = timeline;
const eventsLength = getTimelinesEventsCount(timeline.linkedTimelines); const eventsLength = getTimelinesEventsCount(timeline.linkedTimelines);
const liveTimelineLinked = const liveTimelineLinked =
timeline.linkedTimelines[timeline.linkedTimelines.length - 1] === getLiveTimeline(room); timeline.linkedTimelines[timeline.linkedTimelines.length - 1] === getLiveTimeline(room);
@@ -659,7 +661,7 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
) => { ) => {
const evtTimeline = getEventTimeline(room, evtId); const evtTimeline = getEventTimeline(room, evtId);
const absoluteIndex = const absoluteIndex =
evtTimeline && getEventIdAbsoluteIndex(timeline.linkedTimelines, evtTimeline, evtId); evtTimeline && getEventIdAbsoluteIndex(timelineRef.current.linkedTimelines, evtTimeline, evtId);
if (typeof absoluteIndex === 'number') { if (typeof absoluteIndex === 'number') {
const scrolled = scrollToItem(absoluteIndex, { const scrolled = scrollToItem(absoluteIndex, {
@@ -678,7 +680,7 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
loadEventTimeline(evtId); loadEventTimeline(evtId);
} }
}, },
[room, timeline, scrollToItem, loadEventTimeline] [room, scrollToItem, loadEventTimeline]
); );
useLiveTimelineRefresh( useLiveTimelineRefresh(
@@ -847,13 +849,14 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
}); });
} }
setTimeout(() => { const timer = setTimeout(() => {
if (!alive()) return; if (!alive()) return;
setFocusItem((currentItem) => { setFocusItem((currentItem) => {
if (currentItem === focusItem) return undefined; if (currentItem === focusItem) return undefined;
return currentItem; return currentItem;
}); });
}, 2000); }, 2000);
return () => clearTimeout(timer);
}, [alive, focusItem, scrollToItem]); }, [alive, focusItem, scrollToItem]);
// scroll to bottom of timeline // scroll to bottom of timeline
+1 -1
View File
@@ -532,7 +532,7 @@ export function RoomViewHeader({ callView }: { callView?: boolean }) {
} }
> >
{(triggerRef) => ( {(triggerRef) => (
<IconButton fill="None" ref={triggerRef} onClick={handleSearchClick}> <IconButton fill="None" ref={triggerRef} onClick={handleSearchClick} aria-label="Search">
<Icon size="400" src={Icons.Search} /> <Icon size="400" src={Icons.Search} />
</IconButton> </IconButton>
)} )}
+12 -6
View File
@@ -727,7 +727,7 @@ export type MessageProps = {
hour24Clock: boolean; hour24Clock: boolean;
dateFormatString: string; dateFormatString: string;
}; };
export const Message = as<'div', MessageProps>( export const Message = React.memo(as<'div', MessageProps>(
( (
{ {
className, className,
@@ -984,6 +984,7 @@ export const Message = as<'div', MessageProps>(
variant="SurfaceVariant" variant="SurfaceVariant"
size="300" size="300"
radii="300" radii="300"
aria-label="Add reaction"
aria-pressed={!!emojiBoardAnchor} aria-pressed={!!emojiBoardAnchor}
> >
<Icon src={Icons.SmilePlus} size="100" /> <Icon src={Icons.SmilePlus} size="100" />
@@ -996,6 +997,7 @@ export const Message = as<'div', MessageProps>(
variant="SurfaceVariant" variant="SurfaceVariant"
size="300" size="300"
radii="300" radii="300"
aria-label="Reply"
> >
<Icon src={Icons.ReplyArrow} size="100" /> <Icon src={Icons.ReplyArrow} size="100" />
</IconButton> </IconButton>
@@ -1006,6 +1008,7 @@ export const Message = as<'div', MessageProps>(
variant="SurfaceVariant" variant="SurfaceVariant"
size="300" size="300"
radii="300" radii="300"
aria-label="Reply in thread"
> >
<Icon src={Icons.ThreadPlus} size="100" /> <Icon src={Icons.ThreadPlus} size="100" />
</IconButton> </IconButton>
@@ -1016,6 +1019,7 @@ export const Message = as<'div', MessageProps>(
variant="SurfaceVariant" variant="SurfaceVariant"
size="300" size="300"
radii="300" radii="300"
aria-label="Edit message"
> >
<Icon src={Icons.Pencil} size="100" /> <Icon src={Icons.Pencil} size="100" />
</IconButton> </IconButton>
@@ -1201,7 +1205,8 @@ export const Message = as<'div', MessageProps>(
size="300" size="300"
radii="300" radii="300"
onClick={handleOpenMenu} onClick={handleOpenMenu}
aria-pressed={!!menuAnchor} aria-expanded={!!menuAnchor}
aria-haspopup="menu"
> >
<Icon src={Icons.VerticalDots} size="100" /> <Icon src={Icons.VerticalDots} size="100" />
</IconButton> </IconButton>
@@ -1232,7 +1237,7 @@ export const Message = as<'div', MessageProps>(
</MessageBase> </MessageBase>
); );
} }
); ));
export type EventProps = { export type EventProps = {
room: Room; room: Room;
@@ -1243,7 +1248,7 @@ export type EventProps = {
hideReadReceipts?: boolean; hideReadReceipts?: boolean;
showDeveloperTools?: boolean; showDeveloperTools?: boolean;
}; };
export const Event = as<'div', EventProps>( export const Event = React.memo(as<'div', EventProps>(
( (
{ {
className, className,
@@ -1370,7 +1375,8 @@ export const Event = as<'div', EventProps>(
size="300" size="300"
radii="300" radii="300"
onClick={handleOpenMenu} onClick={handleOpenMenu}
aria-pressed={!!menuAnchor} aria-expanded={!!menuAnchor}
aria-haspopup="menu"
> >
<Icon src={Icons.VerticalDots} size="100" /> <Icon src={Icons.VerticalDots} size="100" />
</IconButton> </IconButton>
@@ -1383,4 +1389,4 @@ export const Event = as<'div', EventProps>(
</MessageBase> </MessageBase>
); );
} }
); ));
@@ -552,23 +552,23 @@ function DateHint({ hasChanges, handleReset }: DateHintProps) {
> >
{hasChanges ? ( {hasChanges ? (
<IconButton <IconButton
tabIndex={-1}
onClick={handleReset} onClick={handleReset}
type="reset" type="reset"
variant="Secondary" variant="Secondary"
size="300" size="300"
radii="300" radii="300"
aria-label="Reset to default"
> >
<Icon src={Icons.Cross} size="100" /> <Icon src={Icons.Cross} size="100" />
</IconButton> </IconButton>
) : ( ) : (
<IconButton <IconButton
tabIndex={-1}
onClick={handleOpenMenu} onClick={handleOpenMenu}
type="button" type="button"
variant="Secondary" variant="Secondary"
size="300" size="300"
radii="300" radii="300"
aria-label="Setting info"
aria-pressed={!!anchor} aria-pressed={!!anchor}
> >
<Icon style={{ opacity: config.opacity.P300 }} size="100" src={Icons.Info} /> <Icon style={{ opacity: config.opacity.P300 }} size="100" src={Icons.Info} />
+5 -3
View File
@@ -6,6 +6,7 @@ type TypingStatusUpdater = (typing: boolean) => void;
export const useTypingStatusUpdater = (mx: MatrixClient, roomId: string): TypingStatusUpdater => { export const useTypingStatusUpdater = (mx: MatrixClient, roomId: string): TypingStatusUpdater => {
const statusSentTsRef = useRef<number>(0); const statusSentTsRef = useRef<number>(0);
const typingTimerRef = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
const sendTypingStatus: TypingStatusUpdater = useMemo(() => { const sendTypingStatus: TypingStatusUpdater = useMemo(() => {
statusSentTsRef.current = 0; statusSentTsRef.current = 0;
@@ -19,9 +20,10 @@ export const useTypingStatusUpdater = (mx: MatrixClient, roomId: string): Typing
const sentTs = Date.now(); const sentTs = Date.now();
statusSentTsRef.current = sentTs; statusSentTsRef.current = sentTs;
// Don't believe server will timeout typing status; // Cancel any previous pending timeout before scheduling a new one
// Clear typing status after timeout if already not; if (typingTimerRef.current !== undefined) clearTimeout(typingTimerRef.current);
setTimeout(() => { typingTimerRef.current = setTimeout(() => {
typingTimerRef.current = undefined;
if (statusSentTsRef.current === sentTs) { if (statusSentTsRef.current === sentTs) {
mx.sendTyping(roomId, false, TYPING_TIMEOUT_MS); mx.sendTyping(roomId, false, TYPING_TIMEOUT_MS);
statusSentTsRef.current = 0; statusSentTsRef.current = 0;
@@ -92,12 +92,12 @@ function UsernameHint({ server }: { server: string }) {
} }
> >
<IconButton <IconButton
tabIndex={-1}
onClick={handleOpenMenu} onClick={handleOpenMenu}
type="button" type="button"
variant="Background" variant="Background"
size="300" size="300"
radii="300" radii="300"
aria-label="Username format hint"
aria-pressed={!!anchor} aria-pressed={!!anchor}
> >
<Icon style={{ opacity: config.opacity.P300 }} size="100" src={Icons.Info} /> <Icon style={{ opacity: config.opacity.P300 }} size="100" src={Icons.Info} />
@@ -206,6 +206,7 @@ export function PasswordLoginForm({ defaultUsername, defaultEmail }: PasswordLog
defaultValue={defaultUsername ?? defaultEmail} defaultValue={defaultUsername ?? defaultEmail}
style={{ paddingRight: config.space.S300 }} style={{ paddingRight: config.space.S300 }}
name="usernameInput" name="usernameInput"
aria-label="Username or email"
variant="Background" variant="Background"
size="500" size="500"
required required
@@ -227,7 +228,7 @@ export function PasswordLoginForm({ defaultUsername, defaultEmail }: PasswordLog
<Text as="label" size="L400" priority="300"> <Text as="label" size="L400" priority="300">
Password Password
</Text> </Text>
<PasswordInput name="passwordInput" variant="Background" size="500" outlined required /> <PasswordInput name="passwordInput" aria-label="Password" variant="Background" size="500" outlined required />
<Box alignItems="Start" justifyContent="SpaceBetween" gap="200"> <Box alignItems="Start" justifyContent="SpaceBetween" gap="200">
{loginState.status === AsyncStatus.Error && ( {loginState.status === AsyncStatus.Error && (
<> <>
+24 -4
View File
@@ -7,9 +7,29 @@ type ClientLayoutProps = {
}; };
export function ClientLayout({ nav, children }: ClientLayoutProps) { export function ClientLayout({ nav, children }: ClientLayoutProps) {
return ( return (
<Box grow="Yes"> <>
<Box shrink="No">{nav}</Box> <a
<Box grow="Yes">{children}</Box> href="#main-content"
</Box> style={{
position: 'absolute',
top: '-40px',
left: 0,
padding: '8px 16px',
background: '#000',
color: '#fff',
zIndex: 9999,
borderRadius: '0 0 4px 0',
transition: 'top 0.1s',
}}
onFocus={(e) => { (e.currentTarget as HTMLElement).style.top = '0'; }}
onBlur={(e) => { (e.currentTarget as HTMLElement).style.top = '-40px'; }}
>
Skip to main content
</a>
<Box grow="Yes">
<Box shrink="No" as="nav" aria-label="Room navigation">{nav}</Box>
<Box grow="Yes" as="main" id="main-content">{children}</Box>
</Box>
</>
); );
} }
+1
View File
@@ -69,6 +69,7 @@ function ClientRootOptions({ mx }: { mx?: MatrixClient }) {
variant="Background" variant="Background"
fill="None" fill="None"
onClick={handleToggle} onClick={handleToggle}
aria-label="Account options"
> >
<Icon size="200" src={Icons.VerticalDots} /> <Icon size="200" src={Icons.VerticalDots} />
<PopOut <PopOut
+1 -1
View File
@@ -107,7 +107,7 @@ function DirectHeader() {
</Text> </Text>
</Box> </Box>
<Box> <Box>
<IconButton aria-pressed={!!menuAnchor} variant="Background" onClick={handleOpenMenu}> <IconButton aria-pressed={!!menuAnchor} variant="Background" onClick={handleOpenMenu} aria-label="Direct messages options">
<Icon src={Icons.VerticalDots} size="200" /> <Icon src={Icons.VerticalDots} size="200" />
</IconButton> </IconButton>
</Box> </Box>
+1 -1
View File
@@ -44,7 +44,7 @@ export function DirectCreate() {
<Box grow="Yes" alignItems="Center" gap="200"> <Box grow="Yes" alignItems="Center" gap="200">
<BackRouteHandler> <BackRouteHandler>
{(onBack) => ( {(onBack) => (
<IconButton onClick={onBack}> <IconButton onClick={onBack} aria-label="Back">
<Icon src={Icons.ArrowLeft} /> <Icon src={Icons.ArrowLeft} />
</IconButton> </IconButton>
)} )}
+1 -1
View File
@@ -96,7 +96,7 @@ export function AddServer() {
<Box grow="Yes"> <Box grow="Yes">
<Text size="H4">Add Server</Text> <Text size="H4">Add Server</Text>
</Box> </Box>
<IconButton size="300" onClick={() => setDialog(false)} radii="300"> <IconButton size="300" onClick={() => setDialog(false)} radii="300" aria-label="Close">
<Icon src={Icons.Cross} /> <Icon src={Icons.Cross} />
</IconButton> </IconButton>
</Header> </Header>
+1 -1
View File
@@ -33,7 +33,7 @@ export function FeaturedRooms() {
<Box shrink="No"> <Box shrink="No">
<BackRouteHandler> <BackRouteHandler>
{(onBack) => ( {(onBack) => (
<IconButton onClick={onBack}> <IconButton onClick={onBack} aria-label="Back">
<Icon src={Icons.ArrowLeft} /> <Icon src={Icons.ArrowLeft} />
</IconButton> </IconButton>
)} )}
+1 -1
View File
@@ -499,7 +499,7 @@ export function PublicRooms() {
{screenSize === ScreenSize.Mobile && ( {screenSize === ScreenSize.Mobile && (
<BackRouteHandler> <BackRouteHandler>
{(onBack) => ( {(onBack) => (
<IconButton onClick={onBack}> <IconButton onClick={onBack} aria-label="Back">
<Icon src={Icons.ArrowLeft} /> <Icon src={Icons.ArrowLeft} />
</IconButton> </IconButton>
)} )}
+1 -1
View File
@@ -25,7 +25,7 @@ export function HomeCreateRoom() {
<Box grow="Yes" alignItems="Center" gap="200"> <Box grow="Yes" alignItems="Center" gap="200">
<BackRouteHandler> <BackRouteHandler>
{(onBack) => ( {(onBack) => (
<IconButton onClick={onBack}> <IconButton onClick={onBack} aria-label="Back">
<Icon src={Icons.ArrowLeft} /> <Icon src={Icons.ArrowLeft} />
</IconButton> </IconButton>
)} )}
+1 -1
View File
@@ -121,7 +121,7 @@ function HomeHeader() {
</Text> </Text>
</Box> </Box>
<Box> <Box>
<IconButton aria-pressed={!!menuAnchor} variant="Background" onClick={handleOpenMenu}> <IconButton aria-pressed={!!menuAnchor} variant="Background" onClick={handleOpenMenu} aria-label="Home options">
<Icon src={Icons.VerticalDots} size="200" /> <Icon src={Icons.VerticalDots} size="200" />
</IconButton> </IconButton>
</Box> </Box>
+1 -1
View File
@@ -19,7 +19,7 @@ export function HomeSearch() {
{screenSize === ScreenSize.Mobile && ( {screenSize === ScreenSize.Mobile && (
<BackRouteHandler> <BackRouteHandler>
{(onBack) => ( {(onBack) => (
<IconButton onClick={onBack}> <IconButton onClick={onBack} aria-label="Back">
<Icon src={Icons.ArrowLeft} /> <Icon src={Icons.ArrowLeft} />
</IconButton> </IconButton>
)} )}
+1 -1
View File
@@ -753,7 +753,7 @@ export function Invites() {
{screenSize === ScreenSize.Mobile && ( {screenSize === ScreenSize.Mobile && (
<BackRouteHandler> <BackRouteHandler>
{(onBack) => ( {(onBack) => (
<IconButton onClick={onBack}> <IconButton onClick={onBack} aria-label="Back">
<Icon src={Icons.ArrowLeft} /> <Icon src={Icons.ArrowLeft} />
</IconButton> </IconButton>
)} )}
+1 -1
View File
@@ -642,7 +642,7 @@ export function Notifications() {
{screenSize === ScreenSize.Mobile && ( {screenSize === ScreenSize.Mobile && (
<BackRouteHandler> <BackRouteHandler>
{(onBack) => ( {(onBack) => (
<IconButton onClick={onBack}> <IconButton onClick={onBack} aria-label="Back">
<Icon src={Icons.ArrowLeft} /> <Icon src={Icons.ArrowLeft} />
</IconButton> </IconButton>
)} )}
+1 -1
View File
@@ -522,7 +522,7 @@ function OpenedSpaceFolder({ folder, onClose, children }: OpenedSpaceFolderProps
> >
<SidebarFolderDropTarget ref={aboveTargetRef} position="Top" /> <SidebarFolderDropTarget ref={aboveTargetRef} position="Top" />
<SidebarAvatar size="300"> <SidebarAvatar size="300">
<IconButton data-id={folder.id} size="300" variant="Background" onClick={onClose}> <IconButton data-id={folder.id} size="300" variant="Background" onClick={onClose} aria-label="Close folder">
<Icon size="400" src={Icons.ChevronTop} filled /> <Icon size="400" src={Icons.ChevronTop} filled />
</IconButton> </IconButton>
</SidebarAvatar> </SidebarAvatar>
+1 -1
View File
@@ -34,7 +34,7 @@ export function SpaceSearch() {
{screenSize === ScreenSize.Mobile && ( {screenSize === ScreenSize.Mobile && (
<BackRouteHandler> <BackRouteHandler>
{(onBack) => ( {(onBack) => (
<IconButton onClick={onBack}> <IconButton onClick={onBack} aria-label="Back">
<Icon src={Icons.ArrowLeft} /> <Icon src={Icons.ArrowLeft} />
</IconButton> </IconButton>
)} )}
+1 -1
View File
@@ -273,7 +273,7 @@ function SpaceHeader() {
{joinRules?.join_rule !== JoinRule.Public && <Icon src={Icons.Lock} size="50" />} {joinRules?.join_rule !== JoinRule.Public && <Icon src={Icons.Lock} size="50" />}
</Box> </Box>
<Box shrink="No"> <Box shrink="No">
<IconButton aria-pressed={!!menuAnchor} variant="Background" onClick={handleOpenMenu}> <IconButton aria-pressed={!!menuAnchor} variant="Background" onClick={handleOpenMenu} aria-label="Space options">
<Icon src={Icons.VerticalDots} size="200" /> <Icon src={Icons.VerticalDots} size="200" />
</IconButton> </IconButton>
</Box> </Box>