Skip to content

Commit 02737db

Browse files
authored
feat(VDatePicker): re-introduce first-day-of-year prop (#21760)
resolves #20270
1 parent ed4d8cf commit 02737db

File tree

8 files changed

+59
-12
lines changed

8 files changed

+59
-12
lines changed

packages/api-generator/src/locale/en/VCalendar.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
{
22
"props": {
33
"allowedDates": "Determines which dates are selectable.",
4-
"firstDayOfWeek": "Sets the first day of the week, starting with 0 for Sunday.",
54
"hideHeader": "Determines whether the header is hidden in the calendar view.",
65
"hideWeekNumber": "Toggles the display of week numbers in a calendar view.",
76
"intervals": "Total number of intervals in a day view.",

packages/api-generator/src/locale/en/generic.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@
1818
"end": "Applies margin at the start of the component.",
1919
"falseIcon": "The icon used when inactive.",
2020
"falseValue": "Sets value for falsy state.",
21-
"firstDayOfWeek": "Sets the first day of the week, starting with 0 for Sunday.",
21+
"firstDayOfWeek": "Sets the first day of the week, starting with 0 for Sunday. (Note: not guaranteed to work when using custom date adapters.)",
22+
"firstDayOfYear": "Sets the day that determines the first week of the year, starting with 0 for Sunday. For ISO 8601 this should be 4. (Note: not guaranteed to work when using custom date adapters.)",
2223
"fullWidth": "Sets the component width to 100%.",
2324
"height": "Sets the height for the component.",
2425
"hideNoData": "Hides the menu when there are no options to show. Useful for preventing the menu from opening before results are fetched asynchronously. Also has the effect of opening the menu when the `items` array changes if not already open.",

packages/docs/src/data/new-in.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@
5757
"VDatePicker": {
5858
"props": {
5959
"controlHeight": "3.8.0",
60+
"firstDayOfYear": "3.9.3",
6061
"headerColor": "3.8.0"
6162
}
6263
},

packages/vuetify/src/composables/calendar.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ export interface CalendarProps {
2323
year: number | string | undefined
2424
weeksInMonth: 'dynamic' | 'static'
2525
firstDayOfWeek: number | string | undefined
26+
firstDayOfYear: number | string | undefined
2627
weekdayFormat: 'long' | 'short' | 'narrow' | undefined
2728

2829
'onUpdate:modelValue': ((value: unknown[]) => void) | undefined
@@ -77,6 +78,10 @@ export const makeCalendarProps = propsFactory({
7778
type: [Number, String],
7879
default: undefined,
7980
},
81+
firstDayOfYear: {
82+
type: [Number, String],
83+
default: undefined,
84+
},
8085
weekdayFormat: String as PropType<'long' | 'short' | 'narrow' | undefined>,
8186
}, 'calendar')
8287

@@ -206,7 +211,7 @@ export function useCalendar (props: CalendarProps) {
206211

207212
const weekNumbers = computed(() => {
208213
return weeksInMonth.value.map(week => {
209-
return week.length ? adapter.getWeek(week[0], props.firstDayOfWeek) : null
214+
return week.length ? adapter.getWeek(week[0], props.firstDayOfWeek, props.firstDayOfYear) : null
210215
})
211216
})
212217

packages/vuetify/src/composables/date/DateAdapter.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ export interface DateAdapter<T = unknown> {
3737
getDiff (date: T, comparing: T | string, unit?: string): number
3838
getWeekArray (date: T, firstDayOfWeek?: number | string): T[][]
3939
getWeekdays (firstDayOfWeek?: number | string, weekdayFormat?: 'long' | 'short' | 'narrow'): string[]
40-
getWeek (date: T, firstDayOfWeek?: number | string, firstWeekMinSize?: number): number
40+
getWeek (date: T, firstDayOfWeek?: number | string, firstDayOfYear?: number | string): number
4141
getMonth (date: T): number
4242
setMonth (date: T, month: number): T
4343
getDate (date: T): number

packages/vuetify/src/composables/date/__tests__/date.spec.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,16 @@ describe('VuetifyDateAdapter', () => {
178178
expect(adapter.getWeek(adapter.parseISO('2025-11-03'))).toBe(45)
179179
})
180180
})
181+
182+
describe('week numbers with first-day-of-week', () => {
183+
it('should calculate weeks correctly when adapting for UK', () => {
184+
const adapterUS = new VuetifyDateAdapter({ locale: 'en-US' })
185+
const adapterGB = new VuetifyDateAdapter({ locale: 'en-GB' })
186+
expect(adapterUS.getWeek(adapterUS.parseISO('2025-03-16'))).toBe(12)
187+
expect(adapterGB.getWeek(adapterGB.parseISO('2025-03-16'))).toBe(11)
188+
expect(adapterUS.getWeek(adapterUS.parseISO('2025-03-16'), 1, 4)).toBe(11)
189+
})
190+
})
181191
})
182192

183193
describe('StringDateAdapter', () => {

packages/vuetify/src/composables/date/adapters/string.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -112,8 +112,8 @@ export class StringDateAdapter implements DateAdapter<string> {
112112
return this.base.getMonth(this.base.date(date)!)
113113
}
114114

115-
getWeek (date: string, firstDayOfWeek?: number | string, firstWeekMinSize?: number): number {
116-
return this.base.getWeek(this.base.date(date)!, firstDayOfWeek, firstWeekMinSize)
115+
getWeek (date: string, firstDayOfWeek?: number | string, firstDayOfYear?: number | string): number {
116+
return this.base.getWeek(this.base.date(date)!, firstDayOfWeek, firstDayOfYear)
117117
}
118118

119119
getNextMonth (date: string): string {

packages/vuetify/src/composables/date/adapters/vuetify.ts

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -315,17 +315,48 @@ function getMonth (date: Date) {
315315
return date.getMonth()
316316
}
317317

318-
function getWeek (date: Date, locale: string, firstDayOfWeek?: number, firstWeekMinSize?: number) {
318+
function getWeek (date: Date, locale: string, firstDayOfWeek?: number, firstDayOfYear?: number) {
319319
const weekInfoFromLocale = weekInfo(locale)
320320
const weekStart = firstDayOfWeek ?? weekInfoFromLocale?.firstDay ?? 0
321-
const minWeekSize = firstWeekMinSize ?? weekInfoFromLocale?.firstWeekSize ?? 1
321+
const minWeekSize = weekInfoFromLocale?.firstWeekSize ?? 1
322+
323+
return firstDayOfYear !== undefined
324+
? calculateWeekWithFirstDayOfYear(date, locale, weekStart, firstDayOfYear)
325+
: calculateWeekWithMinWeekSize(date, locale, weekStart, minWeekSize)
326+
}
327+
328+
function calculateWeekWithFirstDayOfYear (date: Date, locale: string, weekStart: number, firstDayOfYear: number) {
329+
const firstDayOfYearOffset = (7 + firstDayOfYear - weekStart) % 7
330+
const currentWeekStart = startOfWeek(date, locale, weekStart)
331+
const currentWeekEnd = addDays(currentWeekStart, 6)
332+
333+
function yearStartWeekdayOffset (year: number) {
334+
return (7 + new Date(year, 0, 1).getDay() - weekStart) % 7
335+
}
336+
337+
let year = getYear(date)
338+
if (year < getYear(currentWeekEnd) && yearStartWeekdayOffset(year + 1) <= firstDayOfYearOffset) {
339+
year++
340+
}
341+
342+
const yearStart = new Date(year, 0, 1)
343+
const offset = yearStartWeekdayOffset(year)
344+
const d1w1 = offset <= firstDayOfYearOffset
345+
? addDays(yearStart, -offset)
346+
: addDays(yearStart, 7 - offset)
347+
348+
return 1 + getDiff(endOfDay(date), startOfDay(d1w1), 'weeks')
349+
}
350+
351+
function calculateWeekWithMinWeekSize (date: Date, locale: string, weekStart: number, minWeekSize: number) {
352+
const currentWeekEnd = addDays(startOfWeek(date, locale, weekStart), 6)
353+
322354
function firstWeekSize (year: number) {
323355
const yearStart = new Date(year, 0, 1)
324356
return 7 - getDiff(yearStart, startOfWeek(yearStart, locale, weekStart), 'days')
325357
}
326358

327359
let year = getYear(date)
328-
const currentWeekEnd = addDays(startOfWeek(date, locale, weekStart), 6)
329360
if (year < getYear(currentWeekEnd) && firstWeekSize(year + 1) >= minWeekSize) {
330361
year++
331362
}
@@ -335,7 +366,6 @@ function getWeek (date: Date, locale: string, firstDayOfWeek?: number, firstWeek
335366
const d1w1 = size >= minWeekSize
336367
? addDays(yearStart, size - 7)
337368
: addDays(yearStart, size)
338-
339369
return 1 + getDiff(endOfDay(date), startOfDay(d1w1), 'weeks')
340370
}
341371

@@ -616,9 +646,10 @@ export class VuetifyDateAdapter implements DateAdapter<Date> {
616646
return getMonth(date)
617647
}
618648

619-
getWeek (date: Date, firstDayOfWeek?: number | string, firstWeekMinSize?: number) {
649+
getWeek (date: Date, firstDayOfWeek?: number | string, firstDayOfYear?: number | string) {
620650
const firstDay = firstDayOfWeek !== undefined ? Number(firstDayOfWeek) : undefined
621-
return getWeek(date, this.locale, firstDay, firstWeekMinSize)
651+
const firstWeekStart = firstDayOfYear !== undefined ? Number(firstDayOfYear) : undefined
652+
return getWeek(date, this.locale, firstDay, firstWeekStart)
622653
}
623654

624655
getDate (date: Date) {

0 commit comments

Comments
 (0)