You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

自定义DateTime选择器小屏幕打开时内容溢出问题求助

解决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)

问题原因

  1. 固定宽度与绝对定位冲突:时间/时区选择器使用固定宽度(w-[120px]、w-[280px]),同时设置负left值和固定right值,小屏幕下容器宽度超过视口导致溢出。
  2. 外层容器无响应式适配:最外层容器使用固定宽度w-96,小屏幕下无法自适应收缩。
  3. 长文本无截断处理:时区名称较长,未做文本截断,易引发横向溢出。
  4. 重复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
相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.28 23:47:55