自定义DateTime选择器小屏幕打开时内容溢出问题求助
解决DateTime选择器小屏幕内容溢出问题
问题描述
手动实现的DateTime输入组件在小屏幕设备上打开选择器时,出现内容向右溢出屏幕的情况:

组件包含日期、时间、时区三个可弹出选择器,实现代码如下:
import React, { memo, useEffect, useMemo, useRef, useState } from 'react' import { CalendarIcon, ClockIcon, WorldIcon } from 'src/assets/icons' import Calendar from 'src/components/Calendar/Calendar' import moment from 'moment' import { generateMeetingTimes } from 'src/constants' import { default as momentTimezone } from 'moment-timezone' import { UseFormReturn } from 'react-hook-form' export interface DateTimeInputProps { form: UseFormReturn<any> hasError?: string className?: string } const DateTime = ({ form, hasError, className }: DateTimeInputProps) => { const date = form.getValues('when.date') const time = form.getValues('when.time') const offset = form.getValues('when.offset') const timezoneNames = momentTimezone.tz.names() const timezoneArray = timezoneNames.map((timezone) => { const abbreviation = moment.tz(timezone).zoneAbbr() return `${timezone} (${abbreviation})` }) const [timezonePicker, setTimezonePicker] = useState<boolean>(false) const [datePicker, setDatePicker] = useState<boolean>(false) const [timePicker, setTimePicker] = useState<boolean>(false) const divRef = useRef<any>(null) const meetingDefaultTimes = useMemo(() => { return generateMeetingTimes() }, []) const toggleDatePicker = () => { setTimezonePicker(false) setTimePicker(false) setDatePicker(!datePicker) } const toggleTimePicker = () => { setTimezonePicker(false) setDatePicker(false) setTimePicker(!timePicker) } const toggleTimezonePicker = () => { setTimePicker(false) setDatePicker(false) setTimezonePicker(true) } const handleTime = (time: string) => { form.setValue('when.time', time) setTimePicker(false) } const handleOffset = (offset: string) => { form.setValue('when.offset', offset) setTimezonePicker(false) } const handleDate = (date: Date) => { console.log('called') form.setValue('when.date', date) setDatePicker(false) } useEffect(() => { const handleClickOutside = (event: any) => { if (divRef.current && !divRef.current?.contains(event.target)) { setTimezonePicker(false) setTimePicker(false) setDatePicker(false) } } document.addEventListener('mousedown', handleClickOutside) return () => { document.removeEventListener('mousedown', handleClickOutside) } }, [divRef]) return ( <div ref={divRef} className={`relative px-2 w-96 min-w-58 h-h-input flex bg-primary ${hasError ? 'border-error' : 'border-input-border'} border-1 rounded-input bg-transparent items-center ${className}`} > <span className="absolute top-0 -translate-y-1/2 text-placeholder pointer-events-none left-2.5 ease-linear duration-150 bg-background font text-[12px] px-[4px]"> When </span> <div className="relative flex-[0.6] mr-2 h-10 flex bg-gray items-center justify-center rounded-input cursor-pointer z-10"> <input type="text" value={date && moment(date).format('MMM DD, YYYY')} className="w-full bg-gray outline-none text-center" onClick={toggleDatePicker} {...(form.register('date') as any)} /> {!date && ( <CalendarIcon fill={'grey'} width={22} height={22} className="absolute w-full left-0 right-0 pointer-events-none" /> )} {datePicker && ( <div className="absolute top-14 z-10"> <Calendar selectedDate={date} setSelectedDate={(date: Date) => handleDate(date)} /> </div> )} </div> <div className="relative px-[6px] h-10 flex bg-gray items-center justify-items-center rounded-input mr-2 cursor-pointer flex-[0.4]" ref={divRef} > <input type="text" value={time} className="w-full bg-gray outline-none text-center" onClick={toggleTimePicker} {...(form.register('time') as any)} /> {!timePicker && !time && ( <ClockIcon fill={'grey'} width={22} height={22} className="absolute w-full left-0 right-0 pointer-events-none" /> )} {timePicker && ( <div className="border-input-border border-1 absolute top-0 left-[-15px] flex flex-col items-start rounded-input right-[55px] mt-56px max-h-56 overflow-y-scroll z-10 bg-background w-[120px] z-10 scrollbar-custom"> {meetingDefaultTimes.map((option, index) => ( <div key={index} className="hover:bg-primary hover:text-white w-full p-2 rounded-input cursor-pointer" onClick={() => { handleTime(option) }} > <span>{option}</span> </div> ))} </div> )} </div> <div className="relative flex-[0.4] px-4 h-10 flex bg-gray items-center justify-center rounded-input cursor-pointer"> <input value={offset} className="w-full bg-gray outline-none text-center" onClick={toggleTimezonePicker} {...(form.register('timezone') as any)} /> {!offset && ( <WorldIcon fill={'grey'} width={22} height={22} className="absolute w-full left-0 right-0 pointer-events-none" /> )} {timezonePicker && ( <div className="border-input-border border-1 absolute top-0 left-[-15px] flex flex-col items-start rounded-input right-[55px] mt-56px max-h-56 overflow-y-scroll z-10 bg-background w-[280px] z-10 scrollbar-custom"> {timezoneArray.map((timezone, index) => ( <div key={index} className="hover:bg-primary hover:text-white w-full p-2 rounded-input cursor-pointer" onClick={() => { handleOffset(timezone) }} > <span>{timezone}</span> </div> ))} </div> )} </div> </div> ) } export default memo(DateTime)
问题原因
- 固定宽度与绝对定位冲突:时间/时区选择器使用固定宽度(
w-[120px]、w-[280px]),同时设置负left值和固定right值,小屏幕下容器宽度超过视口导致溢出。 - 外层容器无响应式适配:最外层容器使用固定宽度
w-96,小屏幕下无法自适应收缩。 - 长文本无截断处理:时区名称较长,未做文本截断,易引发横向溢出。
- 重复ref绑定:
divRef同时绑定到最外层容器和时间选择器父容器,导致外部点击关闭逻辑异常。
解决方案
1. 调整选择器容器定位与宽度
将选择器改为相对于父元素左对齐,去掉固定right值,用max-w-full限制宽度不超出视口:
- 时间选择器容器修改:
{timePicker && ( <div className="border-input-border border-1 absolute top-14 left-0 right-0 max-w-full flex flex-col items-start rounded-input max-h-56 overflow-y-scroll z-10 bg-background w-auto scrollbar-custom"> {meetingDefaultTimes.map((option, index) => ( <div key={index} className="hover:bg-primary hover:text-white w-full p-2 rounded-input cursor-pointer" onClick={() => { handleTime(option) }} > <span>{option}</span> </div> ))} </div> )}
- 时区选择器容器修改:
{timezonePicker && ( <div className="border-input-border border-1 absolute top-14 left-0 right-0 max-w-full flex flex-col items-start rounded-input max-h-56 overflow-y-scroll z-10 bg-background w-auto scrollbar-custom"> {timezoneArray.map((timezone, index) => ( <div key={index} className="hover:bg-primary hover:text-white w-full p-2 rounded-input cursor-pointer" onClick={() => { handleOffset(timezone) }} > <span className="truncate">{timezone}</span> </div> ))} </div> )}
2. 外层容器添加响应式宽度
替换固定宽度为响应式设置,保证小屏幕下正常显示:
<div ref={rootRef} className={`relative px-2 w-full max-w-[384px] min-w-[280px] h-h-input flex bg-primary ${hasError ? 'border-error' : 'border-input-border'} border-1 rounded-input bg-transparent items-center ${className}`} >
3. 修复重复ref问题
给不同容器创建独立ref,确保外部点击关闭逻辑正常:
const rootRef = useRef<any>(null) const timePickerRef = useRef<any>(null) const timezonePickerRef = useRef<any>(null) // 更新useEffect逻辑 useEffect(() => { const handleClickOutside = (event: any) => { const isInsideRoot = rootRef.current?.contains(event.target) const isInsideTimePicker = timePickerRef.current?.contains(event.target) const isInsideTimezonePicker = timezonePickerRef.current?.contains(event.target) if (!isInsideRoot && !isInsideTimePicker && !isInsideTimezonePicker) { setTimezonePicker(false) setTimePicker(false) setDatePicker(false) } } document.addEventListener('mousedown', handleClickOutside) return () => { document.removeEventListener('mousedown', handleClickOutside) } }, [])
修改后的完整代码
import React, { memo, useEffect, useMemo, useRef, useState } from 'react' import { CalendarIcon, ClockIcon, WorldIcon } from 'src/assets/icons' import Calendar from 'src/components/Calendar/Calendar' import moment from 'moment' import { generateMeetingTimes } from 'src/constants' import { default as momentTimezone } from 'moment-timezone' import { UseFormReturn } from 'react-hook-form' export interface DateTimeInputProps { form: UseFormReturn<any> hasError?: string className?: string } const DateTime = ({ form, hasError, className }: DateTimeInputProps) => { const date = form.getValues('when.date') const time = form.getValues('when.time') const offset = form.getValues('when.offset') const timezoneNames = momentTimezone.tz.names() const timezoneArray = timezoneNames.map((timezone) => { const abbreviation = moment.tz(timezone).zoneAbbr() return `${timezone} (${abbreviation})` }) const [timezonePicker, setTimezonePicker] = useState<boolean>(false) const [datePicker, setDatePicker] = useState<boolean>(false) const [timePicker, setTimePicker] = useState<boolean>(false) const rootRef = useRef<any>(null) const timePickerRef = useRef<any>(null) const timezonePickerRef = useRef<any>(null) const meetingDefaultTimes = useMemo(() => { return generateMeetingTimes() }, []) const toggleDatePicker = () => { setTimezonePicker(false) setTimePicker(false) setDatePicker(!datePicker) } const toggleTimePicker = () => { setTimezonePicker(false) setDatePicker(false) setTimePicker(!timePicker) } const toggleTimezonePicker = () => { setTimePicker(false) setDatePicker(false) setTimezonePicker(true) } const handleTime = (time: string) => { form.setValue('when.time', time) setTimePicker(false) } const handleOffset = (offset: string) => { form.setValue('when.offset', offset) setTimezonePicker(false) } const handleDate = (date: Date) => { console.log('called') form.setValue('when.date', date) setDatePicker(false) } useEffect(() => { const handleClickOutside = (event: any) => { const isInsideRoot = rootRef.current?.contains(event.target) const isInsideTimePicker = timePickerRef.current?.contains(event.target) const isInsideTimezonePicker = timezonePickerRef.current?.contains(event.target) if (!isInsideRoot && !isInsideTimePicker && !isInsideTimezonePicker) { setTimezonePicker(false) setTimePicker(false) setDatePicker(false) } } document.addEventListener('mousedown', handleClickOutside) return () => { document.removeEventListener('mousedown', handleClickOutside) } }, []) return ( <div ref={rootRef} className={`relative px-2 w-full max-w-[384px] min-w-[280px] h-h-input flex bg-primary ${hasError ? 'border-error' : 'border-input-border'} border-1 rounded-input bg-transparent items-center ${className}`} > <span className="absolute top-0 -translate-y-1/2 text-placeholder pointer-events-none left-2.5 ease-linear duration-150 bg-background font text-[12px] px-[4px]"> When </span> <div className="relative flex-[0.6] mr-2 h-10 flex bg-gray items-center justify-center rounded-input cursor-pointer z-10"> <input type="text" value={date && moment(date).format('MMM DD, YYYY')} className="w-full bg-gray outline-none text-center" onClick={toggleDatePicker} {...(form.register('date') as any)} /> {!date && ( <CalendarIcon fill={'grey'} width={22} height={22} className="absolute w-full left-0 right-0 pointer-events-none" /> )} {datePicker && ( <div className="absolute top-14 z-10 left-0 right-0 max-w-full"> <Calendar selectedDate={date} setSelectedDate={(date: Date) => handleDate(date)} /> </div> )} </div> <div className="relative px-[6px] h-10 flex bg-gray items-center justify-items-center rounded-input mr-2 cursor-pointer flex-[0.4]" ref={timePickerRef} > <input type="text" value={time} className="w-full bg-gray outline-none text-center" onClick={toggleTimePicker} {...(form.register('time') as any)} /> {!timePicker && !time && ( <ClockIcon fill={'grey'} width={22} height={22} className="absolute w-full left-0 right-0 pointer-events-none" /> )} {timePicker && ( <div className="border-input-border border-1 absolute top-14 left-0 right-0 max-w-full flex flex-col items-start rounded-input max-h-56 overflow-y-scroll z-10 bg-background w-auto scrollbar-custom"> {meetingDefaultTimes.map((option, index) => ( <div key={index} className="hover:bg-primary hover:text-white w-full p-2 rounded-input cursor-pointer" onClick={() => { handleTime(option) }} > <span>{option}</span> </div> ))} </div> )} </div> <div className="relative
相关产品推荐
相关产品推荐

