Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(VDatePicker): add events and event-color support for date picker #20965

Open
wants to merge 2 commits into
base: dev
Choose a base branch
from
Open
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
4 changes: 2 additions & 2 deletions packages/api-generator/src/locale/en/VDatePicker.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
"calendarIcon": "The icon shown in the header when in 'input' **input-mode**.",
"dayFormat": "Allows you to customize the format of the day string that appears in the date table. Called with date (ISO 8601 **date** string) arguments.",
"displayDate": "The date displayed in the picker header.",
"eventColor": "Sets the color for event dot. It can be string (all events will have the same color) or `object` where attribute is the event date and value is boolean/color/array of colors for specified date or `function` taking date as a parameter and returning boolean/color/array of colors for that date.",
"events": "Array of dates or object defining events or colors or function returning boolean/color/array of colors.",
"eventColor": "Sets the color for event dots. It can be string (all events will have the same color) or `object` where attribute is the event date and value is boolean/color/array of colors for specified date or `function` taking date as a parameter and returning boolean/color/array of colors for that date.",
"events": "Array of dates, object-defining events, colors, or function returning boolean/color/array of colors.",
"expandIcon": "Icon used for **view-mode** toggle.",
"hideHeader": "Hides the header.",
"hideWeekdays": "Hides the weekdays.",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,34 +1,37 @@
<template>
<v-row justify="space-between">
<div>
<div class="subheading">
Defined by array
<v-container>
<v-row justify="space-between">
<div>
<div class="subheading mb-5 font-weight-medium">
Defined by array
</div>
<v-date-picker
v-model="date1"
:events="arrayEvents"
event-color="green lighten-1"
></v-date-picker>
</div>
<v-date-picker
v-model="date1"
:events="arrayEvents"
event-color="green lighten-1"
></v-date-picker>
</div>
<div>
<div class="subheading">
Defined by function
<div>
<div class="subheading mb-5 font-weight-medium">
Defined by function
</div>
<v-date-picker
v-model="date2"
:event-color="date => date[9] % 2 ? 'red' : 'yellow'"
:events="functionEvents"
></v-date-picker>
</div>
<v-date-picker
v-model="date2"
:event-color="date => date[9] % 2 ? 'red' : 'yellow'"
:events="functionEvents"
></v-date-picker>
</div>
</v-row>
</v-row>
</v-container>
</template>

<script setup>
import { onMounted, ref } from 'vue'

const arrayEvents = ref(null)
const date1 = ref((new Date(Date.now() - (new Date()).getTimezoneOffset() * 60000)).toISOString().substr(0, 10))
const date2 = ref((new Date(Date.now() - (new Date()).getTimezoneOffset() * 60000)).toISOString().substr(0, 10))
const date1 = ref((new Date(Date.now() - (new Date()).getTimezoneOffset() * 60000)))
const date2 = ref((new Date(Date.now() - (new Date()).getTimezoneOffset() * 60000)))

