feat(components): [date-picker] type add months params (#17342)

* feat(components): [date-picker] `type` add `months` params

closed #17317

* feat(components): [date-picker] optimized code

* docs(components): [date-picker] enhanced multiple selection example

* test(components): [date-picker] add test

---------

Co-authored-by: Panzer_Jack <shenchang@bilibili.com>
This commit is contained in:
Panzer_Jack 2024-07-19 11:47:25 +08:00 committed by GitHub
parent ff4aadb6ab
commit dbfa93bab1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 141 additions and 17 deletions

View File

@ -30,6 +30,7 @@ declare module '@vue/runtime-core' {
Icons: typeof import('./.vitepress/vitepress/components/globals/icons.vue')['default']
IRiCodeLine: typeof import('~icons/ri/code-line')['default']
IRiExternalLinkLine: typeof import('~icons/ri/external-link-line')['default']
IRiFileCopyLine: typeof import('~icons/ri/file-copy-line')['default']
IRiFlaskLine: typeof import('~icons/ri/flask-line')['default']
IRiGithubLine: typeof import('~icons/ri/github-line')['default']
IRiTranslate2: typeof import('~icons/ri/translate2')['default']

View File

@ -158,7 +158,7 @@ Note, date time locale (month name, first day of the week ...) are also configur
| placeholder | placeholder in non-range mode | ^[string] | '' |
| start-placeholder | placeholder for the start date in range mode | ^[string] | — |
| end-placeholder | placeholder for the end date in range mode | ^[string] | — |
| type | type of the picker | ^[enum]`'year' \| 'years' \|'month' \| 'date' \| 'dates' \| 'datetime' \| 'week' \| 'datetimerange' \| 'daterange' \| 'monthrange'` | date |
| type | type of the picker | ^[enum]`'year' \| 'years' \| 'month' \| 'months' \| 'date' \| 'dates' \| 'datetime' \| 'week' \| 'datetimerange' \| 'daterange' \| 'monthrange'` | date |
| format | format of the displayed value in the input box | ^[string] see [date formats](/en-US/component/date-picker#date-formats) | YYYY-MM-DD |
| popper-class | custom class name for DatePicker's dropdown | ^[string] | — |
| popper-options | Customized popper option see more at [popper.js](https://popper.js.org/docs/v2/) | ^[object]`Partial<PopperOptions>` | {} |

View File

@ -11,11 +11,11 @@
/>
</div>
<div class="block">
<span class="demonstration">Month</span>
<span class="demonstration">Dates</span>
<el-date-picker
v-model="value2"
type="month"
placeholder="Pick a month"
type="dates"
placeholder="Pick one or more dates"
/>
</div>
</div>
@ -29,11 +29,29 @@
/>
</div>
<div class="block">
<span class="demonstration">Dates</span>
<span class="demonstration">Years</span>
<el-date-picker
v-model="value4"
type="dates"
placeholder="Pick one or more dates"
type="years"
placeholder="Pick one or more years"
/>
</div>
</div>
<div class="container">
<div class="block">
<span class="demonstration">Month</span>
<el-date-picker
v-model="value5"
type="month"
placeholder="Pick a month"
/>
</div>
<div class="block">
<span class="demonstration">Months</span>
<el-date-picker
v-model="value6"
type="months"
placeholder="Pick one or more months"
/>
</div>
</div>
@ -46,6 +64,8 @@ const value1 = ref('')
const value2 = ref('')
const value3 = ref('')
const value4 = ref('')
const value5 = ref('')
const value6 = ref('')
</script>
<style scoped>
.demo-date-picker {
@ -61,6 +81,7 @@ const value4 = ref('')
border-right: solid 1px var(--el-border-color);
flex: 1;
}
.demo-date-picker .block:last-child {
border-right: none;
}
@ -69,12 +90,15 @@ const value4 = ref('')
flex: 1;
border-right: solid 1px var(--el-border-color);
}
.demo-date-picker .container .block {
border-right: none;
}
.demo-date-picker .container .block:last-child {
border-top: solid 1px var(--el-border-color);
}
.demo-date-picker .container:last-child {
border-right: none;
}

View File

@ -1025,6 +1025,58 @@ describe('DatePicker dates', () => {
})
})
describe('DatePicker months', () => {
it('create', async () => {
const wrapper = _mount(
`<el-date-picker
type='months'
v-model="value"
/>`,
() => ({ value: '' })
)
const input = wrapper.find('input')
input.trigger('blur')
input.trigger('focus')
await nextTick()
const td = document.querySelectorAll(
'.el-month-table tr td'
) as NodeListOf<HTMLElement>
const vm = wrapper.vm as any
td[0].click()
await nextTick()
expect(vm.value.length).toBe(1)
td[1].click()
await nextTick()
expect(vm.value.length).toBe(2)
expect(
document.querySelectorAll('.el-month-table tr .current').length
).toBe(2)
td[0].click()
await nextTick()
expect(vm.value.length).toBe(1)
td[1].click()
await nextTick()
expect(vm.value.length).toBe(0)
})
it('selected', async () => {
const wrapper = _mount(
`<el-date-picker
type="months"
v-model="value"
/>`,
() => ({ value: [new Date()] })
)
const input = wrapper.find('input')
input.trigger('blur')
input.trigger('focus')
await nextTick()
expect(
document.querySelectorAll('.el-month-table tr .current').length
).toBe(1)
})
})
describe('DatePicker keyboard events', () => {
it('enter', async () => {
const wrapper = _mount(

View File

@ -223,7 +223,20 @@ const handleMonthTableClick = (event: MouseEvent | KeyboardEvent) => {
const row = (target.parentNode as HTMLTableRowElement).rowIndex
const month = row * 4 + column
const newDate = props.date.startOf('year').month(month)
if (props.selectionMode === 'range') {
if (props.selectionMode === 'months') {
if (event.type === 'keydown') {
emit('pick', castArray(props.parsedValue), false)
return
}
const newMonth = props.date.startOf('month').month(month)
const newValue = hasClass(target, 'current')
? castArray(props.parsedValue).filter(
(d) => Number(d) !== Number(newMonth)
)
: castArray(props.parsedValue).concat([dayjs(newMonth)])
emit('pick', newValue)
} else if (props.selectionMode === 'range') {
if (!props.rangeState.selecting) {
emit('pick', { minDate: newDate, maxDate: null })
emit('select', true)

View File

@ -151,6 +151,7 @@
<month-table
v-if="currentView === 'month'"
ref="currentViewRef"
:selection-mode="selectionMode"
:date="innerDate"
:parsed-value="parsedValue"
:disabled-date="disabledDate"
@ -161,7 +162,7 @@
</div>
<div v-show="footerVisible" :class="ppNs.e('footer')">
<el-button
v-show="selectionMode !== 'dates' && selectionMode !== 'years'"
v-show="!isMultipleType"
text
size="small"
:class="ppNs.e('link-btn')"
@ -225,6 +226,7 @@ import type { PanelDatePickProps } from '../props/panel-date-pick'
import type {
DateTableEmits,
DatesPickerEmits,
MonthsPickerEmits,
WeekPickerEmits,
YearsPickerEmits,
} from '../props/basic-date-table'
@ -393,10 +395,19 @@ const handleShortcutClick = (shortcut: Shortcut) => {
const selectionMode = computed<DatePickType>(() => {
const { type } = props
if (['week', 'month', 'year', 'years', 'dates'].includes(type)) return type
if (['week', 'month', 'months', 'year', 'years', 'dates'].includes(type))
return type
return 'date' as DatePickType
})
const isMultipleType = computed(() => {
return (
selectionMode.value === 'dates' ||
selectionMode.value === 'months' ||
selectionMode.value === 'years'
)
})
const keyboardMode = computed<string>(() => {
return selectionMode.value === 'date'
? currentView.value
@ -405,11 +416,17 @@ const keyboardMode = computed<string>(() => {
const hasShortcuts = computed(() => !!shortcuts.length)
const handleMonthPick = async (month: number) => {
innerDate.value = innerDate.value.startOf('month').month(month)
const handleMonthPick = async (
month: number | MonthsPickerEmits,
keepOpen?: boolean
) => {
if (selectionMode.value === 'month') {
innerDate.value = innerDate.value.startOf('month').month(month as number)
emit(innerDate.value, false)
} else if (selectionMode.value === 'months') {
emit(month as MonthsPickerEmits, keepOpen ?? true)
} else {
innerDate.value = innerDate.value.startOf('month').month(month as number)
currentView.value = 'date'
if (['month', 'year', 'date', 'week'].includes(selectionMode.value)) {
emit(innerDate.value, true)
@ -454,9 +471,15 @@ const showTime = computed(
const footerVisible = computed(() => {
const showDateFooter = showTime.value || selectionMode.value === 'dates'
const showYearFooter = selectionMode.value === 'years'
const showMonthFooter = selectionMode.value === 'months'
const isDateView = currentView.value === 'date'
const isYearView = currentView.value === 'year'
return (showDateFooter && isDateView) || (showYearFooter && isYearView)
const isMonthView = currentView.value === 'month'
return (
(showDateFooter && isDateView) ||
(showYearFooter && isYearView) ||
(showMonthFooter && isMonthView)
)
})
const disabledConfirm = computed(() => {
@ -468,7 +491,7 @@ const disabledConfirm = computed(() => {
return disabledDate(props.parsedValue.toDate())
})
const onConfirm = () => {
if (selectionMode.value === 'dates' || selectionMode.value === 'years') {
if (isMultipleType.value) {
emit(props.parsedValue as Dayjs[])
} else {
// deal with the scenario where: user opens the date time picker, then confirm without doing anything
@ -740,6 +763,9 @@ watch(
} else if (val === 'years') {
currentView.value = 'year'
return
} else if (val === 'months') {
currentView.value = 'month'
return
}
currentView.value = 'date'
},
@ -767,8 +793,7 @@ watch(
() => props.parsedValue,
(val) => {
if (val) {
if (selectionMode.value === 'dates' || selectionMode.value === 'years')
return
if (isMultipleType.value) return
if (Array.isArray(val)) return
innerDate.value = val
} else {

View File

@ -4,6 +4,7 @@ export declare type IDatePickerType =
| 'year'
| 'years'
| 'month'
| 'months'
| 'date'
| 'dates'
| 'week'

View File

@ -21,6 +21,7 @@ export type BasicDateTableEmits = typeof basicDateTableEmits
export type RangePickerEmits = { minDate: Dayjs; maxDate: null }
export type DatePickerEmits = Dayjs
export type DatesPickerEmits = Dayjs[]
export type MonthsPickerEmits = Dayjs[]
export type YearsPickerEmits = Dayjs[]
export type WeekPickerEmits = {
year: number

View File

@ -11,6 +11,7 @@ const selectionModes = [
'year',
'years',
'month',
'months',
'week',
'range',
]

View File

@ -37,6 +37,7 @@
!editable ||
readonly ||
isDatesPicker ||
isMonthsPicker ||
isYearsPicker ||
type === 'week'
"
@ -483,7 +484,7 @@ const displayValue = computed<UserInput>(() => {
if (!isTimePicker.value && valueIsEmpty.value) return ''
if (!pickerVisible.value && valueIsEmpty.value) return ''
if (formattedValue) {
return isDatesPicker.value || isYearsPicker.value
return isDatesPicker.value || isMonthsPicker.value || isYearsPicker.value
? (formattedValue as Array<string>).join(', ')
: formattedValue
}
@ -496,6 +497,8 @@ const isTimePicker = computed(() => props.type.startsWith('time'))
const isDatesPicker = computed(() => props.type === 'dates')
const isMonthsPicker = computed(() => props.type === 'months')
const isYearsPicker = computed(() => props.type === 'years')
const triggerIcon = computed(

View File

@ -9,6 +9,7 @@ export const DEFAULT_FORMATS_DATEPICKER = {
year: 'YYYY',
years: 'YYYY',
month: 'YYYY-MM',
months: 'YYYY-MM',
datetime: `${DEFAULT_FORMATS_DATE} ${DEFAULT_FORMATS_TIME}`,
monthrange: 'YYYY-MM',
daterange: DEFAULT_FORMATS_DATE,

View File

@ -2,6 +2,7 @@ export const datePickTypes = [
'year',
'years',
'month',
'months',
'date',
'dates',
'week',

View File

@ -13,6 +13,7 @@ describe('validator', () => {
expect(isValidDatePickType('year')).toBe(true)
expect(isValidDatePickType('years')).toBe(true)
expect(isValidDatePickType('month')).toBe(true)
expect(isValidDatePickType('months')).toBe(true)
expect(isValidDatePickType('date')).toBe(true)
expect(isValidDatePickType('dates')).toBe(true)
expect(isValidDatePickType('week')).toBe(true)