モダンUIコンポーネントの実装パターン
実装ガイドReact、Vue、Angularで共通して使えるUIパターンを解説
時間を選択するための入力コンポーネント。ドロップダウンやスピナーで直感的に時刻を設定
5分、10分、15分、30分単位など、用途に応じて時間の刻み幅を自由に設定可能。会議予約や診療予約など様々なシーンに対応
国際的な利用を考慮し、12時間形式(AM/PM表示)と24時間形式の両方に対応。ユーザーの地域設定に合わせて切り替え可能
ドロップダウン形式で素早く時刻を選択。選択済みの時刻はハイライト表示され、視覚的にわかりやすい設計
プロンプト例:
診療予約システム用のタイムピッカーを実装してください。営業時間(9:00-18:00)内のみ選択可能、30分単位、昼休み(12:00-13:00)は選択不可、既に予約済みの時間帯は無効化表示、選択可能な直近の時刻を自動提案する機能を含めてください。
プロンプト例:
勤怠管理用のタイムピッカーを作成してください。出勤・退勤時刻の入力、深夜勤務の判定(22:00-5:00)、休憩時間の自動計算、残業時間の警告表示、前日からの連続勤務対応(24時間を超える表示)を実装してください。
プロンプト例:
会議スケジューラー用のタイムピッカーを実装してください。複数タイムゾーン対応、参加者の空き時間をリアルタイム表示、会議室の予約状況連携、定期会議の時間パターン設定、カレンダーAPIとの連携機能を含めてください。
import React, { useState, useRef, useEffect } from 'react';
interface TimePickerProps {
value?: string;
onChange?: (time: string) => void;
format?: '12h' | '24h';
minuteStep?: number;
disabled?: boolean;
placeholder?: string;
className?: string;
}
export const TimePicker: React.FC<TimePickerProps> = ({
value = '',
onChange,
format = '24h',
minuteStep = 15,
disabled = false,
placeholder = '時刻を選択',
className = ''
}) => {
const [isOpen, setIsOpen] = useState(false);
const [selectedTime, setSelectedTime] = useState(value);
const dropdownRef = useRef<HTMLDivElement>(null);
const hours = format === '24h'
? Array.from({ length: 24 }, (_, i) => i)
: Array.from({ length: 12 }, (_, i) => i + 1);
const minutes = Array.from(
{ length: 60 / minuteStep },
(_, i) => i * minuteStep
);
const formatTime = (hour: number, minute: number, period?: 'AM' | 'PM') => {
const h = hour.toString().padStart(2, '0');
const m = minute.toString().padStart(2, '0');
if (format === '12h' && period) {
const displayHour = hour === 0 ? 12 : hour;
return `${displayHour}:${m} ${period}`;
}
return `${h}:${m}`;
};
const handleTimeSelect = (hour: number, minute: number, period?: 'AM' | 'PM') => {
const time = formatTime(hour, minute, period);
setSelectedTime(time);
onChange?.(time);
setIsOpen(false);
};
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
setIsOpen(false);
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => document.removeEventListener('mousedown', handleClickOutside);
}, []);
const buttonClasses = [
'w-full px-4 py-2 pr-10 text-left bg-white border rounded-lg',
'focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent',
disabled ? 'bg-gray-100 cursor-not-allowed' : 'hover:border-gray-400',
isOpen ? 'ring-2 ring-blue-500 border-transparent' : 'border-gray-300'
].join(' ');
const chevronClasses = [
'absolute right-3 top-1/2 transform -translate-y-1/2 w-5 h-5 text-gray-400',
'transition-transform duration-200',
isOpen ? 'rotate-180' : ''
].join(' ');
return (
<div className={`relative ${className}`} ref={dropdownRef}>
<button
type="button"
onClick={() => !disabled && setIsOpen(!isOpen)}
disabled={disabled}
className={buttonClasses}
>
<div className="flex items-center">
<svg className="w-5 h-5 mr-2 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span className={selectedTime ? 'text-gray-900' : 'text-gray-500'}>
{selectedTime || placeholder}
</span>
</div>
<svg
className={chevronClasses}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
</button>
{isOpen && (
<div className="absolute z-50 w-full mt-1 bg-white border border-gray-200 rounded-lg shadow-lg">
<div className="p-2 max-h-64 overflow-y-auto">
{format === '24h' ? (
<div className="grid grid-cols-4 gap-1">
{hours.map(hour =>
minutes.map(minute => {
const timeStr = formatTime(hour, minute);
const isSelected = selectedTime === timeStr;
const btnClasses = [
'px-2 py-1 text-sm rounded hover:bg-blue-50',
isSelected ? 'bg-blue-500 text-white hover:bg-blue-600' : 'text-gray-700'
].join(' ');
return (
<button
key={`${hour}-${minute}`}
onClick={() => handleTimeSelect(hour, minute)}
className={btnClasses}
>
{timeStr}
</button>
);
})
)}
</div>
) : (
<div>
<div className="mb-2">
<div className="text-xs font-semibold text-gray-500 mb-1">午前</div>
<div className="grid grid-cols-4 gap-1">
{hours.map(hour =>
minutes.map(minute => {
const timeStr = formatTime(hour, minute, 'AM');
const isSelected = selectedTime === timeStr;
const btnClasses = [
'px-2 py-1 text-sm rounded hover:bg-blue-50',
isSelected ? 'bg-blue-500 text-white hover:bg-blue-600' : 'text-gray-700'
].join(' ');
return (
<button
key={`am-${hour}-${minute}`}
onClick={() => handleTimeSelect(hour, minute, 'AM')}
className={btnClasses}
>
{timeStr}
</button>
);
})
)}
</div>
</div>
<div>
<div className="text-xs font-semibold text-gray-500 mb-1">午後</div>
<div className="grid grid-cols-4 gap-1">
{hours.map(hour =>
minutes.map(minute => {
const timeStr = formatTime(hour, minute, 'PM');
const isSelected = selectedTime === timeStr;
const btnClasses = [
'px-2 py-1 text-sm rounded hover:bg-blue-50',
isSelected ? 'bg-blue-500 text-white hover:bg-blue-600' : 'text-gray-700'
].join(' ');
return (
<button
key={`pm-${hour}-${minute}`}
onClick={() => handleTimeSelect(hour, minute, 'PM')}
className={btnClasses}
>
{timeStr}
</button>
);
})
)}
</div>
</div>
</div>
)}
</div>
</div>
)}
</div>
);
};
// 基本的な使用例
import { TimePicker } from './components/time-picker';
import { useState } from 'react';
function AppointmentForm() {
const [startTime, setStartTime] = useState('');
const [endTime, setEndTime] = useState('');
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (!startTime || !endTime) {
alert('開始時刻と終了時刻を選択してください');
return;
}
// 時刻の妥当性チェック
const start = new Date(`2024-01-01 ${startTime}`);
const end = new Date(`2024-01-01 ${endTime}`);
if (start >= end) {
alert('終了時刻は開始時刻より後に設定してください');
return;
}
console.log('予約時間:', { startTime, endTime });
};
return (
<form onSubmit={handleSubmit} className="space-y-4 max-w-md">
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium mb-1">
開始時刻
</label>
<TimePicker
value={startTime}
onChange={setStartTime}
format="24h"
minuteStep={30}
placeholder="開始時刻"
/>
</div>
<div>
<label className="block text-sm font-medium mb-1">
終了時刻
</label>
<TimePicker
value={endTime}
onChange={setEndTime}
format="24h"
minuteStep={30}
placeholder="終了時刻"
/>
</div>
</div>
<button
type="submit"
className="w-full px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
disabled={!startTime || !endTime}
>
予約を確定
</button>
</form>
);
}