Phone: 44 px hit areas for icon buttons, reaction chips and short sender names #125
11 changed files with 207 additions and 10 deletions
|
|
@ -1,6 +1,9 @@
|
|||
import { createVar, style } from '@vanilla-extract/css';
|
||||
import { DefaultReset, FocusOutline, color, config, toRem } from 'folds';
|
||||
import { MOBILE_BREAKPOINT } from '../../hooks/useScreenSize';
|
||||
import { PHONE_TAP } from '../../styles/PhoneHit.css';
|
||||
|
||||
const PHONE = `screen and (max-width: ${MOBILE_BREAKPOINT}px)`;
|
||||
const Container = createVar();
|
||||
const ContainerHover = createVar();
|
||||
const ContainerActive = createVar();
|
||||
|
|
@ -25,6 +28,14 @@ export const Reaction = style([
|
|||
selectors: {
|
||||
'button&': {
|
||||
cursor: 'pointer',
|
||||
// Phone tap target: the chip is 26 px tall, so its hit area grows 9 px above and below (an ::after) and the
|
||||
// same 9 px of margin opens the row, so the chips of one row and of the next never share a tap.
|
||||
'@media': { [PHONE]: { position: 'relative', marginBlock: (PHONE_TAP - 26) / 2 } },
|
||||
},
|
||||
'button&::after': {
|
||||
'@media': {
|
||||
[PHONE]: { content: '""', position: 'absolute', inset: `${-(PHONE_TAP - 26) / 2}px 0` },
|
||||
},
|
||||
},
|
||||
'&[aria-pressed=true]': {
|
||||
vars: {
|
||||
|
|
|
|||
|
|
@ -299,7 +299,10 @@ export const BubbleLeftArrow = style({
|
|||
});
|
||||
|
||||
// Phone tap target: a sender name is 23 px tall, so the button grows 11 px above and below (44+ px to tap) and the
|
||||
// negative margin takes the growth back out of the layout. Hit area only: no background, border or outline moves.
|
||||
// negative margin takes the growth back out of the layout. Sideways it cannot borrow from the layout the
|
||||
// same way (a negative inline margin makes the button shrink inside its shrink-to-fit parent and clip the name), so
|
||||
// a name under 44 px is 44 px wide, text kept at the start: real growth of at most 16 px, in a row with room for it.
|
||||
// No background, border or outline moves.
|
||||
const SCREEN_PHONE = `screen and (max-width: ${MOBILE_BREAKPOINT}px)`;
|
||||
export const Username = style({
|
||||
overflow: 'hidden',
|
||||
|
|
@ -309,7 +312,7 @@ export const Username = style({
|
|||
'button&': {
|
||||
cursor: 'pointer',
|
||||
'@media': {
|
||||
[SCREEN_PHONE]: { paddingBlock: 11, marginBlock: -11 },
|
||||
[SCREEN_PHONE]: { paddingBlock: 11, marginBlock: -11, minWidth: 44, textAlign: 'start' },
|
||||
},
|
||||
},
|
||||
'button&:hover, button&:focus-visible': {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import React, { ComponentProps, forwardRef } from 'react';
|
||||
import { Icon, IconButton, Input, config, Icons } from 'folds';
|
||||
import { UseStateProvider } from '../UseStateProvider';
|
||||
import { PhoneHit } from '../../styles/PhoneHit.css';
|
||||
|
||||
type PasswordInputProps = Omit<ComponentProps<typeof Input>, 'type' | 'size'> & {
|
||||
size: '400' | '500';
|
||||
|
|
@ -23,6 +24,7 @@ export const PasswordInput = forwardRef<HTMLInputElement, PasswordInputProps>(
|
|||
<>
|
||||
{after}
|
||||
<IconButton
|
||||
className={PhoneHit}
|
||||
onClick={() => setVisible(!visible)}
|
||||
type="button"
|
||||
variant={visible ? 'Warning' : variant}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import { Box, Icon, IconButton, Icons, Scroll, Text } from 'folds';
|
||||
import { PhoneHit } from '../../styles/PhoneHit.css';
|
||||
import { Page, PageContent, PageContentCenter, PageHeader } from '../../components/page';
|
||||
import { ScreenSize, useScreenSizeContext } from '../../hooks/useScreenSize';
|
||||
import { getHomePath } from '../../pages/pathUtils';
|
||||
|
|
@ -33,7 +34,11 @@ export function Desk() {
|
|||
<Box grow="Yes" alignItems="Center" gap="200">
|
||||
<Box grow="Yes" basis="No">
|
||||
{phone && (
|
||||
<IconButton aria-label="Back to rooms" onClick={() => navigate(getHomePath())}>
|
||||
<IconButton
|
||||
className={PhoneHit}
|
||||
aria-label="Back to rooms"
|
||||
onClick={() => navigate(getHomePath())}
|
||||
>
|
||||
<Icon src={Icons.ArrowLeft} />
|
||||
</IconButton>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -116,6 +116,7 @@ import { useTheme } from '../../hooks/useTheme';
|
|||
import { useRoomCreatorsTag } from '../../hooks/useRoomCreatorsTag';
|
||||
import { usePowerLevelTags } from '../../hooks/usePowerLevelTags';
|
||||
import { useComposingCheck } from '../../hooks/useComposingCheck';
|
||||
import { PhoneHitSpread32 } from '../../styles/PhoneHit.css';
|
||||
|
||||
interface RoomInputProps {
|
||||
editor: Editor;
|
||||
|
|
@ -605,6 +606,7 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
|
|||
style={{ padding: `${config.space.S200} ${config.space.S300} 0` }}
|
||||
>
|
||||
<IconButton
|
||||
className={PhoneHitSpread32}
|
||||
onClick={() => setReplyDraft(undefined)}
|
||||
variant="SurfaceVariant"
|
||||
size="300"
|
||||
|
|
@ -638,6 +640,7 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
|
|||
}
|
||||
before={
|
||||
<IconButton
|
||||
className={PhoneHitSpread32}
|
||||
onClick={() => pickFile('*')}
|
||||
variant="SurfaceVariant"
|
||||
size="300"
|
||||
|
|
@ -649,6 +652,7 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
|
|||
after={
|
||||
<>
|
||||
<IconButton
|
||||
className={PhoneHitSpread32}
|
||||
variant="SurfaceVariant"
|
||||
size="300"
|
||||
radii="300"
|
||||
|
|
@ -691,6 +695,7 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
|
|||
>
|
||||
{!hideStickerBtn && (
|
||||
<IconButton
|
||||
className={PhoneHitSpread32}
|
||||
aria-pressed={emojiBoardTab === EmojiBoardTab.Sticker}
|
||||
onClick={() => setEmojiBoardTab(EmojiBoardTab.Sticker)}
|
||||
variant="SurfaceVariant"
|
||||
|
|
@ -704,6 +709,7 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
|
|||
</IconButton>
|
||||
)}
|
||||
<IconButton
|
||||
className={PhoneHitSpread32}
|
||||
ref={emojiBtnRef}
|
||||
aria-pressed={
|
||||
hideStickerBtn ? !!emojiBoardTab : emojiBoardTab === EmojiBoardTab.Emoji
|
||||
|
|
@ -723,7 +729,13 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
|
|||
</PopOut>
|
||||
)}
|
||||
</UseStateProvider>
|
||||
<IconButton onClick={submit} variant="SurfaceVariant" size="300" radii="300">
|
||||
<IconButton
|
||||
className={PhoneHitSpread32}
|
||||
onClick={submit}
|
||||
variant="SurfaceVariant"
|
||||
size="300"
|
||||
radii="300"
|
||||
>
|
||||
<Icon src={Icons.Send} />
|
||||
</IconButton>
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -72,6 +72,7 @@ import { RoomSettingsPage } from '../../state/roomSettings';
|
|||
import { useCallEmbed, useCallStart } from '../../hooks/useCallEmbed';
|
||||
import { useLivekitSupport } from '../../hooks/useLivekitSupport';
|
||||
import { webRTCSupported } from '../../utils/rtc';
|
||||
import { PhoneHitSpread40 } from '../../styles/PhoneHit.css';
|
||||
|
||||
type RoomMenuProps = {
|
||||
room: Room;
|
||||
|
|
@ -336,6 +337,7 @@ function CallButton() {
|
|||
>
|
||||
{(triggerRef) => (
|
||||
<IconButton
|
||||
className={PhoneHitSpread40}
|
||||
variant="Surface"
|
||||
fill="None"
|
||||
ref={triggerRef}
|
||||
|
|
@ -455,7 +457,7 @@ export function RoomViewHeader({ callView }: { callView?: boolean }) {
|
|||
<BackRouteHandler>
|
||||
{(onBack) => (
|
||||
<Box shrink="No" alignItems="Center">
|
||||
<IconButton fill="None" onClick={onBack}>
|
||||
<IconButton className={PhoneHitSpread40} fill="None" onClick={onBack}>
|
||||
<Icon src={Icons.ArrowLeft} />
|
||||
</IconButton>
|
||||
</Box>
|
||||
|
|
@ -531,7 +533,12 @@ export function RoomViewHeader({ callView }: { callView?: boolean }) {
|
|||
}
|
||||
>
|
||||
{(triggerRef) => (
|
||||
<IconButton fill="None" ref={triggerRef} onClick={handleSearchClick}>
|
||||
<IconButton
|
||||
className={PhoneHitSpread40}
|
||||
fill="None"
|
||||
ref={triggerRef}
|
||||
onClick={handleSearchClick}
|
||||
>
|
||||
<Icon size="400" src={Icons.Search} />
|
||||
</IconButton>
|
||||
)}
|
||||
|
|
@ -548,6 +555,7 @@ export function RoomViewHeader({ callView }: { callView?: boolean }) {
|
|||
>
|
||||
{(triggerRef) => (
|
||||
<IconButton
|
||||
className={PhoneHitSpread40}
|
||||
fill="None"
|
||||
style={{ position: 'relative' }}
|
||||
onClick={handleOpenPinMenu}
|
||||
|
|
@ -612,7 +620,12 @@ export function RoomViewHeader({ callView }: { callView?: boolean }) {
|
|||
}
|
||||
>
|
||||
{(triggerRef) => (
|
||||
<IconButton fill="None" ref={triggerRef} onClick={handleMemberToggle}>
|
||||
<IconButton
|
||||
className={PhoneHitSpread40}
|
||||
fill="None"
|
||||
ref={triggerRef}
|
||||
onClick={handleMemberToggle}
|
||||
>
|
||||
<Icon size="400" src={Icons.User} />
|
||||
</IconButton>
|
||||
)}
|
||||
|
|
@ -631,6 +644,7 @@ export function RoomViewHeader({ callView }: { callView?: boolean }) {
|
|||
>
|
||||
{(triggerRef) => (
|
||||
<IconButton
|
||||
className={PhoneHitSpread40}
|
||||
fill="None"
|
||||
onClick={handleOpenMenu}
|
||||
ref={triggerRef}
|
||||
|
|
|
|||
|
|
@ -86,6 +86,7 @@ import colorMXID from '../../../../util/colorMXID';
|
|||
import { useSkyInk } from '../../../hooks/useTheme';
|
||||
import { userColorInk } from '../../scene/skyInk';
|
||||
import { getPowerTagIconSrc } from '../../../hooks/useMemberPowerTag';
|
||||
import { PhoneHitRoom26, PhoneHitSpread32 } from '../../../styles/PhoneHit.css';
|
||||
|
||||
export type ReactionHandler = (keyOrMxc: string, shortcode: string) => void;
|
||||
|
||||
|
|
@ -111,7 +112,7 @@ export const MessageQuickReactions = as<'div', MessageQuickReactionsProps>(
|
|||
{recentEmojis.map((emoji) => (
|
||||
<IconButton
|
||||
key={emoji.unicode}
|
||||
className={css.MessageQuickReaction}
|
||||
className={classNames(css.MessageQuickReaction, PhoneHitSpread32)}
|
||||
size="300"
|
||||
variant="SurfaceVariant"
|
||||
radii="Pill"
|
||||
|
|
@ -459,7 +460,12 @@ export const MessageDeleteItem = as<
|
|||
<Box grow="Yes">
|
||||
<Text size="H4">Delete Message</Text>
|
||||
</Box>
|
||||
<IconButton size="300" onClick={handleClose} radii="300">
|
||||
<IconButton
|
||||
className={PhoneHitSpread32}
|
||||
size="300"
|
||||
onClick={handleClose}
|
||||
radii="300"
|
||||
>
|
||||
<Icon src={Icons.Cross} />
|
||||
</IconButton>
|
||||
</Header>
|
||||
|
|
@ -589,7 +595,12 @@ export const MessageReportItem = as<
|
|||
<Box grow="Yes">
|
||||
<Text size="H4">Report Message</Text>
|
||||
</Box>
|
||||
<IconButton size="300" onClick={handleClose} radii="300">
|
||||
<IconButton
|
||||
className={PhoneHitSpread32}
|
||||
size="300"
|
||||
onClick={handleClose}
|
||||
radii="300"
|
||||
>
|
||||
<Icon src={Icons.Cross} />
|
||||
</IconButton>
|
||||
</Header>
|
||||
|
|
@ -950,6 +961,7 @@ export const Message = as<'div', MessageProps>(
|
|||
}
|
||||
>
|
||||
<IconButton
|
||||
className={PhoneHitRoom26}
|
||||
onClick={handleOpenEmojiBoard}
|
||||
variant="SurfaceVariant"
|
||||
size="300"
|
||||
|
|
@ -961,6 +973,7 @@ export const Message = as<'div', MessageProps>(
|
|||
</PopOut>
|
||||
)}
|
||||
<IconButton
|
||||
className={PhoneHitRoom26}
|
||||
onClick={onReplyClick}
|
||||
data-event-id={mEvent.getId()}
|
||||
variant="SurfaceVariant"
|
||||
|
|
@ -971,6 +984,7 @@ export const Message = as<'div', MessageProps>(
|
|||
</IconButton>
|
||||
{!isThreadedMessage && (
|
||||
<IconButton
|
||||
className={PhoneHitRoom26}
|
||||
onClick={(ev) => onReplyClick(ev, true)}
|
||||
data-event-id={mEvent.getId()}
|
||||
variant="SurfaceVariant"
|
||||
|
|
@ -982,6 +996,7 @@ export const Message = as<'div', MessageProps>(
|
|||
)}
|
||||
{canEditEvent(mx, mEvent) && onEditId && (
|
||||
<IconButton
|
||||
className={PhoneHitRoom26}
|
||||
onClick={() => onEditId(mEvent.getId())}
|
||||
variant="SurfaceVariant"
|
||||
size="300"
|
||||
|
|
@ -1147,6 +1162,7 @@ export const Message = as<'div', MessageProps>(
|
|||
}
|
||||
>
|
||||
<IconButton
|
||||
className={PhoneHitRoom26}
|
||||
variant="SurfaceVariant"
|
||||
size="300"
|
||||
radii="300"
|
||||
|
|
@ -1320,6 +1336,7 @@ export const Event = as<'div', EventProps>(
|
|||
}
|
||||
>
|
||||
<IconButton
|
||||
className={PhoneHitRoom26}
|
||||
variant="SurfaceVariant"
|
||||
size="300"
|
||||
radii="300"
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ import {
|
|||
setSceneStyle,
|
||||
type ServedSceneStyle,
|
||||
} from './sceneStyle';
|
||||
import { PhoneHit } from '../../styles/PhoneHit.css';
|
||||
|
||||
const STYLE_LABEL: Record<ServedSceneStyle, string> = { terrain: 'Terrain', ridge: 'Ridges' };
|
||||
|
||||
|
|
@ -149,6 +150,7 @@ export function SkyStateControl() {
|
|||
}
|
||||
>
|
||||
<IconButton
|
||||
className={PhoneHit}
|
||||
variant="Background"
|
||||
aria-label={`Sky: ${LABEL[state]}`}
|
||||
title={`Sky: ${LABEL[state]}`}
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ import { loginErrorMessage } from '../authMessages';
|
|||
import { ForgotLink } from '../styles.css';
|
||||
import { getResetPasswordPath } from '../../pathUtils';
|
||||
import { stopPropagation } from '../../../utils/keyboard';
|
||||
import { PhoneHit } from '../../../styles/PhoneHit.css';
|
||||
|
||||
function UsernameHint({ server }: { server: string }) {
|
||||
const [anchor, setAnchor] = useState<RectCords>();
|
||||
|
|
@ -94,6 +95,7 @@ function UsernameHint({ server }: { server: string }) {
|
|||
}
|
||||
>
|
||||
<IconButton
|
||||
className={PhoneHit}
|
||||
tabIndex={-1}
|
||||
onClick={handleOpenMenu}
|
||||
type="button"
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { readFileSync } from 'fs';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import zlib from 'zlib';
|
||||
import { compile, processVanillaFile } from '@vanilla-extract/integration';
|
||||
|
|
@ -116,3 +117,86 @@ describe('phone navigator rows', () => {
|
|||
expect(phone.match(/min-height: 2\.75rem/g)?.length).toBeGreaterThanOrEqual(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('phone icon-button hit areas', () => {
|
||||
const stripPhone = (css: string) => css.replace(/@media [^{]+ \{\n[\s\S]*?\n\}(?:\n|$)/g, '');
|
||||
|
||||
it('draw a 44 x 44 box around the button on a phone and nothing on any other screen', async () => {
|
||||
const css = await emitted('styles/PhoneHit.css.ts');
|
||||
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\);/);
|
||||
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%\);/);
|
||||
expect(stripPhone(css).trim()).toBe('');
|
||||
});
|
||||
|
||||
it('open a row up by half the shortfall, so neighbouring hit areas meet and do not overlap', async () => {
|
||||
const css = await emitted('styles/PhoneHit.css.ts');
|
||||
const margins = (selector: RegExp) =>
|
||||
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]));
|
||||
expect(rulesFor(css, /PhoneHit_PhoneHitRoom26__/, PHONE)).toEqual([{ 'margin-block': 9 }]);
|
||||
});
|
||||
|
||||
it("make a message's reaction chip 44 px to tap by opening 9 px above and below it", async () => {
|
||||
const css = await emitted('components/message/Reaction.css.ts');
|
||||
const [chip] = rulesFor(css, /^button\.Reaction_Reaction__\w+$/, PHONE);
|
||||
expect(chip['margin-block']).toBe((TAP - 26) / 2);
|
||||
expect(css).toMatch(/::after \{[^}]*inset: -9px 0;/);
|
||||
rulesFor(css, /^button\.Reaction_Reaction__\w+$/, undefined).forEach((d) =>
|
||||
expect(Object.keys(d)).not.toContain('margin-block')
|
||||
);
|
||||
});
|
||||
|
||||
it('give a sender name a 44 px wide button too, text kept at its start', async () => {
|
||||
const css = await emitted('components/message/layout/layout.css.ts');
|
||||
const [phone] = rulesFor(css, /^button\.layout_Username__/, PHONE);
|
||||
expect(phone['min-width']).toBe(TAP);
|
||||
expect(css).toMatch(/text-align: start;/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('icon buttons in the room view', () => {
|
||||
// The opening tag of every <IconButton in a source file, read to its closing > outside any {} expression.
|
||||
const iconButtonTags = (src: string) => {
|
||||
const tags: string[] = [];
|
||||
for (let i = src.indexOf('<IconButton'); i !== -1; i = src.indexOf('<IconButton', i + 1)) {
|
||||
let depth = 0;
|
||||
let j = i;
|
||||
for (; j < src.length; j += 1) {
|
||||
if (src[j] === '{') depth += 1;
|
||||
else if (src[j] === '}') depth -= 1;
|
||||
else if (src[j] === '>' && depth === 0) break;
|
||||
}
|
||||
tags.push(src.slice(i, j + 1));
|
||||
}
|
||||
return tags;
|
||||
};
|
||||
|
||||
// The class each IconButton must carry, in source order: the variant is chosen by the button's drawn size
|
||||
// and whether a clipping container or neighbours surround it, so a swapped variant is a wrong hit area.
|
||||
const expected: Record<string, string[]> = {
|
||||
'features/room/message/Message.tsx': [
|
||||
'PhoneHitSpread32', // quick-reaction emoji buttons
|
||||
'PhoneHitSpread32', // delete dialog close
|
||||
'PhoneHitSpread32', // report dialog close
|
||||
...Array(6).fill('PhoneHitRoom26'), // message options bar, inside a clipping menu
|
||||
],
|
||||
'features/room/RoomInput.tsx': Array(6).fill('PhoneHitSpread32'),
|
||||
'features/room/RoomViewHeader.tsx': Array(6).fill('PhoneHitSpread40'),
|
||||
};
|
||||
|
||||
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);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
|
|
|||
45
src/app/styles/PhoneHit.css.ts
Normal file
45
src/app/styles/PhoneHit.css.ts
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
import { style } from '@vanilla-extract/css';
|
||||
import { MOBILE_BREAKPOINT } from '../hooks/useScreenSize';
|
||||
|
||||
// Phone tap target for an icon button: on a phone the button's hit area is 44 x 44 px centred on the drawn
|
||||
// button (a ::after the button's own click reaches), so nothing drawn changes size. A button with neighbours
|
||||
// 44 px closer than that would take their taps, so the `Spread` classes also give it horizontal margin of half
|
||||
// the shortfall: the row grows by that, on phones only, and the hit areas meet instead of overlapping.
|
||||
// Vertical room is cheaper than horizontal on a phone, so only the horizontal axis pays.
|
||||
export const PHONE_TAP = 44;
|
||||
const PHONE = `screen and (max-width: ${MOBILE_BREAKPOINT}px)`;
|
||||
|
||||
const hitArea = {
|
||||
'@media': {
|
||||
[PHONE]: {
|
||||
content: '""',
|
||||
position: 'absolute',
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
width: `max(100%, ${PHONE_TAP}px)`,
|
||||
height: `max(100%, ${PHONE_TAP}px)`,
|
||||
transform: 'translate(-50%, -50%)',
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
/** An icon button on its own: the hit area grows, nothing else moves. */
|
||||
export const PhoneHit = style({
|
||||
'@media': { [PHONE]: { position: 'relative' } },
|
||||
selectors: { '&::after': hitArea },
|
||||
});
|
||||
|
||||
/** 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 } } }]);
|
||||
|
||||
const PhoneHitSpread26 = spread(26);
|
||||
export const PhoneHitSpread32 = spread(32);
|
||||
export const PhoneHitSpread40 = spread(40);
|
||||
|
||||
/** For a button in a clipping container (a Menu's overflow hidden eats a hit area drawn outside the box): the row
|
||||
* opens up on both axes, so the 44 px sits inside the container. */
|
||||
export const PhoneHitRoom26 = style([
|
||||
PhoneHitSpread26,
|
||||
{ '@media': { [PHONE]: { marginBlock: (PHONE_TAP - 26) / 2 } } },
|
||||
]);
|
||||
Loading…
Reference in a new issue