function functionEvents (date) {
const [, , day] = date.split('-')
if ([12, 17, 28].includes(parseInt(day, 10))) return true
Expand All @@ -49,8 +52,8 @@
export default {
data: () => ({
arrayEvents: null,
date1: (new Date(Date.now() - (new Date()).getTimezoneOffset() * 60000)).toISOString().substr(0, 10),
date2: (new Date(Date.now() - (new Date()).getTimezoneOffset() * 60000)).toISOString().substr(0, 10),
date1: (new Date(Date.now() - (new Date()).getTimezoneOffset() * 60000)),
date2: (new Date(Date.now() - (new Date()).getTimezoneOffset() * 60000)),
}),

mounted () {
Expand Down
6 changes: 6 additions & 0 deletions packages/docs/src/pages/en/components/date-pickers.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,12 @@ Specify allowed dates using objects or functions. When using objects, accepts a

<ExamplesExample file="v-date-picker/prop-allowed-dates" />

#### Date events

You can specify events using arrays, objects or functions. To change the default color of the event use **event-color** prop. Your **events** function or object can return an array of colors (material or css) in case you want to display multiple event indicators.

<ExamplesExample file="v-date-picker/prop-events" />

### Internationalization

Vuetify components can localize date formats by utilizing the [i18n](/features/internationalization) feature. This determines the appropriate locale for date display. When the default date adapter is in use, localization is managed automatically.
Expand Down
17 changes: 17 additions & 0 deletions packages/vuetify/src/components/VDatePicker/VDatePickerMonth.sass
Original file line number Diff line number Diff line change
Expand Up @@ -53,3 +53,20 @@

.v-date-picker-month__day--hide-adjacent
opacity: 0
.v-date-picker-month__events
height: $date-picker-event-size
left: 0
position: absolute
text-align: center
white-space: pre
width: 100%

> div
height: $date-picker-event-size
margin: $date-picker-event-margin
width: $date-picker-event-size
margin-bottom: -1px

.v-date-picker-month__day .v-date-picker-month__events
bottom: $date-picker-event-date-bottom

73 changes: 69 additions & 4 deletions packages/vuetify/src/components/VDatePicker/VDatePickerMonth.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import './VDatePickerMonth.sass'

// Components
import { VBadge } from '@/components/VBadge'
import { VBtn } from '@/components/VBtn'
import { VDefaultsProvider } from '@/components/VDefaultsProvider'

Expand All @@ -12,11 +13,19 @@ import { MaybeTransition } from '@/composables/transition'

// Utilities
import { computed, ref, shallowRef, watch } from 'vue'
import { genericComponent, omit, propsFactory } from '@/util'
import { genericComponent, omit, propsFactory, wrapInArray } from '@/util'

// Types
import type { PropType } from 'vue'

export type DatePickerEventColorValue = string | string[]

export type DatePickerEventColors = DatePickerEventColorValue |
Record<string, DatePickerEventColorValue> | ((date: string) => DatePickerEventColorValue)

export type DatePickerEvents = string[] |
((date: string) => boolean | DatePickerEventColorValue) | Record<string, DatePickerEventColorValue>

export type VDatePickerMonthSlots = {
day: {
props: {
Expand All @@ -40,7 +49,14 @@ export const makeVDatePickerMonthProps = propsFactory({
type: String,
default: 'picker-reverse-transition',
},

events: {
type: [Array, Function, Object] as PropType<DatePickerEvents | null>,
default: () => null,
},
eventColor: {
type: [Array, Function, Object, String] as PropType<DatePickerEventColors>,
default: () => 'warning',
},
...omit(makeCalendarProps(), ['displayValue']),
}, 'VDatePickerMonth')

Expand Down Expand Up @@ -152,6 +168,53 @@ export const VDatePickerMonth = genericComponent<VDatePickerMonthSlots>()({
}
}

function getEventColors (date: string): string[] {
let eventData: boolean | DatePickerEventColorValue
let eventColors: string[] = []

if (Array.isArray(props.events)) {
eventData = props.events.includes(date)
} else if (props.events instanceof Function) {
eventData = props.events(date) || false
} else if (props.events) {
eventData = props.events[date] || false
} else {
eventData = false
}

if (!eventData) {
return []
} else if (eventData !== true) {
eventColors = wrapInArray(eventData)
} else if (typeof props.eventColor === 'string') {
eventColors = [props.eventColor]
} else if (typeof props.eventColor === 'function') {
eventColors = wrapInArray(props.eventColor(date))
} else if (Array.isArray(props.eventColor)) {
eventColors = props.eventColor
} else {
eventColors = wrapInArray(props.eventColor[date])
}

return eventColors.filter(v => v)
}

function genEvents (date: string): JSX.Element | null {
const eventColors = getEventColors(date)

if (!eventColors.length) return null

return (
<div class="v-date-picker-month__events">
{ eventColors.map((color: string) => {
return (
<VBadge dot color={ color } />
)
})}
</div>
)
}

return () => (
<div class="v-date-picker-month">
{ props.showWeek && (
Expand Down Expand Up @@ -224,7 +287,6 @@ export const VDatePickerMonth = genericComponent<VDatePickerMonthSlots>()({
disabled: item.isDisabled,
icon: true,
ripple: false,
text: item.localized,
variant: item.isDisabled
? item.isToday ? 'outlined' : 'text'
: item.isToday && !item.isSelected ? 'outlined' : 'flat',
Expand All @@ -233,7 +295,10 @@ export const VDatePickerMonth = genericComponent<VDatePickerMonthSlots>()({
}}
>
{ slots.day?.(slotProps) ?? (
<VBtn { ...slotProps.props } />
<VBtn { ...slotProps.props }>
{ item.localized }
{ genEvents(item.isoDate) }
</VBtn>
)}
</VDefaultsProvider>
)}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// @ts-nocheck
/* eslint-disable */

import { render, screen } from '@test'
import { ref } from 'vue'
import { VDatePicker } from '../VDatePicker'

describe('VDatePicker events', () => {
it('renders event markers when events is an array', async () => {
render(() => (
<VDatePicker
type="month"
events={['2025-02-09']}
eventColor="red"
/>
))

const eventElements = document.querySelectorAll('.v-badge')
expect(eventElements.length).toBeGreaterThan(0)
})

it('renders event markers when events is a function', async () => {
render(() => (
<VDatePicker
events={(date: string) => date === '2025-02-09'}
eventColor="red"
/>
))

// Query the DOM for the event markers
const eventElements = document.querySelectorAll('.v-badge')
expect(eventElements.length).toBeGreaterThan(0)
})

it('renders event markers with colors defined by an object', async () => {
render(() => (
<VDatePicker
events={['2025-02-09', '2025-02-20']}
eventColor={{ '2025-02-09': 'red', '2025-02-20': 'blue lighten-1' }}
/>
))

const eventElements = document.querySelectorAll('.v-badge')
expect(eventElements.length).toBeGreaterThan(0)
})

it('renders event markers with colors defined by a function', async () => {
render(() => (
<VDatePicker
events={['2025-02-09', '2025-02-20']}
eventColor={(date: string) => ({ '2025-02-09': 'red' }[date])}
/>
))

const eventElements = document.querySelectorAll('.v-badge')
expect(eventElements.length).toBeGreaterThan(0)
})
})
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html

exports[`VDatePicker.ts should handle date range picker with null value 1`] = `
<div class="v-picker__title__btn v-date-picker-title__date v-picker__title__btn--active">
Expand Down
4 changes: 4 additions & 0 deletions packages/vuetify/src/components/VDatePicker/_variables.scss
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,7 @@ $date-picker-months-height: 288px !default;

$date-picker-years-height: 288px !default;
$date-picker-years-padding-inline: 32px !default;

$date-picker-event-size: 8px !default;
$date-picker-event-margin: 0 1px !default;
$date-picker-event-date-bottom: 8px !default;