From 29a5baa9f7b3640758e4d0c7be887a230eba2ed0 Mon Sep 17 00:00:00 2001 From: TheGreatAlgo <37487508+TheGreatAlgo@users.noreply.github.com> Date: Tue, 14 Oct 2025 14:49:59 -0400 Subject: [PATCH 1/3] fix: initial --- src/components/DateRange/index.js | 109 ++++++++++++++++++++++----- src/components/DateRange/index.scss | 2 + src/components/TimePicker/index.js | 107 ++++++++++++++++++++++++++ src/components/TimePicker/index.scss | 84 +++++++++++++++++++++ src/styles.js | 14 ++++ src/styles.scss | 1 + 6 files changed, 299 insertions(+), 18 deletions(-) create mode 100644 src/components/TimePicker/index.js create mode 100644 src/components/TimePicker/index.scss diff --git a/src/components/DateRange/index.js b/src/components/DateRange/index.js index 3c963ecd2..e1b74fe04 100644 --- a/src/components/DateRange/index.js +++ b/src/components/DateRange/index.js @@ -1,9 +1,10 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; import Calendar from '../Calendar'; +import TimePicker from '../TimePicker'; import { rangeShape } from '../DayCell'; import { findNextRangeIndex, generateStyles } from '../../utils'; -import { isBefore, differenceInCalendarDays, addDays, min, isWithinInterval, max } from 'date-fns'; +import { isBefore, differenceInCalendarDays, addDays, min, isWithinInterval, max, setHours, setMinutes, getHours, getMinutes } from 'date-fns'; import classnames from 'classnames'; import coreStyles from '../../styles'; @@ -97,10 +98,25 @@ class DateRange extends Component { const selectedRange = ranges[focusedRangeIndex]; if (!selectedRange) return; const newSelection = this.calcNewSelection(value, isSingleValue); + + // Preserve time from existing dates when changing date + let { startDate, endDate } = newSelection.range; + if (selectedRange.startDate && startDate) { + const hours = getHours(selectedRange.startDate); + const minutes = getMinutes(selectedRange.startDate); + startDate = setMinutes(setHours(startDate, hours), minutes); + } + if (selectedRange.endDate && endDate) { + const hours = getHours(selectedRange.endDate); + const minutes = getMinutes(selectedRange.endDate); + endDate = setMinutes(setHours(endDate, hours), minutes); + } + onChange({ [selectedRange.key || `range${focusedRangeIndex + 1}`]: { ...selectedRange, - ...newSelection.range, + startDate, + endDate, }, }); this.setState({ @@ -123,24 +139,75 @@ class DateRange extends Component { const color = ranges[focusedRange[0]]?.color || rangeColors[focusedRange[0]] || color; this.setState({ preview: { ...val.range, color } }); }; + handleTimeChange = (date, isStart) => { + const { onChange, ranges } = this.props; + const focusedRange = this.props.focusedRange || this.state.focusedRange; + const focusedRangeIndex = focusedRange[0]; + const selectedRange = ranges[focusedRangeIndex]; + if (!selectedRange) return; + + const key = selectedRange.key || `range${focusedRangeIndex + 1}`; + onChange({ + [key]: { + ...selectedRange, + [isStart ? 'startDate' : 'endDate']: date, + }, + }); + }; + render() { + const { showTimePicker, showHours, showMinutes, ranges } = this.props; + const focusedRange = this.props.focusedRange || this.state.focusedRange; + const focusedRangeIndex = focusedRange[0]; + const selectedRange = ranges[focusedRangeIndex]; + return ( - { - this.updatePreview(value ? this.calcNewSelection(value) : null); - }} - {...this.props} - displayMode="dateRange" - className={classnames(this.styles.dateRangeWrapper, this.props.className)} - onChange={this.setSelection} - updateRange={val => this.setSelection(val, false)} - ref={target => { - this.calendar = target; - }} - /> +
+ { + this.updatePreview(value ? this.calcNewSelection(value) : null); + }} + {...this.props} + displayMode="dateRange" + className={classnames(this.props.className)} + onChange={this.setSelection} + updateRange={val => this.setSelection(val, false)} + ref={target => { + this.calendar = target; + }} + /> + {showTimePicker && selectedRange && ( +
+
+ Start Time + this.handleTimeChange(date, true)} + showHours={showHours} + showMinutes={showMinutes} + disabled={selectedRange.disabled} + styles={this.styles} + ariaLabels={this.props.ariaLabels} + /> +
+
+ End Time + this.handleTimeChange(date, false)} + showHours={showHours} + showMinutes={showMinutes} + disabled={selectedRange.disabled} + styles={this.styles} + ariaLabels={this.props.ariaLabels} + /> +
+
+ )} +
); } } @@ -152,6 +219,9 @@ DateRange.defaultProps = { retainEndDateOnFirstSelection: false, rangeColors: ['#3d91ff', '#3ecf8e', '#fed14c'], disabledDates: [], + showTimePicker: false, + showHours: true, + showMinutes: true, }; DateRange.propTypes = { @@ -162,6 +232,9 @@ DateRange.propTypes = { ranges: PropTypes.arrayOf(rangeShape), moveRangeOnFirstSelection: PropTypes.bool, retainEndDateOnFirstSelection: PropTypes.bool, + showTimePicker: PropTypes.bool, + showHours: PropTypes.bool, + showMinutes: PropTypes.bool, }; export default DateRange; diff --git a/src/components/DateRange/index.scss b/src/components/DateRange/index.scss index 89dcf26d2..441b02f34 100644 --- a/src/components/DateRange/index.scss +++ b/src/components/DateRange/index.scss @@ -1,3 +1,5 @@ .rdrDateRangeWrapper{ user-select: none; + display: flex; + flex-direction: row; } diff --git a/src/components/TimePicker/index.js b/src/components/TimePicker/index.js new file mode 100644 index 000000000..358e0f568 --- /dev/null +++ b/src/components/TimePicker/index.js @@ -0,0 +1,107 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import classnames from 'classnames'; +import { setHours, setMinutes, getHours, getMinutes } from 'date-fns'; + +class TimeWheel extends Component { + handleClick = (value) => { + this.props.onChange(value); + }; + + render() { + const { value, max, disabled, styles } = this.props; + const items = Array.from({ length: max }, (_, i) => i); + + return ( +
+
+ {items.map((item) => ( +
this.handleClick(item)} + className={classnames(styles.timeWheelItem, { + [styles.timeWheelItemActive]: item === value, + })}> + {String(item).padStart(2, '0')} +
+ ))} +
+
+ ); + } +} + +class TimePicker extends Component { + handleHourChange = (hour) => { + const { date, onChange } = this.props; + const newDate = setHours(date || new Date(), hour); + onChange && onChange(newDate); + }; + + handleMinuteChange = (minute) => { + const { date, onChange } = this.props; + const newDate = setMinutes(date || new Date(), minute); + onChange && onChange(newDate); + }; + + render() { + const { date, showHours, showMinutes, disabled, styles } = this.props; + const currentDate = date || new Date(); + const hours = getHours(currentDate); + const minutes = getMinutes(currentDate); + + if (!showHours && !showMinutes) { + return null; + } + + return ( +
+ {showHours && ( +
+
Hour
+ +
+ )} + {showHours && showMinutes && ( +
:
+ )} + {showMinutes && ( +
+
Minute
+ +
+ )} +
+ ); + } +} + +TimePicker.defaultProps = { + showHours: true, + showMinutes: true, + disabled: false, +}; + +TimePicker.propTypes = { + date: PropTypes.object, + onChange: PropTypes.func, + showHours: PropTypes.bool, + showMinutes: PropTypes.bool, + disabled: PropTypes.bool, + styles: PropTypes.object, + ariaLabels: PropTypes.object, +}; + +export default TimePicker; diff --git a/src/components/TimePicker/index.scss b/src/components/TimePicker/index.scss new file mode 100644 index 000000000..cf62e9d42 --- /dev/null +++ b/src/components/TimePicker/index.scss @@ -0,0 +1,84 @@ +.rdrTimePickerContainer { + display: flex; + flex-direction: row; + justify-content: flex-start; + padding: 20px 15px; + border-left: 1px solid #eff2f7; + background: #fff; + min-width: 200px; + gap: 20px; +} + +.rdrTimePickerSection { + display: flex; + flex-direction: column; + align-items: center; +} + +.rdrTimePickerWrapper { + display: flex; + align-items: center; + justify-content: center; + gap: 10px; + padding: 10px; +} + +.rdrTimePicker { + display: flex; + flex-direction: column; + align-items: center; + gap: 8px; +} + +.rdrTimePickerLabel { + font-size: 12px; + font-weight: 500; + color: #849095; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.rdrTimePickerDivider { + font-size: 24px; + font-weight: 600; + color: #7d888d; + margin: 0 8px; + margin-top: 24px; +} + +.rdrTimeWheel { + width: 60px; + height: 250px; + overflow: hidden; +} + +.rdrTimeWheelScroller { + height: 100%; + overflow-y: auto; +} + +.rdrTimeWheelItem { + height: 32px; + display: flex; + align-items: center; + justify-content: center; + font-size: 16px; + color: #7d888d; + user-select: none; + cursor: pointer; + + &:hover { + background: #f4f5f7; + } +} + +.rdrTimeWheelItemActive { + color: #3d91ff; + font-weight: 600; + background: rgba(61, 145, 255, 0.08); +} + +.rdrTimeWheelDisabled { + opacity: 0.5; + pointer-events: none; +} diff --git a/src/styles.js b/src/styles.js index cf7a5eb2a..20d1aa45a 100644 --- a/src/styles.js +++ b/src/styles.js @@ -50,4 +50,18 @@ export default { infiniteMonths: 'rdrInfiniteMonths', monthsVertical: 'rdrMonthsVertical', monthsHorizontal: 'rdrMonthsHorizontal', + timePickerContainer: 'rdrTimePickerContainer', + timePickerSection: 'rdrTimePickerSection', + timePickerLabel: 'rdrTimePickerLabel', + timePickerWrapper: 'rdrTimePickerWrapper', + timePicker: 'rdrTimePicker', + timePickerSelect: 'rdrTimePickerSelect', + timePickerDivider: 'rdrTimePickerDivider', + timeWheel: 'rdrTimeWheel', + timeWheelDisabled: 'rdrTimeWheelDisabled', + timeWheelScroller: 'rdrTimeWheelScroller', + timeWheelItem: 'rdrTimeWheelItem', + timeWheelItemActive: 'rdrTimeWheelItemActive', + timeWheelHighlight: 'rdrTimeWheelHighlight', + timeWheelPadding: 'rdrTimeWheelPadding', }; diff --git a/src/styles.scss b/src/styles.scss index ad1901ab3..17ff6721b 100644 --- a/src/styles.scss +++ b/src/styles.scss @@ -4,3 +4,4 @@ @import 'components/DayCell/index.scss'; @import 'components/DateRangePicker/index.scss'; @import 'components/DefinedRange/index.scss'; +@import 'components/TimePicker/index.scss'; From eab62b8d9b31b8a1dd4ad6af1526557a2c79164f Mon Sep 17 00:00:00 2001 From: TheGreatAlgo <37487508+TheGreatAlgo@users.noreply.github.com> Date: Tue, 14 Oct 2025 15:07:06 -0400 Subject: [PATCH 2/3] fix: disable hours outside of max min --- DATETIME_BOUNDARIES_GUIDE.md | 265 +++++++++++++++++++++++++++ MIN_MAX_TIME_SUPPORT.md | 192 +++++++++++++++++++ src/components/DateRange/index.js | 19 +- src/components/TimePicker/index.js | 94 ++++++++-- src/components/TimePicker/index.scss | 11 ++ src/styles.js | 1 + src/theme/default.scss | 5 +- 7 files changed, 570 insertions(+), 17 deletions(-) create mode 100644 DATETIME_BOUNDARIES_GUIDE.md create mode 100644 MIN_MAX_TIME_SUPPORT.md diff --git a/DATETIME_BOUNDARIES_GUIDE.md b/DATETIME_BOUNDARIES_GUIDE.md new file mode 100644 index 000000000..044731725 --- /dev/null +++ b/DATETIME_BOUNDARIES_GUIDE.md @@ -0,0 +1,265 @@ +# DateTime Boundaries Guide - Min/Max Time Support + +This guide explains how to use the **absolute datetime boundaries** feature with `minTime` and `maxTime` props. + +## How It Works + +The `minTime` and `maxTime` props accept **Date objects** representing **absolute datetime boundaries**. This means: + +- **minTime**: The earliest allowed datetime (e.g., January 1, 2024 at 10:00 PM) +- **maxTime**: The latest allowed datetime (e.g., January 31, 2024 at 8:00 PM) + +Any time outside these boundaries will be **displayed in red** and **cannot be selected**. + +## Example Scenario + +```javascript +// Scenario: Allow selection from Jan 1, 2024 10:00 PM to Jan 31, 2024 8:00 PM + +const minTime = new Date(2024, 0, 1, 22, 0); // January 1, 2024, 10:00 PM +const maxTime = new Date(2024, 0, 31, 20, 0); // January 31, 2024, 8:00 PM +``` + +**What happens:** + +### January 1st (Start boundary) +- ✅ Hours 22 (10 PM) and 23 (11 PM) are **available** +- ❌ Hours 0-21 (12 AM - 9 PM) are **disabled (red)** + +### January 2nd - 30th (Dates in between) +- ✅ **All hours (0-23) are available** +- No restrictions applied + +### January 31st (End boundary) +- ✅ Hours 0-20 (12 AM - 8 PM) are **available** +- ❌ Hours 21-23 (9 PM - 11 PM) are **disabled (red)** + +## Usage Examples + +### Basic Usage + +```jsx +import { DateRange } from 'react-date-range'; +import 'react-date-range/dist/styles.css'; +import 'react-date-range/dist/theme/default.css'; + +function MyDateRangePicker() { + const [state, setState] = useState([ + { + startDate: new Date(2024, 0, 1, 22, 0), // Jan 1, 2024, 10 PM + endDate: new Date(2024, 0, 31, 20, 0), // Jan 31, 2024, 8 PM + key: 'selection' + } + ]); + + const minTime = new Date(2024, 0, 1, 22, 0); // Jan 1, 2024, 10:00 PM + const maxTime = new Date(2024, 0, 31, 20, 0); // Jan 31, 2024, 8:00 PM + + return ( + setState([item.selection])} + showTimePicker={true} + showHours={true} + showMinutes={true} + minTime={minTime} + maxTime={maxTime} + /> + ); +} +``` + +### QueryCalendar Usage (dclimate-monorepo) + +```tsx +import QueryCalendar from 'components/General/QueryCalendar'; + +function MyComponent() { + const [calendarState, setCalendarState] = useState([ + { + startDate: new Date(2024, 0, 1, 22, 0), + endDate: new Date(2024, 0, 31, 20, 0), + key: 'selection' + } + ]); + + // Define absolute datetime boundaries + const minTime = new Date(2024, 0, 1, 22, 0); // Jan 1, 10 PM + const maxTime = new Date(2024, 0, 31, 20, 0); // Jan 31, 8 PM + + return ( + + ); +} +``` + +## Common Scenarios + +### 1. Business Hours Across a Week + +```javascript +// Monday 9 AM to Friday 5 PM +const minTime = new Date(2024, 0, 1, 9, 0); // Monday, Jan 1, 9:00 AM +const maxTime = new Date(2024, 0, 5, 17, 0); // Friday, Jan 5, 5:00 PM + +// Result: +// - Monday: Only 9 AM onwards available +// - Tuesday-Thursday: All hours available +// - Friday: Only up to 5 PM available +``` + +### 2. Event Duration (Single Day with Time Range) + +```javascript +// Conference from 8 AM to 6 PM on Jan 15 +const minTime = new Date(2024, 0, 15, 8, 0); // Jan 15, 8:00 AM +const maxTime = new Date(2024, 0, 15, 18, 0); // Jan 15, 6:00 PM + +// Result: +// - Jan 15: Only hours 8-18 available +// - Other dates: Not selectable (use minDate/maxDate for this) +``` + +### 3. Multi-Day Event with Specific Start/End Times + +```javascript +// Weekend retreat: Friday 6 PM to Sunday 2 PM +const minTime = new Date(2024, 0, 12, 18, 0); // Friday, Jan 12, 6:00 PM +const maxTime = new Date(2024, 0, 14, 14, 0); // Sunday, Jan 14, 2:00 PM + +// Result: +// - Friday (Jan 12): Only 6 PM - 11 PM available +// - Saturday (Jan 13): All hours available +// - Sunday (Jan 14): Only 12 AM - 2 PM available +``` + +### 4. Late Night Operations + +```javascript +// Available from 8 PM Jan 1 to 4 AM Jan 2 +const minTime = new Date(2024, 0, 1, 20, 0); // Jan 1, 8:00 PM +const maxTime = new Date(2024, 0, 2, 4, 0); // Jan 2, 4:00 AM + +// Result: +// - Jan 1: Only 8 PM - 11 PM available +// - Jan 2: Only 12 AM - 4 AM available +``` + +## Props + +### DateRange Component + +| Prop | Type | Default | Description | +|------|------|---------|-------------| +| `minTime` | `Date` | `undefined` | Absolute minimum datetime boundary | +| `maxTime` | `Date` | `undefined` | Absolute maximum datetime boundary | +| `showTimePicker` | `boolean` | `false` | Show/hide the time picker | +| `showHours` | `boolean` | `true` | Show/hide hour selection | +| `showMinutes` | `boolean` | `true` | Show/hide minute selection | + +### QueryCalendar Component + +| Prop | Type | Default | Description | +|------|------|---------|-------------| +| `minTime` | `Date` | `undefined` | Absolute minimum datetime (e.g., Jan 1, 2024 10:00 PM) | +| `maxTime` | `Date` | `undefined` | Absolute maximum datetime (e.g., Jan 31, 2024 8:00 PM) | +| `enabled` | `boolean` | `true` | Enable/disable the calendar | +| `filterCalendar` | `boolean` | `false` | Apply filter calendar styling | +| `currentFormState` | `any` | `undefined` | Current selected date range state | + +## Visual Feedback + +### Disabled Hours/Minutes +- **Text Color**: Red (`#dc3545`) +- **Background**: Light red (`rgba(220, 53, 69, 0.05)`) +- **Opacity**: 0.6 +- **Cursor**: `not-allowed` +- **Behavior**: Not clickable + +### Available Hours/Minutes +- **Text Color**: Normal (`#7d888d`) +- **Background**: Normal (hover: `#f4f5f7`) +- **Cursor**: `pointer` +- **Behavior**: Clickable + +## Important Notes + +1. **Date Objects Required**: Both `minTime` and `maxTime` must be JavaScript Date objects representing the exact datetime boundaries. + +2. **Full DateTime Comparison**: The validation compares the full datetime (date + time), not just the time portion. + +3. **Boundaries are Inclusive**: Times at exactly `minTime` and `maxTime` are selectable. + +4. **Works with Date Restrictions**: You can combine `minTime`/`maxTime` with `minDate`/`maxDate` props to restrict both dates and times. + +5. **Validation in UI and Handler**: Time restrictions are enforced both visually (red disabled items) and programmatically (change handler validates before applying). + +6. **No Timezone Handling**: All dates use the browser's local timezone. Ensure your min/max dates are in the correct timezone. + +## Creating DateTime Boundaries Dynamically + +```javascript +// Example: Allow selection for next 7 days, 9 AM to 5 PM daily +const now = new Date(); + +// Start from tomorrow 9 AM +const minTime = new Date(now); +minTime.setDate(now.getDate() + 1); +minTime.setHours(9, 0, 0, 0); + +// End 7 days later at 5 PM +const maxTime = new Date(now); +maxTime.setDate(now.getDate() + 7); +maxTime.setHours(17, 0, 0, 0); + +// Result: +// - Tomorrow: 9 AM onwards +// - Days 2-6: All hours +// - Day 7: Up to 5 PM +``` + +## Troubleshooting + +### Issue: All hours are disabled +**Cause**: `minTime` is after `maxTime`, or the selected date is outside the datetime range. +**Solution**: Verify that `minTime < maxTime` and the date you're viewing is within the range. + +### Issue: Wrong hours disabled +**Cause**: Timezone mismatch or incorrect date object creation. +**Solution**: Use `console.log(minTime, maxTime)` to verify the exact datetimes you're creating. + +### Issue: Changes not applying +**Cause**: The react-date-range library might not be updated. +**Solution**: Rebuild the library with `npm run build` in the react-date-range directory. + +## Comparison: Old vs New Behavior + +### ❌ Old (Incorrect) Behavior +```javascript +minTime = 9 AM +maxTime = 5 PM + +// Every single day had 9 AM - 5 PM restrictions +// - Jan 1: 9 AM - 5 PM only +// - Jan 2: 9 AM - 5 PM only +// - Jan 3: 9 AM - 5 PM only +``` + +### ✅ New (Correct) Behavior +```javascript +minTime = new Date(2024, 0, 1, 9, 0) // Jan 1, 9 AM +maxTime = new Date(2024, 0, 31, 17, 0) // Jan 31, 5 PM + +// Absolute datetime boundaries +// - Jan 1: >= 9 AM +// - Jan 2-30: All hours available +// - Jan 31: <= 5 PM +``` diff --git a/MIN_MAX_TIME_SUPPORT.md b/MIN_MAX_TIME_SUPPORT.md new file mode 100644 index 000000000..d6ab7194e --- /dev/null +++ b/MIN_MAX_TIME_SUPPORT.md @@ -0,0 +1,192 @@ +# Min/Max Time Support for DateRange Component + +This document explains how to use the new min/max time support feature that makes unavailable dates and hours appear in red. + +## Features Added + +1. **minTime and maxTime props** - Control which hours and minutes are available for selection +2. **Red styling for disabled dates** - Dates marked as disabled (via `disabledDates` prop) now appear in red +3. **Red styling for disabled hours/minutes** - Time slots outside the min/max range appear in red +4. **Click prevention** - Users cannot select disabled time slots + +## Usage + +### Basic Example with Time Constraints + +```jsx +import { DateRange } from 'react-date-range'; +import 'react-date-range/dist/styles.css'; +import 'react-date-range/dist/theme/default.css'; + +function MyDateRangePicker() { + const [state, setState] = useState([ + { + startDate: new Date(), + endDate: new Date(), + key: 'selection' + } + ]); + + return ( + setState([item.selection])} + showTimePicker={true} + showHours={true} + showMinutes={true} + // Only allow times between 9:00 AM and 5:30 PM + minTime={new Date(2024, 0, 1, 9, 0)} // 9:00 AM + maxTime={new Date(2024, 0, 1, 17, 30)} // 5:30 PM + /> + ); +} +``` + +### Using Number Format (Total Minutes) + +You can also specify time as total minutes since midnight: + +```jsx + setState([item.selection])} + showTimePicker={true} + minTime={540} // 9:00 AM (9 * 60 = 540 minutes) + maxTime={1050} // 5:30 PM (17 * 60 + 30 = 1050 minutes) +/> +``` + +### Combining with Disabled Dates + +Disabled dates will now appear in red: + +```jsx +const disabledDates = [ + new Date(2024, 0, 15), // January 15, 2024 + new Date(2024, 0, 25), // January 25, 2024 +]; + + setState([item.selection])} + disabledDates={disabledDates} + showTimePicker={true} + minTime={new Date(2024, 0, 1, 8, 0)} + maxTime={new Date(2024, 0, 1, 18, 0)} +/> +``` + +## Props + +### DateRange Component Props + +| Prop | Type | Default | Description | +|------|------|---------|-------------| +| `minTime` | `Date \| number` | `undefined` | Minimum allowed time. Can be a Date object or total minutes since midnight | +| `maxTime` | `Date \| number` | `undefined` | Maximum allowed time. Can be a Date object or total minutes since midnight | +| `showTimePicker` | `boolean` | `false` | Show/hide the time picker | +| `showHours` | `boolean` | `true` | Show/hide hour selection | +| `showMinutes` | `boolean` | `true` | Show/hide minute selection | + +## Visual Feedback + +### Disabled Dates (Calendar) +- Background: Light red (`rgba(220, 53, 69, 0.1)`) +- Text color: Red (`#dc3545`) +- Opacity: 0.7 +- Not clickable + +### Disabled Time Slots (Time Picker) +- Text color: Red (`#dc3545`) +- Background: Light red (`rgba(220, 53, 69, 0.05)`) +- Opacity: 0.6 +- Cursor: `not-allowed` +- Not clickable + +## Examples + +### Business Hours (9 AM - 5 PM) + +```jsx + setState([item.selection])} + showTimePicker={true} + minTime={new Date(2024, 0, 1, 9, 0)} + maxTime={new Date(2024, 0, 1, 17, 0)} +/> +``` + +### Late Night Hours (8 PM - 2 AM) + +```jsx + setState([item.selection])} + showTimePicker={true} + minTime={new Date(2024, 0, 1, 20, 0)} // 8 PM + maxTime={new Date(2024, 0, 2, 2, 0)} // 2 AM next day +/> +``` + +### 30-Minute Intervals with Restrictions + +```jsx + setState([item.selection])} + showTimePicker={true} + showMinutes={true} + minTime={new Date(2024, 0, 1, 10, 30)} // 10:30 AM + maxTime={new Date(2024, 0, 1, 15, 45)} // 3:45 PM +/> +``` + +## Styling Customization + +If you want to customize the red color for disabled items, you can override the CSS: + +```css +/* Custom disabled date styling */ +.rdrDayDisabled { + background-color: rgba(255, 0, 0, 0.1) !important; +} + +.rdrDayDisabled .rdrDayNumber span { + color: #ff0000 !important; +} + +/* Custom disabled time slot styling */ +.rdrTimeWheelItemDisabled { + color: #ff0000 !important; + background: rgba(255, 0, 0, 0.05) !important; +} +``` + +## How It Works + +1. **Date Validation**: The existing `disabledDates` prop continues to work as before, but now disabled dates are styled in red instead of gray. + +2. **Time Validation**: + - When `minTime` or `maxTime` is set, the TimePicker component calculates which hours and minutes should be disabled + - Hours outside the range are disabled completely + - Minutes are context-aware (e.g., if current hour is the min hour, only minutes before the min minute are disabled) + +3. **Click Prevention**: + - Disabled time slots cannot be clicked + - The `handleTimeChange` method validates the time before allowing the change + - Invalid times are silently rejected (no change occurs) + +## Browser Support + +This feature works in all modern browsers that support: +- CSS rgba colors +- CSS opacity +- ES6+ JavaScript features + +## Notes + +- The date/time validation is performed both in the UI (preventing clicks) and in the change handler (preventing programmatic changes) +- Time validation is inclusive (min and max times are selectable) +- When using Date objects for `minTime`/`maxTime`, only the hours and minutes are used; date portion is ignored +- The red styling uses Bootstrap's standard danger color (`#dc3545`) for consistency diff --git a/src/components/DateRange/index.js b/src/components/DateRange/index.js index e1b74fe04..430b39dcb 100644 --- a/src/components/DateRange/index.js +++ b/src/components/DateRange/index.js @@ -140,12 +140,21 @@ class DateRange extends Component { this.setState({ preview: { ...val.range, color } }); }; handleTimeChange = (date, isStart) => { - const { onChange, ranges } = this.props; + const { onChange, ranges, minTime, maxTime } = this.props; const focusedRange = this.props.focusedRange || this.state.focusedRange; const focusedRangeIndex = focusedRange[0]; const selectedRange = ranges[focusedRangeIndex]; if (!selectedRange) return; + // Validate time is within min/max absolute datetime bounds + if (minTime && typeof minTime === 'object' && date < minTime) { + return; // Date is before minimum allowed datetime + } + + if (maxTime && typeof maxTime === 'object' && date > maxTime) { + return; // Date is after maximum allowed datetime + } + const key = selectedRange.key || `range${focusedRangeIndex + 1}`; onChange({ [key]: { @@ -156,7 +165,7 @@ class DateRange extends Component { }; render() { - const { showTimePicker, showHours, showMinutes, ranges } = this.props; + const { showTimePicker, showHours, showMinutes, ranges, minTime, maxTime } = this.props; const focusedRange = this.props.focusedRange || this.state.focusedRange; const focusedRangeIndex = focusedRange[0]; const selectedRange = ranges[focusedRangeIndex]; @@ -189,6 +198,8 @@ class DateRange extends Component { showHours={showHours} showMinutes={showMinutes} disabled={selectedRange.disabled} + minTime={minTime} + maxTime={maxTime} styles={this.styles} ariaLabels={this.props.ariaLabels} /> @@ -201,6 +212,8 @@ class DateRange extends Component { showHours={showHours} showMinutes={showMinutes} disabled={selectedRange.disabled} + minTime={minTime} + maxTime={maxTime} styles={this.styles} ariaLabels={this.props.ariaLabels} /> @@ -235,6 +248,8 @@ DateRange.propTypes = { showTimePicker: PropTypes.bool, showHours: PropTypes.bool, showMinutes: PropTypes.bool, + minTime: PropTypes.object, // Date object representing absolute minimum datetime + maxTime: PropTypes.object, // Date object representing absolute maximum datetime }; export default DateRange; diff --git a/src/components/TimePicker/index.js b/src/components/TimePicker/index.js index 358e0f568..4293919d9 100644 --- a/src/components/TimePicker/index.js +++ b/src/components/TimePicker/index.js @@ -1,30 +1,38 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; import classnames from 'classnames'; -import { setHours, setMinutes, getHours, getMinutes } from 'date-fns'; +import { setHours, setMinutes, getHours, getMinutes, startOfDay } from 'date-fns'; class TimeWheel extends Component { handleClick = (value) => { - this.props.onChange(value); + const { isDisabled } = this.props; + if (!isDisabled || !isDisabled(value)) { + this.props.onChange(value); + } }; render() { - const { value, max, disabled, styles } = this.props; + const { value, max, disabled, styles, isDisabled } = this.props; const items = Array.from({ length: max }, (_, i) => i); return (
- {items.map((item) => ( -
this.handleClick(item)} - className={classnames(styles.timeWheelItem, { - [styles.timeWheelItemActive]: item === value, - })}> - {String(item).padStart(2, '0')} -
- ))} + {items.map((item) => { + const itemDisabled = isDisabled && isDisabled(item); + return ( +
this.handleClick(item)} + className={classnames(styles.timeWheelItem, { + [styles.timeWheelItemActive]: item === value, + [styles.timeWheelItemDisabled]: itemDisabled, + })} + style={itemDisabled ? { color: '#dc3545', opacity: 0.6, cursor: 'not-allowed' } : undefined}> + {String(item).padStart(2, '0')} +
+ ); + })}
); @@ -44,6 +52,62 @@ class TimePicker extends Component { onChange && onChange(newDate); }; + isHourDisabled = (hour) => { + const { minTime, maxTime, date } = this.props; + if (!minTime && !maxTime) return false; + if (!date) return false; + + // Create a test datetime with the current date and the hour being tested + const testDateTime = setHours(setMinutes(startOfDay(date), 0), hour); + + // Check if this datetime is before the minimum allowed datetime + if (minTime) { + const minDateTime = typeof minTime === 'object' ? minTime : new Date(new Date().setHours(Math.floor(minTime / 60), minTime % 60, 0, 0)); + if (testDateTime < minDateTime) { + return true; + } + } + + // Check if this datetime is after the maximum allowed datetime + if (maxTime) { + const maxDateTime = typeof maxTime === 'object' ? maxTime : new Date(new Date().setHours(Math.floor(maxTime / 60), maxTime % 60, 0, 0)); + if (testDateTime > maxDateTime) { + return true; + } + } + + return false; + }; + + isMinuteDisabled = (minute) => { + const { minTime, maxTime, date } = this.props; + if (!minTime && !maxTime) return false; + if (!date) return false; + + const currentHour = getHours(date); + + // Create a test datetime with the current date, hour, and the minute being tested + const testDateTime = setMinutes(setHours(startOfDay(date), currentHour), minute); + + // Check if this datetime is before the minimum allowed datetime + if (minTime) { + const minDateTime = typeof minTime === 'object' ? minTime : new Date(new Date().setHours(Math.floor(minTime / 60), minTime % 60, 0, 0)); + if (testDateTime < minDateTime) { + return true; + } + } + + // Check if this datetime is after the maximum allowed datetime + if (maxTime) { + const maxDateTime = typeof maxTime === 'object' ? maxTime : new Date(new Date().setHours(Math.floor(maxTime / 60), maxTime % 60, 0, 0)); + if (testDateTime > maxDateTime) { + return true; + } + } + + return false; + }; + render() { const { date, showHours, showMinutes, disabled, styles } = this.props; const currentDate = date || new Date(); @@ -64,6 +128,7 @@ class TimePicker extends Component { max={24} onChange={this.handleHourChange} disabled={disabled} + isDisabled={this.isHourDisabled} styles={styles} /> @@ -79,6 +144,7 @@ class TimePicker extends Component { max={60} onChange={this.handleMinuteChange} disabled={disabled} + isDisabled={this.isMinuteDisabled} styles={styles} /> @@ -100,6 +166,8 @@ TimePicker.propTypes = { showHours: PropTypes.bool, showMinutes: PropTypes.bool, disabled: PropTypes.bool, + minTime: PropTypes.object, + maxTime: PropTypes.object, styles: PropTypes.object, ariaLabels: PropTypes.object, }; diff --git a/src/components/TimePicker/index.scss b/src/components/TimePicker/index.scss index cf62e9d42..740555511 100644 --- a/src/components/TimePicker/index.scss +++ b/src/components/TimePicker/index.scss @@ -78,6 +78,17 @@ background: rgba(61, 145, 255, 0.08); } +.rdrTimeWheelItemDisabled { + color: #dc3545; + opacity: 0.6; + cursor: not-allowed; + background: rgba(220, 53, 69, 0.05); + + &:hover { + background: rgba(220, 53, 69, 0.05); + } +} + .rdrTimeWheelDisabled { opacity: 0.5; pointer-events: none; diff --git a/src/styles.js b/src/styles.js index 20d1aa45a..71777095d 100644 --- a/src/styles.js +++ b/src/styles.js @@ -62,6 +62,7 @@ export default { timeWheelScroller: 'rdrTimeWheelScroller', timeWheelItem: 'rdrTimeWheelItem', timeWheelItemActive: 'rdrTimeWheelItemActive', + timeWheelItemDisabled: 'rdrTimeWheelItemDisabled', timeWheelHighlight: 'rdrTimeWheelHighlight', timeWheelPadding: 'rdrTimeWheelPadding', }; diff --git a/src/theme/default.scss b/src/theme/default.scss index 0e17aecac..99c6adfa3 100644 --- a/src/theme/default.scss +++ b/src/theme/default.scss @@ -379,9 +379,10 @@ } .rdrDayDisabled { - background-color: rgb(248, 248, 248); + background-color: rgba(220, 53, 69, 0.1); .rdrDayNumber span{ - color: #aeb9bf; + color: #dc3545; + opacity: 0.7; } .rdrInRange, .rdrStartEdge, .rdrEndEdge, .rdrSelected, .rdrDayStartPreview, .rdrDayInPreview, .rdrDayEndPreview{ filter: grayscale(100%) opacity(60%); From dffb001ee2890b77f246026a694b6caeb6cd0f1c Mon Sep 17 00:00:00 2001 From: TheGreatAlgo <37487508+TheGreatAlgo@users.noreply.github.com> Date: Tue, 14 Oct 2025 15:24:01 -0400 Subject: [PATCH 3/3] fix: styling --- src/components/DateRange/index.scss | 3 +++ src/components/TimePicker/index.scss | 9 ++++++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/components/DateRange/index.scss b/src/components/DateRange/index.scss index 441b02f34..22d1703d8 100644 --- a/src/components/DateRange/index.scss +++ b/src/components/DateRange/index.scss @@ -2,4 +2,7 @@ user-select: none; display: flex; flex-direction: row; + overflow: hidden; + max-width: 100%; + box-sizing: border-box; } diff --git a/src/components/TimePicker/index.scss b/src/components/TimePicker/index.scss index 740555511..d014ef060 100644 --- a/src/components/TimePicker/index.scss +++ b/src/components/TimePicker/index.scss @@ -6,13 +6,18 @@ border-left: 1px solid #eff2f7; background: #fff; min-width: 200px; - gap: 20px; + max-width: 100%; + margin-left: 15px; + gap: 8px; + overflow-x: auto; + box-sizing: border-box; } .rdrTimePickerSection { display: flex; flex-direction: column; align-items: center; + flex-shrink: 0; } .rdrTimePickerWrapper { @@ -21,6 +26,8 @@ justify-content: center; gap: 10px; padding: 10px; + flex-wrap: nowrap; + box-sizing: border-box; } .rdrTimePicker {