diff --git a/packages/x-scheduler-headless/src/scheduler-selectors/schedulerOtherSelectors.ts b/packages/x-scheduler-headless/src/scheduler-selectors/schedulerOtherSelectors.ts index cd467c8e5b0a5..458deee8f8b09 100644 --- a/packages/x-scheduler-headless/src/scheduler-selectors/schedulerOtherSelectors.ts +++ b/packages/x-scheduler-headless/src/scheduler-selectors/schedulerOtherSelectors.ts @@ -1,9 +1,14 @@ -import { createSelector } from '@base-ui-components/utils/store'; +import { createSelector, createSelectorMemoized } from '@base-ui-components/utils/store'; import { SchedulerState as State } from '../utils/SchedulerStore/SchedulerStore.types'; // Warning: Only add selectors here that do not belong to any specific feature. export const schedulerOtherSelectors = { - visibleDate: createSelector((state: State) => state.visibleDate), + visibleDate: createSelectorMemoized( + (state: State) => state.adapter, + (state: State) => state.visibleDate, + (state: State) => state.timezone, + (adapter, visibleDate, timezone) => adapter.setTimezone(visibleDate, timezone), + ), isScopeDialogOpen: createSelector( (state: State) => state.pendingUpdateRecurringEventParameters != null, ), diff --git a/packages/x-scheduler-headless/src/utils/SchedulerStore/SchedulerStore.ts b/packages/x-scheduler-headless/src/utils/SchedulerStore/SchedulerStore.ts index c8d408e1922ab..12aecdf6f0f3d 100644 --- a/packages/x-scheduler-headless/src/utils/SchedulerStore/SchedulerStore.ts +++ b/packages/x-scheduler-headless/src/utils/SchedulerStore/SchedulerStore.ts @@ -31,6 +31,7 @@ import { } from './SchedulerStore.utils'; import { TimeoutManager } from '../TimeoutManager'; import { DEFAULT_EVENT_COLOR } from '../../constants'; +import { getNowInRenderTimezone, getStartOfTodayInRenderTimezone } from '../timezone-utils'; const ONE_MINUTE_IN_MS = 60 * 1000; @@ -63,6 +64,8 @@ export class SchedulerStore< instanceName: string, mapper: SchedulerParametersToStateMapper, ) { + const timezone = parameters.timezone ?? 'default'; + const schedulerInitialState: SchedulerState = { ...SchedulerStore.deriveStateFromParameters(parameters, adapter), ...buildEventsState(parameters, adapter), @@ -70,14 +73,14 @@ export class SchedulerStore< preferences: DEFAULT_SCHEDULER_PREFERENCES, adapter, occurrencePlaceholder: null, - nowUpdatedEveryMinute: adapter.now('default'), + nowUpdatedEveryMinute: getNowInRenderTimezone(adapter, timezone), pendingUpdateRecurringEventParameters: null, - timezone: parameters.timezone ?? 'default', + timezone, visibleResources: new Map(), visibleDate: parameters.visibleDate ?? parameters.defaultVisibleDate ?? - adapter.startOfDay(adapter.now('default')), + adapter.startOfDay(adapter.now(timezone)), }; const initialState = mapper.getInitialState(schedulerInitialState, parameters, adapter); @@ -92,9 +95,9 @@ export class SchedulerStore< ONE_MINUTE_IN_MS - (currentDate.getSeconds() * 1000 + currentDate.getMilliseconds()); this.timeoutManager.startTimeout('set-now', timeUntilNextMinuteMs, () => { - this.set('nowUpdatedEveryMinute', adapter.now('default')); + this.set('nowUpdatedEveryMinute', getNowInRenderTimezone(adapter, timezone)); this.timeoutManager.startInterval('set-now', ONE_MINUTE_IN_MS, () => { - this.set('nowUpdatedEveryMinute', adapter.now('default')); + this.set('nowUpdatedEveryMinute', getNowInRenderTimezone(adapter, timezone)); }); }); @@ -283,7 +286,7 @@ export class SchedulerStore< */ public goToToday = (event: React.UIEvent) => { const { adapter } = this.state; - this.setVisibleDate(adapter.startOfDay(adapter.now('default')), event); + this.setVisibleDate(getStartOfTodayInRenderTimezone(adapter, this.state.timezone), event); }; /** diff --git a/packages/x-scheduler-headless/src/utils/SchedulerStore/SchedulerStore.types.ts b/packages/x-scheduler-headless/src/utils/SchedulerStore/SchedulerStore.types.ts index 1eb3f03305d96..b63832bd61bce 100644 --- a/packages/x-scheduler-headless/src/utils/SchedulerStore/SchedulerStore.types.ts +++ b/packages/x-scheduler-headless/src/utils/SchedulerStore/SchedulerStore.types.ts @@ -134,9 +134,8 @@ export interface SchedulerState { * The timezone used by the scheduler. * Typically an IANA timezone name (e.g. "America/New_York", "Europe/Paris") * or "default" to use the adapter's default timezone. - * @default "default" */ - timezone?: TemporalTimezone; + timezone: TemporalTimezone; } export interface SchedulerParameters { diff --git a/packages/x-scheduler-headless/src/utils/SchedulerStore/tests/core.SchedulerStore.test.ts b/packages/x-scheduler-headless/src/utils/SchedulerStore/tests/core.SchedulerStore.test.ts index 0b1e02ad30a73..17aa7a5abd22e 100644 --- a/packages/x-scheduler-headless/src/utils/SchedulerStore/tests/core.SchedulerStore.test.ts +++ b/packages/x-scheduler-headless/src/utils/SchedulerStore/tests/core.SchedulerStore.test.ts @@ -31,6 +31,16 @@ storeClasses.forEach((storeClass) => { expect(schedulerEventSelectors.processedEvent(store.state, '2')!.title).to.equal('Event 2'); expect(schedulerEventSelectors.modelList(store.state)).to.equal(events); }); + + it('should set visibleDate to today in the render timezone when defaultVisibleDate is not provided', () => { + const timezone = 'Pacific/Kiritimati'; + const store = new storeClass.Value({ ...DEFAULT_PARAMS, timezone }, adapter); + + const expectedToday = adapter.startOfDay(adapter.now(timezone)); + + expect(store.state.visibleDate).toEqualDateTime(expectedToday); + expect(adapter.getTimezone(store.state.visibleDate)).to.equal(timezone); + }); }); describe('updater', () => { diff --git a/packages/x-scheduler-headless/src/utils/SchedulerStore/tests/date.SchedulerStore.test.ts b/packages/x-scheduler-headless/src/utils/SchedulerStore/tests/date.SchedulerStore.test.ts index 3a1ec521b007c..71eba5cbc145a 100644 --- a/packages/x-scheduler-headless/src/utils/SchedulerStore/tests/date.SchedulerStore.test.ts +++ b/packages/x-scheduler-headless/src/utils/SchedulerStore/tests/date.SchedulerStore.test.ts @@ -54,6 +54,32 @@ storeClasses.forEach((storeClass) => { expect(store.state.visibleDate).toEqualDateTime(todayStart); expect(onVisibleDateChange.called).to.equal(false); }); + + it('should use the provided timezone when going to today (uncontrolled)', () => { + const onVisibleDateChange = spy(); + const timezone = 'Pacific/Kiritimati'; + + const yesterday = adapter.addDays(adapter.startOfDay(adapter.now('default')), -1); + + const store = new storeClass.Value( + { + ...DEFAULT_PARAMS, + defaultVisibleDate: yesterday, + onVisibleDateChange, + timezone, + }, + adapter, + ); + + store.goToToday({} as any); + + const expected = adapter.startOfDay(adapter.now(timezone)); + + expect(store.state.visibleDate).toEqualDateTime(expected); + expect(store.state.timezone).to.equal(timezone); + expect(onVisibleDateChange.calledOnce).to.equal(true); + expect(onVisibleDateChange.lastCall.firstArg).toEqualDateTime(expected); + }); }); }); }); diff --git a/packages/x-scheduler-headless/src/utils/timezone-utils.ts b/packages/x-scheduler-headless/src/utils/timezone-utils.ts new file mode 100644 index 0000000000000..97b766a484714 --- /dev/null +++ b/packages/x-scheduler-headless/src/utils/timezone-utils.ts @@ -0,0 +1,10 @@ +import { TemporalTimezone } from '../base-ui-copy/types'; +import { Adapter } from '../use-adapter'; + +export function getNowInRenderTimezone(adapter: Adapter, timezone: TemporalTimezone) { + return adapter.now(timezone); +} + +export function getStartOfTodayInRenderTimezone(adapter: Adapter, timezone: TemporalTimezone) { + return adapter.startOfDay(getNowInRenderTimezone(adapter, timezone)); +}