Phone: 44 px hit areas for the chat path's remaining icon buttons, and tests that pin them #127
10 changed files with 90 additions and 10 deletions
|
|
@ -66,6 +66,7 @@ import { useAutoDiscoveryInfo } from '../../hooks/useAutoDiscoveryInfo';
|
|||
import { livekitSupport } from '../../hooks/useLivekitSupport';
|
||||
import { StateEvent } from '../../../types/matrix/room';
|
||||
import { webRTCSupported } from '../../utils/rtc';
|
||||
import { PhoneHitSpread24 } from '../../styles/PhoneHit.css';
|
||||
|
||||
type RoomNavItemMenuProps = {
|
||||
room: Room;
|
||||
|
|
@ -229,6 +230,7 @@ function CallChatToggle() {
|
|||
|
||||
return (
|
||||
<IconButton
|
||||
className={PhoneHitSpread24}
|
||||
onClick={() => setChat(!chat)}
|
||||
aria-pressed={chat}
|
||||
aria-label="Toggle Chat"
|
||||
|
|
@ -436,6 +438,7 @@ export function RoomNavItem({
|
|||
}
|
||||
>
|
||||
<IconButton
|
||||
className={PhoneHitSpread24}
|
||||
onClick={handleOpenMenu}
|
||||
aria-pressed={!!menuAnchor}
|
||||
aria-controls={`menu-${room.roomId}`}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import { Page, PageHeader } from '../../components/page';
|
|||
import { callChatAtom } from '../../state/callEmbed';
|
||||
import { RoomView } from './RoomView';
|
||||
import { ScreenSize, useScreenSizeContext } from '../../hooks/useScreenSize';
|
||||
import { PhoneHit } from '../../styles/PhoneHit.css';
|
||||
|
||||
export function CallChatView() {
|
||||
const { eventId } = useParams();
|
||||
|
|
@ -41,7 +42,12 @@ export function CallChatView() {
|
|||
}
|
||||
>
|
||||
{(triggerRef) => (
|
||||
<IconButton ref={triggerRef} variant="Surface" onClick={handleClose}>
|
||||
<IconButton
|
||||
className={PhoneHit}
|
||||
ref={triggerRef}
|
||||
variant="Surface"
|
||||
onClick={handleClose}
|
||||
>
|
||||
<Icon src={Icons.Cross} />
|
||||
</IconButton>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -59,6 +59,7 @@ import { useSpaceOptionally } from '../../hooks/useSpace';
|
|||
import { ContainerColor } from '../../styles/ContainerColor.css';
|
||||
import { useFlattenPowerTagMembers, useGetMemberPowerTag } from '../../hooks/useMemberPowerTag';
|
||||
import { useRoomCreators } from '../../hooks/useRoomCreators';
|
||||
import { PhoneHit } from '../../styles/PhoneHit.css';
|
||||
|
||||
type MemberDrawerHeaderProps = {
|
||||
room: Room;
|
||||
|
|
@ -87,6 +88,7 @@ function MemberDrawerHeader({ room }: MemberDrawerHeaderProps) {
|
|||
>
|
||||
{(triggerRef) => (
|
||||
<IconButton
|
||||
className={PhoneHit}
|
||||
ref={triggerRef}
|
||||
variant="Background"
|
||||
onClick={() => setPeopleDrawer(false)}
|
||||
|
|
@ -359,6 +361,7 @@ export function MembersDrawer({ room, members }: MembersDrawerProps) {
|
|||
|
||||
<ScrollTopContainer scrollRef={scrollRef} anchorRef={scrollTopAnchorRef}>
|
||||
<IconButton
|
||||
className={PhoneHit}
|
||||
onClick={() => virtualizer.scrollToOffset(0)}
|
||||
variant="Surface"
|
||||
radii="Pill"
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import { getMxIdLocalPart } from '../../utils/matrix';
|
|||
import * as css from './RoomViewTyping.css';
|
||||
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
||||
import { useRoomTypingMember } from '../../hooks/useRoomTypingMembers';
|
||||
import { PhoneHit } from '../../styles/PhoneHit.css';
|
||||
|
||||
export type RoomViewTypingProps = {
|
||||
room: Room;
|
||||
|
|
@ -111,7 +112,13 @@ export const RoomViewTyping = as<'div', RoomViewTypingProps>(
|
|||
</>
|
||||
)}
|
||||
</Text>
|
||||
<IconButton title="Drop Typing Status" size="300" radii="Pill" onClick={handleDropAll}>
|
||||
<IconButton
|
||||
className={PhoneHit}
|
||||
title="Drop Typing Status"
|
||||
size="300"
|
||||
radii="Pill"
|
||||
onClick={handleDropAll}
|
||||
>
|
||||
<Icon size="50" src={Icons.Cross} />
|
||||
</IconButton>
|
||||
</Box>
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ import { getToday, getYesterday, timeDayMonthYear, timeHourMinute } from '../../
|
|||
import { DatePicker, TimePicker } from '../../../components/time-date';
|
||||
import { useSetting } from '../../../state/hooks/settings';
|
||||
import { settingsAtom } from '../../../state/settings';
|
||||
import { PhoneHit } from '../../../styles/PhoneHit.css';
|
||||
|
||||
type JumpToTimeProps = {
|
||||
onCancel: () => void;
|
||||
|
|
@ -108,7 +109,7 @@ export function JumpToTime({ onCancel, onSubmit }: JumpToTimeProps) {
|
|||
<Box grow="Yes">
|
||||
<Text size="H4">Jump to Time</Text>
|
||||
</Box>
|
||||
<IconButton size="300" onClick={onCancel} radii="300">
|
||||
<IconButton className={PhoneHit} size="300" onClick={onCancel} radii="300">
|
||||
<Icon src={Icons.Cross} />
|
||||
</IconButton>
|
||||
</Header>
|
||||
|
|
|
|||
|
|
@ -54,6 +54,7 @@ import { useMatrixClient } from '../../../hooks/useMatrixClient';
|
|||
import { getEditedEvent, getMentionContent, trimReplyFromFormattedBody } from '../../../utils/room';
|
||||
import { mobileOrTablet } from '../../../utils/user-agent';
|
||||
import { useComposingCheck } from '../../../hooks/useComposingCheck';
|
||||
import { PhoneHitSpread32 } from '../../../styles/PhoneHit.css';
|
||||
|
||||
type MessageEditorProps = {
|
||||
roomId: string;
|
||||
|
|
@ -289,6 +290,7 @@ export const MessageEditor = as<'div', MessageEditorProps>(
|
|||
</Box>
|
||||
<Box gap="Inherit">
|
||||
<IconButton
|
||||
className={PhoneHitSpread32}
|
||||
variant="SurfaceVariant"
|
||||
size="300"
|
||||
radii="300"
|
||||
|
|
@ -322,6 +324,7 @@ export const MessageEditor = as<'div', MessageEditorProps>(
|
|||
}
|
||||
>
|
||||
<IconButton
|
||||
className={PhoneHitSpread32}
|
||||
aria-pressed={anchor !== undefined}
|
||||
onClick={
|
||||
((evt) =>
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ import { useMediaAuthentication } from '../../../hooks/useMediaAuthentication';
|
|||
import { useOpenUserRoomProfile } from '../../../state/hooks/userRoomProfile';
|
||||
import { useSpaceOptionally } from '../../../hooks/useSpace';
|
||||
import { getMouseEventCords } from '../../../utils/dom';
|
||||
import { PhoneHit } from '../../../styles/PhoneHit.css';
|
||||
|
||||
export type ReactionViewerProps = {
|
||||
room: Room;
|
||||
|
|
@ -100,7 +101,7 @@ export const ReactionViewer = as<'div', ReactionViewerProps>(
|
|||
<Box grow="Yes">
|
||||
<Text size="H3" truncate>{`Reacted with :${selectedShortcode}:`}</Text>
|
||||
</Box>
|
||||
<IconButton size="300" onClick={requestClose}>
|
||||
<IconButton className={PhoneHit} size="300" onClick={requestClose}>
|
||||
<Icon src={Icons.Cross} />
|
||||
</IconButton>
|
||||
</Header>
|
||||
|
|
|
|||
|
|
@ -86,6 +86,7 @@ import {
|
|||
useGetMemberPowerTag,
|
||||
} from '../../../hooks/useMemberPowerTag';
|
||||
import { useRoomCreatorsTag } from '../../../hooks/useRoomCreatorsTag';
|
||||
import { PhoneHit, PhoneHitSpread26 } from '../../../styles/PhoneHit.css';
|
||||
|
||||
type PinnedMessageProps = {
|
||||
room: Room;
|
||||
|
|
@ -146,6 +147,7 @@ function PinnedMessage({
|
|||
</Chip>
|
||||
{canPinEvent && (
|
||||
<IconButton
|
||||
className={PhoneHitSpread26}
|
||||
data-event-id={eventId}
|
||||
variant="Secondary"
|
||||
size="300"
|
||||
|
|
@ -457,7 +459,7 @@ export const RoomPinMenu = forwardRef<HTMLDivElement, RoomPinMenuProps>(
|
|||
<Text size="H5">Pinned Messages</Text>
|
||||
</Box>
|
||||
<Box shrink="No">
|
||||
<IconButton size="300" onClick={requestClose} radii="300">
|
||||
<IconButton className={PhoneHit} size="300" onClick={requestClose} radii="300">
|
||||
<Icon src={Icons.Cross} size="400" />
|
||||
</IconButton>
|
||||
</Box>
|
||||
|
|
|
|||
|
|
@ -126,11 +126,35 @@ describe('phone icon-button hit areas', () => {
|
|||
const phone = [...css.matchAll(/@media ([^{]+) \{\n([\s\S]*?\n)\}(?:\n|$)/g)];
|
||||
expect(phone.map((m) => m[1])).toEqual([PHONE]);
|
||||
expect(css).toMatch(/::after \{[^}]*width: max\(100%, 44px\);[^}]*height: max\(100%, 44px\);/);
|
||||
// no content, no box: the pseudo-element is not rendered at all
|
||||
expect(css).toMatch(/::after \{[^}]*content: "";/);
|
||||
expect(css).toMatch(/::after \{[^}]*position: absolute;/);
|
||||
// centred on the button: top and left at half, pulled back by half its own size
|
||||
expect(css).toMatch(/::after \{[^}]*top: 50%;/);
|
||||
expect(css).toMatch(/::after \{[^}]*left: 50%;/);
|
||||
expect(css).toMatch(/::after \{[^}]*transform: translate\(-50%, -50%\);/);
|
||||
// the box anchors to its own button, receives the tap, and sits above nothing it should not
|
||||
expect(css).toMatch(/@media [^{]+ \{\n\s*\.PhoneHit_PhoneHit__\w+ \{\n\s*position: relative;/);
|
||||
const afterDecls = [...css.matchAll(/::after \{\n([^}]*)\}/g)].flatMap((m) =>
|
||||
m[1]
|
||||
.split('\n')
|
||||
.map((l) => l.trim().split(':')[0])
|
||||
.filter(Boolean)
|
||||
);
|
||||
expect([...afterDecls].sort()).toEqual([
|
||||
'content',
|
||||
'height',
|
||||
'left',
|
||||
'position',
|
||||
'top',
|
||||
'transform',
|
||||
'width',
|
||||
]);
|
||||
// nothing on a phone moves a button off its own anchor: only relative (the button) and absolute (the box)
|
||||
expect([...css.matchAll(/position: (\w+);/g)].map((m) => m[1]).sort()).toEqual([
|
||||
'absolute',
|
||||
'relative',
|
||||
]);
|
||||
expect(stripPhone(css).trim()).toBe('');
|
||||
});
|
||||
|
||||
|
|
@ -140,7 +164,9 @@ describe('phone icon-button hit areas', () => {
|
|||
rulesFor(css, selector, PHONE).map((d) => d['margin-inline']);
|
||||
// 32 px composer buttons, 40 px header buttons, 26 px message-option buttons (which also open vertically,
|
||||
// the menu around them clips a hit area drawn outside its box)
|
||||
expect(margins(/PhoneHit_spread__/)).toEqual(expect.arrayContaining([9, 6, 2]));
|
||||
// each class owns its own margin: (44 - drawn size) / 2
|
||||
const own = (size: number) => margins(new RegExp(`PhoneHit_PhoneHitSpread${size}__`));
|
||||
expect([own(24), own(26), own(32), own(40)]).toEqual([[10], [9], [6], [2]]);
|
||||
expect(rulesFor(css, /PhoneHit_PhoneHitRoom26__/, PHONE)).toEqual([{ 'margin-block': 9 }]);
|
||||
});
|
||||
|
||||
|
|
@ -190,13 +216,37 @@ describe('icon buttons in the room view', () => {
|
|||
],
|
||||
'features/room/RoomInput.tsx': Array(6).fill('PhoneHitSpread32'),
|
||||
'features/room/RoomViewHeader.tsx': Array(6).fill('PhoneHitSpread40'),
|
||||
// The chat path's other buttons. Two neighbours in a row: Spread32. One on its own: the plain PhoneHit.
|
||||
'features/room/message/MessageEditor.tsx': ['PhoneHitSpread32', 'PhoneHitSpread32'], // toolbar, emoji
|
||||
'features/room-nav/RoomNavItem.tsx': ['PhoneHitSpread24', 'PhoneHitSpread24'], // chat toggle, options: 16 px icon + 8 padding = 24 px, 0 gap
|
||||
'features/room/MembersDrawer.tsx': ['PhoneHit', 'PhoneHit'], // close, scroll to top
|
||||
'features/room/reaction-viewer/ReactionViewer.tsx': ['PhoneHit'],
|
||||
'features/room/room-pin-menu/RoomPinMenu.tsx': ['PhoneHitSpread26', 'PhoneHit'], // unpin (26 px, 8 px from the Open chip), close
|
||||
'features/room/RoomViewTyping.tsx': ['PhoneHit'],
|
||||
'features/room/CallChatView.tsx': ['PhoneHit'],
|
||||
'features/room/jump-to-time/JumpToTime.tsx': ['PhoneHit'],
|
||||
// Buttons that were fixed with #125 and had no test.
|
||||
'components/password-input/PasswordInput.tsx': ['PhoneHit'],
|
||||
'features/desk/Desk.tsx': ['PhoneHit'],
|
||||
'features/scene/SkyStateControl.tsx': ['PhoneHit'],
|
||||
'pages/auth/login/PasswordLoginForm.tsx': ['PhoneHit'],
|
||||
};
|
||||
|
||||
it.each(Object.entries(expected))(
|
||||
'%s: each IconButton carries its own PhoneHit class',
|
||||
(file, want) => {
|
||||
const tags = iconButtonTags(readFileSync(`${process.cwd()}/src/app/${file}`, 'utf8'));
|
||||
expect(tags.map((t) => t.match(/\bPhoneHit\w+/)?.[0] ?? null)).toEqual(want);
|
||||
const src = readFileSync(`${process.cwd()}/src/app/${file}`, 'utf8');
|
||||
expect(
|
||||
iconButtonTags(src).map((t) => t.match(/className=\{[^}]*?\b(PhoneHit\w*)\b/)?.[1] ?? null)
|
||||
).toEqual(want);
|
||||
// the name is the one PhoneHit.css.ts exports, not a local of the same name
|
||||
[...new Set(want)].forEach((name) =>
|
||||
expect(src).toMatch(
|
||||
new RegExp(
|
||||
`import \\{(?:[^}]*,)?\\s*${name}\\s*(?:,[^}]*)?\\} from '(\\.\\./)+styles/PhoneHit\\.css'`
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -31,9 +31,13 @@ export const PhoneHit = style({
|
|||
|
||||
/** An icon button in a row, `visual` px wide: the row opens up so its 44 px hit area meets its neighbours'. */
|
||||
const spread = (visual: number) =>
|
||||
style([PhoneHit, { '@media': { [PHONE]: { marginInline: (PHONE_TAP - visual) / 2 } } }]);
|
||||
style(
|
||||
[PhoneHit, { '@media': { [PHONE]: { marginInline: (PHONE_TAP - visual) / 2 } } }],
|
||||
`PhoneHitSpread${visual}`
|
||||
);
|
||||
|
||||
const PhoneHitSpread26 = spread(26);
|
||||
export const PhoneHitSpread24 = spread(24);
|
||||
export const PhoneHitSpread26 = spread(26);
|
||||
export const PhoneHitSpread32 = spread(32);
|
||||
export const PhoneHitSpread40 = spread(40);
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue