fix(mobile): 44px touch targets for primary call/thread/mod controls (P1)
Deep-audit follow-up. New shared MobileTouchTarget class (@media <=750px -> minWidth/minHeight 44px) applied via className to the primary interactive controls folds renders below 44px: - in-call control bar (7 buttons) + persistent call-status bar (4 buttons) - thread "N replies" chip - knock Approve/Deny buttons - server-ACL entry remove button folds size variants set only padding (no width/height/min-*), so the class raises the hit-area floor to 44px with the icon/label staying centered at its normal size; desktop is untouched (@media-gated). Verified by two review passes (no distortion, no layout overflow, counts exact). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -5,6 +5,7 @@ import { StatusDivider } from './components';
|
||||
import { CallEmbed, useCallControlState } from '../../plugins/call';
|
||||
import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback';
|
||||
import { callEmbedAtom } from '../../state/callEmbed';
|
||||
import { MobileTouchTarget } from '../../styles/mobile.css';
|
||||
|
||||
type MicrophoneButtonProps = {
|
||||
enabled: boolean;
|
||||
@@ -31,6 +32,7 @@ function MicrophoneButton({ enabled, onToggle, disabled }: MicrophoneButtonProps
|
||||
fill="Soft"
|
||||
radii="300"
|
||||
size="300"
|
||||
className={MobileTouchTarget}
|
||||
onClick={toggleMic}
|
||||
outlined
|
||||
disabled={disabled || loading}
|
||||
@@ -66,6 +68,7 @@ function SoundButton({ enabled, onToggle, disabled }: SoundButtonProps) {
|
||||
fill="Soft"
|
||||
radii="300"
|
||||
size="300"
|
||||
className={MobileTouchTarget}
|
||||
onClick={() => onToggle()}
|
||||
aria-label={enabled ? 'Deafen' : 'Undeafen'}
|
||||
aria-pressed={enabled}
|
||||
@@ -108,6 +111,7 @@ function VideoButton({ enabled, onToggle, disabled }: VideoButtonProps) {
|
||||
fill="Soft"
|
||||
radii="300"
|
||||
size="300"
|
||||
className={MobileTouchTarget}
|
||||
onClick={toggleVideo}
|
||||
aria-label={enabled ? 'Stop Video' : 'Start Video'}
|
||||
aria-pressed={enabled}
|
||||
@@ -147,6 +151,7 @@ function ScreenShareButton({ enabled, onToggle, disabled }: ScreenShareButtonPro
|
||||
fill="Soft"
|
||||
radii="300"
|
||||
size="300"
|
||||
className={MobileTouchTarget}
|
||||
onClick={onToggle}
|
||||
aria-label={enabled ? 'Stop Screenshare' : 'Start Screenshare'}
|
||||
aria-pressed={enabled}
|
||||
|
||||
@@ -2,6 +2,7 @@ import React from 'react';
|
||||
import { Icon, IconButton, Icons, Line, Text, Tooltip, TooltipProvider } from 'folds';
|
||||
import { useAtom } from 'jotai';
|
||||
import * as css from './styles.css';
|
||||
import { MobileTouchTarget } from '../../styles/mobile.css';
|
||||
import { callChatAtom } from '../../state/callEmbed';
|
||||
import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback';
|
||||
|
||||
@@ -36,6 +37,7 @@ export function MicrophoneButton({ enabled, onToggle }: MicrophoneButtonProps) {
|
||||
fill="Soft"
|
||||
radii="400"
|
||||
size="400"
|
||||
className={MobileTouchTarget}
|
||||
onClick={toggleMic}
|
||||
aria-label={enabled ? 'Turn Off Microphone' : 'Turn On Microphone'}
|
||||
outlined
|
||||
@@ -70,6 +72,7 @@ export function SoundButton({ enabled, onToggle }: SoundButtonProps) {
|
||||
fill="Soft"
|
||||
radii="400"
|
||||
size="400"
|
||||
className={MobileTouchTarget}
|
||||
onClick={() => onToggle()}
|
||||
aria-label={enabled ? 'Undeafen' : 'Deafen'}
|
||||
outlined
|
||||
@@ -113,6 +116,7 @@ export function VideoButton({ enabled, onToggle, disabled }: VideoButtonProps) {
|
||||
fill="Soft"
|
||||
radii="400"
|
||||
size="400"
|
||||
className={MobileTouchTarget}
|
||||
onClick={toggleVideo}
|
||||
outlined
|
||||
disabled={disabled || loading}
|
||||
@@ -155,6 +159,7 @@ export function ScreenShareButton({ enabled, onToggle }: ScreenShareButtonProps)
|
||||
fill="Soft"
|
||||
radii="400"
|
||||
size="400"
|
||||
className={MobileTouchTarget}
|
||||
onClick={() => onToggle()}
|
||||
aria-label={enabled ? 'Stop Screenshare' : 'Start Screenshare'}
|
||||
outlined
|
||||
@@ -200,6 +205,7 @@ export function FullscreenButton({ isFullscreen, onToggle }: FullscreenButtonPro
|
||||
fill="Soft"
|
||||
radii="400"
|
||||
size="400"
|
||||
className={MobileTouchTarget}
|
||||
onClick={onToggle}
|
||||
aria-label={isFullscreen ? 'Exit Fullscreen' : 'Fullscreen'}
|
||||
aria-pressed={isFullscreen}
|
||||
@@ -234,6 +240,7 @@ export function ScreenshareAudioButton({ muted, onToggle }: ScreenshareAudioButt
|
||||
fill="Soft"
|
||||
radii="400"
|
||||
size="400"
|
||||
className={MobileTouchTarget}
|
||||
onClick={onToggle}
|
||||
aria-label={muted ? 'Unmute Screenshare Audio' : 'Mute Screenshare Audio'}
|
||||
aria-pressed={muted}
|
||||
@@ -266,6 +273,7 @@ export function ChatButton() {
|
||||
fill="Soft"
|
||||
radii="400"
|
||||
size="400"
|
||||
className={MobileTouchTarget}
|
||||
onClick={() => setChat(!chat)}
|
||||
aria-label={chat ? 'Close Chat' : 'Open Chat'}
|
||||
aria-pressed={chat}
|
||||
|
||||
@@ -33,6 +33,7 @@ import { SequenceCard } from '../../components/sequence-card';
|
||||
import { SequenceCardStyle } from '../common-settings/styles.css';
|
||||
import { stopPropagation } from '../../utils/keyboard';
|
||||
import { isValidServerPattern, matchesAnyGlob } from '../../utils/serverAcl';
|
||||
import { MobileTouchTarget } from '../../styles/mobile.css';
|
||||
import { useModalStyle } from '../../hooks/useModalStyle';
|
||||
|
||||
// ── Types ─────────────────────────────────────────────────────────────────────
|
||||
@@ -148,6 +149,7 @@ function ServerList({ label, entries, canEdit, onAdd, onRemove }: ServerListProp
|
||||
size="300"
|
||||
variant="Background"
|
||||
radii="300"
|
||||
className={MobileTouchTarget}
|
||||
aria-label={`Remove ${entry}`}
|
||||
onClick={() => onRemove(i)}
|
||||
style={{ flexShrink: 0 }}
|
||||
|
||||
@@ -30,6 +30,7 @@ import {
|
||||
import { MatrixClient, Room, RoomMember } from 'matrix-js-sdk';
|
||||
import { useVirtualizer } from '@tanstack/react-virtual';
|
||||
import classNames from 'classnames';
|
||||
import { MobileTouchTarget } from '../../styles/mobile.css';
|
||||
import { Membership } from '../../../types/matrix/room';
|
||||
|
||||
import * as css from './MembersDrawer.css';
|
||||
@@ -460,6 +461,7 @@ export function MembersDrawer({ room, members }: MembersDrawerProps) {
|
||||
variant="Success"
|
||||
radii="300"
|
||||
fill="Soft"
|
||||
className={MobileTouchTarget}
|
||||
onClick={() => mx.invite(room.roomId, knockMember.userId)}
|
||||
>
|
||||
<Text size="B300">Approve</Text>
|
||||
@@ -469,6 +471,7 @@ export function MembersDrawer({ room, members }: MembersDrawerProps) {
|
||||
variant="Critical"
|
||||
radii="300"
|
||||
fill="Soft"
|
||||
className={MobileTouchTarget}
|
||||
onClick={() => mx.kick(room.roomId, knockMember.userId)}
|
||||
>
|
||||
<Text size="B300">Deny</Text>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React from 'react';
|
||||
import { Badge, Box, Chip, Icon, Icons, Text, config } from 'folds';
|
||||
import { MatrixEvent, Room } from 'matrix-js-sdk';
|
||||
import { MobileTouchTarget } from '../../../styles/mobile.css';
|
||||
import { useThreadSummary } from '../../../hooks/useThreadSummary';
|
||||
import { useSetting } from '../../../state/hooks/settings';
|
||||
import { settingsAtom } from '../../../state/settings';
|
||||
@@ -31,6 +32,7 @@ export function ThreadSummary({ rootEvent, room, onOpen }: ThreadSummaryProps) {
|
||||
<Chip
|
||||
variant="SurfaceVariant"
|
||||
radii="300"
|
||||
className={MobileTouchTarget}
|
||||
before={<Icon size="50" src={Icons.Thread} />}
|
||||
after={
|
||||
unread > 0 ? <Badge variant="Success" fill="Solid" radii="Pill" size="200" /> : undefined
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import { style } from '@vanilla-extract/css';
|
||||
import { toRem } from 'folds';
|
||||
|
||||
/**
|
||||
* A 44px minimum touch target on phones for otherwise-small interactive controls
|
||||
* (folds `IconButton`/`Chip` at `size="300"`/`"400"` render ~24–40px, below the
|
||||
* 44px guideline). Apply via `className`; the icon/label stays its normal visual
|
||||
* size — only the hit area grows — and desktop is unchanged (the rule is gated to
|
||||
* `@media (max-width: 750px)`, matching MOBILE_BREAKPOINT).
|
||||
*/
|
||||
export const MobileTouchTarget = style({
|
||||
'@media': {
|
||||
'(max-width: 750px)': {
|
||||
minWidth: toRem(44),
|
||||
minHeight: toRem(44),
|
||||
},
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user