fix(web): submit schedule picker on enter (#703)

* fix(web): submit schedule picker on enter

* fix(web): center schedule picker on touch tablets
This commit is contained in:
SSU-WEI HUANG
2026-05-27 07:14:14 +08:00
committed by GitHub
parent cfc8508651
commit 166b711fec
3 changed files with 66 additions and 11 deletions
@@ -467,8 +467,8 @@ export function HappyComposer(props: {
}, [haptic])
const handleSubmit = useCallback((event?: ReactFormEvent<HTMLFormElement>) => {
if (event && !attachmentsReady) {
event.preventDefault()
event?.preventDefault()
if (!attachmentsReady) {
return
}
setShowContinueHint(false)
@@ -0,0 +1,41 @@
import { fireEvent, render, screen } from '@testing-library/react'
import { describe, expect, it, vi } from 'vitest'
import { I18nProvider } from '@/lib/i18n-context'
import { ScheduleTimePicker } from './ScheduleTimePicker'
describe('ScheduleTimePicker interactions', () => {
it('submits the specific datetime when Enter is pressed in the datetime input', () => {
const anchorRef = { current: document.createElement('button') }
const onSchedule = vi.fn()
const onClose = vi.fn()
const future = new Date(Date.now() + 60 * 60 * 1000)
const pad = (n: number) => String(n).padStart(2, '0')
const value = `${future.getFullYear()}-${pad(future.getMonth() + 1)}-${pad(future.getDate())}T${pad(future.getHours())}:${pad(future.getMinutes())}`
const onParentKeyDown = vi.fn()
render(
<I18nProvider>
<div onKeyDown={onParentKeyDown}>
<ScheduleTimePicker
anchorRef={anchorRef}
onSchedule={onSchedule}
onClose={onClose}
pendingSchedule={null}
/>
</div>
</I18nProvider>
)
fireEvent.click(screen.getByRole('button', { name: /specific/i }))
const input = screen.getByDisplayValue('')
fireEvent.change(input, { target: { value } })
const defaultNotPrevented = fireEvent.keyDown(input, { key: 'Enter' })
expect(defaultNotPrevented).toBe(false)
expect(onParentKeyDown).not.toHaveBeenCalled()
expect(onSchedule).toHaveBeenCalledWith({ type: 'absolute', ms: new Date(value).getTime() })
expect(onClose).toHaveBeenCalledTimes(1)
})
})
@@ -1,4 +1,4 @@
import { useEffect, useLayoutEffect, useRef, useState } from 'react'
import { useEffect, useLayoutEffect, useRef, useState, type KeyboardEvent } from 'react'
import { useTranslation } from '@/lib/use-translation'
// ---------------------------------------------------------------------------
@@ -166,6 +166,7 @@ export function ScheduleTimePicker({ onSchedule, onClose, anchorRef, pendingSche
const panelRef = useRef<HTMLDivElement>(null)
const [pos, setPos] = useState<SchedulePickerPlacement | null>(null)
const [isMobilePanel, setIsMobilePanel] = useState(false)
const [isContentConstrained, setIsContentConstrained] = useState(false)
// Compute fixed position and keep it inside the visual viewport. On mobile,
// use a bottom panel instead of anchoring to the tiny toolbar button.
@@ -176,23 +177,28 @@ export function ScheduleTimePicker({ onSchedule, onClose, anchorRef, pendingSche
if (!anchor) return
const mobile = window.matchMedia('(max-width: 640px), (pointer: coarse)').matches
setIsMobilePanel(mobile)
const fullHeight = (panel?.scrollHeight ?? (tab === 'specific' ? 260 : 180)) + 4
if (mobile) {
const viewportHeight = window.visualViewport?.height ?? window.innerHeight
setIsContentConstrained(fullHeight > Math.min(viewportHeight * 0.85, viewportHeight - 24))
setPos(null)
return
}
const rect = anchor.getBoundingClientRect()
const viewport = window.visualViewport
setPos(computeSchedulePickerPlacement({
const placement = computeSchedulePickerPlacement({
anchor: rect,
panelWidth: panel?.offsetWidth || 288,
panelHeight: panel?.offsetHeight || 280,
panelWidth: panel?.offsetWidth || 320,
panelHeight: fullHeight,
viewport: {
width: viewport?.width ?? window.innerWidth,
height: viewport?.height ?? window.innerHeight,
offsetLeft: viewport?.offsetLeft ?? 0,
offsetTop: viewport?.offsetTop ?? 0,
},
}))
})
setIsContentConstrained(placement.maxHeight < fullHeight)
setPos(placement)
}
measure()
window.addEventListener('resize', measure, { passive: true })
@@ -271,6 +277,13 @@ export function ScheduleTimePicker({ onSchedule, onClose, anchorRef, pendingSche
if (specificError) setSpecificError(null)
}
const handleSpecificKeyDown = (event: KeyboardEvent<HTMLInputElement>) => {
if (event.key !== 'Enter') return
event.preventDefault()
event.stopPropagation()
handleSpecificSubmit()
}
return (
<div
ref={panelRef}
@@ -285,8 +298,8 @@ export function ScheduleTimePicker({ onSchedule, onClose, anchorRef, pendingSche
}
className={
isMobilePanel
? 'z-50 box-border max-h-[min(60dvh,calc(var(--app-viewport-height,100dvh)-24px))] overflow-y-auto rounded-xl border border-[var(--app-border)] bg-[var(--app-bg)] shadow-lg fixed inset-x-3 bottom-[calc(env(safe-area-inset-bottom)+12px)]'
: 'z-50 box-border w-72 overflow-y-auto rounded-xl border border-[var(--app-border)] bg-[var(--app-bg)] shadow-lg'
? `z-50 box-border max-h-[min(85dvh,calc(var(--app-viewport-height,100dvh)-24px))] ${isContentConstrained ? 'overflow-y-auto' : 'overflow-y-visible'} rounded-xl border border-[var(--app-border)] bg-[var(--app-bg)] shadow-lg fixed inset-x-3 bottom-[calc(env(safe-area-inset-bottom)+12px)] sm:left-1/2 sm:right-auto sm:w-80 sm:-translate-x-1/2`
: `z-50 box-border w-80 max-w-[calc(100vw-16px)] ${isContentConstrained ? 'overflow-y-auto' : 'overflow-y-visible'} rounded-xl border border-[var(--app-border)] bg-[var(--app-bg)] shadow-lg`
}
onPointerDown={(e) => e.stopPropagation()}
>
@@ -353,7 +366,8 @@ export function ScheduleTimePicker({ onSchedule, onClose, anchorRef, pendingSche
min={minDatetimeLocal}
max={maxDatetimeLocal}
onChange={(e) => handleSpecificChange(e.target.value)}
className="w-full rounded-lg border border-[var(--app-border)] bg-[var(--app-bg)] px-2 py-1.5 text-sm text-[var(--app-fg)] focus:outline-none focus:ring-1 focus:ring-[var(--app-link)]"
onKeyDown={handleSpecificKeyDown}
className="w-full rounded-lg border border-[var(--app-border)] bg-[var(--app-bg)] px-3 py-2 text-sm text-[var(--app-fg)] focus:outline-none focus:ring-1 focus:ring-[var(--app-link)]"
/>
{specificError ? (
<p className="text-xs text-red-500">{specificError}</p>
@@ -366,7 +380,7 @@ export function ScheduleTimePicker({ onSchedule, onClose, anchorRef, pendingSche
type="button"
disabled={!specificValue}
onClick={handleSpecificSubmit}
className="w-full rounded-lg bg-blue-500 px-3 py-1.5 text-sm font-medium text-white transition-colors hover:bg-blue-600 disabled:cursor-not-allowed disabled:opacity-40"
className="w-full rounded-lg bg-blue-500 px-3 py-2 text-sm font-medium text-white transition-colors hover:bg-blue-600 disabled:cursor-not-allowed disabled:opacity-40"
>
{t('composer.scheduleSend')}
</button>