Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion bestax-mcp/data/components/DateInput.json
Original file line number Diff line number Diff line change
Expand Up @@ -394,7 +394,7 @@
"name": "min",
"type": "Date",
"default": null,
"description": "Earliest selectable date.",
"description": "Earliest selectable date. A `min` before year 1 is raised to 1 January of year 1, where the range starts without one too: HTML's date and month inputs hold no earlier year, so the calendar and typing stop there.",
"required": false,
"inherited": true,
"deprecated": false
Expand Down
2 changes: 1 addition & 1 deletion bestax-mcp/data/components/DateTimeInput.json
Original file line number Diff line number Diff line change
Expand Up @@ -390,7 +390,7 @@
"name": "min",
"type": "Date",
"default": null,
"description": "Lower bound for the combined date-time.",
"description": "Lower bound for the combined date-time. A `min` before year 1 is raised to midnight on 1 January of year 1, where the range starts without one too: HTML's datetime-local input holds no earlier year, so the calendar, the time wheels and typing stop there.",
"required": false,
"inherited": true,
"deprecated": false
Expand Down
2 changes: 1 addition & 1 deletion bestax-mcp/data/components/TimeInput.json
Original file line number Diff line number Diff line change
Expand Up @@ -398,7 +398,7 @@
"name": "min",
"type": "Date",
"default": null,
"description": "Earliest selectable time.",
"description": "Earliest selectable time. A `min` before year 1 is raised to midnight on 1 January of year 1, and a `value` dated before then is out of range, so the wheels and typing change nothing on it, as in the date pickers.",
"required": false,
"inherited": true,
"deprecated": false
Expand Down
28 changes: 20 additions & 8 deletions bulma-ui/src/form/DateInputBase.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ import {
import {
isWithin,
clampDate,
FIRST_YEAR,
floorMin,
isSameDay,
isPeriodUnselectable,
makeDate,
Expand All @@ -48,9 +50,11 @@ const pad2 = (n: number): string => String(n).padStart(2, '0');
* The value as the native input and the hidden form input carry it:
* `YYYY-MM-DD`, `YYYY-MM` or `YYYY`, the shapes `<input type="date">` and
* `<input type="month">` use. The year is padded to four digits, as those
* inputs require and as the `YYYY` token displays it.
* inputs require and as the `YYYY` token displays it. HTML has no such shape
* for a year before 1, so a date then is empty, as those inputs would make it.
*/
const toIsoValue = (d: Date, granularity: DateGranularity): string => {
if (d.getFullYear() < FIRST_YEAR) return '';
const year = String(d.getFullYear()).padStart(4, '0');
if (granularity === 'year') return year;
const month = `${year}-${pad2(d.getMonth() + 1)}`;
Expand Down Expand Up @@ -116,7 +120,11 @@ export interface DateInputBaseProps
onOpen?: () => void;
/** Fired when the popover closes. */
onClose?: () => void;
/** Earliest selectable date. */
/**
* Earliest selectable date. A `min` before year 1 is raised to 1 January of
* year 1, where the range starts without one too: HTML's date and month
* inputs hold no earlier year, so the calendar and typing stop there.
*/
min?: Date;
/** Latest selectable date. */
max?: Date;
Expand Down Expand Up @@ -290,8 +298,11 @@ export const DateInputBase = forwardRef<HTMLInputElement, DateInputBaseProps>(
);
const value = isControlled ? (controlledValue ?? null) : internalValue;

// Nothing before year 1 is in range, as in HTML's date inputs.
const lowerBound = useMemo(() => floorMin(min), [min]);

const initialFocused = useMemo(
() => clampDate(value ?? new Date(), min, max),
() => clampDate(value ?? new Date(), lowerBound, max),
// intentionally only on mount: keep focusedDate stable until value/open change
// eslint-disable-next-line react-hooks/exhaustive-deps
[]
Expand All @@ -300,8 +311,8 @@ export const DateInputBase = forwardRef<HTMLInputElement, DateInputBaseProps>(
// Re-clamp focusedDate if min/max change after mount so the focused cell
// never disappears outside the displayable range.
useEffect(() => {
setFocusedDate(prev => clampDate(prev, min, max));
}, [min, max]);
setFocusedDate(prev => clampDate(prev, lowerBound, max));
}, [lowerBound, max]);
const [open, setOpenState] = useState(false);
const [text, setText] = useState<string>(
value ? formatDate(value, resolvedFormat, locale) : ''
Expand Down Expand Up @@ -373,8 +384,9 @@ export const DateInputBase = forwardRef<HTMLInputElement, DateInputBaseProps>(
// At month or year granularity the bounds widen to the periods holding
// them, so the month or year containing `min` stays reachable.
const periodMin = useMemo(
() => (min && !isDayGranularity ? startOfPeriod(min, granularity) : min),
[min, isDayGranularity, granularity]
() =>
isDayGranularity ? lowerBound : startOfPeriod(lowerBound, granularity),
[lowerBound, isDayGranularity, granularity]
);
const periodMax = useMemo(
() => (max && !isDayGranularity ? endOfPeriod(max, granularity) : max),
Expand Down Expand Up @@ -523,7 +535,7 @@ export const DateInputBase = forwardRef<HTMLInputElement, DateInputBaseProps>(
: null;
commitValue(parsed);
}}
min={min ? toIsoValue(min, granularity) : undefined}
min={min ? toIsoValue(lowerBound, granularity) : undefined}
Comment thread
claude[bot] marked this conversation as resolved.
max={max ? toIsoValue(max, granularity) : undefined}
disabled={disabled}
readOnly={readOnly}
Expand Down
35 changes: 24 additions & 11 deletions bulma-ui/src/form/DateTimeInputBase.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ import {
isWithin,
setTimeOfDay,
clampDate,
FIRST_YEAR,
floorMin,
isSameDay,
makeDate,
} from './_pickerInternals/dateUtils';
Expand All @@ -38,8 +40,11 @@ import { useNativeMobilePicker } from './_pickerInternals/useNativeMobilePicker'
import { useSegmentedEntry } from './_pickerInternals/useSegmentedEntry';
import { Icon } from '../elements/Icon';

// The year is padded to four digits, as `datetime-local` requires.
// The year is padded to four digits, as `datetime-local` requires. HTML has
// no such shape for a year before 1, so a date then is empty, as that input
// would make it.
const toIsoDateTime = (d: Date, withSeconds: boolean): string => {
if (d.getFullYear() < FIRST_YEAR) return '';
const yyyy = String(d.getFullYear()).padStart(4, '0');
const mo = String(d.getMonth() + 1).padStart(2, '0');
const dd = String(d.getDate()).padStart(2, '0');
Expand Down Expand Up @@ -95,7 +100,12 @@ export interface DateTimeInputBaseProps
onOpen?: () => void;
/** Fired when the popover closes. */
onClose?: () => void;
/** Lower bound for the combined date-time. */
/**
* Lower bound for the combined date-time. A `min` before year 1 is raised
* to midnight on 1 January of year 1, where the range starts without one
* too: HTML's datetime-local input holds no earlier year, so the calendar,
* the time wheels and typing stop there.
*/
min?: Date;
/** Upper bound for the combined date-time. */
max?: Date;
Expand Down Expand Up @@ -275,16 +285,19 @@ export const DateTimeInputBase = forwardRef<
);
const value = isControlled ? (controlledValue ?? null) : internalValue;

// Nothing before year 1 is in range, as in HTML's date inputs.
const lowerBound = useMemo(() => floorMin(min), [min]);

const initialFocused = useMemo(
() => clampDate(value ?? new Date(), min, max),
() => clampDate(value ?? new Date(), lowerBound, max),
// eslint-disable-next-line react-hooks/exhaustive-deps
[]
);
const [focusedDate, setFocusedDate] = useState<Date>(initialFocused);
// Re-clamp focusedDate when min/max change so the focused cell stays valid.
useEffect(() => {
setFocusedDate(prev => clampDate(prev, min, max));
}, [min, max]);
setFocusedDate(prev => clampDate(prev, lowerBound, max));
}, [lowerBound, max]);

const defaultFormat: DateFormatOption =
format ??
Expand Down Expand Up @@ -406,21 +419,21 @@ export const DateTimeInputBase = forwardRef<
minutes: value?.getMinutes() ?? 0,
seconds: enableSeconds ? (value?.getSeconds() ?? 0) : undefined,
});
if (!isWithin(merged, min, max)) return;
if (!isWithin(merged, lowerBound, max)) return;
commitValue(merged);
setFocusedDate(d);
},
[value, enableSeconds, min, max, commitValue]
[value, enableSeconds, lowerBound, max, commitValue]
);

const handleTimeChange = useCallback(
(parts: { hours: number; minutes: number; seconds?: number }) => {
const base = value ?? focusedDate;
const next = setTimeOfDay(base, parts);
if (!isWithin(next, min, max)) return;
if (!isWithin(next, lowerBound, max)) return;
commitValue(next);
},
[value, focusedDate, min, max, commitValue]
[value, focusedDate, lowerBound, max, commitValue]
);

const tryParse = useCallback(
Expand Down Expand Up @@ -463,7 +476,7 @@ export const DateTimeInputBase = forwardRef<
setText,
makeBaseDate,
locale,
min,
min: lowerBound,
max,
isBlocked,
disabled,
Expand Down Expand Up @@ -509,7 +522,7 @@ export const DateTimeInputBase = forwardRef<
: null;
commitValue(parsed);
}}
min={min ? toIsoDateTime(min, enableSeconds) : undefined}
min={min ? toIsoDateTime(lowerBound, enableSeconds) : undefined}
max={max ? toIsoDateTime(max, enableSeconds) : undefined}
disabled={disabled}
readOnly={readOnly}
Expand Down
17 changes: 13 additions & 4 deletions bulma-ui/src/form/TimeInputBase.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import React, {
useCallback,
useEffect,
useId,
useMemo,
useRef,
useState,
} from 'react';
Expand All @@ -24,6 +25,7 @@ import {
} from './_pickerInternals/formatters';
import {
setTimeOfDay,
floorMin,
isWithin,
snapTimeToIncrement,
} from './_pickerInternals/dateUtils';
Expand Down Expand Up @@ -88,7 +90,11 @@ export interface TimeInputBaseProps
onOpen?: () => void;
/** Fired when the popover closes. */
onClose?: () => void;
/** Earliest selectable time. */
/**
* Earliest selectable time. A `min` before year 1 is raised to midnight on
* 1 January of year 1, and a `value` dated before then is out of range, so
* the wheels and typing change nothing on it, as in the date pickers.
*/
min?: Date;
/** Latest selectable time. */
max?: Date;
Expand Down Expand Up @@ -332,6 +338,9 @@ export const TimeInputBase = forwardRef<HTMLInputElement, TimeInputBaseProps>(
setText(value ? formatTime(value, defaultFormat, locale) : '');
}, [value, defaultFormat, locale]);

// A value's date before year 1 is out of range, as in the date pickers.
const lowerBound = useMemo(() => floorMin(min), [min]);

const commitValue = useCallback(
(next: Date | null) => {
if (!isControlled) setInternalValue(next);
Expand All @@ -344,10 +353,10 @@ export const TimeInputBase = forwardRef<HTMLInputElement, TimeInputBaseProps>(
(parts: { hours: number; minutes: number; seconds?: number }) => {
const base = value ?? new Date();
const next = setTimeOfDay(base, parts);
if (!isWithin(next, min, max)) return;
if (!isWithin(next, lowerBound, max)) return;
commitValue(next);
},
[value, min, max, commitValue]
[value, lowerBound, max, commitValue]
);

const tryParse = useCallback(
Expand Down Expand Up @@ -389,7 +398,7 @@ export const TimeInputBase = forwardRef<HTMLInputElement, TimeInputBaseProps>(
setText,
makeBaseDate,
locale,
min,
min: lowerBound,
max,
isBlocked: unselectableTimes,
disabled,
Expand Down
Loading
Loading