From 283a4881de65ac1bbdc8f04a2f0ec90f55603cec Mon Sep 17 00:00:00 2001 From: Lourens Schep Date: Wed, 9 Sep 2026 09:13:02 +0200 Subject: [PATCH 1/2] Premium Analytics: add the Month to date and Year to date periods MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both sit in the design's period menu and neither had been built on any surface. Each joins the group of the window it pairs with — Month to date beside Last month, Year to date beside Last 12 months. Neither completes its running unit the way `last-12-months` does, so `completeToDateRange` is untouched. Completed, each shifts onto a unit of another length and its previous period stops mirroring the selection: month to date read on 8 March came back three days shorter than the eight days it covers, and year to date read on 1 January 2028 came back inverted, its start after its end. Read as they stand, both shift by their own day count and stay equal; Previous year still compares against the same days a year earlier either way. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_015eDqAWndvFNBrBRdKcsYus --- .../changelog/uni-754-to-date-presets | 4 ++ .../compute-date-range-from-preset.test.ts | 16 +++++++ .../__tests__/get-comparison-range.test.ts | 44 +++++++++++++++++++ .../__tests__/menu-surface-presets.test.ts | 4 +- .../src/__tests__/to-date-range.test.ts | 9 ++++ .../packages/datetime/src/index.ts | 2 + .../packages/datetime/src/presets/index.ts | 2 + .../packages/datetime/src/presets/primary.ts | 18 ++++++++ .../packages/datetime/src/presets/types.ts | 8 +++- .../__tests__/report-params-field.test.tsx | 2 + .../__tests__/date-filters-panel.test.tsx | 2 + .../__tests__/date-period-dropdown.test.tsx | 2 + .../jetpack/changelog/uni-754-to-date-presets | 4 ++ .../changelog/uni-754-to-date-presets | 4 ++ 14 files changed, 117 insertions(+), 4 deletions(-) create mode 100644 projects/packages/premium-analytics/changelog/uni-754-to-date-presets create mode 100644 projects/plugins/jetpack/changelog/uni-754-to-date-presets create mode 100644 projects/plugins/premium-analytics/changelog/uni-754-to-date-presets diff --git a/projects/packages/premium-analytics/changelog/uni-754-to-date-presets b/projects/packages/premium-analytics/changelog/uni-754-to-date-presets new file mode 100644 index 000000000000..4aebad88e7f3 --- /dev/null +++ b/projects/packages/premium-analytics/changelog/uni-754-to-date-presets @@ -0,0 +1,4 @@ +Significance: minor +Type: added + +Date picker: add the Month to date and Year to date periods. diff --git a/projects/packages/premium-analytics/packages/data/src/utils/__tests__/compute-date-range-from-preset.test.ts b/projects/packages/premium-analytics/packages/data/src/utils/__tests__/compute-date-range-from-preset.test.ts index 736b9fe462a2..39ce9b15bfcb 100644 --- a/projects/packages/premium-analytics/packages/data/src/utils/__tests__/compute-date-range-from-preset.test.ts +++ b/projects/packages/premium-analytics/packages/data/src/utils/__tests__/compute-date-range-from-preset.test.ts @@ -110,6 +110,14 @@ describe( 'computeDateRangeFromPreset', () => { expect( range!.to ).toBe( toSiteISO( TODAY_END ) ); } ); + it( 'returns the running calendar month through the end of today for "month-to-date"', () => { + const range = computeDateRangeFromPreset( 'month-to-date' ); + + expect( range ).toBeDefined(); + expect( range!.from ).toBe( toSiteISO( startOfMonth( TODAY_START, { in: UTC } ) ) ); + expect( range!.to ).toBe( toSiteISO( TODAY_END ) ); + } ); + it( 'returns last calendar month for "last-month"', () => { const range = computeDateRangeFromPreset( 'last-month' ); @@ -118,6 +126,14 @@ describe( 'computeDateRangeFromPreset', () => { expect( range!.to ).toBe( toSiteISO( endOfMonth( LAST_MONTH, { in: UTC } ) ) ); } ); + it( 'returns the running calendar year through the end of today for "year-to-date"', () => { + const range = computeDateRangeFromPreset( 'year-to-date' ); + + expect( range ).toBeDefined(); + expect( range!.from ).toBe( toSiteISO( startOfYear( TODAY_START, { in: UTC } ) ) ); + expect( range!.to ).toBe( toSiteISO( TODAY_END ) ); + } ); + it( 'returns twelve whole calendar months ending today for "last-12-months"', () => { const range = computeDateRangeFromPreset( 'last-12-months' ); diff --git a/projects/packages/premium-analytics/packages/datetime/src/__tests__/get-comparison-range.test.ts b/projects/packages/premium-analytics/packages/datetime/src/__tests__/get-comparison-range.test.ts index 48c97c2f7637..08b0bfe1518b 100644 --- a/projects/packages/premium-analytics/packages/datetime/src/__tests__/get-comparison-range.test.ts +++ b/projects/packages/premium-analytics/packages/datetime/src/__tests__/get-comparison-range.test.ts @@ -480,5 +480,49 @@ describe( 'getComparisonRangeFromPreset', () => { to: new Date( 2025, 7, 31, 23, 59, 59, 999 ), } ); } ); + + // Completed, it would shift onto a unit of another length: month to date + // read on 8 March came back three days short, and year to date read on + // 1 January 2028 came back inverted. + it.each( [ + [ 'March, after a shorter February', new Date( 2026, 2, 1 ), new Date( 2026, 2, 8 ) ], + [ 'February, after a longer January', new Date( 2026, 1, 1 ), new Date( 2026, 1, 15 ) ], + [ 'September, after a longer August', new Date( 2026, 8, 1 ), new Date( 2026, 8, 8 ) ], + ] )( 'mirrors month to date with an equal-length window in %s', ( _label, first, last ) => { + const monthToDate = { + from: new Date( first.getFullYear(), first.getMonth(), first.getDate(), 0, 0, 0, 0 ), + to: new Date( last.getFullYear(), last.getMonth(), last.getDate(), 23, 59, 59, 999 ), + }; + + const comparison = getComparisonRangeFromPreset( monthToDate, 'previous-period', { + primaryPresetId: 'month-to-date', + } ); + + expect( differenceInDays( comparison!.to!, comparison!.from! ) ).toBe( + differenceInDays( monthToDate.to, monthToDate.from ) + ); + expect( comparison!.to!.getTime() ).toBeLessThan( monthToDate.from.getTime() ); + } ); + + it.each( [ + [ 'the first day of a leap year', new Date( 2028, 0, 1 ), new Date( 2028, 0, 1 ) ], + [ 'the first day after one', new Date( 2025, 0, 1 ), new Date( 2025, 0, 1 ) ], + [ 'a day well into a leap year', new Date( 2028, 0, 1 ), new Date( 2028, 2, 5 ) ], + ] )( 'mirrors year to date with an equal-length window on %s', ( _label, first, last ) => { + const yearToDate = { + from: new Date( first.getFullYear(), first.getMonth(), first.getDate(), 0, 0, 0, 0 ), + to: new Date( last.getFullYear(), last.getMonth(), last.getDate(), 23, 59, 59, 999 ), + }; + + const comparison = getComparisonRangeFromPreset( yearToDate, 'previous-period', { + primaryPresetId: 'year-to-date', + } ); + + expect( comparison!.to!.getTime() ).toBeGreaterThan( comparison!.from!.getTime() ); + expect( differenceInDays( comparison!.to!, comparison!.from! ) ).toBe( + differenceInDays( yearToDate.to, yearToDate.from ) + ); + expect( comparison!.to!.getTime() ).toBeLessThan( yearToDate.from.getTime() ); + } ); } ); } ); diff --git a/projects/packages/premium-analytics/packages/datetime/src/__tests__/menu-surface-presets.test.ts b/projects/packages/premium-analytics/packages/datetime/src/__tests__/menu-surface-presets.test.ts index 94625b543dbe..c9de033c957d 100644 --- a/projects/packages/premium-analytics/packages/datetime/src/__tests__/menu-surface-presets.test.ts +++ b/projects/packages/premium-analytics/packages/datetime/src/__tests__/menu-surface-presets.test.ts @@ -21,8 +21,8 @@ describe( 'getMenuSurfacePresetGroups', () => { 'last-90-days', 'last-365-days', ], - [ 'last-month' ], - [ 'last-12-months', 'last-year' ], + [ 'month-to-date', 'last-month' ], + [ 'year-to-date', 'last-12-months', 'last-year' ], ] ); } ); diff --git a/projects/packages/premium-analytics/packages/datetime/src/__tests__/to-date-range.test.ts b/projects/packages/premium-analytics/packages/datetime/src/__tests__/to-date-range.test.ts index e44f0a689e98..6df9e1f8fa51 100644 --- a/projects/packages/premium-analytics/packages/datetime/src/__tests__/to-date-range.test.ts +++ b/projects/packages/premium-analytics/packages/datetime/src/__tests__/to-date-range.test.ts @@ -79,6 +79,15 @@ describe( 'completeToDateRange', () => { expect( completeToDateRange( complete, 'last-12-months' ) ).toEqual( complete ); } ); + // Completing either shifts it onto a unit of another length, and its + // previous period stops coming back equal — see the comparison tests. + it.each( [ + [ 'month-to-date' as const, { from: at( 2026, 8, 1 ), to: endOf( 2026, 8, 20 ) } ], + [ 'year-to-date' as const, { from: at( 2026, 1, 1 ), to: endOf( 2026, 8, 20 ) } ], + ] )( 'leaves %s alone', ( presetId, range ) => { + expect( completeToDateRange( range, presetId ) ).toBe( range ); + } ); + it( 'returns any other preset’s range as is, even one starting on the first', () => { // A hand-picked range has no running month to complete. expect( completeToDateRange( toDate, 'custom' ) ).toBe( toDate ); diff --git a/projects/packages/premium-analytics/packages/datetime/src/index.ts b/projects/packages/premium-analytics/packages/datetime/src/index.ts index fba3832bddee..388d53a40966 100644 --- a/projects/packages/premium-analytics/packages/datetime/src/index.ts +++ b/projects/packages/premium-analytics/packages/datetime/src/index.ts @@ -49,7 +49,9 @@ export { PRESET_LAST_30_DAYS, PRESET_LAST_90_DAYS, PRESET_LAST_365_DAYS, + PRESET_MONTH_TO_DATE, PRESET_LAST_MONTH, + PRESET_YEAR_TO_DATE, PRESET_LAST_12_MONTHS, PRESET_LAST_YEAR, PRESET_CUSTOM, diff --git a/projects/packages/premium-analytics/packages/datetime/src/presets/index.ts b/projects/packages/premium-analytics/packages/datetime/src/presets/index.ts index 899af5fefea2..c3f235925de7 100644 --- a/projects/packages/premium-analytics/packages/datetime/src/presets/index.ts +++ b/projects/packages/premium-analytics/packages/datetime/src/presets/index.ts @@ -7,7 +7,9 @@ export { PRESET_LAST_30_DAYS, PRESET_LAST_90_DAYS, PRESET_LAST_365_DAYS, + PRESET_MONTH_TO_DATE, PRESET_LAST_MONTH, + PRESET_YEAR_TO_DATE, PRESET_LAST_12_MONTHS, PRESET_LAST_YEAR, PRESET_CUSTOM, diff --git a/projects/packages/premium-analytics/packages/datetime/src/presets/primary.ts b/projects/packages/premium-analytics/packages/datetime/src/presets/primary.ts index 6abeb30b4e31..6b3393e098f7 100644 --- a/projects/packages/premium-analytics/packages/datetime/src/presets/primary.ts +++ b/projects/packages/premium-analytics/packages/datetime/src/presets/primary.ts @@ -28,7 +28,9 @@ import { PRESET_LAST_30_DAYS, PRESET_LAST_90_DAYS, PRESET_LAST_365_DAYS, + PRESET_MONTH_TO_DATE, PRESET_LAST_MONTH, + PRESET_YEAR_TO_DATE, PRESET_LAST_12_MONTHS, PRESET_LAST_YEAR, PRESET_CUSTOM, @@ -139,6 +141,14 @@ export const PRESET_DEFINITIONS: ReadonlyArray< PresetDefinition > = [ to: endOfToday, } ), }, + { + id: PRESET_MONTH_TO_DATE, + getLabel: () => __( 'Month to date', 'jetpack-premium-analytics-pkg' ), + getRange: ( { initOfToday, endOfToday } ) => ( { + from: startOfMonth( initOfToday ), + to: endOfToday, + } ), + }, { id: PRESET_LAST_MONTH, getLabel: () => __( 'Last month', 'jetpack-premium-analytics-pkg' ), @@ -147,6 +157,14 @@ export const PRESET_DEFINITIONS: ReadonlyArray< PresetDefinition > = [ to: endOfLastMonth, } ), }, + { + id: PRESET_YEAR_TO_DATE, + getLabel: () => __( 'Year to date', 'jetpack-premium-analytics-pkg' ), + getRange: ( { initOfToday, endOfToday } ) => ( { + from: startOfYear( initOfToday ), + to: endOfToday, + } ), + }, { id: PRESET_LAST_12_MONTHS, getLabel: () => __( 'Last 12 months', 'jetpack-premium-analytics-pkg' ), diff --git a/projects/packages/premium-analytics/packages/datetime/src/presets/types.ts b/projects/packages/premium-analytics/packages/datetime/src/presets/types.ts index 4654b5f218c2..d49e0faa557f 100644 --- a/projects/packages/premium-analytics/packages/datetime/src/presets/types.ts +++ b/projects/packages/premium-analytics/packages/datetime/src/presets/types.ts @@ -8,7 +8,9 @@ export const PRESET_LAST_7_DAYS = 'last-7-days' as const; export const PRESET_LAST_30_DAYS = 'last-30-days' as const; export const PRESET_LAST_90_DAYS = 'last-90-days' as const; export const PRESET_LAST_365_DAYS = 'last-365-days' as const; +export const PRESET_MONTH_TO_DATE = 'month-to-date' as const; export const PRESET_LAST_MONTH = 'last-month' as const; +export const PRESET_YEAR_TO_DATE = 'year-to-date' as const; export const PRESET_LAST_12_MONTHS = 'last-12-months' as const; export const PRESET_LAST_YEAR = 'last-year' as const; @@ -23,7 +25,9 @@ export const SELECTABLE_PRESETS = [ PRESET_LAST_30_DAYS, PRESET_LAST_90_DAYS, PRESET_LAST_365_DAYS, + PRESET_MONTH_TO_DATE, PRESET_LAST_MONTH, + PRESET_YEAR_TO_DATE, PRESET_LAST_12_MONTHS, PRESET_LAST_YEAR, ] as const; @@ -70,8 +74,8 @@ export const MENU_SURFACE_PRESET_GROUPS = [ PRESET_LAST_90_DAYS, PRESET_LAST_365_DAYS, ], - [ PRESET_LAST_MONTH ], - [ PRESET_LAST_12_MONTHS, PRESET_LAST_YEAR ], + [ PRESET_MONTH_TO_DATE, PRESET_LAST_MONTH ], + [ PRESET_YEAR_TO_DATE, PRESET_LAST_12_MONTHS, PRESET_LAST_YEAR ], [ PRESET_ALL_TIME ], ] as const; diff --git a/projects/packages/premium-analytics/packages/fields/src/report-params-field/__tests__/report-params-field.test.tsx b/projects/packages/premium-analytics/packages/fields/src/report-params-field/__tests__/report-params-field.test.tsx index 3722a0796588..00507f083a18 100644 --- a/projects/packages/premium-analytics/packages/fields/src/report-params-field/__tests__/report-params-field.test.tsx +++ b/projects/packages/premium-analytics/packages/fields/src/report-params-field/__tests__/report-params-field.test.tsx @@ -159,7 +159,9 @@ describe( 'report params field', () => { 'Last 30 days', 'Last 90 days', 'Last 365 days', + 'Month to date', 'Last month', + 'Year to date', 'Last 12 months', 'Last year', 'Custom range', diff --git a/projects/packages/premium-analytics/packages/ui/src/date-filters-panel/__tests__/date-filters-panel.test.tsx b/projects/packages/premium-analytics/packages/ui/src/date-filters-panel/__tests__/date-filters-panel.test.tsx index cb3c56f7b347..f8d310fa5b2e 100644 --- a/projects/packages/premium-analytics/packages/ui/src/date-filters-panel/__tests__/date-filters-panel.test.tsx +++ b/projects/packages/premium-analytics/packages/ui/src/date-filters-panel/__tests__/date-filters-panel.test.tsx @@ -112,7 +112,9 @@ describe( 'DateFiltersPanel', () => { 'Last 30 days', 'Last 90 days', 'Last 365 days', + 'Month to date', 'Last month', + 'Year to date', 'Last 12 months', 'Last year', 'All time', diff --git a/projects/packages/premium-analytics/packages/ui/src/date-period-dropdown/__tests__/date-period-dropdown.test.tsx b/projects/packages/premium-analytics/packages/ui/src/date-period-dropdown/__tests__/date-period-dropdown.test.tsx index de6363761944..78ad16be6d14 100644 --- a/projects/packages/premium-analytics/packages/ui/src/date-period-dropdown/__tests__/date-period-dropdown.test.tsx +++ b/projects/packages/premium-analytics/packages/ui/src/date-period-dropdown/__tests__/date-period-dropdown.test.tsx @@ -65,7 +65,9 @@ describe( 'DatePeriodDropdown', () => { 'Last 30 days', 'Last 90 days', 'Last 365 days', + 'Month to date', 'Last month', + 'Year to date', 'Last 12 months', 'Last year', 'Custom range', diff --git a/projects/plugins/jetpack/changelog/uni-754-to-date-presets b/projects/plugins/jetpack/changelog/uni-754-to-date-presets new file mode 100644 index 000000000000..c58051096d02 --- /dev/null +++ b/projects/plugins/jetpack/changelog/uni-754-to-date-presets @@ -0,0 +1,4 @@ +Significance: minor +Type: enhancement + +Premium Analytics: add the Month to date and Year to date periods to the date picker. diff --git a/projects/plugins/premium-analytics/changelog/uni-754-to-date-presets b/projects/plugins/premium-analytics/changelog/uni-754-to-date-presets new file mode 100644 index 000000000000..4aebad88e7f3 --- /dev/null +++ b/projects/plugins/premium-analytics/changelog/uni-754-to-date-presets @@ -0,0 +1,4 @@ +Significance: minor +Type: added + +Date picker: add the Month to date and Year to date periods. From c6a7ec4d72eea0f8c08d8c577893dec215893770 Mon Sep 17 00:00:00 2001 From: Lourens Schep Date: Wed, 9 Sep 2026 09:13:02 +0200 Subject: [PATCH 2/2] Premium Analytics: run the period menu's seam the full height of the panel The seam is drawn as the calendar's inline-start border, and the panel aligned both halves to its start, so the border only ever spanned the calendar. Two more periods make the list the taller half for the first time, leaving Custom range with no seam beside it. Stretching both halves gives the border the panel's height whichever side is taller. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_015eDqAWndvFNBrBRdKcsYus --- .../ui/src/date-period-dropdown/date-period-dropdown.scss | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/projects/packages/premium-analytics/packages/ui/src/date-period-dropdown/date-period-dropdown.scss b/projects/packages/premium-analytics/packages/ui/src/date-period-dropdown/date-period-dropdown.scss index 4caceabddfc4..29f1bb873729 100644 --- a/projects/packages/premium-analytics/packages/ui/src/date-period-dropdown/date-period-dropdown.scss +++ b/projects/packages/premium-analytics/packages/ui/src/date-period-dropdown/date-period-dropdown.scss @@ -38,10 +38,11 @@ } // The calendar opens beside the list rather than replacing it, so the panel - // is the row holding both. + // is the row holding both. Both halves stretch: the seam below is drawn on + // the calendar, so a calendar shorter than the list would cut it short. &__panel { display: flex; - align-items: flex-start; + align-items: stretch; } // Owned here rather than by the calendar: it is the seam between the