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
5 changes: 4 additions & 1 deletion package/src/formats/date.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,9 @@ export type { WeekDescriptor } from "../compute-weeks-in-window";

export type { FirstWeekSpec, ScrollToWeekSnap } from "../resolve-first-week";

export type { OverflowBehavior, MonthOverflowBehavior } from "../overflow";
export type {
OutOfRangeBehavior,
MonthOutOfRangeBehavior,
} from "../out-of-range";

export type { MonthSeparatorState } from "../month-separator";
5 changes: 4 additions & 1 deletion package/src/formats/object.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,9 @@ export type { WeekDescriptor } from "../compute-weeks-in-window";

export type { FirstWeekSpec, ScrollToWeekSnap } from "../resolve-first-week";

export type { OverflowBehavior, MonthOverflowBehavior } from "../overflow";
export type {
OutOfRangeBehavior,
MonthOutOfRangeBehavior,
} from "../out-of-range";

export type { MonthSeparatorState } from "../month-separator";
5 changes: 4 additions & 1 deletion package/src/formats/plain-date-time.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,9 @@ export type { WeekDescriptor } from "../compute-weeks-in-window";

export type { FirstWeekSpec, ScrollToWeekSnap } from "../resolve-first-week";

export type { OverflowBehavior, MonthOverflowBehavior } from "../overflow";
export type {
OutOfRangeBehavior,
MonthOutOfRangeBehavior,
} from "../out-of-range";

export type { MonthSeparatorState } from "../month-separator";
5 changes: 4 additions & 1 deletion package/src/formats/plain-date.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,9 @@ export type { WeekDescriptor } from "../compute-weeks-in-window";

export type { FirstWeekSpec, ScrollToWeekSnap } from "../resolve-first-week";

export type { OverflowBehavior, MonthOverflowBehavior } from "../overflow";
export type {
OutOfRangeBehavior,
MonthOutOfRangeBehavior,
} from "../out-of-range";

export type { MonthSeparatorState } from "../month-separator";
5 changes: 4 additions & 1 deletion package/src/formats/plain-month-day.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,9 @@ export type { WeekDescriptor } from "../compute-weeks-in-window";

export type { FirstWeekSpec, ScrollToWeekSnap } from "../resolve-first-week";

export type { OverflowBehavior, MonthOverflowBehavior } from "../overflow";
export type {
OutOfRangeBehavior,
MonthOutOfRangeBehavior,
} from "../out-of-range";

export type { MonthSeparatorState } from "../month-separator";
5 changes: 4 additions & 1 deletion package/src/formats/plain-year-month.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,9 @@ export type { WeekDescriptor } from "../compute-weeks-in-window";

export type { FirstWeekSpec, ScrollToWeekSnap } from "../resolve-first-week";

export type { OverflowBehavior, MonthOverflowBehavior } from "../overflow";
export type {
OutOfRangeBehavior,
MonthOutOfRangeBehavior,
} from "../out-of-range";

export type { MonthSeparatorState } from "../month-separator";
5 changes: 4 additions & 1 deletion package/src/formats/zoned-date-time.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,9 @@ export type { WeekDescriptor } from "../compute-weeks-in-window";

export type { FirstWeekSpec, ScrollToWeekSnap } from "../resolve-first-week";

export type { OverflowBehavior, MonthOverflowBehavior } from "../overflow";
export type {
OutOfRangeBehavior,
MonthOutOfRangeBehavior,
} from "../out-of-range";

export type { MonthSeparatorState } from "../month-separator";
2 changes: 1 addition & 1 deletion package/src/grid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -460,7 +460,7 @@ function WeeksViewGridFn(
numberOfMonths: 1,
fixedWeeks: false,
outsideDays: "enabled" as const,
overflowBehavior: "unbounded" as const,
outOfRangeBehavior: "unbounded" as const,
goNextMonth: () => {},
goPrevMonth: () => {},
setGridLabelId: () => {},
Expand Down
5 changes: 4 additions & 1 deletion package/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,5 +124,8 @@ export { useWeeksViewStable, useWeeksViewState } from "./weeks-view-context";

export type { WeekDescriptor } from "./compute-weeks-in-window";
export type { FirstWeekSpec, ScrollToWeekSnap } from "./resolve-first-week";
export type { OverflowBehavior, MonthOverflowBehavior } from "./overflow";
export type {
OutOfRangeBehavior,
MonthOutOfRangeBehavior,
} from "./out-of-range";
export type { MonthSeparatorState } from "./month-separator";
17 changes: 11 additions & 6 deletions package/src/month-view-types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { Temporal } from "@js-temporal/polyfill";
import type { MonthOverflowBehavior } from "./overflow";
import type { MonthOutOfRangeBehavior } from "./out-of-range";
import type { MonthData, OutsideDays, RootState } from "./types";

// ---------------------------------------------------------------------------
Expand All @@ -25,12 +25,17 @@ export interface MonthViewRootProps {
*/
outsideDays?: OutsideDays | undefined;
/**
* How month navigation behaves at `min`/`max` bounds.
* - `"unbounded"` — navigation is always allowed.
* - `"stop"` — navigation buttons disable at the boundary.
* How month navigation behaves at the `min`/`max` bounds. `min`/`max` always
* restrict which **days are selectable**; this controls whether you can still
* **view** months outside them. See {@link MonthOutOfRangeBehavior}.
*
* - `"unbounded"` — Prev/Next are never disabled by `min`/`max`; you can page
* to any month, with out-of-range days rendered disabled.
* - `"stop"` — Prev/Next disable once the destination month crosses the bound.
*
* @default "unbounded"
*/
overflowBehavior?: MonthOverflowBehavior | undefined;
outOfRangeBehavior?: MonthOutOfRangeBehavior | undefined;
/**
* The controlled visible month. When provided, the component is controlled.
*
Expand Down Expand Up @@ -65,7 +70,7 @@ export interface MonthViewStableContextValue {
/** How outside-month days are displayed. */
outsideDays: OutsideDays;
/** How month navigation behaves at bounds. */
overflowBehavior: MonthOverflowBehavior;
outOfRangeBehavior: MonthOutOfRangeBehavior;
/** Navigate to the next month(s). */
goNextMonth: () => void;
/** Navigate to the previous month(s). */
Expand Down
46 changes: 44 additions & 2 deletions package/src/month-view.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -758,14 +758,15 @@ describe("numberOfMonths", () => {
unmount();
});

it("next button disabled when last visible month reaches max", () => {
it('outOfRangeBehavior="stop": next button disabled when last visible month reaches max', () => {
const maxDate = Temporal.PlainDate.from("2026-05-31");
const { container, unmount } = render(
<MonthView
{...defaultProps}
defaultValue={march15}
numberOfMonths={3}
max={maxDate}
outOfRangeBehavior="stop"
>
<NextMonthButton data-testid="next" />
</MonthView>,
Expand All @@ -778,14 +779,15 @@ describe("numberOfMonths", () => {
unmount();
});

it("prev button disabled when first visible month reaches min", () => {
it('outOfRangeBehavior="stop": prev button disabled when first visible month reaches min', () => {
const minDate = Temporal.PlainDate.from("2026-03-01");
const { container, unmount } = render(
<MonthView
{...defaultProps}
defaultValue={march15}
numberOfMonths={2}
min={minDate}
outOfRangeBehavior="stop"
>
<PrevMonthButton data-testid="prev" />
</MonthView>,
Expand All @@ -798,6 +800,46 @@ describe("numberOfMonths", () => {
unmount();
});

it('outOfRangeBehavior="unbounded" (default): next button stays enabled past max', () => {
const maxDate = Temporal.PlainDate.from("2026-05-31");
const { container, unmount } = render(
<MonthView
{...defaultProps}
defaultValue={march15}
numberOfMonths={3}
max={maxDate}
>
<NextMonthButton data-testid="next" />
</MonthView>,
);

// Default is "unbounded": max only restricts selectable days, not which
// months can be viewed, so the button must NOT disable at the bound.
const btn = container.querySelector('[data-testid="next"]')!;
expect(btn.getAttribute("disabled")).toBeNull();

unmount();
});

it('outOfRangeBehavior="unbounded" (default): prev button stays enabled past min', () => {
const minDate = Temporal.PlainDate.from("2026-03-01");
const { container, unmount } = render(
<MonthView
{...defaultProps}
defaultValue={march15}
numberOfMonths={2}
min={minDate}
>
<PrevMonthButton data-testid="prev" />
</MonthView>,
);

const btn = container.querySelector('[data-testid="prev"]')!;
expect(btn.getAttribute("disabled")).toBeNull();

unmount();
});

it("clicking next shifts view by one month", () => {
let captured:
| { currentMonth: { year: number; month: number } }
Expand Down
12 changes: 6 additions & 6 deletions package/src/month-view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ function MonthViewRoot(props: MonthViewRootProps) {
numberOfMonths: numberOfMonthsProp,
fixedWeeks: fixedWeeksProp,
outsideDays: outsideDaysProp,
overflowBehavior: overflowBehaviorProp,
outOfRangeBehavior: outOfRangeBehaviorProp,
month: monthProp,
defaultMonth: defaultMonthProp,
onMonthChange,
Expand All @@ -43,7 +43,7 @@ function MonthViewRoot(props: MonthViewRootProps) {
const numberOfMonths = Math.max(1, Math.min(numberOfMonthsProp ?? 1, 12));
const fixedWeeks = fixedWeeksProp ?? false;
const outsideDays = outsideDaysProp ?? "enabled";
const overflowBehavior = overflowBehaviorProp ?? "unbounded";
const outOfRangeBehavior = outOfRangeBehaviorProp ?? "unbounded";

// Read calendar-level context
const calStable = useCalendarStable();
Expand Down Expand Up @@ -383,7 +383,7 @@ function MonthViewRoot(props: MonthViewRootProps) {
numberOfMonths,
fixedWeeks,
outsideDays,
overflowBehavior,
outOfRangeBehavior,
goNextMonth,
goPrevMonth,
setGridLabelId,
Expand All @@ -393,7 +393,7 @@ function MonthViewRoot(props: MonthViewRootProps) {
numberOfMonths,
fixedWeeks,
outsideDays,
overflowBehavior,
outOfRangeBehavior,
goNextMonth,
goPrevMonth,
setGridLabelId,
Expand Down Expand Up @@ -447,7 +447,7 @@ function MonthView<F extends ValueFormat = "PlainDate">(
numberOfMonths,
fixedWeeks,
outsideDays,
overflowBehavior,
outOfRangeBehavior,
month,
defaultMonth,
onMonthChange,
Expand All @@ -461,7 +461,7 @@ function MonthView<F extends ValueFormat = "PlainDate">(
numberOfMonths={numberOfMonths}
fixedWeeks={fixedWeeks}
outsideDays={outsideDays}
overflowBehavior={overflowBehavior}
outOfRangeBehavior={outOfRangeBehavior}
month={month}
defaultMonth={defaultMonth}
onMonthChange={onMonthChange}
Expand Down
14 changes: 13 additions & 1 deletion package/src/navigation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,7 @@ function useNavButton(direction: "prev" | "next") {
goNextMonth: goToNextMonth,
goPrevMonth: goToPrevMonth,
numberOfMonths,
outOfRangeBehavior,
} = monthViewStable;
const {
currentMonth: currentDateTime,
Expand Down Expand Up @@ -285,6 +286,10 @@ function useNavButton(direction: "prev" | "next") {

const isDisabled = useMemo(() => {
if (globalDisabled) return true;
// "unbounded" never disables on min/max — the bounds only gate which days
// are selectable, not which months can be viewed. "stop" halts navigation
// once the destination month crosses the boundary.
if (outOfRangeBehavior === "unbounded") return false;
if (!boundValue) return false;
if (direction === "prev") {
return (
Expand All @@ -296,7 +301,14 @@ function useNavButton(direction: "prev" | "next") {
destYear > boundValue.year ||
(destYear === boundValue.year && destMonth > boundValue.month)
);
}, [globalDisabled, destYear, destMonth, boundValue, direction]);
}, [
globalDisabled,
outOfRangeBehavior,
destYear,
destMonth,
boundValue,
direction,
]);

// ISO — `destYear`/`destMonth` are ISO numbers; the locale calendar only
// affects display, so it must not be injected into this value.
Expand Down
Loading
Loading