mirror of
https://github.com/bulwarkmail/native.git
synced 2026-11-01 08:06:13 +00:00
fix: give widget buttons a 48dp tap area #52
Most widget buttons were 32dp tall (Archive, Delete, Reply and Next on the triage card, Accept, Maybe and Decline, Send wishes, Mail my free times), Up next's were 36dp and the round compose/new buttons in the headers 28-32dp, all under the 48dp a finger needs, so taps landed beside them and did nothing. Button and Fab take a transparent tap area around what they draw (`slop`), and the layouts give up the same amount of padding or gap next to them, so the widgets look the same but the buttons take a 48dp tap.
This commit is contained in:
@@ -192,7 +192,7 @@ export const AgendaLayout: Layout = ({ s, p, f, now, height }) => {
|
||||
iconName="calendar"
|
||||
title={f.t('widgets.calendar.agenda', 'Agenda')}
|
||||
click={open(links.calendar())}
|
||||
trailing={<Fab p={p} iconName="plus" size={compact ? 28 : 32} click={open(links.calendar())} />}
|
||||
trailing={<Fab p={p} iconName="plus" size={compact ? 28 : 32} slop={compact ? 5 : 6} click={open(links.calendar())} />}
|
||||
/>
|
||||
);
|
||||
const days = agendaDays(s.calendar.events, now, 14);
|
||||
@@ -1008,7 +1008,8 @@ export const UpNextLayout: Layout = ({ s, p, f, now }) => {
|
||||
: f.t('widgets.calendar.join', 'Join');
|
||||
|
||||
return (
|
||||
<Surface p={p} style={{ padding: 14 }}>
|
||||
// 36dp buttons in a 48dp tap area (slop 6), which takes 6dp of the bottom padding.
|
||||
<Surface p={p} style={{ padding: 14, paddingBottom: 8 }}>
|
||||
<FlexWidget {...open(links.event(e))} style={{ flexDirection: 'row', alignItems: 'center', width: 'match_parent' }}>
|
||||
<OverlapWidget style={{ width: 64, height: 64 }}>
|
||||
<SvgWidget svg={ring(left.fraction, p.border, p.primary, 5)} style={{ width: 64, height: 64 }} />
|
||||
@@ -1040,9 +1041,9 @@ export const UpNextLayout: Layout = ({ s, p, f, now }) => {
|
||||
<Spacer />
|
||||
<FlexWidget style={{ flexDirection: 'row', width: 'match_parent' }}>
|
||||
{e.videoUrl ? (
|
||||
<Button p={p} variant="primary" iconName="video" label={joinLabel} height={36} flex={1} click={open(e.videoUrl)} />
|
||||
<Button p={p} variant="primary" iconName="video" label={joinLabel} height={36} flex={1} slop={6} click={open(e.videoUrl)} />
|
||||
) : (
|
||||
<Button p={p} iconName="calendar" label={f.t('widgets.calendar.open', 'Open')} height={36} flex={1} click={open(links.event(e))} />
|
||||
<Button p={p} iconName="calendar" label={f.t('widgets.calendar.open', 'Open')} height={36} flex={1} slop={6} click={open(links.event(e))} />
|
||||
)}
|
||||
<Spacer size={8} horizontal />
|
||||
<Button
|
||||
@@ -1051,6 +1052,7 @@ export const UpNextLayout: Layout = ({ s, p, f, now }) => {
|
||||
label={f.t('widgets.calendar.running_late', 'Running late')}
|
||||
height={36}
|
||||
flex={1}
|
||||
slop={6}
|
||||
click={open(late)}
|
||||
/>
|
||||
</FlexWidget>
|
||||
|
||||
@@ -432,7 +432,7 @@ export const RecentFilesLayout: Layout = ({ s, p, f, now, height }) => {
|
||||
title={f.t('widgets.shortcuts.files', 'Files')}
|
||||
count={f.t('widgets.files.recent', 'Recently changed')}
|
||||
click={open(links.files())}
|
||||
trailing={<GhostIcon p={p} iconName="upload" click={open(links.files())} />}
|
||||
trailing={<GhostIcon p={p} iconName="upload" size={40} click={open(links.files())} />}
|
||||
/>
|
||||
);
|
||||
const items = s.files.items;
|
||||
|
||||
@@ -60,9 +60,9 @@ export const InboxLayout: Layout = ({ s, p, f, now, width, height }) => {
|
||||
<FlexWidget style={{ flexDirection: 'row', alignItems: 'center' }}>
|
||||
<Spacer size={8} horizontal />
|
||||
<CountText p={p} f={f} unread={unread} total={total} short={narrow} />
|
||||
{compact || narrow ? null : <GhostIcon p={p} iconName="search" click={open(links.search())} />}
|
||||
{compact || narrow ? null : <GhostIcon p={p} iconName="search" size={headerHeight} click={open(links.search())} />}
|
||||
<Spacer size={6} horizontal />
|
||||
<Fab p={p} iconName="edit" size={compact ? 28 : 32} click={open(links.compose())} />
|
||||
<Fab p={p} iconName="edit" size={compact ? 28 : 32} slop={compact ? 5 : 6} click={open(links.compose())} />
|
||||
</FlexWidget>
|
||||
)}
|
||||
/>
|
||||
@@ -115,7 +115,9 @@ export const TriageLayout: Layout = ({ s, p, f, now, width, widgetId, local }) =
|
||||
const allFit = buttonWidth(labels.archive) + buttonWidth(labels.trash) + buttonWidth(labels.reply) <= room;
|
||||
const replyFits = 2 * 32 + buttonWidth(labels.reply) <= room;
|
||||
return (
|
||||
<Surface p={p} style={{ padding: 12 }}>
|
||||
// The buttons draw 32dp tall in a 48dp tap area (slop 8), which takes the
|
||||
// gap above them and 8dp of the bottom padding.
|
||||
<Surface p={p} style={{ padding: 12, paddingBottom: 4 }}>
|
||||
<FlexWidget {...open(links.message(current))} style={{ width: 'match_parent', flex: 1 }}>
|
||||
<FlexWidget style={{ flexDirection: 'row', alignItems: 'center', width: 'match_parent' }}>
|
||||
<Avatar initials={current.initials} color={current.color} size={32} />
|
||||
@@ -139,15 +141,14 @@ export const TriageLayout: Layout = ({ s, p, f, now, width, widgetId, local }) =
|
||||
{current.preview && !notice ? <Txt text={current.preview} color={p.muted} size={13} lines={2} /> : null}
|
||||
</FlexWidget>
|
||||
{notice ? <NoticeBar p={p} f={f} notice={notice} /> : null}
|
||||
<Spacer size={8} />
|
||||
<FlexWidget style={{ flexDirection: 'row', width: 'match_parent' }}>
|
||||
<Button p={p} label={allFit ? labels.archive : undefined} iconName="archive" flex={1} click={action('archive', target)} />
|
||||
<Button p={p} label={allFit ? labels.archive : undefined} iconName="archive" flex={1} slop={8} click={action('archive', target)} />
|
||||
<Spacer size={6} horizontal />
|
||||
<Button p={p} label={allFit ? labels.trash : undefined} iconName="trash" flex={1} click={action('trash', target)} />
|
||||
<Button p={p} label={allFit ? labels.trash : undefined} iconName="trash" flex={1} slop={8} click={action('trash', target)} />
|
||||
<Spacer size={6} horizontal />
|
||||
<Button p={p} label={allFit || replyFits ? labels.reply : undefined} iconName="reply" variant="primary" flex={1} click={open(links.reply(current))} />
|
||||
<Button p={p} label={allFit || replyFits ? labels.reply : undefined} iconName="reply" variant="primary" flex={1} slop={8} click={open(links.reply(current))} />
|
||||
<Spacer size={6} horizontal />
|
||||
<Button p={p} iconName="chevronRight" click={action('triageNext', { widgetId, id: current.id })} />
|
||||
<Button p={p} iconName="chevronRight" slop={8} click={action('triageNext', { widgetId, id: current.id })} />
|
||||
</FlexWidget>
|
||||
</Surface>
|
||||
);
|
||||
|
||||
@@ -98,6 +98,8 @@ export const InvitationsLayout: Layout = ({ s, p, f, height }) => {
|
||||
const pad = height < 140 ? 10 : 14;
|
||||
const showPeople = inv.participants.length > 0 && height >= 132;
|
||||
const buttonHeight = height < 116 ? 28 : 32;
|
||||
// Tap area of 48dp around the buttons; it takes the bottom padding.
|
||||
const slop = Math.min(pad, Math.round((48 - buttonHeight) / 2));
|
||||
|
||||
const label = (
|
||||
inv.organizerName
|
||||
@@ -111,7 +113,7 @@ export const InvitationsLayout: Layout = ({ s, p, f, height }) => {
|
||||
const notice = noticeFor(s.notice, 'rsvp');
|
||||
|
||||
return (
|
||||
<Surface p={p} style={{ padding: pad }}>
|
||||
<Surface p={p} style={{ padding: pad, paddingBottom: pad - slop }}>
|
||||
<FlexWidget {...open(links.event(inv))} style={{ flexDirection: 'row', width: 'match_parent' }}>
|
||||
<FlexWidget
|
||||
style={{
|
||||
@@ -156,12 +158,13 @@ export const InvitationsLayout: Layout = ({ s, p, f, height }) => {
|
||||
label={f.t('widgets.invites.accept', 'Accept')}
|
||||
flex={1}
|
||||
height={buttonHeight}
|
||||
slop={slop}
|
||||
click={reply('accepted')}
|
||||
/>
|
||||
<Spacer size={6} horizontal />
|
||||
<Button p={p} label={f.t('widgets.invites.maybe', 'Maybe')} flex={1} height={buttonHeight} click={reply('tentative')} />
|
||||
<Button p={p} label={f.t('widgets.invites.maybe', 'Maybe')} flex={1} height={buttonHeight} slop={slop} click={reply('tentative')} />
|
||||
<Spacer size={6} horizontal />
|
||||
<Button p={p} label={f.t('widgets.invites.decline', 'Decline')} flex={1} height={buttonHeight} click={reply('declined')} />
|
||||
<Button p={p} label={f.t('widgets.invites.decline', 'Decline')} flex={1} height={buttonHeight} slop={slop} click={reply('declined')} />
|
||||
</FlexWidget>
|
||||
</Surface>
|
||||
);
|
||||
@@ -360,7 +363,9 @@ export const FreeTimeLayout: Layout = ({ s, p, f, now, width, height }) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<Surface p={p} style={{ paddingHorizontal: 14, paddingVertical: tier.pad }}>
|
||||
// The full-width button draws tier.button tall; its tap area also takes
|
||||
// the bottom padding (slop).
|
||||
<Surface p={p} style={{ paddingHorizontal: 14, paddingTop: tier.pad, paddingBottom: tier.button ? 0 : tier.pad }}>
|
||||
<FlexWidget
|
||||
{...open(links.calendar())}
|
||||
style={{ flexDirection: 'row', alignItems: 'center', width: 'match_parent', height: FREE_HEADLINE_HEIGHT }}
|
||||
@@ -427,7 +432,6 @@ export const FreeTimeLayout: Layout = ({ s, p, f, now, width, height }) => {
|
||||
</FlexWidget>
|
||||
) : null}
|
||||
</FlexWidget>
|
||||
{tier.button ? <Spacer size={tier.gap} /> : null}
|
||||
<Spacer />
|
||||
{tier.button ? (
|
||||
<FlexWidget style={{ flexDirection: 'row', width: 'match_parent' }}>
|
||||
@@ -437,6 +441,8 @@ export const FreeTimeLayout: Layout = ({ s, p, f, now, width, height }) => {
|
||||
label={f.t('widgets.free.mail', 'Mail my free times')}
|
||||
flex={1}
|
||||
height={tier.button}
|
||||
// Above, the tap area takes the gap and any height to spare; below, the padding.
|
||||
slop={{ top: Math.min(tier.pad, tier.gap + Math.max(0, height - freeTierHeight(tier))), bottom: tier.pad }}
|
||||
click={mail}
|
||||
/>
|
||||
</FlexWidget>
|
||||
@@ -481,7 +487,7 @@ export const TasksLayout: Layout = ({ s, p, f, now, height }) => {
|
||||
title={f.t('widgets.tasks.title', 'Tasks')}
|
||||
count={f.t('widgets.tasks.open_count', '{count} open', { count: openCount })}
|
||||
click={open(links.calendar())}
|
||||
trailing={<Fab p={p} iconName="plus" size={compact ? 28 : 32} click={open(links.calendar())} />}
|
||||
trailing={<Fab p={p} iconName="plus" size={compact ? 28 : 32} slop={compact ? 4 : 6} click={open(links.calendar())} />}
|
||||
/>
|
||||
{notice ? (
|
||||
<FlexWidget style={{ width: 'match_parent', paddingHorizontal: 8, paddingTop: 6, paddingBottom: 6 }}>
|
||||
@@ -665,6 +671,8 @@ export const BirthdaysLayout: Layout = ({ s, p, f, now, width, height }) => {
|
||||
variant="primary"
|
||||
iconName="send"
|
||||
label={narrow ? undefined : f.t('widgets.birthdays.send_wishes', 'Send wishes')}
|
||||
// The row is 50dp; the tap area takes its full height.
|
||||
slop={Math.floor((BIRTHDAY_ROW_HEIGHT - 32) / 2)}
|
||||
click={open(links.compose({ to: wish, subject: f.t('widgets.birthdays.wishes_subject', 'Happy birthday!') }))}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
@@ -222,6 +222,7 @@ export function Button({
|
||||
click,
|
||||
height = 32,
|
||||
flex,
|
||||
slop = 0,
|
||||
}: {
|
||||
p: WidgetPalette;
|
||||
label?: string;
|
||||
@@ -230,18 +231,26 @@ export function Button({
|
||||
click?: WidgetClick;
|
||||
height?: number;
|
||||
flex?: number;
|
||||
/**
|
||||
* Transparent tap area above and below the drawn button, so a 32dp button
|
||||
* still takes a 48dp tap. Take the same amount off the spacing around it.
|
||||
*/
|
||||
slop?: number | { top: number; bottom: number };
|
||||
}) {
|
||||
const primary = variant === 'primary';
|
||||
const [slopTop, slopBottom] = typeof slop === 'number' ? [slop, slop] : [slop.top, slop.bottom];
|
||||
const fg = primary ? p.primaryFg : p.fg;
|
||||
return (
|
||||
const face = (
|
||||
<FlexWidget
|
||||
{...click}
|
||||
{...(slop ? {} : click)}
|
||||
style={{
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
height,
|
||||
...(flex ? { flex } : { paddingHorizontal: label ? 12 : 0, width: label ? 'wrap_content' : height }),
|
||||
...(slop && flex
|
||||
? { width: 'match_parent' as const }
|
||||
: flex ? { flex } : { paddingHorizontal: label ? 12 : 0, width: label ? 'wrap_content' : height }),
|
||||
borderRadius: 6,
|
||||
backgroundColor: primary ? p.primary : p.bg,
|
||||
...(primary ? {} : { borderWidth: 1, borderColor: p.input }),
|
||||
@@ -252,6 +261,19 @@ export function Button({
|
||||
{label ? <TextWidget text={label} maxLines={1} style={{ color: fg, fontSize: 13, fontWeight: '500' }} /> : null}
|
||||
</FlexWidget>
|
||||
);
|
||||
if (!slop) return face;
|
||||
return (
|
||||
<FlexWidget
|
||||
{...click}
|
||||
style={{
|
||||
paddingTop: slopTop,
|
||||
paddingBottom: slopBottom,
|
||||
...(flex ? { flex } : { width: label ? 'wrap_content' : height }),
|
||||
}}
|
||||
>
|
||||
{face}
|
||||
</FlexWidget>
|
||||
);
|
||||
}
|
||||
|
||||
/** Round primary action (compose, new event), the webmail's floating button. */
|
||||
@@ -260,15 +282,18 @@ export function Fab({
|
||||
iconName,
|
||||
size = 32,
|
||||
click,
|
||||
slop = 0,
|
||||
}: {
|
||||
p: WidgetPalette;
|
||||
iconName: IconName;
|
||||
size?: number;
|
||||
click?: WidgetClick;
|
||||
/** Transparent tap area around the button (see Button). */
|
||||
slop?: number;
|
||||
}) {
|
||||
return (
|
||||
const face = (
|
||||
<FlexWidget
|
||||
{...click}
|
||||
{...(slop ? {} : click)}
|
||||
style={{
|
||||
width: size,
|
||||
height: size,
|
||||
@@ -281,6 +306,12 @@ export function Fab({
|
||||
<Icon name={iconName} color={p.primaryFg} size={Math.round(size / 2)} />
|
||||
</FlexWidget>
|
||||
);
|
||||
if (!slop) return face;
|
||||
return (
|
||||
<FlexWidget {...click} style={{ padding: slop, justifyContent: 'center', alignItems: 'center' }}>
|
||||
{face}
|
||||
</FlexWidget>
|
||||
);
|
||||
}
|
||||
|
||||
/** Icon-only ghost button (search in a header). */
|
||||
|
||||
Reference in New Issue
Block a user