Compare commits

...

6 Commits

Author SHA1 Message Date
renovate[bot]
80ca6fbad5
chore(deps): update all patch dependencies (#12812)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: neverland <jait.chen@foxmail.com>
2024-05-02 01:21:41 +00:00
renovate[bot]
86df4c1441
chore(deps): update codecov/codecov-action action to v4 (#12815)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-05-02 09:20:27 +08:00
renovate[bot]
3d472a2e92
chore(deps): update github/issue-labeler action to v3.4 (#12813)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-05-02 09:18:10 +08:00
renovate[bot]
f8b9084dea
chore(deps): update actions/setup-node action to v4 (#12814)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-05-02 09:17:53 +08:00
inottn
27e080e8aa
feat(Calendar): add switch-mode prop (#12836) 2024-05-01 10:52:09 +08:00
Jungzl
92f373c17f
fix(IndexBar): render active anchor correctly when passing sticky & stickyOffsetTop (#12837) 2024-05-01 10:51:40 +08:00
32 changed files with 2551 additions and 390 deletions

View File

@ -21,7 +21,7 @@ jobs:
- name: Install pnpm
run: corepack enable
- uses: actions/setup-node@v3
- uses: actions/setup-node@v4
with:
node-version: '18'
cache: 'pnpm'

View File

@ -11,7 +11,7 @@ jobs:
name: Labeling for changes
runs-on: ubuntu-latest
steps:
- uses: github/issue-labeler@v3.2
- uses: github/issue-labeler@v3.4
with:
repo-token: "${{ secrets.GITHUB_TOKEN }}"
configuration-path: .github/pr-labeler.yml

View File

@ -20,7 +20,7 @@ jobs:
- name: Install pnpm
run: corepack enable
- uses: actions/setup-node@v3
- uses: actions/setup-node@v4
with:
node-version: '18'
cache: 'pnpm'
@ -39,7 +39,7 @@ jobs:
- name: Install pnpm
run: corepack enable
- uses: actions/setup-node@v3
- uses: actions/setup-node@v4
with:
node-version: '18'
cache: 'pnpm'
@ -51,7 +51,7 @@ jobs:
run: pnpm run test:coverage
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
uses: codecov/codecov-action@v4
with:
token: ${{ secrets.CODECOV_TOKEN }}
directory: ./packages/vant/test/coverage
@ -64,7 +64,7 @@ jobs:
- name: Install pnpm
run: corepack enable
- uses: actions/setup-node@v3
- uses: actions/setup-node@v4
with:
node-version: '18'
cache: 'pnpm'

View File

@ -19,7 +19,7 @@
"engines": {
"pnpm": ">= 9.0.0"
},
"packageManager": "pnpm@9.0.4",
"packageManager": "pnpm@9.0.6",
"devDependencies": {
"@vant/cli": "workspace:*",
"@vant/eslint-config": "workspace:*",

View File

@ -33,10 +33,10 @@
"devDependencies": {
"@types/fs-extra": "^11.0.4",
"rimraf": "^5.0.5",
"typescript": "^5.0.4"
"typescript": "^5.4.5"
},
"dependencies": {
"rslog": "^1.2.1",
"rslog": "^1.2.2",
"fast-glob": "^3.3.2",
"fs-extra": "^11.2.0",
"enquirer": "2.4.1",

View File

@ -40,6 +40,6 @@
"devDependencies": {
"esbuild": "^0.20.2",
"rimraf": "^5.0.5",
"typescript": "^5.0.4"
"typescript": "^5.4.5"
}
}

View File

@ -40,6 +40,6 @@
"license": "MIT",
"devDependencies": {
"rimraf": "^5.0.5",
"typescript": "^5.0.4"
"typescript": "^5.4.5"
}
}

View File

@ -41,17 +41,17 @@
"@types/fs-extra": "^11.0.4",
"@types/less": "^3.0.6",
"@types/lodash": "^4.17.0",
"@types/markdown-it": "^13.0.7",
"@types/markdown-it": "^13.0.8",
"rimraf": "^5.0.5",
"vue": "^3.4.23"
"vue": "^3.4.26"
},
"dependencies": {
"@babel/core": "^7.24.4",
"@babel/core": "^7.24.5",
"@babel/preset-typescript": "^7.24.1",
"@rsbuild/core": "0.6.4",
"@rsbuild/plugin-babel": "0.6.4",
"@rsbuild/plugin-vue": "0.6.4",
"@rsbuild/plugin-vue-jsx": "0.6.4",
"@rsbuild/core": "0.6.8",
"@rsbuild/plugin-babel": "0.6.8",
"@rsbuild/plugin-vue": "0.6.8",
"@rsbuild/plugin-vue-jsx": "0.6.8",
"@vant/eslint-config": "workspace:^",
"@vant/touch-emulator": "workspace:^",
"@vitejs/plugin-vue": "^5.0.4",
@ -59,7 +59,7 @@
"@vue/babel-plugin-jsx": "^1.2.2",
"autoprefixer": "^10.4.19",
"commander": "^12.0.0",
"rslog": "^1.2.1",
"rslog": "^1.2.2",
"esbuild": "^0.20.2",
"eslint": "^8.57.0",
"enquirer": "2.4.1",
@ -80,8 +80,8 @@
"rspack-plugin-virtual-module": "^0.1.12",
"terser": "^5.30.3",
"transliteration": "^2.3.5",
"typescript": "^5.0.4",
"vite": "^5.0.12",
"typescript": "^5.4.5",
"vite": "^5.2.10",
"vue-router": "^4.3.2"
}
}

View File

@ -36,11 +36,11 @@
"author": "chenjiahan",
"license": "MIT",
"devDependencies": {
"@vue/runtime-core": "^3.4.23",
"@vue/runtime-core": "^3.4.26",
"vant": "workspace:*",
"vue": "^3.4.23",
"vue": "^3.4.26",
"esbuild": "^0.20.2",
"rimraf": "^5.0.5",
"typescript": "^5.0.4"
"typescript": "^5.4.5"
}
}

View File

@ -20,14 +20,14 @@
"author": "chenjiahan",
"license": "MIT",
"dependencies": {
"@typescript-eslint/eslint-plugin": "^6.2.1",
"@typescript-eslint/parser": "^6.2.1",
"@typescript-eslint/eslint-plugin": "^6.21.0",
"@typescript-eslint/parser": "^6.21.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-vue": "^9.25.0"
},
"devDependencies": {
"eslint": "^8.57.0",
"typescript": "^5.0.4"
"typescript": "^5.4.5"
},
"peerDependencies": {
"eslint": "^7.32.0 || ^8.2.0"

View File

@ -40,6 +40,6 @@
"@popperjs/core": "^2.11.8",
"esbuild": "^0.20.2",
"rimraf": "^5.0.5",
"typescript": "^5.0.4"
"typescript": "^5.4.5"
}
}

View File

@ -40,8 +40,8 @@
"devDependencies": {
"esbuild": "^0.20.2",
"rimraf": "^5.0.5",
"typescript": "^5.0.4",
"vue": "^3.4.23"
"typescript": "^5.4.5",
"vue": "^3.4.26"
},
"peerDependencies": {
"vue": "^3.0.0"

View File

@ -48,7 +48,7 @@
"dependencies": {
"@vant/popperjs": "workspace:^",
"@vant/use": "workspace:^",
"@vue/shared": "^3.4.23"
"@vue/shared": "^3.4.26"
},
"peerDependencies": {
"vue": "^3.0.0"
@ -61,17 +61,17 @@
"@vant/icons": "workspace:*",
"@vitejs/plugin-vue": "^5.0.4",
"@vitejs/plugin-vue-jsx": "^3.1.0",
"@vitest/coverage-istanbul": "^1.1.3",
"@vue/runtime-core": "^3.4.23",
"@vitest/coverage-istanbul": "^1.5.3",
"@vue/runtime-core": "^3.4.26",
"@vue/test-utils": "^2.4.5",
"csstype": "^3.1.3",
"diffable-html": "^5.0.0",
"jsdom": "^22.1.0",
"typescript": "^5.0.4",
"vite": "^5.0.12",
"vitest": "^1.1.3",
"typescript": "^5.4.5",
"vite": "^5.2.10",
"vitest": "^1.5.3",
"vitest-canvas-mock": "^0.3.3",
"vue": "^3.4.23",
"vue": "^3.4.26",
"vue-router": "^4.3.2"
},
"sideEffects": [

View File

@ -31,6 +31,7 @@ import {
calcDateNum,
compareMonth,
getDayByOffset,
getMonthByOffset,
} from './utils';
// Composables
@ -48,6 +49,7 @@ import CalendarHeader from './CalendarHeader';
// Types
import type {
CalendarType,
CalendarSwitchMode,
CalendarExpose,
CalendarDayItem,
CalendarMonthInstance,
@ -56,6 +58,7 @@ import type {
export const calendarProps = {
show: Boolean,
type: makeStringProp<CalendarType>('single'),
switchMode: makeStringProp<CalendarSwitchMode>('none'),
title: String,
color: String,
round: truthProp,
@ -84,15 +87,10 @@ export const calendarProps = {
minDate: {
type: Date,
validator: isDate,
default: getToday,
},
maxDate: {
type: Date,
validator: isDate,
default: () => {
const now = getToday();
return new Date(now.getFullYear(), now.getMonth() + 6, now.getDate());
},
},
firstDayOfWeek: {
type: numericProp,
@ -117,25 +115,44 @@ export default defineComponent({
'update:show',
'clickSubtitle',
'clickDisabledDate',
'panelChange',
],
setup(props, { emit, slots }) {
const canSwitch = computed(() => props.switchMode !== 'none');
const minDate = computed(() => {
if (!props.minDate && !canSwitch.value) {
return getToday();
}
return props.minDate;
});
const maxDate = computed(() => {
if (!props.maxDate && !canSwitch.value) {
return getMonthByOffset(getToday(), 6);
}
return props.maxDate;
});
const limitDateRange = (
date: Date,
minDate = props.minDate,
maxDate = props.maxDate,
min = minDate.value,
max = maxDate.value,
) => {
if (compareDay(date, minDate) === -1) {
return minDate;
if (min && compareDay(date, min) === -1) {
return min;
}
if (compareDay(date, maxDate) === 1) {
return maxDate;
if (max && compareDay(date, max) === 1) {
return max;
}
return date;
};
const getInitialDate = (defaultDate = props.defaultDate) => {
const { type, minDate, maxDate, allowSameDay } = props;
const { type, allowSameDay } = props;
if (defaultDate === null) {
return defaultDate;
@ -147,15 +164,21 @@ export default defineComponent({
if (!Array.isArray(defaultDate)) {
defaultDate = [];
}
const min = minDate.value;
const max = maxDate.value;
const start = limitDateRange(
defaultDate[0] || now,
minDate,
allowSameDay ? maxDate : getPrevDay(maxDate),
min,
max ? (allowSameDay ? max : getPrevDay(max)) : undefined,
);
const end = limitDateRange(
defaultDate[1] || now,
allowSameDay ? minDate : getNextDay(minDate),
defaultDate[1] || (allowSameDay ? now : getNextDay(now)),
min ? (allowSameDay ? min : getNextDay(min)) : undefined,
);
return [start, end];
}
@ -172,16 +195,24 @@ export default defineComponent({
return limitDateRange(defaultDate);
};
const getInitialPanelDate = () => {
const date = Array.isArray(currentDate.value)
? currentDate.value[0]
: currentDate.value;
return date ? date : limitDateRange(getToday());
};
let bodyHeight: number;
const bodyRef = ref<HTMLElement>();
const subtitle = ref<{ textFn: () => string; date?: Date }>({
textFn: () => '',
date: undefined,
});
const currentDate = ref(getInitialDate());
const currentPanelDate = ref<Date>(getInitialPanelDate());
const currentMonthRef = ref<CalendarMonthInstance>();
const [monthRefs, setMonthRefs] = useRefs<CalendarMonthInstance>();
const dayOffset = computed(() =>
@ -190,14 +221,19 @@ export default defineComponent({
const months = computed(() => {
const months: Date[] = [];
const cursor = new Date(props.minDate);
if (!minDate.value || !maxDate.value) {
return months;
}
const cursor = new Date(minDate.value);
cursor.setDate(1);
do {
months.push(new Date(cursor));
cursor.setMonth(cursor.getMonth() + 1);
} while (compareMonth(cursor, props.maxDate) !== 1);
} while (compareMonth(cursor, maxDate.value) !== 1);
return months;
});
@ -271,28 +307,29 @@ export default defineComponent({
/* istanbul ignore else */
if (currentMonth) {
subtitle.value = {
textFn: currentMonth.getTitle,
date: currentMonth.date,
};
currentMonthRef.value = currentMonth;
}
};
const scrollToDate = (targetDate: Date) => {
raf(() => {
months.value.some((month, index) => {
if (compareMonth(month, targetDate) === 0) {
if (bodyRef.value) {
monthRefs.value[index].scrollToDate(bodyRef.value, targetDate);
if (canSwitch.value) {
currentPanelDate.value = targetDate;
} else {
raf(() => {
months.value.some((month, index) => {
if (compareMonth(month, targetDate) === 0) {
if (bodyRef.value) {
monthRefs.value[index].scrollToDate(bodyRef.value, targetDate);
}
return true;
}
return true;
}
return false;
return false;
});
onScroll();
});
onScroll();
});
}
};
const scrollToCurrentDate = () => {
@ -308,7 +345,7 @@ export default defineComponent({
if (isDate(targetDate)) {
scrollToDate(targetDate);
}
} else {
} else if (!canSwitch.value) {
raf(onScroll);
}
};
@ -318,11 +355,14 @@ export default defineComponent({
return;
}
raf(() => {
// add Math.floor to avoid decimal height issues
// https://github.com/vant-ui/vant/issues/5640
bodyHeight = Math.floor(useRect(bodyRef).height);
});
if (!canSwitch.value) {
raf(() => {
// add Math.floor to avoid decimal height issues
// https://github.com/vant-ui/vant/issues/5640
bodyHeight = Math.floor(useRect(bodyRef).height);
});
}
scrollToCurrentDate();
};
@ -345,6 +385,11 @@ export default defineComponent({
return true;
};
const onPanelChange = (date: Date) => {
currentPanelDate.value = date;
emit('panelChange', { date });
};
const onConfirm = () =>
emit('confirm', currentDate.value ?? cloneDates(currentDate.value!));
@ -469,20 +514,20 @@ export default defineComponent({
return (
<CalendarMonth
v-slots={pick(slots, ['top-info', 'bottom-info', 'month-title'])}
ref={setMonthRefs(index)}
ref={canSwitch.value ? currentMonthRef : setMonthRefs(index)}
date={date}
currentDate={currentDate.value}
showMonthTitle={showMonthTitle}
firstDayOfWeek={dayOffset.value}
lazyRender={canSwitch.value ? false : props.lazyRender}
maxDate={maxDate.value}
minDate={minDate.value}
{...pick(props, [
'type',
'color',
'minDate',
'maxDate',
'showMark',
'formatter',
'rowHeight',
'lazyRender',
'showSubtitle',
'allowSameDay',
])}
@ -529,33 +574,45 @@ export default defineComponent({
</div>
);
const renderCalendar = () => {
const subTitle = subtitle.value.textFn();
return (
<div class={bem()}>
<CalendarHeader
v-slots={pick(slots, ['title', 'subtitle'])}
date={subtitle.value.date}
title={props.title}
subtitle={subTitle}
showTitle={props.showTitle}
showSubtitle={props.showSubtitle}
firstDayOfWeek={dayOffset.value}
onClickSubtitle={(event: MouseEvent) =>
emit('clickSubtitle', event)
}
/>
<div ref={bodyRef} class={bem('body')} onScroll={onScroll}>
{months.value.map(renderMonth)}
</div>
{renderFooter()}
const renderCalendar = () => (
<div class={bem()}>
<CalendarHeader
v-slots={pick(slots, [
'title',
'subtitle',
'prev-month',
'prev-year',
'next-month',
'next-year',
])}
date={currentMonthRef.value?.date}
maxDate={maxDate.value}
minDate={minDate.value}
title={props.title}
subtitle={currentMonthRef.value?.getTitle()}
showTitle={props.showTitle}
showSubtitle={props.showSubtitle}
switchMode={props.switchMode}
firstDayOfWeek={dayOffset.value}
onClickSubtitle={(event: MouseEvent) => emit('clickSubtitle', event)}
onPanelChange={onPanelChange}
/>
<div
ref={bodyRef}
class={bem('body')}
onScroll={canSwitch.value ? undefined : onScroll}
>
{canSwitch.value
? renderMonth(currentPanelDate.value, 0)
: months.value.map(renderMonth)}
</div>
);
};
{renderFooter()}
</div>
);
watch(() => props.show, init);
watch(
() => [props.type, props.minDate, props.maxDate],
() => [props.type, props.minDate, props.maxDate, props.switchMode],
() => reset(getInitialDate(currentDate.value)),
);
watch(

View File

@ -1,6 +1,21 @@
import { defineComponent } from 'vue';
import { createNamespace } from '../utils';
import { t, bem } from './utils';
import { computed, defineComponent } from 'vue';
// Utils
import { createNamespace, HAPTICS_FEEDBACK, makeStringProp } from '../utils';
import {
t,
bem,
getPrevMonth,
getPrevYear,
getNextMonth,
getNextYear,
} from './utils';
// Components
import { Icon } from '../icon';
// Types
import type { CalendarSwitchMode } from './types';
const [name] = createNamespace('calendar-header');
@ -9,16 +24,39 @@ export default defineComponent({
props: {
date: Date,
minDate: Date,
maxDate: Date,
title: String,
subtitle: String,
showTitle: Boolean,
showSubtitle: Boolean,
firstDayOfWeek: Number,
switchMode: makeStringProp<CalendarSwitchMode>('none'),
},
emits: ['clickSubtitle'],
emits: ['clickSubtitle', 'panelChange'],
setup(props, { slots, emit }) {
const prevMonthDisabled = computed(() => {
const prevMonth = getPrevMonth(props.date!);
return props.minDate && prevMonth < props.minDate;
});
const prevYearDisabled = computed(() => {
const prevYear = getPrevYear(props.date!);
return props.minDate && prevYear < props.minDate;
});
const nextMonthDisabled = computed(() => {
const nextMonth = getNextMonth(props.date!);
return props.maxDate && nextMonth > props.maxDate;
});
const nextYearDisabled = computed(() => {
const nextYear = getNextYear(props.date!);
return props.maxDate && nextYear > props.maxDate;
});
const renderTitle = () => {
if (props.showTitle) {
const text = props.title || t('title');
@ -29,6 +67,60 @@ export default defineComponent({
const onClickSubtitle = (event: MouseEvent) => emit('clickSubtitle', event);
const onPanelChange = (date: Date) => emit('panelChange', date);
const renderAction = (isNext?: boolean) => {
const showYearAction = props.switchMode === 'year-month';
const monthSlot = slots[isNext ? 'next-month' : 'prev-month'];
const yearSlot = slots[isNext ? 'next-year' : 'prev-year'];
const monthDisabled = isNext
? nextMonthDisabled.value
: prevMonthDisabled.value;
const yearDisabled = isNext
? nextYearDisabled.value
: prevYearDisabled.value;
const monthIconName = isNext ? 'arrow' : 'arrow-left';
const yearIconName = isNext ? 'arrow-double-right' : 'arrow-double-left';
const onMonthChange = () =>
onPanelChange((isNext ? getNextMonth : getPrevMonth)(props.date!));
const onYearChange = () =>
onPanelChange((isNext ? getNextYear : getPrevYear)(props.date!));
const MonthAction = (
<view
class={bem('header-action', { disabled: monthDisabled })}
onClick={monthDisabled ? undefined : onMonthChange}
>
{monthSlot ? (
monthSlot({ disabled: monthDisabled })
) : (
<Icon
class={{ [HAPTICS_FEEDBACK]: !monthDisabled }}
name={monthIconName}
/>
)}
</view>
);
const YearAction = showYearAction && (
<view
class={bem('header-action', { disabled: yearDisabled })}
onClick={yearDisabled ? undefined : onYearChange}
>
{yearSlot ? (
yearSlot({ disabled: yearDisabled })
) : (
<Icon
class={{ [HAPTICS_FEEDBACK]: !yearDisabled }}
name={yearIconName}
/>
)}
</view>
);
return isNext ? [MonthAction, YearAction] : [YearAction, MonthAction];
};
const renderSubtitle = () => {
if (props.showSubtitle) {
const title = slots.subtitle
@ -37,9 +129,20 @@ export default defineComponent({
text: props.subtitle,
})
: props.subtitle;
const canSwitch = props.switchMode !== 'none';
return (
<div class={bem('header-subtitle')} onClick={onClickSubtitle}>
{title}
<div
class={bem('header-subtitle', { 'with-swicth': canSwitch })}
onClick={onClickSubtitle}
>
{canSwitch
? [
renderAction(),
<div class={bem('header-subtitle-text')}>{title}</div>,
renderAction(true),
]
: title}
</div>
);
}

View File

@ -42,8 +42,8 @@ const calendarMonthProps = {
date: makeRequiredProp(Date),
type: String as PropType<CalendarType>,
color: String,
minDate: makeRequiredProp(Date),
maxDate: makeRequiredProp(Date),
minDate: Date,
maxDate: Date,
showMark: Boolean,
rowHeight: numericProp,
formatter: Function as PropType<(item: CalendarDayItem) => CalendarDayItem>,
@ -73,11 +73,14 @@ export default defineComponent({
const title = computed(() => formatMonthTitle(props.date));
const rowHeight = computed(() => addUnit(props.rowHeight));
const offset = computed(() => {
const realDay = props.date.getDay();
const date = props.date.getDate();
const day = props.date.getDay();
const realDay = (day - (date % 7) + 8) % 7;
if (props.firstDayOfWeek) {
return (realDay + 7 - props.firstDayOfWeek) % 7;
}
return realDay;
});
@ -150,7 +153,10 @@ export default defineComponent({
const getDayType = (day: Date): CalendarDayType => {
const { type, minDate, maxDate, currentDate } = props;
if (compareDay(day, minDate) < 0 || compareDay(day, maxDate) > 0) {
if (
(minDate && compareDay(day, minDate) < 0) ||
(maxDate && compareDay(day, maxDate) > 0)
) {
return 'disabled';
}

View File

@ -18,6 +18,14 @@ app.use(Calendar);
## Usage
### Select Switch Mode
By default, all months will be displayed without showing the switch button. When there are too many months, it may affect the page's interactivity performance. You can display the year and month switching buttons by setting the `switch-mode` prop.
```html
<van-calendar v-model:show="show" switch-mode="year-month" />
```
### Select Single Date
The `confirm` event will be emitted after the date selection is completed.
@ -250,10 +258,11 @@ Set `poppable` to `false`, the calendar will be displayed directly on the page i
| Attribute | Description | Type | Default |
| --- | --- | --- | --- |
| type | Type, can be set to `range` `multiple` | _string_ | `single` |
| switch-mode | Switch mode:<br>`none` Display all months in a tiled format without switch buttons <br>`month` Support switching by month, displaying buttons for previous month/next month <br>`year-month` Support switching by year, as well as by month, displaying buttons for previous year/next year and previous month/next month | _string_ | `none` |
| title | Title of calendar | _string_ | `Calendar` |
| color | Color for the bottom button and selected date | _string_ | `#1989fa` |
| min-date | Min date | _Date_ | Today |
| max-date | Max date | _Date_ | Six months after the today |
| min-date | Min date | _Date_ | When `switch-mode` is set to `none`, the default value is the today |
| max-date | Max date | _Date_ | When `switch-mode` is set to `none`, the default value is six months after the today |
| default-date | Default selected date | _Date \| Date[] \| null_ | Today |
| row-height | Row height | _number \| string_ | `64` |
| formatter | Day formatter | _(day: Day) => Day_ | - |
@ -329,6 +338,7 @@ Following props are supported when the type is multiple
| over-range | Emitted when exceeded max range | - |
| click-subtitle | Emitted when clicking the subtitle | _event: MouseEvent_ |
| click-disabled-date `v4.7.0` | Emitted when clicking disabled date | _value: Date \| Date[]_ |
| panel-change | Emitted when switching calendar panel | _{ date: Date }_ |
### Slots
@ -341,6 +351,10 @@ Following props are supported when the type is multiple
| confirm-text | Custom confirm text | _{ disabled: boolean }_ |
| top-info | Custom top info of day | _day: Day_ |
| bottom-info | Custom bottom info of day | _day: Day_ |
| prev-month | Custom previous month button | _{ disabled: boolean }_ |
| prev-year | Custom previous year button | _{ disabled: boolean }_ |
| next-month | Custom next month button | _{ disabled: boolean }_ |
| next-year | Custom next year button | _{ disabled: boolean }_ |
### Methods
@ -358,6 +372,7 @@ The component exports the following type definitions:
```ts
import type {
CalendarSwitchMode,
CalendarType,
CalendarProps,
CalendarDayItem,
@ -391,6 +406,9 @@ The component provides the following CSS variables, which can be used to customi
| --van-calendar-header-title-height | _44px_ | - |
| --van-calendar-header-title-font-size | _var(--van-font-size-lg)_ | - |
| --van-calendar-header-subtitle-font-size | _var(--van-font-size-md)_ | - |
| --van-calendar-header-action-width | 28px | - |
| --van-calendar-header-action-color | _var(--van-text-color)_ | - |
| --van-calendar-header-action-disabled-color | _var(--van-text-color-3)_ | - |
| --van-calendar-weekdays-height | _30px_ | - |
| --van-calendar-weekdays-font-size | _var(--van-font-size-sm)_ | - |
| --van-calendar-month-title-font-size | _var(--van-font-size-md)_ | - |

View File

@ -18,6 +18,14 @@ app.use(Calendar);
## 代码演示
### 选择切换模式
默认所有月份将以平铺方式展示,不显示切换按钮,当月份过多时可能会影响页面交互性能。可以通过设置 `switch-mode` 属性,展示年月切换按钮。
```html
<van-calendar v-model:show="show" switch-mode="year-month" />
```
### 选择单个日期
下面演示了结合单元格来使用日历组件的用法,日期选择完成后会触发 `confirm` 事件。
@ -253,11 +261,12 @@ export default {
| 参数 | 说明 | 类型 | 默认值 |
| --- | --- | --- | --- |
| type | 选择类型:<br>`single` 表示选择单个日期,<br>`multiple` 表示选择多个日期,<br>`range` 表示选择日期区间 | _string_ | `single` |
| type | 选择类型:<br>`single` 表示选择单个日期,<br>`multiple` 表示选择多个日期,<br>`range` 表示选择日期区间 | _string_ | `single` |
| switch-mode | 切换模式:<br>`none` 平铺展示所有月份,不展示切换按钮,<br>`month` 支持按月切换,展示上个月/下个月按钮,<br>`year-month` 支持按年切换,也支持按月切换,展示上一年/下一年,上个月/下个月按钮 | _string_ | `none` |
| title | 日历标题 | _string_ | `日期选择` |
| color | 主题色,对底部按钮和选中日期生效 | _string_ | `#1989fa` |
| min-date | 可选择的最小日期 | _Date_ | 当前日期 |
| max-date | 可选择的最大日期 | _Date_ | 当前日期的六个月后 |
| min-date | 可选择的最小日期 | _Date_ | `switch-mode``none` 时为当前日期 |
| max-date | 可选择的最大日期 | _Date_ | `switch-mode``none` 时为当前日期的六个月后 |
| default-date | 默认选中的日期,`type``multiple``range` 时为数组,传入 `null` 表示默认不选择 | _Date \| Date[] \| null_ | 今天 |
| row-height | 日期行高 | _number \| string_ | `64` |
| formatter | 日期格式化函数 | _(day: Day) => Day_ | - |
@ -335,6 +344,7 @@ export default {
| over-range | 范围选择超过最多可选天数时触发 | - |
| click-subtitle | 点击日历副标题时触发 | _event: MouseEvent_ |
| click-disabled-date `v4.7.0` | 点击禁用日期时触发 | _value: Date \| Date[]_ |
| panel-change | 日历面板切换时触发 | _{ date: Date }_ |
### Slots
@ -347,6 +357,10 @@ export default {
| confirm-text | 自定义确认按钮的内容 | _{ disabled: boolean }_ |
| top-info | 自定义日期上方的提示信息 | _day: Day_ |
| bottom-info | 自定义日期下方的提示信息 | _day: Day_ |
| prev-month | 自定义上个月按钮 | _{ disabled: boolean }_ |
| prev-year | 自定义上一年按钮 | _{ disabled: boolean }_ |
| next-month | 自定义下个月按钮 | _{ disabled: boolean }_ |
| next-year | 自定义下一年按钮 | _{ disabled: boolean }_ |
### 方法
@ -364,6 +378,7 @@ export default {
```ts
import type {
CalendarSwitchMode,
CalendarType,
CalendarProps,
CalendarDayItem,
@ -397,6 +412,9 @@ calendarRef.value?.reset();
| --van-calendar-header-title-height | _44px_ | - |
| --van-calendar-header-title-font-size | _var(--van-font-size-lg)_ | - |
| --van-calendar-header-subtitle-font-size | _var(--van-font-size-md)_ | - |
| --van-calendar-header-action-width | 28px | - |
| --van-calendar-header-action-color | _var(--van-text-color)_ | - |
| --van-calendar-header-action-disabled-color | _var(--van-text-color-3)_ | - |
| --van-calendar-weekdays-height | _30px_ | - |
| --van-calendar-weekdays-font-size | _var(--van-font-size-sm)_ | - |
| --van-calendar-month-title-font-size | _var(--van-font-size-md)_ | - |

View File

@ -0,0 +1,58 @@
<script setup lang="ts">
import VanCell from '../../cell';
import VanPicker, { type PickerConfirmEventParams } from '../../picker';
import VanPopup from '../../popup';
import { ref } from 'vue';
import { useTranslate } from '../../../docs/site';
import type { CalendarSwitchMode } from '../types';
const t = useTranslate({
'zh-CN': {
switchMode: '选择切换模式',
},
'en-US': {
switchMode: 'Select Switch Mode',
},
});
const switchMode = defineModel<CalendarSwitchMode>({
default: 'none',
});
const showPicker = ref(false);
const switchModeColumns = [
{ text: 'none', value: 'none' },
{ text: 'month', value: 'month' },
{ text: 'year-month', value: 'year-month' },
];
const onClickField = () => {
showPicker.value = true;
};
const onPickerCancel = () => {
showPicker.value = false;
};
const onPickerConfirm = ({ selectedOptions }: PickerConfirmEventParams) => {
showPicker.value = false;
switchMode.value = selectedOptions[0]!.value as CalendarSwitchMode;
};
</script>
<template>
<demo-block card :title="t('switchMode')">
<van-cell
is-link
:title="t('switchMode')"
:value="switchMode"
@click="onClickField"
/>
<van-popup v-model:show="showPicker" round position="bottom">
<van-picker
:columns="switchModeColumns"
@cancel="onPickerCancel"
@confirm="onPickerConfirm"
/>
</van-popup>
</demo-block>
</template>

View File

@ -2,8 +2,11 @@
import VanCalendar from '..';
import { useTranslate } from '../../../docs/site';
const { switchMode } = defineProps({
switchMode: String,
});
const minDate = new Date(2012, 0, 10);
const maxDate = new Date(2012, 2, 20);
const maxDate = new Date(2013, 2, 20);
const t = useTranslate({
'zh-CN': {
@ -26,6 +29,7 @@ const t = useTranslate({
:min-date="minDate"
:max-date="maxDate"
:default-date="minDate"
:switch-mode="switchMode"
:style="{ height: '500px' }"
/>
</demo-block>

View File

@ -1,10 +1,11 @@
<script setup lang="ts">
import VanCell from '../../cell';
import VanCalendar from '..';
import { reactive } from 'vue';
import { reactive, ref } from 'vue';
import { useTranslate } from '../../../docs/site';
import SwicthModeField from './SwicthModeField.vue';
import TiledDisplay from './TiledDisplay.vue';
import type { CalendarDayItem } from '../types';
import type { CalendarDayItem, CalendarSwitchMode } from '../types';
const t = useTranslate({
'zh-CN': {
@ -191,9 +192,13 @@ const onConfirm = (date: Date | Date[]) => {
state.showCalendar = false;
state.date[state.id] = date;
};
const switchMode = ref<CalendarSwitchMode>('none');
</script>
<template>
<SwicthModeField v-model="switchMode" />
<demo-block card :title="t('basicUsage')">
<van-cell
is-link
@ -283,7 +288,7 @@ const onConfirm = (date: Date | Date[]) => {
/>
</demo-block>
<TiledDisplay />
<TiledDisplay :switch-mode="switchMode" />
<van-calendar
v-model:show="state.showCalendar"
@ -296,6 +301,7 @@ const onConfirm = (date: Date | Date[]) => {
:max-range="state.maxRange"
:formatter="state.formatter"
:show-confirm="state.showConfirm"
:switch-mode="switchMode"
:confirm-text="state.confirmText"
:first-day-of-week="state.firstDayOfWeek"
:confirm-disabled-text="state.confirmDisabledText"

View File

@ -6,6 +6,9 @@
--van-calendar-header-title-height: 44px;
--van-calendar-header-title-font-size: var(--van-font-size-lg);
--van-calendar-header-subtitle-font-size: var(--van-font-size-md);
--van-calendar-header-action-width: 28px;
--van-calendar-header-action-color: var(--van-text-color);
--van-calendar-header-action-disabled-color: var(--van-text-color-3);
--van-calendar-weekdays-height: 30px;
--van-calendar-weekdays-font-size: var(--van-font-size-sm);
--van-calendar-month-title-font-size: var(--van-font-size-md);
@ -76,6 +79,31 @@
&__header-subtitle {
font-size: var(--van-calendar-header-subtitle-font-size);
&--with-swicth {
display: flex;
align-items: center;
padding: 0 var(--van-padding-base);
}
}
&__header-subtitle-text {
flex: 1;
}
&__header-action {
display: flex;
align-items: center;
justify-content: center;
min-width: var(--van-calendar-header-action-width);
height: 100%;
color: var(--van-calendar-header-action-color);
cursor: pointer;
&--disabled {
color: var(--van-calendar-header-action-disabled-color);
cursor: not-allowed;
}
}
&__month-title {

View File

@ -2,6 +2,35 @@
exports[`should render demo and match snapshot 1`] = `
<!--[-->
<div>
<!--[-->
<div
class="van-cell van-cell--clickable"
role="button"
tabindex="0"
>
<div
class="van-cell__title"
style
>
<span>
Select Switch Mode
</span>
</div>
<div class="van-cell__value">
<span>
none
</span>
</div>
<i
class="van-badge__wrapper van-icon van-icon-arrow van-cell__right-icon"
style
>
<!--[-->
</i>
</div>
<!--[-->
</div>
<div>
<!--[-->
<div
@ -395,6 +424,453 @@ exports[`should render demo and match snapshot 1`] = `
</div>
</div>
</div>
<div class="van-calendar__month">
<div class="van-calendar__month-title">
2012/4
</div>
<div
role="grid"
class="van-calendar__days"
>
<!--[-->
<div
class="van-calendar__day"
style="width:100%;"
>
</div>
<div
class="van-calendar__day"
style="width:100%;"
>
</div>
<div
class="van-calendar__day"
style="width:100%;"
>
</div>
<div
class="van-calendar__day"
style="width:100%;"
>
</div>
<div
class="van-calendar__day"
style="width:100%;"
>
</div>
</div>
</div>
<div class="van-calendar__month">
<div class="van-calendar__month-title">
2012/5
</div>
<div
role="grid"
class="van-calendar__days"
>
<!--[-->
<div
class="van-calendar__day"
style="width:100%;"
>
</div>
<div
class="van-calendar__day"
style="width:100%;"
>
</div>
<div
class="van-calendar__day"
style="width:100%;"
>
</div>
<div
class="van-calendar__day"
style="width:100%;"
>
</div>
<div
class="van-calendar__day"
style="width:100%;"
>
</div>
</div>
</div>
<div class="van-calendar__month">
<div class="van-calendar__month-title">
2012/6
</div>
<div
role="grid"
class="van-calendar__days"
>
<!--[-->
<div
class="van-calendar__day"
style="width:100%;"
>
</div>
<div
class="van-calendar__day"
style="width:100%;"
>
</div>
<div
class="van-calendar__day"
style="width:100%;"
>
</div>
<div
class="van-calendar__day"
style="width:100%;"
>
</div>
<div
class="van-calendar__day"
style="width:100%;"
>
</div>
</div>
</div>
<div class="van-calendar__month">
<div class="van-calendar__month-title">
2012/7
</div>
<div
role="grid"
class="van-calendar__days"
>
<!--[-->
<div
class="van-calendar__day"
style="width:100%;"
>
</div>
<div
class="van-calendar__day"
style="width:100%;"
>
</div>
<div
class="van-calendar__day"
style="width:100%;"
>
</div>
<div
class="van-calendar__day"
style="width:100%;"
>
</div>
<div
class="van-calendar__day"
style="width:100%;"
>
</div>
</div>
</div>
<div class="van-calendar__month">
<div class="van-calendar__month-title">
2012/8
</div>
<div
role="grid"
class="van-calendar__days"
>
<!--[-->
<div
class="van-calendar__day"
style="width:100%;"
>
</div>
<div
class="van-calendar__day"
style="width:100%;"
>
</div>
<div
class="van-calendar__day"
style="width:100%;"
>
</div>
<div
class="van-calendar__day"
style="width:100%;"
>
</div>
<div
class="van-calendar__day"
style="width:100%;"
>
</div>
</div>
</div>
<div class="van-calendar__month">
<div class="van-calendar__month-title">
2012/9
</div>
<div
role="grid"
class="van-calendar__days"
>
<!--[-->
<div
class="van-calendar__day"
style="width:100%;"
>
</div>
<div
class="van-calendar__day"
style="width:100%;"
>
</div>
<div
class="van-calendar__day"
style="width:100%;"
>
</div>
<div
class="van-calendar__day"
style="width:100%;"
>
</div>
<div
class="van-calendar__day"
style="width:100%;"
>
</div>
<div
class="van-calendar__day"
style="width:100%;"
>
</div>
</div>
</div>
<div class="van-calendar__month">
<div class="van-calendar__month-title">
2012/10
</div>
<div
role="grid"
class="van-calendar__days"
>
<!--[-->
<div
class="van-calendar__day"
style="width:100%;"
>
</div>
<div
class="van-calendar__day"
style="width:100%;"
>
</div>
<div
class="van-calendar__day"
style="width:100%;"
>
</div>
<div
class="van-calendar__day"
style="width:100%;"
>
</div>
<div
class="van-calendar__day"
style="width:100%;"
>
</div>
</div>
</div>
<div class="van-calendar__month">
<div class="van-calendar__month-title">
2012/11
</div>
<div
role="grid"
class="van-calendar__days"
>
<!--[-->
<div
class="van-calendar__day"
style="width:100%;"
>
</div>
<div
class="van-calendar__day"
style="width:100%;"
>
</div>
<div
class="van-calendar__day"
style="width:100%;"
>
</div>
<div
class="van-calendar__day"
style="width:100%;"
>
</div>
<div
class="van-calendar__day"
style="width:100%;"
>
</div>
</div>
</div>
<div class="van-calendar__month">
<div class="van-calendar__month-title">
2012/12
</div>
<div
role="grid"
class="van-calendar__days"
>
<!--[-->
<div
class="van-calendar__day"
style="width:100%;"
>
</div>
<div
class="van-calendar__day"
style="width:100%;"
>
</div>
<div
class="van-calendar__day"
style="width:100%;"
>
</div>
<div
class="van-calendar__day"
style="width:100%;"
>
</div>
<div
class="van-calendar__day"
style="width:100%;"
>
</div>
<div
class="van-calendar__day"
style="width:100%;"
>
</div>
</div>
</div>
<div class="van-calendar__month">
<div class="van-calendar__month-title">
2013/1
</div>
<div
role="grid"
class="van-calendar__days"
>
<!--[-->
<div
class="van-calendar__day"
style="width:100%;"
>
</div>
<div
class="van-calendar__day"
style="width:100%;"
>
</div>
<div
class="van-calendar__day"
style="width:100%;"
>
</div>
<div
class="van-calendar__day"
style="width:100%;"
>
</div>
<div
class="van-calendar__day"
style="width:100%;"
>
</div>
</div>
</div>
<div class="van-calendar__month">
<div class="van-calendar__month-title">
2013/2
</div>
<div
role="grid"
class="van-calendar__days"
>
<!--[-->
<div
class="van-calendar__day"
style="width:100%;"
>
</div>
<div
class="van-calendar__day"
style="width:100%;"
>
</div>
<div
class="van-calendar__day"
style="width:100%;"
>
</div>
<div
class="van-calendar__day"
style="width:100%;"
>
</div>
<div
class="van-calendar__day"
style="width:100%;"
>
</div>
</div>
</div>
<div class="van-calendar__month">
<div class="van-calendar__month-title">
2013/3
</div>
<div
role="grid"
class="van-calendar__days"
>
<!--[-->
<div
class="van-calendar__day"
style="width:100%;"
>
</div>
<div
class="van-calendar__day"
style="width:100%;"
>
</div>
<div
class="van-calendar__day"
style="width:100%;"
>
</div>
<div
class="van-calendar__day"
style="width:100%;"
>
</div>
<div
class="van-calendar__day"
style="width:100%;"
>
</div>
<div
class="van-calendar__day"
style="width:100%;"
>
</div>
</div>
</div>
</div>
<div class="van-calendar__footer van-safe-area-bottom">
</div>

View File

@ -1,6 +1,42 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`should render demo and match snapshot 1`] = `
<div>
<div
class="van-cell van-cell--clickable"
role="button"
tabindex="0"
>
<div class="van-cell__title">
<span>
Select Switch Mode
</span>
</div>
<div class="van-cell__value">
<span>
none
</span>
</div>
<i class="van-badge__wrapper van-icon van-icon-arrow van-cell__right-icon">
</i>
</div>
<transition-stub
name="van-fade"
appear="true"
persisted="false"
css="true"
role="button"
tabindex="0"
>
</transition-stub>
<transition-stub
name="van-popup-slide-bottom"
appear="false"
persisted="false"
css="true"
>
</transition-stub>
</div>
<div>
<div
class="van-cell van-cell--clickable"
@ -301,6 +337,441 @@ exports[`should render demo and match snapshot 1`] = `
</div>
</div>
</div>
<div class="van-calendar__month">
<div class="van-calendar__month-title">
2012/4
</div>
<div
role="grid"
class="van-calendar__days"
>
<div
class="van-calendar__day"
style="width: 100%;"
>
</div>
<div
class="van-calendar__day"
style="width: 100%;"
>
</div>
<div
class="van-calendar__day"
style="width: 100%;"
>
</div>
<div
class="van-calendar__day"
style="width: 100%;"
>
</div>
<div
class="van-calendar__day"
style="width: 100%;"
>
</div>
</div>
</div>
<div class="van-calendar__month">
<div class="van-calendar__month-title">
2012/5
</div>
<div
role="grid"
class="van-calendar__days"
>
<div
class="van-calendar__day"
style="width: 100%;"
>
</div>
<div
class="van-calendar__day"
style="width: 100%;"
>
</div>
<div
class="van-calendar__day"
style="width: 100%;"
>
</div>
<div
class="van-calendar__day"
style="width: 100%;"
>
</div>
<div
class="van-calendar__day"
style="width: 100%;"
>
</div>
</div>
</div>
<div class="van-calendar__month">
<div class="van-calendar__month-title">
2012/6
</div>
<div
role="grid"
class="van-calendar__days"
>
<div
class="van-calendar__day"
style="width: 100%;"
>
</div>
<div
class="van-calendar__day"
style="width: 100%;"
>
</div>
<div
class="van-calendar__day"
style="width: 100%;"
>
</div>
<div
class="van-calendar__day"
style="width: 100%;"
>
</div>
<div
class="van-calendar__day"
style="width: 100%;"
>
</div>
</div>
</div>
<div class="van-calendar__month">
<div class="van-calendar__month-title">
2012/7
</div>
<div
role="grid"
class="van-calendar__days"
>
<div
class="van-calendar__day"
style="width: 100%;"
>
</div>
<div
class="van-calendar__day"
style="width: 100%;"
>
</div>
<div
class="van-calendar__day"
style="width: 100%;"
>
</div>
<div
class="van-calendar__day"
style="width: 100%;"
>
</div>
<div
class="van-calendar__day"
style="width: 100%;"
>
</div>
</div>
</div>
<div class="van-calendar__month">
<div class="van-calendar__month-title">
2012/8
</div>
<div
role="grid"
class="van-calendar__days"
>
<div
class="van-calendar__day"
style="width: 100%;"
>
</div>
<div
class="van-calendar__day"
style="width: 100%;"
>
</div>
<div
class="van-calendar__day"
style="width: 100%;"
>
</div>
<div
class="van-calendar__day"
style="width: 100%;"
>
</div>
<div
class="van-calendar__day"
style="width: 100%;"
>
</div>
</div>
</div>
<div class="van-calendar__month">
<div class="van-calendar__month-title">
2012/9
</div>
<div
role="grid"
class="van-calendar__days"
>
<div
class="van-calendar__day"
style="width: 100%;"
>
</div>
<div
class="van-calendar__day"
style="width: 100%;"
>
</div>
<div
class="van-calendar__day"
style="width: 100%;"
>
</div>
<div
class="van-calendar__day"
style="width: 100%;"
>
</div>
<div
class="van-calendar__day"
style="width: 100%;"
>
</div>
<div
class="van-calendar__day"
style="width: 100%;"
>
</div>
</div>
</div>
<div class="van-calendar__month">
<div class="van-calendar__month-title">
2012/10
</div>
<div
role="grid"
class="van-calendar__days"
>
<div
class="van-calendar__day"
style="width: 100%;"
>
</div>
<div
class="van-calendar__day"
style="width: 100%;"
>
</div>
<div
class="van-calendar__day"
style="width: 100%;"
>
</div>
<div
class="van-calendar__day"
style="width: 100%;"
>
</div>
<div
class="van-calendar__day"
style="width: 100%;"
>
</div>
</div>
</div>
<div class="van-calendar__month">
<div class="van-calendar__month-title">
2012/11
</div>
<div
role="grid"
class="van-calendar__days"
>
<div
class="van-calendar__day"
style="width: 100%;"
>
</div>
<div
class="van-calendar__day"
style="width: 100%;"
>
</div>
<div
class="van-calendar__day"
style="width: 100%;"
>
</div>
<div
class="van-calendar__day"
style="width: 100%;"
>
</div>
<div
class="van-calendar__day"
style="width: 100%;"
>
</div>
</div>
</div>
<div class="van-calendar__month">
<div class="van-calendar__month-title">
2012/12
</div>
<div
role="grid"
class="van-calendar__days"
>
<div
class="van-calendar__day"
style="width: 100%;"
>
</div>
<div
class="van-calendar__day"
style="width: 100%;"
>
</div>
<div
class="van-calendar__day"
style="width: 100%;"
>
</div>
<div
class="van-calendar__day"
style="width: 100%;"
>
</div>
<div
class="van-calendar__day"
style="width: 100%;"
>
</div>
<div
class="van-calendar__day"
style="width: 100%;"
>
</div>
</div>
</div>
<div class="van-calendar__month">
<div class="van-calendar__month-title">
2013/1
</div>
<div
role="grid"
class="van-calendar__days"
>
<div
class="van-calendar__day"
style="width: 100%;"
>
</div>
<div
class="van-calendar__day"
style="width: 100%;"
>
</div>
<div
class="van-calendar__day"
style="width: 100%;"
>
</div>
<div
class="van-calendar__day"
style="width: 100%;"
>
</div>
<div
class="van-calendar__day"
style="width: 100%;"
>
</div>
</div>
</div>
<div class="van-calendar__month">
<div class="van-calendar__month-title">
2013/2
</div>
<div
role="grid"
class="van-calendar__days"
>
<div
class="van-calendar__day"
style="width: 100%;"
>
</div>
<div
class="van-calendar__day"
style="width: 100%;"
>
</div>
<div
class="van-calendar__day"
style="width: 100%;"
>
</div>
<div
class="van-calendar__day"
style="width: 100%;"
>
</div>
<div
class="van-calendar__day"
style="width: 100%;"
>
</div>
</div>
</div>
<div class="van-calendar__month">
<div class="van-calendar__month-title">
2013/3
</div>
<div
role="grid"
class="van-calendar__days"
>
<div
class="van-calendar__day"
style="width: 100%;"
>
</div>
<div
class="van-calendar__day"
style="width: 100%;"
>
</div>
<div
class="van-calendar__day"
style="width: 100%;"
>
</div>
<div
class="van-calendar__day"
style="width: 100%;"
>
</div>
<div
class="van-calendar__day"
style="width: 100%;"
>
</div>
<div
class="van-calendar__day"
style="width: 100%;"
>
</div>
</div>
</div>
</div>
<div class="van-calendar__footer van-safe-area-bottom">
</div>

View File

@ -0,0 +1,205 @@
import { Calendar, CalendarInstance } from '..';
import { mount, later } from '../../../test';
import {
formatMonthTitle,
getMonthByOffset,
getPrevMonth,
getNextMonth,
getPrevYear,
getNextYear,
getYearByOffset,
} from '../utils';
import { minDate, maxDate } from './utils';
const disabledActionClass = 'van-calendar__header-action--disabled';
test('the action buttons should be displayed correctly', async () => {
const wrapper = mount(Calendar, {
props: {
minDate,
maxDate,
poppable: false,
switchMode: 'month',
},
});
await later();
expect(wrapper.findAll('.van-calendar__header-action')).toHaveLength(2);
await wrapper.setProps({ switchMode: 'year-month' });
expect(wrapper.findAll('.van-calendar__header-action')).toHaveLength(4);
});
test('disable previous and next month buttons', async () => {
const maxDate = getNextMonth(minDate);
const wrapper = mount(Calendar, {
props: {
minDate,
maxDate,
poppable: false,
switchMode: 'month',
},
});
await later();
const title = wrapper.find('.van-calendar__header-subtitle-text');
const [prevMonth, nextMonth] = wrapper.findAll(
'.van-calendar__header-action',
);
expect(title.text()).toEqual(formatMonthTitle(maxDate));
expect(prevMonth.classes()).not.toContain(disabledActionClass);
expect(nextMonth.classes()).toContain(disabledActionClass);
await nextMonth.trigger('click');
expect(title.text()).toEqual(formatMonthTitle(maxDate));
expect(prevMonth.classes()).not.toContain(disabledActionClass);
expect(nextMonth.classes()).toContain(disabledActionClass);
await prevMonth.trigger('click');
expect(title.text()).toEqual(formatMonthTitle(minDate));
expect(prevMonth.classes()).toContain(disabledActionClass);
expect(nextMonth.classes()).not.toContain(disabledActionClass);
});
test('disable previous and next year buttons', async () => {
const maxDate = getNextYear(minDate);
const wrapper = mount(Calendar, {
props: {
minDate,
maxDate,
poppable: false,
switchMode: 'year-month',
},
});
await later();
let currentDate = maxDate;
const title = wrapper.find('.van-calendar__header-subtitle-text');
const [prevYear, prevMonth, nextMonth, nextYear] = wrapper.findAll(
'.van-calendar__header-action',
);
expect(title.text()).toEqual(formatMonthTitle(currentDate));
expect(prevYear.classes()).not.toContain(disabledActionClass);
expect(prevMonth.classes()).not.toContain(disabledActionClass);
expect(nextMonth.classes()).toContain(disabledActionClass);
expect(nextYear.classes()).toContain(disabledActionClass);
await prevMonth.trigger('click');
currentDate = getPrevMonth(currentDate);
expect(title.text()).toEqual(formatMonthTitle(currentDate));
expect(prevYear.classes()).toContain(disabledActionClass);
expect(prevMonth.classes()).not.toContain(disabledActionClass);
expect(nextMonth.classes()).not.toContain(disabledActionClass);
expect(nextYear.classes()).toContain(disabledActionClass);
await nextMonth.trigger('click');
currentDate = getNextMonth(currentDate);
expect(title.text()).toEqual(formatMonthTitle(currentDate));
await prevYear.trigger('click');
currentDate = getPrevYear(currentDate);
expect(title.text()).toEqual(formatMonthTitle(currentDate));
expect(prevYear.classes()).toContain(disabledActionClass);
expect(prevMonth.classes()).toContain(disabledActionClass);
expect(nextMonth.classes()).not.toContain(disabledActionClass);
expect(nextYear.classes()).not.toContain(disabledActionClass);
});
test('should switch to the provided date after calling the scrollToDate method', async () => {
const maxDate = getNextYear(minDate);
const wrapper = mount(Calendar, {
props: {
minDate,
maxDate,
poppable: false,
switchMode: 'month',
},
});
await later();
let currentDate = maxDate;
const title = wrapper.find('.van-calendar__header-subtitle-text');
currentDate = getMonthByOffset(currentDate, -4);
(wrapper.vm as CalendarInstance).scrollToDate(currentDate);
await later();
expect(title.text()).toEqual(formatMonthTitle(currentDate));
});
test('should render action slots correctly', async () => {
const prevYearSlot = vi.fn(() => 'prev year');
const prevMonthSlot = vi.fn(() => 'prev month');
const nextMonthSlot = vi.fn(() => 'next month');
const nextYearSlot = vi.fn(() => 'next year');
const maxDate = getNextYear(minDate);
const wrapper = mount(Calendar, {
props: {
minDate,
maxDate,
poppable: false,
switchMode: 'year-month',
},
slots: {
'prev-year': prevYearSlot,
'prev-month': prevMonthSlot,
'next-month': nextMonthSlot,
'next-year': nextYearSlot,
},
});
await later();
const [prevYear, prevMonth, nextMonth, nextYear] = wrapper.findAll(
'.van-calendar__header-action',
);
expect(prevYearSlot).toHaveBeenLastCalledWith({ disabled: false });
expect(prevMonthSlot).toHaveBeenLastCalledWith({ disabled: false });
expect(nextMonthSlot).toHaveBeenLastCalledWith({ disabled: true });
expect(nextYearSlot).toHaveBeenLastCalledWith({ disabled: true });
expect(prevYear.text()).toEqual('prev year');
expect(prevMonth.text()).toEqual('prev month');
expect(nextMonth.text()).toEqual('next month');
expect(nextYear.text()).toEqual('next year');
await prevMonth.trigger('click');
expect(prevYearSlot).toHaveBeenLastCalledWith({ disabled: true });
expect(prevMonthSlot).toHaveBeenLastCalledWith({ disabled: false });
expect(nextMonthSlot).toHaveBeenLastCalledWith({ disabled: false });
expect(nextYearSlot).toHaveBeenLastCalledWith({ disabled: true });
});
test('should emit panelChange event', async () => {
const onPanelChange = vi.fn();
const maxDate = getYearByOffset(minDate, 10);
const wrapper = mount(Calendar, {
props: {
minDate,
maxDate,
poppable: false,
switchMode: 'year-month',
onPanelChange,
},
});
await later();
let currentDate = maxDate;
const [prevYear, prevMonth, nextMonth, nextYear] = wrapper.findAll(
'.van-calendar__header-action',
);
await prevMonth.trigger('click');
currentDate = getPrevMonth(currentDate);
expect(onPanelChange).toHaveBeenLastCalledWith({ date: currentDate });
await prevYear.trigger('click');
currentDate = getPrevYear(currentDate);
expect(onPanelChange).toHaveBeenLastCalledWith({ date: currentDate });
await nextYear.trigger('click');
currentDate = getNextYear(currentDate);
expect(onPanelChange).toHaveBeenLastCalledWith({ date: currentDate });
await nextMonth.trigger('click');
currentDate = getNextMonth(currentDate);
expect(onPanelChange).toHaveBeenLastCalledWith({ date: currentDate });
});

View File

@ -3,6 +3,8 @@ import type { Numeric } from '../utils';
import type { CalendarProps } from './Calendar';
import type { CalendarMonthProps } from './CalendarMonth';
export type CalendarSwitchMode = 'none' | 'month' | 'year-month';
export type CalendarType = 'single' | 'range' | 'multiple';
export type CalendarDayType =
@ -56,6 +58,9 @@ export type CalendarThemeVars = {
calendarHeaderTitleHeight?: string;
calendarHeaderTitleFontSize?: string;
calendarHeaderSubtitleFontSize?: string;
calendarHeaderActionWidth?: string;
calendarHeaderActionColor?: string;
calendarHeaderActionDisabledColor?: string;
calendarWeekdaysHeight?: string;
calendarWeekdaysFontSize?: string;
calendarMonthTitleFontSize?: string;

View File

@ -43,8 +43,24 @@ export function getDayByOffset(date: Date, offset: number) {
return cloned;
}
export function getMonthByOffset(date: Date, offset: number) {
const cloned = cloneDate(date);
cloned.setMonth(cloned.getMonth() + offset);
return cloned;
}
export function getYearByOffset(date: Date, offset: number) {
const cloned = cloneDate(date);
cloned.setFullYear(cloned.getFullYear() + offset);
return cloned;
}
export const getPrevDay = (date: Date) => getDayByOffset(date, -1);
export const getNextDay = (date: Date) => getDayByOffset(date, 1);
export const getPrevMonth = (date: Date) => getMonthByOffset(date, -1);
export const getNextMonth = (date: Date) => getMonthByOffset(date, 1);
export const getPrevYear = (date: Date) => getYearByOffset(date, -1);
export const getNextYear = (date: Date) => getYearByOffset(date, 1);
export const getToday = () => {
const today = new Date();
today.setHours(0, 0, 0, 0);

View File

@ -140,7 +140,11 @@ export default defineComponent({
const match = getMatchAnchor(selectActiveIndex);
if (match) {
const rect = match.getRect(scrollParent.value, scrollParentRect);
active = getActiveAnchor(rect.top, rects);
if (props.sticky && props.stickyOffsetTop) {
active = getActiveAnchor(rect.top - props.stickyOffsetTop, rects);
} else {
active = getActiveAnchor(rect.top, rects);
}
}
} else {
active = getActiveAnchor(scrollTop, rects);
@ -229,7 +233,11 @@ export default defineComponent({
}
if (props.sticky && props.stickyOffsetTop) {
setRootScrollTop(getRootScrollTop() - props.stickyOffsetTop);
if (getRootScrollTop() === offsetHeight - scrollParentRect.height) {
setRootScrollTop(getRootScrollTop());
} else {
setRootScrollTop(getRootScrollTop() - props.stickyOffsetTop);
}
}
emit('select', match.index);

View File

@ -6,6 +6,389 @@ exports[`should allow to custom anchor content 1`] = `
</div>
`;
exports[`should render active anchor when stick prop is true and has stickyOffsetTop 1`] = `
<div class="van-index-bar">
<div class="van-index-bar__sidebar">
<span
class="van-index-bar__index"
data-index="A"
>
A
</span>
<span
class="van-index-bar__index"
data-index="B"
>
B
</span>
<span
class="van-index-bar__index"
data-index="C"
>
C
</span>
<span
class="van-index-bar__index"
data-index="D"
>
D
</span>
<span
class="van-index-bar__index"
data-index="E"
>
E
</span>
<span
class="van-index-bar__index"
data-index="F"
>
F
</span>
<span
class="van-index-bar__index"
data-index="G"
>
G
</span>
<span
class="van-index-bar__index"
data-index="H"
>
H
</span>
<span
class="van-index-bar__index"
data-index="I"
>
I
</span>
<span
class="van-index-bar__index"
data-index="J"
>
J
</span>
<span
class="van-index-bar__index"
data-index="K"
>
K
</span>
<span
class="van-index-bar__index"
data-index="L"
>
L
</span>
<span
class="van-index-bar__index"
data-index="M"
>
M
</span>
<span
class="van-index-bar__index"
data-index="N"
>
N
</span>
<span
class="van-index-bar__index"
data-index="O"
>
O
</span>
<span
class="van-index-bar__index"
data-index="P"
>
P
</span>
<span
class="van-index-bar__index"
data-index="Q"
>
Q
</span>
<span
class="van-index-bar__index"
data-index="R"
>
R
</span>
<span
class="van-index-bar__index"
data-index="S"
>
S
</span>
<span
class="van-index-bar__index"
data-index="T"
>
T
</span>
<span
class="van-index-bar__index"
data-index="U"
>
U
</span>
<span
class="van-index-bar__index"
data-index="V"
>
V
</span>
<span
class="van-index-bar__index"
data-index="W"
>
W
</span>
<span
class="van-index-bar__index"
data-index="X"
>
X
</span>
<span
class="van-index-bar__index"
data-index="Y"
>
Y
</span>
<span
class="van-index-bar__index"
data-index="Z"
>
Z
</span>
</div>
<div data-index="0">
<div class="van-index-anchor">
A
</div>
</div>
<div style="height: 32px;">
A1
</div>
<div data-index="1">
<div class="van-index-anchor">
B
</div>
</div>
<div style="height: 32px;">
B1
</div>
<div data-index="2">
<div class="van-index-anchor">
C
</div>
</div>
<div style="height: 32px;">
C1
</div>
</div>
`;
exports[`should render active anchor when stick prop is true and has stickyOffsetTop 2`] = `
<div class="van-index-bar">
<div class="van-index-bar__sidebar">
<span
class="van-index-bar__index van-index-bar__index--active"
data-index="A"
>
A
</span>
<span
class="van-index-bar__index"
data-index="B"
>
B
</span>
<span
class="van-index-bar__index"
data-index="C"
>
C
</span>
<span
class="van-index-bar__index"
data-index="D"
>
D
</span>
<span
class="van-index-bar__index"
data-index="E"
>
E
</span>
<span
class="van-index-bar__index"
data-index="F"
>
F
</span>
<span
class="van-index-bar__index"
data-index="G"
>
G
</span>
<span
class="van-index-bar__index"
data-index="H"
>
H
</span>
<span
class="van-index-bar__index"
data-index="I"
>
I
</span>
<span
class="van-index-bar__index"
data-index="J"
>
J
</span>
<span
class="van-index-bar__index"
data-index="K"
>
K
</span>
<span
class="van-index-bar__index"
data-index="L"
>
L
</span>
<span
class="van-index-bar__index"
data-index="M"
>
M
</span>
<span
class="van-index-bar__index"
data-index="N"
>
N
</span>
<span
class="van-index-bar__index"
data-index="O"
>
O
</span>
<span
class="van-index-bar__index"
data-index="P"
>
P
</span>
<span
class="van-index-bar__index"
data-index="Q"
>
Q
</span>
<span
class="van-index-bar__index"
data-index="R"
>
R
</span>
<span
class="van-index-bar__index"
data-index="S"
>
S
</span>
<span
class="van-index-bar__index"
data-index="T"
>
T
</span>
<span
class="van-index-bar__index"
data-index="U"
>
U
</span>
<span
class="van-index-bar__index"
data-index="V"
>
V
</span>
<span
class="van-index-bar__index"
data-index="W"
>
W
</span>
<span
class="van-index-bar__index"
data-index="X"
>
X
</span>
<span
class="van-index-bar__index"
data-index="Y"
>
Y
</span>
<span
class="van-index-bar__index"
data-index="Z"
>
Z
</span>
</div>
<div
data-index="0"
style="height: 10px;"
>
<div
class="van-index-anchor van-index-anchor--sticky van-hairline--bottom"
style="transform: translate3d(0, 42px, 0);"
>
A
</div>
</div>
<div style="height: 32px;">
A1
</div>
<div data-index="1">
<div class="van-index-anchor">
B
</div>
</div>
<div style="height: 32px;">
B1
</div>
<div
data-index="2"
style
>
<div class="van-index-anchor">
C
</div>
</div>
<div style="height: 32px;">
C1
</div>
</div>
`;
exports[`should update active anchor after page scroll 1`] = `
<div class="van-index-bar">
<div class="van-index-bar__sidebar">

View File

@ -3,6 +3,7 @@ import {
mount,
trigger,
triggerDrag,
mockScrollTo,
mockScrollTop,
mockScrollIntoView,
} from '../../../test';
@ -206,3 +207,48 @@ test('should render teleport prop correctly', () => {
expect(root.querySelector('.van-index-bar__sidebar')).toBeTruthy();
});
test('should render active anchor when stick prop is true and has stickyOffsetTop', async () => {
const nativeRect = Element.prototype.getBoundingClientRect;
Element.prototype.getBoundingClientRect = function () {
const { index } = this.dataset;
return {
top: index ? index * (10 + 32) : 0,
height: 10,
};
};
mockScrollTo();
const onSelect = vi.fn();
const onChange = vi.fn();
const wrapper = mount({
render() {
return (
<IndexBar onSelect={onSelect} stickyOffsetTop={42} onChange={onChange}>
<IndexAnchor index="A" data-index="0" />
<div style={{ height: '32px' }}>A1</div>
<IndexAnchor index="B" data-index="1" />
<div style={{ height: '32px' }}>B1</div>
<IndexAnchor index="C" data-index="2" />
<div style={{ height: '32px' }}>C1</div>
</IndexBar>
);
},
});
await nextTick();
expect(wrapper.html()).toMatchSnapshot();
const indexes = wrapper.findAll('.van-index-bar__index');
await indexes[0].trigger('click');
await trigger(window, 'scroll');
expect(wrapper.html()).toMatchSnapshot();
expect(onSelect).toHaveBeenCalledWith('A');
expect(onChange).toHaveBeenCalledWith('A');
wrapper.unmount();
Element.prototype.getBoundingClientRect = nativeRect;
});

View File

@ -37,6 +37,14 @@ function mockHTMLElementOffset() {
});
}
export function mockScrollTo() {
const fn = vi.fn();
if (inBrowser) {
window.scrollTo = fn;
}
return fn;
}
export function mockScrollIntoView() {
const fn = vi.fn();
if (inBrowser) {

769
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff