From c08d1c26a8a4dbd993d05405d74c47068bb49009 Mon Sep 17 00:00:00 2001 From: Klink <85062+dogmar@users.noreply.github.com> Date: Mon, 15 Jun 2026 11:39:03 -0700 Subject: [PATCH 1/4] =?UTF-8?q?feat!:=20implement=20MonthView=20out-of-ran?= =?UTF-8?q?ge=20navigation;=20rename=20overflowBehavior=20=E2=86=92=20outO?= =?UTF-8?q?fRangeBehavior?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit MonthView's `overflowBehavior` prop was inert — the prev/next buttons ignored it and never disabled at the `min`/`max` bounds. Wire it into `useNavButton`: `"stop"` disables navigation once the destination month crosses the boundary, while `"unbounded"` (default) leaves paging unrestricted (out-of-range days still render disabled). Also rename the prop and its types for discoverability — `overflowBehavior` didn't convey that it bounds the *view*, not selection: - prop `overflowBehavior` → `outOfRangeBehavior` (MonthView + WeeksView) - type `OverflowBehavior` → `OutOfRangeBehavior` - type `MonthOverflowBehavior` → `MonthOutOfRangeBehavior` TSDoc on the prop and types is expanded to explain the view-vs-selection distinction and each value (including when `snap`/`snap-shrink` diverge). BREAKING CHANGE: `overflowBehavior` is renamed to `outOfRangeBehavior`, and the exported `OverflowBehavior`/`MonthOverflowBehavior` types become `OutOfRangeBehavior`/`MonthOutOfRangeBehavior`. Accepted values are unchanged. Co-Authored-By: Claude Opus 4.8 --- package/src/formats/date.ts | 2 +- package/src/formats/object.ts | 2 +- package/src/formats/plain-date-time.ts | 2 +- package/src/formats/plain-date.ts | 2 +- package/src/formats/plain-month-day.ts | 2 +- package/src/formats/plain-year-month.ts | 2 +- package/src/formats/zoned-date-time.ts | 2 +- package/src/grid.tsx | 2 +- package/src/index.ts | 2 +- package/src/month-view-types.ts | 17 ++++--- package/src/month-view.test.tsx | 46 +++++++++++++++++- package/src/month-view.tsx | 12 ++--- package/src/navigation.tsx | 14 +++++- package/src/overflow.test.ts | 20 ++++---- package/src/overflow.ts | 54 +++++++++++++++++++-- package/src/weeks-navigation.tsx | 11 +++-- package/src/weeks-view-state.ts | 28 +++++------ package/src/weeks-view-types.ts | 19 ++++++-- package/src/weeks-view.tsx | 6 +-- website/src/components/demo/AppControls.tsx | 32 ++++++------ website/src/examples/styled-month-view.tsx | 6 +-- website/src/examples/styled-weeks-view.tsx | 8 +-- website/src/routes/demo.tsx | 20 ++++---- 23 files changed, 214 insertions(+), 97 deletions(-) diff --git a/package/src/formats/date.ts b/package/src/formats/date.ts index 2d3868c..f0c1d45 100644 --- a/package/src/formats/date.ts +++ b/package/src/formats/date.ts @@ -169,6 +169,6 @@ 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 "../overflow"; export type { MonthSeparatorState } from "../month-separator"; diff --git a/package/src/formats/object.ts b/package/src/formats/object.ts index f33948a..f55dd67 100644 --- a/package/src/formats/object.ts +++ b/package/src/formats/object.ts @@ -169,6 +169,6 @@ 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 "../overflow"; export type { MonthSeparatorState } from "../month-separator"; diff --git a/package/src/formats/plain-date-time.ts b/package/src/formats/plain-date-time.ts index 6793aa0..2fea19f 100644 --- a/package/src/formats/plain-date-time.ts +++ b/package/src/formats/plain-date-time.ts @@ -169,6 +169,6 @@ 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 "../overflow"; export type { MonthSeparatorState } from "../month-separator"; diff --git a/package/src/formats/plain-date.ts b/package/src/formats/plain-date.ts index 953345b..9d18b88 100644 --- a/package/src/formats/plain-date.ts +++ b/package/src/formats/plain-date.ts @@ -169,6 +169,6 @@ 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 "../overflow"; export type { MonthSeparatorState } from "../month-separator"; diff --git a/package/src/formats/plain-month-day.ts b/package/src/formats/plain-month-day.ts index 1af2049..e4cb34c 100644 --- a/package/src/formats/plain-month-day.ts +++ b/package/src/formats/plain-month-day.ts @@ -169,6 +169,6 @@ 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 "../overflow"; export type { MonthSeparatorState } from "../month-separator"; diff --git a/package/src/formats/plain-year-month.ts b/package/src/formats/plain-year-month.ts index d9a38c2..585d79f 100644 --- a/package/src/formats/plain-year-month.ts +++ b/package/src/formats/plain-year-month.ts @@ -169,6 +169,6 @@ 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 "../overflow"; export type { MonthSeparatorState } from "../month-separator"; diff --git a/package/src/formats/zoned-date-time.ts b/package/src/formats/zoned-date-time.ts index 8146710..90fe7c7 100644 --- a/package/src/formats/zoned-date-time.ts +++ b/package/src/formats/zoned-date-time.ts @@ -169,6 +169,6 @@ 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 "../overflow"; export type { MonthSeparatorState } from "../month-separator"; diff --git a/package/src/grid.tsx b/package/src/grid.tsx index 1c41c60..110224b 100644 --- a/package/src/grid.tsx +++ b/package/src/grid.tsx @@ -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: () => {}, diff --git a/package/src/index.ts b/package/src/index.ts index e99024a..b4adda0 100644 --- a/package/src/index.ts +++ b/package/src/index.ts @@ -124,5 +124,5 @@ 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 "./overflow"; export type { MonthSeparatorState } from "./month-separator"; diff --git a/package/src/month-view-types.ts b/package/src/month-view-types.ts index 5954506..f15cc2d 100644 --- a/package/src/month-view-types.ts +++ b/package/src/month-view-types.ts @@ -1,5 +1,5 @@ import type { Temporal } from "@js-temporal/polyfill"; -import type { MonthOverflowBehavior } from "./overflow"; +import type { MonthOutOfRangeBehavior } from "./overflow"; import type { MonthData, OutsideDays, RootState } from "./types"; // --------------------------------------------------------------------------- @@ -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. * @@ -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). */ diff --git a/package/src/month-view.test.tsx b/package/src/month-view.test.tsx index 43581e0..b9dde59 100644 --- a/package/src/month-view.test.tsx +++ b/package/src/month-view.test.tsx @@ -758,7 +758,7 @@ 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( { defaultValue={march15} numberOfMonths={3} max={maxDate} + outOfRangeBehavior="stop" > , @@ -778,7 +779,7 @@ 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( { defaultValue={march15} numberOfMonths={2} min={minDate} + outOfRangeBehavior="stop" > , @@ -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( + + + , + ); + + // 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( + + + , + ); + + 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 } } diff --git a/package/src/month-view.tsx b/package/src/month-view.tsx index a6bbcde..1144cb0 100644 --- a/package/src/month-view.tsx +++ b/package/src/month-view.tsx @@ -33,7 +33,7 @@ function MonthViewRoot(props: MonthViewRootProps) { numberOfMonths: numberOfMonthsProp, fixedWeeks: fixedWeeksProp, outsideDays: outsideDaysProp, - overflowBehavior: overflowBehaviorProp, + outOfRangeBehavior: outOfRangeBehaviorProp, month: monthProp, defaultMonth: defaultMonthProp, onMonthChange, @@ -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(); @@ -383,7 +383,7 @@ function MonthViewRoot(props: MonthViewRootProps) { numberOfMonths, fixedWeeks, outsideDays, - overflowBehavior, + outOfRangeBehavior, goNextMonth, goPrevMonth, setGridLabelId, @@ -393,7 +393,7 @@ function MonthViewRoot(props: MonthViewRootProps) { numberOfMonths, fixedWeeks, outsideDays, - overflowBehavior, + outOfRangeBehavior, goNextMonth, goPrevMonth, setGridLabelId, @@ -447,7 +447,7 @@ function MonthView( numberOfMonths, fixedWeeks, outsideDays, - overflowBehavior, + outOfRangeBehavior, month, defaultMonth, onMonthChange, @@ -461,7 +461,7 @@ function MonthView( numberOfMonths={numberOfMonths} fixedWeeks={fixedWeeks} outsideDays={outsideDays} - overflowBehavior={overflowBehavior} + outOfRangeBehavior={outOfRangeBehavior} month={month} defaultMonth={defaultMonth} onMonthChange={onMonthChange} diff --git a/package/src/navigation.tsx b/package/src/navigation.tsx index 9275a9e..8aad6c8 100644 --- a/package/src/navigation.tsx +++ b/package/src/navigation.tsx @@ -249,6 +249,7 @@ function useNavButton(direction: "prev" | "next") { goNextMonth: goToNextMonth, goPrevMonth: goToPrevMonth, numberOfMonths, + outOfRangeBehavior, } = monthViewStable; const { currentMonth: currentDateTime, @@ -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 ( @@ -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. diff --git a/package/src/overflow.test.ts b/package/src/overflow.test.ts index 20d67fd..ed6c70b 100644 --- a/package/src/overflow.test.ts +++ b/package/src/overflow.test.ts @@ -1,6 +1,6 @@ import { Temporal } from "@js-temporal/polyfill"; import { describe, it, expect } from "vitest"; -import { applyOverflow, canShift, type OverflowBehavior } from "./overflow"; +import { applyOverflow, canShift, type OutOfRangeBehavior } from "./overflow"; const T = Temporal; const pd = (s: string) => T.PlainDate.from(s); @@ -14,7 +14,7 @@ describe("applyOverflow", () => { description: "unbounded — no adjustment", targetFirstWeek: pd("2026-02-01"), weekCount: 8, - behavior: "unbounded" as OverflowBehavior, + behavior: "unbounded" as OutOfRangeBehavior, useMinMax: true, expected: { firstWeek: "2026-02-01", weekCount: 8 }, }, @@ -22,7 +22,7 @@ describe("applyOverflow", () => { description: "stop — target has valid weeks, allowed", targetFirstWeek: pd("2025-12-07"), weekCount: 8, - behavior: "stop" as OverflowBehavior, + behavior: "stop" as OutOfRangeBehavior, useMinMax: true, expected: { firstWeek: "2025-12-07", weekCount: 8 }, }, @@ -30,7 +30,7 @@ describe("applyOverflow", () => { description: "snap — pulls back when target overshoots max", targetFirstWeek: pd("2026-01-11"), weekCount: 8, - behavior: "snap" as OverflowBehavior, + behavior: "snap" as OutOfRangeBehavior, useMinMax: true, expected: { firstWeek: "2025-11-16", @@ -41,7 +41,7 @@ describe("applyOverflow", () => { description: "snap-shrink — pulls back and trims disabled rows", targetFirstWeek: pd("2026-01-11"), weekCount: 8, - behavior: "snap-shrink" as OverflowBehavior, + behavior: "snap-shrink" as OutOfRangeBehavior, useMinMax: true, expected: { firstWeek: "2025-11-30", @@ -52,7 +52,7 @@ describe("applyOverflow", () => { description: "stop-shrink — trims disabled rows", targetFirstWeek: pd("2025-12-07"), weekCount: 8, - behavior: "stop-shrink" as OverflowBehavior, + behavior: "stop-shrink" as OutOfRangeBehavior, useMinMax: true, expected: { firstWeek: "2025-12-07", @@ -63,7 +63,7 @@ describe("applyOverflow", () => { description: "no min/max — all modes behave as unbounded", targetFirstWeek: pd("2026-06-01"), weekCount: 8, - behavior: "snap" as OverflowBehavior, + behavior: "snap" as OutOfRangeBehavior, useMinMax: false, expected: { firstWeek: "2026-06-01", weekCount: 8 }, }, @@ -91,21 +91,21 @@ describe("canShift", () => { it.each([ { description: "unbounded — always true", - behavior: "unbounded" as OverflowBehavior, + behavior: "unbounded" as OutOfRangeBehavior, currentFirstWeek: pd("2026-06-01"), direction: 1 as const, expected: true, }, { description: "stop — false when target has no valid weeks", - behavior: "stop" as OverflowBehavior, + behavior: "stop" as OutOfRangeBehavior, currentFirstWeek: pd("2025-12-28"), direction: 1 as const, expected: false, }, { description: "snap — false when already at clamp position", - behavior: "snap" as OverflowBehavior, + behavior: "snap" as OutOfRangeBehavior, currentFirstWeek: pd("2025-11-16"), direction: 1 as const, expected: false, diff --git a/package/src/overflow.ts b/package/src/overflow.ts index 84b15dd..833a404 100644 --- a/package/src/overflow.ts +++ b/package/src/overflow.ts @@ -1,14 +1,60 @@ import type { Temporal } from "@js-temporal/polyfill"; import type { TemporalNamespace, WeekStartDay } from "./types"; -export type OverflowBehavior = +/** + * Controls how a {@link WeeksView} window behaves when navigation would move it + * past the `min`/`max` bounds. + * + * `min`/`max` always restrict which **days are selectable**. `outOfRangeBehavior` + * is a separate concern: it decides whether the **visible window** may scroll + * beyond those bounds, and how the edge is handled. + * + * - `"unbounded"` — Navigation is never restricted by `min`/`max`; any range of + * weeks can be viewed. Out-of-range days simply render disabled. (Default.) + * - `"stop"` — Navigation halts at the boundary: the Prev/Next button is + * disabled once the next step would show a window containing no in-range day. + * - `"stop-shrink"` — Like `"stop"`, but near the edge the window **shrinks** to + * fewer week rows instead of showing rows that are entirely out of range. + * - `"snap"` — A jump that would overshoot **snaps** the window so its edge + * aligns to the last/first in-range week, rather than stopping or overshooting. + * - `"snap-shrink"` — Snap to the boundary, then **shrink** the window to just + * the in-range weeks. + * + * `"snap"` and `"snap-shrink"` are identical **unless the selectable range spans + * fewer weeks than `weekCount`** — i.e. the window is taller than the range. + * Snapping can only pin *one* edge to a bound; if the range is narrower than the + * window, the opposite edge still overhangs into out-of-range weeks. `"snap"` + * keeps the full `weekCount` (padding the overhang with all-disabled week rows), + * while `"snap-shrink"` trims those rows so only the in-range weeks show. + * + * Example with `weekCount: 6` and a `min`/`max` that span just 2 weeks: `"snap"` + * shows 6 rows (4 of them fully disabled), `"snap-shrink"` shows 2 rows. When + * the range is ≥ `weekCount` weeks wide, the snapped window already fits, so the + * two behave the same. + */ +export type OutOfRangeBehavior = | "unbounded" | "stop" | "stop-shrink" | "snap" | "snap-shrink"; -export type MonthOverflowBehavior = "unbounded" | "stop"; +/** + * Controls how {@link MonthView} navigation behaves when it would move the + * visible month(s) past the `min`/`max` bounds. + * + * `min`/`max` always restrict which **days are selectable**. This decides + * whether you can still **view** months outside them. + * + * - `"unbounded"` — Prev/Next are never disabled by `min`/`max`; you can page to + * any month. Out-of-range days render disabled. (Default.) + * - `"stop"` — Prev/Next become disabled once the destination month crosses the + * boundary. + * + * MonthView renders fixed month grids, so the `"…-shrink"`/`"snap…"` modes that + * {@link OutOfRangeBehavior} offers for WeeksView's flexible window don't apply. + */ +export type MonthOutOfRangeBehavior = "unbounded" | "stop"; // --------------------------------------------------------------------------- // Internal helpers @@ -108,7 +154,7 @@ function shrinkWindow( export interface ApplyOverflowInput { targetFirstWeek: Temporal.PlainDate; weekCount: number; - behavior: OverflowBehavior; + behavior: OutOfRangeBehavior; min?: Temporal.PlainDate; max?: Temporal.PlainDate; weekStartDay: WeekStartDay; @@ -185,7 +231,7 @@ export interface CanShiftInput { weekCount: number; direction: 1 | -1; shiftBy?: number; - behavior: OverflowBehavior; + behavior: OutOfRangeBehavior; min?: Temporal.PlainDate; max?: Temporal.PlainDate; weekStartDay: WeekStartDay; diff --git a/package/src/weeks-navigation.tsx b/package/src/weeks-navigation.tsx index e72df08..6ee691d 100644 --- a/package/src/weeks-navigation.tsx +++ b/package/src/weeks-navigation.tsx @@ -77,7 +77,8 @@ function useWeeksNavButton( temporal: T, weekStartDay, } = useCalendarStable(); - const { weekCount, overflowBehavior, goNext, goPrev } = useWeeksViewStable(); + const { weekCount, outOfRangeBehavior, goNext, goPrev } = + useWeeksViewStable(); const { windowInfo } = useWeeksViewState(); const { rootState } = useMonthViewState(); @@ -90,7 +91,7 @@ function useWeeksNavButton( weekCount, direction: direction === "next" ? 1 : -1, ...(shiftByProp !== undefined && { shiftBy: shiftByProp }), - behavior: overflowBehavior, + behavior: outOfRangeBehavior, ...(minValue !== undefined && { min: minValue }), ...(maxValue !== undefined && { max: maxValue }), weekStartDay, @@ -102,7 +103,7 @@ function useWeeksNavButton( weekCount, direction, shiftByProp, - overflowBehavior, + outOfRangeBehavior, minValue, maxValue, weekStartDay, @@ -149,7 +150,7 @@ function PrevWeeksButtonFn( /** * Button that navigates to the previous week(s). Automatically disabled when - * no more weeks are available based on `overflowBehavior` and `min`. + * no more weeks are available based on `outOfRangeBehavior` and `min`. * Exposes `data-direction="prev"`. */ export const PrevWeeksButton = forwardRef(PrevWeeksButtonFn); @@ -177,7 +178,7 @@ function NextWeeksButtonFn( /** * Button that navigates to the next week(s). Automatically disabled when - * no more weeks are available based on `overflowBehavior` and `max`. + * no more weeks are available based on `outOfRangeBehavior` and `max`. * Exposes `data-direction="next"`. */ export const NextWeeksButton = forwardRef(NextWeeksButtonFn); diff --git a/package/src/weeks-view-state.ts b/package/src/weeks-view-state.ts index 20f6a8a..3227d84 100644 --- a/package/src/weeks-view-state.ts +++ b/package/src/weeks-view-state.ts @@ -2,7 +2,7 @@ import type { Temporal } from "@js-temporal/polyfill"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useCalendarStable, useCalendarState } from "./calendar-context"; import { computeWeeksInWindow } from "./compute-weeks-in-window"; -import { applyOverflow, type OverflowBehavior } from "./overflow"; +import { applyOverflow, type OutOfRangeBehavior } from "./overflow"; import { resolveFirstWeekSpec, resolveFirstWeek, @@ -29,13 +29,13 @@ export function useWeeksViewRootState(props: WeeksViewRootProps) { defaultFirstWeek: defaultFirstWeekProp, onFirstWeekChange, scrollBy: scrollByProp, - overflowBehavior: overflowBehaviorProp, + outOfRangeBehavior: outOfRangeBehaviorProp, onWindowChange, } = props; const scrollBy = scrollByProp ?? "row"; - const overflowBehavior: OverflowBehavior = - overflowBehaviorProp ?? "unbounded"; + const outOfRangeBehavior: OutOfRangeBehavior = + outOfRangeBehaviorProp ?? "unbounded"; // Read calendar-level context const calStable = useCalendarStable(); @@ -87,7 +87,7 @@ export function useWeeksViewRootState(props: WeeksViewRootProps) { applyOverflow({ targetFirstWeek: resolvedFirstWeek, weekCount, - behavior: overflowBehavior, + behavior: outOfRangeBehavior, ...(minValue !== undefined && { min: minValue }), ...(maxValue !== undefined && { max: maxValue }), weekStartDay, @@ -96,7 +96,7 @@ export function useWeeksViewRootState(props: WeeksViewRootProps) { [ resolvedFirstWeek, weekCount, - overflowBehavior, + outOfRangeBehavior, minValue, maxValue, weekStartDay, @@ -313,7 +313,7 @@ export function useWeeksViewRootState(props: WeeksViewRootProps) { const adjusted = applyOverflow({ targetFirstWeek: target, weekCount, - behavior: overflowBehavior, + behavior: outOfRangeBehavior, ...(minValue !== undefined && { min: minValue }), ...(maxValue !== undefined && { max: maxValue }), weekStartDay, @@ -324,7 +324,7 @@ export function useWeeksViewRootState(props: WeeksViewRootProps) { [ resolvedFirstWeek, weekCount, - overflowBehavior, + outOfRangeBehavior, minValue, maxValue, weekStartDay, @@ -340,7 +340,7 @@ export function useWeeksViewRootState(props: WeeksViewRootProps) { const adjusted = applyOverflow({ targetFirstWeek: target, weekCount, - behavior: overflowBehavior, + behavior: outOfRangeBehavior, ...(minValue !== undefined && { min: minValue }), ...(maxValue !== undefined && { max: maxValue }), weekStartDay, @@ -351,7 +351,7 @@ export function useWeeksViewRootState(props: WeeksViewRootProps) { [ resolvedFirstWeek, weekCount, - overflowBehavior, + outOfRangeBehavior, minValue, maxValue, weekStartDay, @@ -376,7 +376,7 @@ export function useWeeksViewRootState(props: WeeksViewRootProps) { const adjusted = applyOverflow({ targetFirstWeek: newFirstWeek, weekCount, - behavior: overflowBehavior, + behavior: outOfRangeBehavior, ...(minValue !== undefined && { min: minValue }), ...(maxValue !== undefined && { max: maxValue }), weekStartDay, @@ -388,7 +388,7 @@ export function useWeeksViewRootState(props: WeeksViewRootProps) { resolvedFirstWeek, weekCount, resolveSpec, - overflowBehavior, + outOfRangeBehavior, minValue, maxValue, weekStartDay, @@ -420,7 +420,7 @@ export function useWeeksViewRootState(props: WeeksViewRootProps) { () => ({ weekCount, scrollBy, - overflowBehavior, + outOfRangeBehavior, goNext, goPrev, scrollToWeek, @@ -430,7 +430,7 @@ export function useWeeksViewRootState(props: WeeksViewRootProps) { [ weekCount, scrollBy, - overflowBehavior, + outOfRangeBehavior, goNext, goPrev, scrollToWeek, diff --git a/package/src/weeks-view-types.ts b/package/src/weeks-view-types.ts index 2ea5b47..4c267de 100644 --- a/package/src/weeks-view-types.ts +++ b/package/src/weeks-view-types.ts @@ -1,6 +1,6 @@ import type { Temporal } from "@js-temporal/polyfill"; import type { WeekDescriptor } from "./compute-weeks-in-window"; -import type { OverflowBehavior } from "./overflow"; +import type { OutOfRangeBehavior } from "./overflow"; import type { FirstWeekSpec, ScrollToWeekSnap } from "./resolve-first-week"; import type { RootState } from "./types"; @@ -26,10 +26,21 @@ export interface WeeksViewRootProps { */ scrollBy?: "row" | "page" | undefined; /** - * How navigation behaves at `min`/`max` bounds. + * How the visible window behaves at the `min`/`max` bounds. `min`/`max` + * always restrict which **days are selectable**; this controls whether the + * window may scroll beyond them, and how the edge is handled. See + * {@link OutOfRangeBehavior}. + * + * - `"unbounded"` — navigation is never restricted by `min`/`max`. + * - `"stop"` — navigation halts once the next step has no in-range day. + * - `"stop-shrink"` — like `"stop"`, but the window shrinks at the edge. + * - `"snap"` — overshooting jumps snap the window edge to the last/first + * in-range week. + * - `"snap-shrink"` — snap to the boundary, shrinking if still out of range. + * * @default "unbounded" */ - overflowBehavior?: OverflowBehavior | undefined; + outOfRangeBehavior?: OutOfRangeBehavior | undefined; /** Called when the visible window changes. */ onWindowChange?: ((info: WindowInfo) => void) | undefined; /** React children. */ @@ -75,7 +86,7 @@ export interface WeeksViewStableContextValue { /** How much to scroll per navigation step. */ scrollBy: "row" | "page"; /** How navigation behaves at bounds. */ - overflowBehavior: OverflowBehavior; + outOfRangeBehavior: OutOfRangeBehavior; /** Navigate to the next week(s). Optionally override the shift amount (in weeks). */ goNext: (shiftBy?: number) => void; /** Navigate to the previous week(s). Optionally override the shift amount (in weeks). */ diff --git a/package/src/weeks-view.tsx b/package/src/weeks-view.tsx index 7806204..3fa4d89 100644 --- a/package/src/weeks-view.tsx +++ b/package/src/weeks-view.tsx @@ -48,7 +48,7 @@ function WeeksViewRootFn( numberOfMonths: 1, fixedWeeks: false, outsideDays: "enabled" as const, - overflowBehavior: "unbounded" as const, + outOfRangeBehavior: "unbounded" as const, goNextMonth: () => {}, goPrevMonth: () => {}, setGridLabelId: () => {}, @@ -106,7 +106,7 @@ function WeeksViewFn( defaultFirstWeek, onFirstWeekChange, scrollBy, - overflowBehavior, + outOfRangeBehavior, onWindowChange, children, ...calendarProps @@ -121,7 +121,7 @@ function WeeksViewFn( defaultFirstWeek={defaultFirstWeek} onFirstWeekChange={onFirstWeekChange} scrollBy={scrollBy} - overflowBehavior={overflowBehavior} + outOfRangeBehavior={outOfRangeBehavior} onWindowChange={onWindowChange} > {children} diff --git a/website/src/components/demo/AppControls.tsx b/website/src/components/demo/AppControls.tsx index c268517..5e6799d 100644 --- a/website/src/components/demo/AppControls.tsx +++ b/website/src/components/demo/AppControls.tsx @@ -3,7 +3,7 @@ import { Fieldset as FieldsetPrimitive } from "@base-ui/react/fieldset"; import { Temporal } from "@js-temporal/polyfill"; import type { OutsideDays, - OverflowBehavior, + OutOfRangeBehavior, RangeMode, } from "@klinking/colander"; import type { ComponentProps } from "react"; @@ -150,14 +150,14 @@ export interface AppControlsProps { setWeekCount: (v: number) => void; scrollBy: "row" | "page"; setScrollBy: (v: "row" | "page") => void; - overflowBehavior: OverflowBehavior; - setOverflowBehavior: (v: OverflowBehavior) => void; + outOfRangeBehavior: OutOfRangeBehavior; + setOutOfRangeBehavior: (v: OutOfRangeBehavior) => void; showMonthSeparators: boolean; setShowMonthSeparators: (v: boolean) => void; disableDateMode: string; setDisableDateMode: (v: string) => void; - monthOverflowBehavior: "unbounded" | "stop"; - setMonthOverflowBehavior: (v: "unbounded" | "stop") => void; + monthOutOfRangeBehavior: "unbounded" | "stop"; + setMonthOutOfRangeBehavior: (v: "unbounded" | "stop") => void; selectionDisplay: string; lastMonthChange: string; } @@ -220,14 +220,14 @@ export function AppControls(props: AppControlsProps) { setWeekCount, scrollBy, setScrollBy, - overflowBehavior, - setOverflowBehavior, + outOfRangeBehavior, + setOutOfRangeBehavior, showMonthSeparators, setShowMonthSeparators, disableDateMode, setDisableDateMode, - monthOverflowBehavior, - setMonthOverflowBehavior, + monthOutOfRangeBehavior, + setMonthOutOfRangeBehavior, } = props; return ( @@ -502,11 +502,11 @@ export function AppControls(props: AppControlsProps) { - overflowBehavior + outOfRangeBehavior - setMonthOverflowBehavior(v as "unbounded" | "stop") + setMonthOutOfRangeBehavior(v as "unbounded" | "stop") } > @@ -561,14 +561,14 @@ export function AppControls(props: AppControlsProps) { - overflowBehavior + outOfRangeBehavior