From 6200d5a1dfbb74005f9edbd0dd0d2f15ce296660 Mon Sep 17 00:00:00 2001 From: saller Date: Mon, 18 Apr 2022 14:32:48 +0800 Subject: [PATCH] feat(comp:date-picker): add datetime type (#837) feat(comp:date-picker): add dateRangePicker component fix(comp:time-picker): fix time-picker scroll problem occured under PM doc(comp:all): update less variable in documents --- .../__snapshots__/datePanel.spec.ts.snap | 512 ++++++++++++++---- .../date-panel/__tests__/datePanel.spec.ts | 251 ++++++++- .../components/_private/date-panel/index.ts | 6 +- .../_private/date-panel/src/DatePanel.tsx | 1 - .../src/composables/useActiveDate.ts | 30 +- .../date-panel/src/panel-body/PanelBody.tsx | 5 +- .../date-panel/src/panel-body/PanelCell.tsx | 84 ++- .../date-panel/src/panel-body/PanelRow.tsx | 6 +- .../_private/date-panel/src/types.ts | 49 +- packages/components/_private/footer/index.ts | 2 +- .../components/_private/footer/src/types.ts | 29 +- .../__tests__/useSelectorScroll.spec.ts | 6 +- .../src/composables/usePanelScroll.ts | 11 +- .../_private/time-panel/style/index.less | 1 + .../style/themes/default.variable.less | 1 + .../__snapshots__/trigger.spec.ts.snap | 9 + .../trigger/__tests__/trigger.spec.ts | 146 +++++ packages/components/_private/trigger/index.ts | 20 + .../_private/trigger/src/Trigger.tsx | 110 ++++ .../components/_private/trigger/src/types.ts | 33 ++ .../_private/trigger/style/index.less | 109 ++++ .../trigger/style/themes/default.less | 3 + .../_private/trigger/style/themes/default.ts | 4 + .../style/themes/default.variable.less | 41 ++ .../components/config/src/defaultConfig.ts | 9 +- packages/components/config/src/types.ts | 5 - .../__snapshots__/datePicker.spec.ts.snap | 17 +- .../dateRangePicker.spec.ts.snap | 10 + .../date-picker/__tests__/datePicker.spec.ts | 262 ++++++++- .../__tests__/dateRangePicker.spec.ts | 284 ++++++++++ .../components/date-picker/demo/AllowInput.md | 14 + .../date-picker/demo/AllowInput.vue | 30 + .../components/date-picker/demo/Basic.vue | 1 + .../components/date-picker/demo/Disabled.md | 2 +- .../date-picker/demo/DisabledDate.vue | 7 +- .../components/date-picker/demo/Format.md | 2 +- .../date-picker/demo/FormatCustom.md | 2 +- packages/components/date-picker/demo/Range.md | 14 + .../components/date-picker/demo/Range.vue | 26 + .../components/date-picker/docs/Index.zh.md | 124 +++-- packages/components/date-picker/index.ts | 15 +- .../components/date-picker/src/DatePicker.tsx | 98 ++-- .../date-picker/src/DateRangePicker.tsx | 110 ++++ .../src/composables/useActiveDate.ts | 149 +++++ .../date-picker/src/composables/useControl.ts | 212 ++++++++ .../date-picker/src/composables/useFormat.ts | 43 +- .../src/composables/useInputEnableStatus.ts | 35 ++ .../src/composables/useKeyboardEvents.ts | 43 ++ .../src/composables/useOverlayState.ts | 18 +- .../src/composables/usePickerState.ts | 87 +++ .../src/composables/useRangeControl.ts | 136 +++++ .../src/composables/useTriggerProps.ts | 58 ++ .../date-picker/src/content/Content.tsx | 158 +++++- .../date-picker/src/content/RangeContent.tsx | 189 +++++++ packages/components/date-picker/src/token.ts | 42 +- .../date-picker/src/trigger/RangeTrigger.tsx | 86 +++ .../date-picker/src/trigger/Trigger.tsx | 104 +--- packages/components/date-picker/src/types.ts | 145 +++-- packages/components/date-picker/src/utils.ts | 47 ++ .../components/date-picker/style/index.less | 110 +++- .../components/date-picker/style/input.less | 196 +++---- .../components/date-picker/style/panel.less | 261 ++++++--- .../style/themes/default.variable.less | 106 ++-- packages/components/default.less | 2 + packages/components/form/docs/Index.zh.md | 230 ++++---- packages/components/index.ts | 3 +- .../components/locales/src/langs/en-US.ts | 5 + .../components/locales/src/langs/zh-CN.ts | 5 + packages/components/locales/src/types.ts | 5 + .../components/style/variable/prefix.less | 2 + packages/components/table/docs/Index.zh.md | 2 + .../components/table/src/main/body/Body.tsx | 4 +- .../components/time-picker/docs/Index.zh.md | 3 +- .../time-picker/src/overlay/Overlay.tsx | 3 +- packages/components/transfer/docs/Index.zh.md | 9 +- .../components/tree-select/docs/Index.zh.md | 41 -- packages/components/types.d.ts | 3 +- packages/pro/transfer/docs/Index.zh.md | 15 + 78 files changed, 4162 insertions(+), 896 deletions(-) create mode 100644 packages/components/_private/trigger/__tests__/__snapshots__/trigger.spec.ts.snap create mode 100644 packages/components/_private/trigger/__tests__/trigger.spec.ts create mode 100644 packages/components/_private/trigger/index.ts create mode 100644 packages/components/_private/trigger/src/Trigger.tsx create mode 100644 packages/components/_private/trigger/src/types.ts create mode 100644 packages/components/_private/trigger/style/index.less create mode 100644 packages/components/_private/trigger/style/themes/default.less create mode 100644 packages/components/_private/trigger/style/themes/default.ts create mode 100644 packages/components/_private/trigger/style/themes/default.variable.less create mode 100644 packages/components/date-picker/__tests__/__snapshots__/dateRangePicker.spec.ts.snap create mode 100644 packages/components/date-picker/__tests__/dateRangePicker.spec.ts create mode 100644 packages/components/date-picker/demo/AllowInput.md create mode 100644 packages/components/date-picker/demo/AllowInput.vue create mode 100644 packages/components/date-picker/demo/Range.md create mode 100644 packages/components/date-picker/demo/Range.vue create mode 100644 packages/components/date-picker/src/DateRangePicker.tsx create mode 100644 packages/components/date-picker/src/composables/useActiveDate.ts create mode 100644 packages/components/date-picker/src/composables/useControl.ts create mode 100644 packages/components/date-picker/src/composables/useInputEnableStatus.ts create mode 100644 packages/components/date-picker/src/composables/useKeyboardEvents.ts create mode 100644 packages/components/date-picker/src/composables/usePickerState.ts create mode 100644 packages/components/date-picker/src/composables/useRangeControl.ts create mode 100644 packages/components/date-picker/src/composables/useTriggerProps.ts create mode 100644 packages/components/date-picker/src/content/RangeContent.tsx create mode 100644 packages/components/date-picker/src/trigger/RangeTrigger.tsx create mode 100644 packages/components/date-picker/src/utils.ts diff --git a/packages/components/_private/date-panel/__tests__/__snapshots__/datePanel.spec.ts.snap b/packages/components/_private/date-panel/__tests__/__snapshots__/datePanel.spec.ts.snap index e956f8ceb..2629cad38 100644 --- a/packages/components/_private/date-panel/__tests__/__snapshots__/datePanel.spec.ts.snap +++ b/packages/components/_private/date-panel/__tests__/__snapshots__/datePanel.spec.ts.snap @@ -1,6 +1,6 @@ // Vitest Snapshot v1 -exports[`DatePanel > disabledDate work 1`] = ` +exports[`DatePanel > date type disabledDate work 1`] = ` "
@@ -20,124 +20,276 @@ exports[`DatePanel > disabledDate work 1`] = ` - -
27
+ +
+
27
+
- -
28
+ +
+
28
+
- -
29
+ +
+
29
+
- -
30
+ +
+
30
+
- -
1
+ +
+
1
+
- -
2
+ +
+
2
+
- -
3
+ +
+
3
+
- -
4
+ +
+
4
+
- -
5
+ +
+
5
+
- -
6
+ +
+
6
+
- -
7
+ +
+
7
+
- -
8
+ +
+
8
+
- -
9
+ +
+
9
+
-
10
+
+
10
+
-
11
+
+
11
+
-
12
+
+
12
+
- -
13
+ +
+
13
+
- -
14
+ +
+
14
+
- -
15
+ +
+
15
+
- -
16
+ +
+
16
+
- -
17
+ +
+
17
+
- -
18
+ +
+
18
+
- -
19
+ +
+
19
+
- -
20
+ +
+
20
+
- -
21
+ +
+
21
+
- -
22
+ +
+
22
+
- -
23
+ +
+
23
+
- -
24
+ +
+
24
+
- -
25
+ +
+
25
+
- -
26
+ +
+
26
+
- -
27
+ +
+
27
+
- -
28
+ +
+
28
+
- -
29
+ +
+
29
+
+ + +
+
30
+
+ + +
+
31
+
+ + + + +
+
" +`; + +exports[`DatePanel > month type disabledDate work 1`] = ` +"
+
+
+
+
+ + + + + + + + + + + + + + + + + + + +
+
+
1月
+
+
+
+
2月
+
+
+
+
3月
+
+
+
+
4月
+
+
+
+
5月
+
+
+
+
6月
+
+
+
+
7月
+
+
+
+
8月
+
+
+
+
9月
+
+
+
+
10月
+
-
30
+
+
11月
+
-
31
+
+
12月
+
-
" `; @@ -162,122 +314,274 @@ exports[`DatePanel > render work 1`] = ` -
27
+
+
27
+
-
28
+
+
28
+
-
29
+
+
29
+
-
30
+
+
30
+
+ + +
+
1
+
+ + +
+
2
+
+ + +
+
3
+
+ + + + +
+
4
+
+ + +
+
5
+
+ + +
+
6
+
+ + +
+
7
+
- -
1
+ +
+
8
+
-
2
+
+
9
+
-
3
+
+
10
+
-
4
+
+
11
+
-
5
+
+
12
+
-
6
+
+
13
+
-
7
+
+
14
+
-
8
+
+
15
+
-
9
+
+
16
+
-
10
+
+
17
+
-
11
+
+
18
+
-
12
+
+
19
+
-
13
+
+
20
+
-
14
+
+
21
+
-
15
+
+
22
+
-
16
+
+
23
+
-
17
+
+
24
+
-
18
+
+
25
+
-
19
+
+
26
+
-
20
+
+
27
+
-
21
+
+
28
+
-
22
+
+
29
+
-
23
+
+
30
+
-
24
+
+
31
+
+ + + + + +" +`; + +exports[`DatePanel > year type disabledDate work 1`] = ` +"
+
+
+
+
+ + + + + + + + + + + + +
+
+
2019
+
+
+
+
2020
+
+
+
+
2021
+
+
+
2022
+
+
+
+
2023
+
+
-
25
+
+
2024
+
-
26
+
+
2025
+
-
27
+
+
2026
+
-
28
+
+
2027
+
-
29
+
+
2028
+
-
30
+
+
2029
+
-
31
+
+
2030
+
-
" `; diff --git a/packages/components/_private/date-panel/__tests__/datePanel.spec.ts b/packages/components/_private/date-panel/__tests__/datePanel.spec.ts index 8cb1ec410..b05f1e52c 100644 --- a/packages/components/_private/date-panel/__tests__/datePanel.spec.ts +++ b/packages/components/_private/date-panel/__tests__/datePanel.spec.ts @@ -1,21 +1,260 @@ -import { MountingOptions, mount } from '@vue/test-utils' +import { MountingOptions, VueWrapper, mount } from '@vue/test-utils' + +import { isArray } from 'lodash-es' import { renderWork } from '@tests' +import { endOfWeek, startOfWeek } from 'date-fns' import DatePanel from '../src/DatePanel' -import { DatePanelProps } from '../src/types' +import PanelCell from '../src/panel-body/PanelCell' +import { DatePanelInstance, DatePanelProps } from '../src/types' describe('DatePanel', () => { const DatePanelMount = (options?: MountingOptions>) => - mount(DatePanel, { ...(options as MountingOptions) }) + mount(DatePanel, { ...(options as MountingOptions) }) as VueWrapper + + const findCell = (wrapper: VueWrapper, cellLabel: string) => + wrapper.findAllComponents(PanelCell).find(cell => cell.find('.ix-date-panel-cell-trigger').text() === cellLabel) renderWork(DatePanel, { - props: { value: new Date('2021-10-01') }, + props: { value: new Date('2021-10-01'), activeDate: new Date('2021-10-01') }, + }) + + test('date type value work', async () => { + const wrapper = DatePanelMount({ + props: { + type: 'date', + value: new Date('2021-10-10'), + activeDate: new Date('2021-10-10'), + }, + }) + + expect(findCell(wrapper, '10')?.classes()).toContain('ix-date-panel-cell-selected') + + await wrapper.setProps({ value: new Date('2021-10-12') }) + + expect(findCell(wrapper, '10')?.classes()).not.toContain('ix-date-panel-cell-selected') + expect(findCell(wrapper, '12')?.classes()).toContain('ix-date-panel-cell-selected') + + await wrapper.setProps({ value: [new Date('2021-10-10'), new Date('2021-10-22')] }) + + expect(findCell(wrapper, '10')?.classes()).toContain('ix-date-panel-cell-selected') + expect(findCell(wrapper, '22')?.classes()).toContain('ix-date-panel-cell-selected') + expect(findCell(wrapper, '10')?.classes()).toContain('ix-date-panel-cell-in-range') + expect(findCell(wrapper, '22')?.classes()).toContain('ix-date-panel-cell-in-range') + expect(findCell(wrapper, '11')?.classes()).toContain('ix-date-panel-cell-in-range') + expect(findCell(wrapper, '21')?.classes()).toContain('ix-date-panel-cell-in-range') + }) + + test('week type value work', async () => { + const wrapper = DatePanelMount({ + props: { + type: 'week', + value: new Date('2021-10-10'), + activeDate: new Date('2021-10-10'), + }, + }) + + const testValue = (value: Date | Date[]) => { + const start = startOfWeek(isArray(value) ? value[0] : value, { weekStartsOn: 1 }) + const end = endOfWeek(isArray(value) ? value[1] : value, { weekStartsOn: 1 }) + + expect(findCell(wrapper, `${start.getDate()}`)?.classes()).toContain('ix-date-panel-cell-selected') + expect(findCell(wrapper, `${start.getDate()}`)?.classes()).toContain('ix-date-panel-cell-in-range') + expect(findCell(wrapper, `${end.getDate()}`)?.classes()).toContain('ix-date-panel-cell-selected') + expect(findCell(wrapper, `${end.getDate()}`)?.classes()).toContain('ix-date-panel-cell-in-range') + + for (let date = start.getDate() + 1; date < end.getDate(); date++) { + expect(findCell(wrapper, `${date}`)?.classes()).toContain('ix-date-panel-cell-in-range') + } + + expect(findCell(wrapper, `${start.getDate() - 1}`)?.classes()).not.toContain('ix-date-panel-cell-selected') + expect(findCell(wrapper, `${start.getDate() - 1}`)?.classes()).not.toContain('ix-date-panel-cell-in-range') + expect(findCell(wrapper, `${end.getDate() + 1}`)?.classes()).not.toContain('ix-date-panel-cell-selected') + expect(findCell(wrapper, `${end.getDate() + 1}`)?.classes()).not.toContain('ix-date-panel-cell-in-range') + } + + testValue(new Date('2021-10-10')) + + await wrapper.setProps({ value: new Date('2021-10-22') }) + testValue(new Date('2021-10-22')) + + await wrapper.setProps({ value: [new Date('2021-10-10'), new Date('2021-10-22')] }) + testValue([new Date('2021-10-10'), new Date('2021-10-22')]) + }) + + test('month type value work', async () => { + const wrapper = DatePanelMount({ + props: { + type: 'month', + value: new Date('2021-10-1'), + activeDate: new Date('2021-10-1'), + }, + }) + + expect(findCell(wrapper, '10月')?.classes()).toContain('ix-date-panel-cell-selected') + + await wrapper.setProps({ value: new Date('2021-12-1') }) + expect(findCell(wrapper, '10月')?.classes()).not.toContain('ix-date-panel-cell-selected') + expect(findCell(wrapper, '12月')?.classes()).toContain('ix-date-panel-cell-selected') + + await wrapper.setProps({ value: [new Date('2021-9-1'), new Date('2021-12-1')] }) + expect(findCell(wrapper, '9月')?.classes()).toContain('ix-date-panel-cell-selected') + expect(findCell(wrapper, '12月')?.classes()).toContain('ix-date-panel-cell-selected') + expect(findCell(wrapper, '9月')?.classes()).toContain('ix-date-panel-cell-in-range') + expect(findCell(wrapper, '12月')?.classes()).toContain('ix-date-panel-cell-in-range') + expect(findCell(wrapper, '10月')?.classes()).toContain('ix-date-panel-cell-in-range') + expect(findCell(wrapper, '11月')?.classes()).toContain('ix-date-panel-cell-in-range') + }) + + test('quarter type value work', async () => { + const wrapper = DatePanelMount({ + props: { + type: 'quarter', + value: new Date('2021-10-1'), + activeDate: new Date('2021-10-1'), + }, + }) + + expect(findCell(wrapper, 'Q4')?.classes()).toContain('ix-date-panel-cell-selected') + + await wrapper.setProps({ value: new Date('2021-1-1') }) + + expect(findCell(wrapper, 'Q4')?.classes()).not.toContain('ix-date-panel-cell-selected') + expect(findCell(wrapper, 'Q1')?.classes()).toContain('ix-date-panel-cell-selected') + + await wrapper.setProps({ value: [new Date('2021-4-1'), new Date('2021-12-1')] }) + expect(findCell(wrapper, 'Q2')?.classes()).toContain('ix-date-panel-cell-selected') + expect(findCell(wrapper, 'Q4')?.classes()).toContain('ix-date-panel-cell-selected') + expect(findCell(wrapper, 'Q2')?.classes()).toContain('ix-date-panel-cell-in-range') + expect(findCell(wrapper, 'Q4')?.classes()).toContain('ix-date-panel-cell-in-range') + expect(findCell(wrapper, 'Q3')?.classes()).toContain('ix-date-panel-cell-in-range') + }) + + test('year type value work', async () => { + const wrapper = DatePanelMount({ + props: { + type: 'year', + value: new Date('2021-1-1'), + activeDate: new Date('2021-1-1'), + }, + }) + + expect(findCell(wrapper, '2021')?.classes()).toContain('ix-date-panel-cell-selected') + + await wrapper.setProps({ value: new Date('2023-1-1') }) + + expect(findCell(wrapper, '2021')?.classes()).not.toContain('ix-date-panel-cell-selected') + expect(findCell(wrapper, '2023')?.classes()).toContain('ix-date-panel-cell-selected') + + await wrapper.setProps({ value: [new Date('2021-1-1'), new Date('2025-1-1')] }) + expect(findCell(wrapper, '2021')?.classes()).toContain('ix-date-panel-cell-selected') + expect(findCell(wrapper, '2025')?.classes()).toContain('ix-date-panel-cell-selected') + expect(findCell(wrapper, '2021')?.classes()).toContain('ix-date-panel-cell-in-range') + expect(findCell(wrapper, '2022')?.classes()).toContain('ix-date-panel-cell-in-range') + expect(findCell(wrapper, '2023')?.classes()).toContain('ix-date-panel-cell-in-range') + expect(findCell(wrapper, '2024')?.classes()).toContain('ix-date-panel-cell-in-range') + expect(findCell(wrapper, '2025')?.classes()).toContain('ix-date-panel-cell-in-range') + }) + + test('date type disabledDate work', async () => { + const wrapper = DatePanelMount({ + props: { + type: 'date', + value: new Date('2021-10-01'), + activeDate: new Date('2021-10-1'), + disabledDate: date => date.getMonth() === 9 && [10, 11, 12].includes(date.getDate()), + }, + }) + + expect(findCell(wrapper, '9')?.classes()).not.toContain('ix-date-panel-cell-disabled') + expect(findCell(wrapper, '10')?.classes()).toContain('ix-date-panel-cell-disabled') + expect(findCell(wrapper, '11')?.classes()).toContain('ix-date-panel-cell-disabled') + expect(findCell(wrapper, '12')?.classes()).toContain('ix-date-panel-cell-disabled') + expect(findCell(wrapper, '13')?.classes()).not.toContain('ix-date-panel-cell-disabled') + + expect(wrapper.html()).toMatchSnapshot() + }) + + test('month type disabledDate work', async () => { + const wrapper = DatePanelMount({ + props: { + type: 'month', + value: new Date('2021-10-01'), + activeDate: new Date('2021-10-1'), + disabledDate: date => [10, 11, 12].includes(date.getMonth() + 1), + }, + }) + + expect(findCell(wrapper, '9月')?.classes()).not.toContain('ix-date-panel-cell-disabled') + expect(findCell(wrapper, '10月')?.classes()).toContain('ix-date-panel-cell-disabled') + expect(findCell(wrapper, '11月')?.classes()).toContain('ix-date-panel-cell-disabled') + expect(findCell(wrapper, '12月')?.classes()).toContain('ix-date-panel-cell-disabled') + + expect(wrapper.html()).toMatchSnapshot() }) - test('disabledDate work', async () => { - const wrapper = DatePanelMount({ props: { value: new Date('2021-10-01'), disabledDate: () => true } }) + test('year type disabledDate work', async () => { + const wrapper = DatePanelMount({ + props: { + type: 'year', + value: new Date('2021-10-01'), + activeDate: new Date('2021-10-1'), + disabledDate: date => [2019, 2020, 2022].includes(date.getFullYear()), + }, + }) + + expect(findCell(wrapper, '2019')?.classes()).toContain('ix-date-panel-cell-disabled') + expect(findCell(wrapper, '2020')?.classes()).toContain('ix-date-panel-cell-disabled') + expect(findCell(wrapper, '2022')?.classes()).toContain('ix-date-panel-cell-disabled') + expect(findCell(wrapper, '2021')?.classes()).not.toContain('ix-date-panel-cell-disabled') + expect(findCell(wrapper, '2023')?.classes()).not.toContain('ix-date-panel-cell-disabled') expect(wrapper.html()).toMatchSnapshot() }) + + test('activeDate work', async () => { + const wrapper = DatePanelMount({ + props: { + type: 'date', + value: new Date('2021-10-01'), + activeDate: new Date('2021-10-1'), + }, + }) + + expect(wrapper.find('.ix-date-panel-header-content').text()).toContain('2021') + + await wrapper.setProps({ activeDate: new Date('2020-10-1') }) + + expect(wrapper.find('.ix-date-panel-header-content').text()).not.toContain('2021') + expect(wrapper.find('.ix-date-panel-header-content').text()).toContain('2020') + }) + + test('onCellClick onCellMouseenter work', async () => { + const onCellClick = vi.fn() + const onCellMouseenter = vi.fn() + const wrapper = DatePanelMount({ + props: { + type: 'date', + value: new Date('2021-10-01'), + activeDate: new Date('2021-10-1'), + disabledDate: date => date.getDate() === 10, + onCellClick, + onCellMouseenter, + }, + }) + + await findCell(wrapper, '15')?.trigger('click') + await findCell(wrapper, '15')?.trigger('mouseenter') + expect(onCellClick).toBeCalled() + expect(onCellMouseenter).toBeCalled() + + onCellClick.mockClear() + onCellMouseenter.mockClear() + + await findCell(wrapper, '10')?.trigger('click') + await findCell(wrapper, '10')?.trigger('mouseenter') + expect(onCellClick).not.toBeCalled() + expect(onCellMouseenter).not.toBeCalled() + }) }) diff --git a/packages/components/_private/date-panel/index.ts b/packages/components/_private/date-panel/index.ts index 589c62f62..2d027eaef 100644 --- a/packages/components/_private/date-panel/index.ts +++ b/packages/components/_private/date-panel/index.ts @@ -13,4 +13,8 @@ const ɵDatePanel = DatePanel as unknown as DatePanelComponent export { ɵDatePanel } -export type { DatePanelInstance as ɵDatePanelInstance, DatePanelPublicProps as ɵDatePanelProps } from './src/types' +export type { + DatePanelType as ɵDatePanelType, + DatePanelInstance as ɵDatePanelInstance, + DatePanelPublicProps as ɵDatePanelProps, +} from './src/types' diff --git a/packages/components/_private/date-panel/src/DatePanel.tsx b/packages/components/_private/date-panel/src/DatePanel.tsx index 5e04eb4c7..d091d1f4a 100644 --- a/packages/components/_private/date-panel/src/DatePanel.tsx +++ b/packages/components/_private/date-panel/src/DatePanel.tsx @@ -50,7 +50,6 @@ export default defineComponent({
- {slots.footer &&
{slots.footer()}
}
) } diff --git a/packages/components/_private/date-panel/src/composables/useActiveDate.ts b/packages/components/_private/date-panel/src/composables/useActiveDate.ts index 7586234f5..842bcd771 100644 --- a/packages/components/_private/date-panel/src/composables/useActiveDate.ts +++ b/packages/components/_private/date-panel/src/composables/useActiveDate.ts @@ -7,9 +7,10 @@ import type { DatePanelProps, DatePanelType } from '../types' import type { DateConfig } from '@idux/components/config' -import type { ComputedRef } from 'vue' -import { computed, shallowRef, watch } from 'vue' +import { type ComputedRef, computed } from 'vue' + +import { useControlledProp } from '@idux/cdk/utils' export interface ActiveDateContext { activeDate: ComputedRef @@ -17,34 +18,15 @@ export interface ActiveDateContext { startActiveDate: ComputedRef } -const monthTypes: DatePanelType[] = ['date', 'week'] - export function useActiveDate( props: DatePanelProps, dateConfig: DateConfig, activeType: ComputedRef, ): ActiveDateContext { - const tempDate = shallowRef(props.value ?? dateConfig.now()) - - const setActiveDate = (date: Date) => { - const type = monthTypes.includes(activeType.value) ? 'month' : 'year' - if (!dateConfig.isSame(date, tempDate.value, type)) { - tempDate.value = date - } - } - - watch( - () => props.value, - value => value && setActiveDate(value), - ) - - watch( - () => props.visible, - visible => visible && props.value && setActiveDate(props.value), - ) + const [activeDate, setActiveDate] = useControlledProp(props, 'activeDate', () => dateConfig.now()) const startActiveDate = computed(() => { - const currDate = tempDate.value + const currDate = activeDate.value const currType = activeType.value const { startOf, set, get } = dateConfig @@ -65,7 +47,7 @@ export function useActiveDate( }) return { - activeDate: computed(() => tempDate.value), + activeDate, setActiveDate, startActiveDate, } diff --git a/packages/components/_private/date-panel/src/panel-body/PanelBody.tsx b/packages/components/_private/date-panel/src/panel-body/PanelBody.tsx index 9b6b9e26e..4046ad2b2 100644 --- a/packages/components/_private/date-panel/src/panel-body/PanelBody.tsx +++ b/packages/components/_private/date-panel/src/panel-body/PanelBody.tsx @@ -66,9 +66,8 @@ function useTheadCells( ) { return computed(() => { const currType = activeType.value - const isWeek = currType === 'week' - const cols: TheadCell[] = isWeek ? [{ key: -1 }] : [] - if (currType === 'date' || isWeek) { + const cols: TheadCell[] = [] + if (currType === 'date' || currType === 'week') { const maxIndex = maxCellIndex.value const labels = dateConfig.getLocalizedLabels('day', maxIndex, 'narrow') const weekStarts = dateConfig.weekStartsOn() diff --git a/packages/components/_private/date-panel/src/panel-body/PanelCell.tsx b/packages/components/_private/date-panel/src/panel-body/PanelCell.tsx index cd16de078..a79a62afb 100644 --- a/packages/components/_private/date-panel/src/panel-body/PanelCell.tsx +++ b/packages/components/_private/date-panel/src/panel-body/PanelCell.tsx @@ -9,7 +9,9 @@ import type { DatePanelType } from '../types' import { computed, defineComponent, inject, normalizeClass } from 'vue' -import { callEmit } from '@idux/cdk/utils' +import { callEmit, convertArray } from '@idux/cdk/utils' +import { DateConfig } from '@idux/components/config' +import { IxTooltip } from '@idux/components/tooltip' import { datePanelToken } from '../token' import { panelCellProps } from '../types' @@ -40,17 +42,22 @@ export default defineComponent({ maxCellIndex, } = inject(datePanelToken)! - const offsetIndex = computed(() => props.rowIndex * maxCellIndex.value + props.cellIndex) + const offsetIndex = computed(() => props.rowIndex! * maxCellIndex.value + props.cellIndex!) const cellDate = computed(() => { const currType = activeType.value const offsetUnit = dayTypes.includes(currType) ? 'day' : currType return dateConfig.add(startActiveDate.value, offsetIndex.value, offsetUnit as 'day') }) + + const startDate = computed(() => getDateValue(dateConfig, panelProps.value, activeType.value, true)) + const endDate = computed(() => getDateValue(dateConfig, panelProps.value, activeType.value, false)) + const isDisabled = computed(() => panelProps.disabledDate?.(cellDate.value)) - const isSelected = computed(() => { - const currValue = panelProps.value - return currValue && dateConfig.isSame(currValue, cellDate.value, activeType.value) - }) + const cellTooltip = computed(() => + panelProps.cellTooltip?.({ value: cellDate.value, disabled: !!isDisabled.value }), + ) + const isStart = computed(() => startDate.value && dateConfig.isSame(startDate.value, cellDate.value, 'date')) + const isEnd = computed(() => endDate.value && dateConfig.isSame(endDate.value, cellDate.value, 'date')) const isToday = computed( () => dayTypes.includes(activeType.value) && dateConfig.isSame(cellDate.value, dateConfig.now(), 'day'), ) @@ -65,22 +72,46 @@ export default defineComponent({ } return false }) + const isSelected = computed(() => { + if (outView.value) { + return false + } + + const compareType = dayTypes.includes(activeType.value) ? 'date' : activeType.value + + if (panelProps.isSelecting) { + return startDate.value && dateConfig.isSame(startDate.value, cellDate.value, compareType) + } + + return ( + (startDate.value && dateConfig.isSame(startDate.value, cellDate.value, compareType)) || + (endDate.value && dateConfig.isSame(endDate.value, cellDate.value, compareType)) + ) + }) + const isInRange = computed(() => { + const compareType = dayTypes.includes(activeType.value) ? 'date' : activeType.value + const cellDateValue = dateConfig.startOf(cellDate.value, compareType).valueOf() + + return ( + !!startDate.value && + !!endDate.value && + cellDateValue >= dateConfig.startOf(startDate.value, compareType).valueOf() && + cellDateValue <= dateConfig.startOf(endDate.value, compareType).valueOf() + ) + }) const classes = computed(() => { const prefixCls = `${mergedPrefixCls.value}-cell` - if (props.isWeek) { - return normalizeClass({ - [prefixCls]: true, - [`${prefixCls}-selected`]: isSelected.value, - [`${prefixCls}-week-number`]: true, - }) - } + return normalizeClass({ [prefixCls]: true, [`${prefixCls}-disabled`]: isDisabled.value, [`${prefixCls}-selected`]: isSelected.value, + [`${prefixCls}-in-range`]: isInRange.value, [`${prefixCls}-today`]: isToday.value, [`${prefixCls}-out-view`]: outView.value, + [`${prefixCls}-start`]: isStart.value, + [`${prefixCls}-end`]: isEnd.value, }) }) @@ -102,16 +133,11 @@ export default defineComponent({ return () => { const currDate = cellDate.value const { format } = dateConfig - if (props.isWeek) { - return ( - - {format(currDate, 'ww')} - - ) - } const cellNode = slots.cell?.({ date: currDate }) ?? ( -
{format(currDate, labelFormat[activeType.value])}
+
+
{format(currDate, labelFormat[activeType.value])}
+
) return ( - {cellNode} + {cellTooltip.value ? {cellNode} : cellNode} ) } }, }) + +function getDateValue( + dateConfig: DateConfig, + value: Date | (Date | undefined)[] | undefined, + type: DatePanelType, + isStart: boolean, +) { + const valueArr = convertArray(value) + if (type === 'week') { + return isStart ? dateConfig.startOf(valueArr[0], 'week') : dateConfig.endOf(valueArr[1] ?? valueArr[0], 'week') + } + + return valueArr[isStart ? 0 : 1] +} diff --git a/packages/components/_private/date-panel/src/panel-body/PanelRow.tsx b/packages/components/_private/date-panel/src/panel-body/PanelRow.tsx index 32b17206f..32f83f4bf 100644 --- a/packages/components/_private/date-panel/src/panel-body/PanelRow.tsx +++ b/packages/components/_private/date-panel/src/panel-body/PanelRow.tsx @@ -15,7 +15,6 @@ interface TbodyCell { key: string rowIndex: number cellIndex: number - isWeek?: boolean } export default defineComponent({ @@ -26,11 +25,10 @@ export default defineComponent({ const cells = computed(() => { const { rowIndex } = props const currType = activeType.value - const isWeek = currType === 'week' - const cells: TbodyCell[] = isWeek ? [{ key: `${currType}-${-1}`, rowIndex, cellIndex: 0, isWeek }] : [] + const cells: TbodyCell[] = [] const maxIndex = maxCellIndex.value for (let cellIndex = 0; cellIndex < maxIndex; cellIndex++) { - cells.push({ key: `${currType}-${cellIndex}`, rowIndex, cellIndex }) + cells.push({ key: `${currType}-${cellIndex}`, rowIndex: rowIndex!, cellIndex }) } return cells }) diff --git a/packages/components/_private/date-panel/src/types.ts b/packages/components/_private/date-panel/src/types.ts index 0821f8b96..2062e256e 100644 --- a/packages/components/_private/date-panel/src/types.ts +++ b/packages/components/_private/date-panel/src/types.ts @@ -5,19 +5,30 @@ * found in the LICENSE file at https://github.com/IDuxFE/idux/blob/main/LICENSE */ -import type { ExtractInnerPropTypes, ExtractPublicPropTypes } from '@idux/cdk/utils' -import type { DefineComponent, HTMLAttributes } from 'vue' - -import { IxPropTypes } from '@idux/cdk/utils' +import type { ExtractInnerPropTypes, ExtractPublicPropTypes, MaybeArray } from '@idux/cdk/utils' +import type { DefineComponent, HTMLAttributes, PropType } from 'vue' export const datePanelProps = { - disabledDate: IxPropTypes.func<(date: Date) => boolean>(), - type: IxPropTypes.oneOf(['date', 'week', 'month', 'quarter', 'year']).def('date'), - value: IxPropTypes.object(), - visible: IxPropTypes.bool, - - onCellClick: IxPropTypes.emit<(date: Date) => void>(), - onCellMouseenter: IxPropTypes.emit<(date: Date) => void>(), + cellTooltip: Function as PropType<(cell: { value: Date; disabled: boolean }) => string | void>, + disabledDate: Function as PropType<(date: Date) => boolean>, + type: { + type: String as PropType, + default: 'date', + }, + value: [Date, Array] as PropType, + activeDate: Date, + visible: { + type: Boolean, + default: undefined, + }, + isSelecting: { + type: Boolean, + default: undefined, + }, + + onCellClick: [Function, Array] as PropType void>>, + onCellMouseenter: [Function, Array] as PropType void>>, + 'onUpdate:activeDate': [Function, Array] as PropType void>>, } export type DatePanelProps = ExtractInnerPropTypes @@ -31,11 +42,19 @@ export type DatePanelType = 'date' | 'week' | 'month' | 'quarter' | 'year' // private export const panelRowProps = { - rowIndex: IxPropTypes.number.isRequired, + rowIndex: { + type: Number, + required: true, + }, } export const panelCellProps = { - rowIndex: IxPropTypes.number.isRequired, - cellIndex: IxPropTypes.number.isRequired, - isWeek: IxPropTypes.bool, + rowIndex: { + type: Number, + required: true, + }, + cellIndex: { + type: Number, + required: true, + }, } diff --git a/packages/components/_private/footer/index.ts b/packages/components/_private/footer/index.ts index 8bd7fdbe2..c2fa57f2e 100644 --- a/packages/components/_private/footer/index.ts +++ b/packages/components/_private/footer/index.ts @@ -10,5 +10,5 @@ import Footer from './src/Footer' const ɵFooter = Footer export { ɵFooter } - +export { footerTypeDef as ɵFooterTypeDef } from './src/types' export type { FooterProps as ɵFooterProps, FooterButtonProps as ɵFooterButtonProps } from './src/types' diff --git a/packages/components/_private/footer/src/types.ts b/packages/components/_private/footer/src/types.ts index dda9b0d09..9c74cc0a7 100644 --- a/packages/components/_private/footer/src/types.ts +++ b/packages/components/_private/footer/src/types.ts @@ -7,9 +7,7 @@ import type { ExtractInnerPropTypes, ExtractPublicPropTypes, VKey } from '@idux/cdk/utils' import type { ButtonProps } from '@idux/components/button' -import type { DefineComponent, HTMLAttributes, VNode } from 'vue' - -import { IxPropTypes } from '@idux/cdk/utils' +import type { DefineComponent, HTMLAttributes, PropType, VNode } from 'vue' export interface FooterButtonProps extends ButtonProps { key?: VKey @@ -17,17 +15,22 @@ export interface FooterButtonProps extends ButtonProps { onClick?: (evt: Event) => void } +export const footerTypeDef = [Boolean, Array, Object] as PropType + export const footerProps = { - cancel: IxPropTypes.func<(evt?: Event | unknown) => Promise>(), - cancelButton: IxPropTypes.object(), - cancelLoading: IxPropTypes.bool, - cancelText: IxPropTypes.string, - cancelVisible: IxPropTypes.bool.def(true), - footer: IxPropTypes.oneOfType([Boolean, IxPropTypes.array(), IxPropTypes.vNode]), - ok: IxPropTypes.func<(evt?: Event | unknown) => Promise>(), - okButton: IxPropTypes.object(), - okLoading: IxPropTypes.bool, - okText: IxPropTypes.string, + cancel: Function as PropType<(evt?: Event | unknown) => Promise | void>, + cancelButton: Object as PropType, + cancelLoading: Boolean, + cancelText: String, + cancelVisible: { + type: Boolean, + default: true, + }, + footer: footerTypeDef, + ok: Function as PropType<(evt?: Event | unknown) => Promise | void>, + okButton: Object as PropType, + okLoading: Boolean, + okText: String, } export type FooterProps = ExtractInnerPropTypes diff --git a/packages/components/_private/time-panel/__tests__/useSelectorScroll.spec.ts b/packages/components/_private/time-panel/__tests__/useSelectorScroll.spec.ts index 194243cbc..623406958 100644 --- a/packages/components/_private/time-panel/__tests__/useSelectorScroll.spec.ts +++ b/packages/components/_private/time-panel/__tests__/useSelectorScroll.spec.ts @@ -117,15 +117,15 @@ describe('usePanelScroll', () => { props.visible = false await wait() props.visible = true - await wait(300) + await wait(400) expect(wrapper.element.scrollTop).toBe(cellHeight) props.selectedValue = 3 - await wait(300) + await wait(400) expect(wrapper.element.scrollTop).toBe(2 * cellHeight) props.selectedValue = 1 - await wait(300) + await wait(400) expect(wrapper.element.scrollTop).toBe(0) }) diff --git a/packages/components/_private/time-panel/src/composables/usePanelScroll.ts b/packages/components/_private/time-panel/src/composables/usePanelScroll.ts index 0be86d0a2..bc80b3054 100644 --- a/packages/components/_private/time-panel/src/composables/usePanelScroll.ts +++ b/packages/components/_private/time-panel/src/composables/usePanelScroll.ts @@ -47,7 +47,16 @@ export function usePanelScroll( } scrollHandlerLocked = true - scrollToTop({ top, target, duration, callback: () => (scrollHandlerLocked = false) }) + scrollToTop({ + top, + target, + duration, + callback: () => { + setTimeout(() => { + scrollHandlerLocked = false + }, 100) + }, + }) } function scrollToSelected(duration?: number) { diff --git a/packages/components/_private/time-panel/style/index.less b/packages/components/_private/time-panel/style/index.less index 32a289d67..6f8ed501b 100644 --- a/packages/components/_private/time-panel/style/index.less +++ b/packages/components/_private/time-panel/style/index.less @@ -6,6 +6,7 @@ height: @time-panel-height; padding: @time-panel-padding-vertical @time-panel-padding-horizontal; font-size: @time-panel-font-size; + background-color: @time-panel-background-color; ::-webkit-scrollbar { width: @time-panel-scrollbar-width; diff --git a/packages/components/_private/time-panel/style/themes/default.variable.less b/packages/components/_private/time-panel/style/themes/default.variable.less index aae218709..46411aaa9 100644 --- a/packages/components/_private/time-panel/style/themes/default.variable.less +++ b/packages/components/_private/time-panel/style/themes/default.variable.less @@ -4,6 +4,7 @@ @time-panel-padding-horizontal: @spacing-lg; @time-panel-padding-vertical: @spacing-sm; @time-panel-font-size: @font-size-md; +@time-panel-background-color: @background-color-component; @time-panel-window-border-width: @form-border-width; @time-panel-window-border-style: @form-border-style; diff --git a/packages/components/_private/trigger/__tests__/__snapshots__/trigger.spec.ts.snap b/packages/components/_private/trigger/__tests__/__snapshots__/trigger.spec.ts.snap new file mode 100644 index 000000000..2ecbcd836 --- /dev/null +++ b/packages/components/_private/trigger/__tests__/__snapshots__/trigger.spec.ts.snap @@ -0,0 +1,9 @@ +// Vitest Snapshot v1 + +exports[`Trigger > render work 1`] = ` +"
+ + + +
" +`; diff --git a/packages/components/_private/trigger/__tests__/trigger.spec.ts b/packages/components/_private/trigger/__tests__/trigger.spec.ts new file mode 100644 index 000000000..ca0d1d6d1 --- /dev/null +++ b/packages/components/_private/trigger/__tests__/trigger.spec.ts @@ -0,0 +1,146 @@ +import { MountingOptions, mount } from '@vue/test-utils' +import { h } from 'vue' + +import { renderWork } from '@tests' + +import Trigger from '../src/Trigger' +import { TriggerProps } from '../src/types' + +describe('Trigger', () => { + const TriggerMount = (options?: MountingOptions>) => + mount(Trigger, { ...(options as MountingOptions) }) + + renderWork(Trigger, { + props: {}, + }) + + test('borderless work', async () => { + const wrapper = TriggerMount({ props: { borderless: true } }) + + expect(wrapper.classes()).toContain('ix-trigger-borderless') + + await wrapper.setProps({ borderless: false }) + + expect(wrapper.classes()).not.toContain('ix-trigger-borderless') + }) + + test('focused work', async () => { + const wrapper = TriggerMount({ props: { focused: true } }) + + expect(wrapper.classes()).toContain('ix-trigger-focused') + + await wrapper.setProps({ focused: false }) + + expect(wrapper.classes()).not.toContain('ix-trigger-focused') + }) + + test('readonly work', async () => { + const wrapper = TriggerMount({ props: { readonly: true } }) + + expect(wrapper.classes()).toContain('ix-trigger-readonly') + + await wrapper.setProps({ readonly: false }) + + expect(wrapper.classes()).not.toContain('ix-trigger-readonly') + }) + + test('disabled work', async () => { + const wrapper = TriggerMount({ props: { disabled: true } }) + + expect(wrapper.classes()).toContain('ix-trigger-disabled') + + await wrapper.setProps({ disabled: false }) + + expect(wrapper.classes()).not.toContain('ix-trigger-disabled') + }) + + test('clearable work', async () => { + const wrapper = TriggerMount({ props: { clearable: true, clearIcon: 'clear' } }) + + expect(wrapper.find('.ix-trigger-clear-icon').exists()).toBeTruthy() + + await wrapper.setProps({ clearable: false }) + + expect(wrapper.find('.ix-trigger-clear-icon').exists()).toBeFalsy() + }) + + test('clearIcon work', async () => { + const wrapper = TriggerMount({ props: { clearable: true, clearIcon: 'clear' } }) + + expect(wrapper.find('.ix-icon-clear').exists()).toBeTruthy() + + await wrapper.setProps({ clearIcon: 'close' }) + + expect(wrapper.find('.ix-icon-clear').exists()).toBeFalsy() + expect(wrapper.find('.ix-icon-close').exists()).toBeTruthy() + }) + + test('slot clearIcon work', async () => { + const wrapper = TriggerMount({ + props: { clearable: true }, + slots: { + clearIcon: () => h('span', { class: 'custom-clear-icon-slot' }), + }, + }) + + expect(wrapper.find('.custom-clear-icon-slot').exists()).toBeTruthy() + }) + + test('suffix work', async () => { + const wrapper = TriggerMount({ props: { suffix: 'suffix' } }) + + expect(wrapper.find('.ix-trigger-suffix').exists()).toBeTruthy() + expect(wrapper.find('.ix-icon-suffix').exists()).toBeTruthy() + }) + + test('slot suffix work', async () => { + const wrapper = TriggerMount({ + props: { clearable: true }, + slots: { + clearIcon: () => h('span', { class: 'custom-suffix-slot' }), + }, + }) + + expect(wrapper.find('.custom-suffix-slot').exists()).toBeTruthy() + }) + + test('onClick work', async () => { + const onClick = vi.fn() + const wrapper = TriggerMount({ props: { onClick } }) + + await wrapper.trigger('click') + expect(onClick).toBeCalled() + + onClick.mockClear() + + await wrapper.setProps({ disabled: true }) + await wrapper.trigger('click') + expect(onClick).not.toBeCalled() + }) + + test('onClear work', async () => { + const onClear = vi.fn() + const wrapper = TriggerMount({ props: { clearable: true, clearIcon: 'clear', onClear } }) + + await wrapper.find('.ix-trigger-clear-icon').trigger('click') + expect(onClear).toBeCalled() + + await wrapper.setProps({ disabled: true }) + expect(wrapper.find('.ix-trigger-clear-icon').exists()).toBeFalsy() + }) + + test('onKeyDown work', async () => { + const onKeyDown = vi.fn() + + const wrapper = TriggerMount({ props: { onKeyDown } }) + + await wrapper.trigger('keydown') + expect(onKeyDown).toBeCalled() + + onKeyDown.mockClear() + + await wrapper.setProps({ disabled: true }) + await wrapper.trigger('keydown') + expect(onKeyDown).not.toBeCalled() + }) +}) diff --git a/packages/components/_private/trigger/index.ts b/packages/components/_private/trigger/index.ts new file mode 100644 index 000000000..7fb6894fb --- /dev/null +++ b/packages/components/_private/trigger/index.ts @@ -0,0 +1,20 @@ +/** + * @license + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://github.com/IDuxFE/idux/blob/main/LICENSE + */ + +import type { TriggerComponent } from './src/types' + +import Trigger from './src/Trigger' + +const ɵTrigger = Trigger as unknown as TriggerComponent + +export { ɵTrigger } + +export type { + TriggerInstance as ɵTriggerInstance, + TriggerComponent as ɵTriggerComponent, + TriggerPublicProps as ɵTriggerProps, +} from './src/types' diff --git a/packages/components/_private/trigger/src/Trigger.tsx b/packages/components/_private/trigger/src/Trigger.tsx new file mode 100644 index 000000000..7581ece86 --- /dev/null +++ b/packages/components/_private/trigger/src/Trigger.tsx @@ -0,0 +1,110 @@ +/** + * @license + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://github.com/IDuxFE/idux/blob/main/LICENSE + */ + +import { computed, defineComponent, normalizeClass, onBeforeUnmount, onMounted, ref, watch } from 'vue' + +import { useSharedFocusMonitor } from '@idux/cdk/a11y' +import { callEmit } from '@idux/cdk/utils' +import { useGlobalConfig } from '@idux/components/config' +import { IxIcon } from '@idux/components/icon' + +import { triggerProps } from './types' + +export default defineComponent({ + props: triggerProps, + setup(props, { slots }) { + const common = useGlobalConfig('common') + const mergedPrefixCls = computed(() => `${common.prefixCls}-trigger`) + + const isDisabled = computed(() => props.disabled) + + const focusMonitor = useSharedFocusMonitor() + const triggerRef = ref() + onMounted(() => { + watch(focusMonitor.monitor(triggerRef.value!, true), evt => { + const { origin, event } = evt + if (event) { + if (origin) { + callEmit(props.onFocus, event) + } else { + callEmit(props.onBlur, event) + } + } + }) + }) + + onBeforeUnmount(() => focusMonitor.stopMonitoring(triggerRef.value!)) + + const classes = computed(() => { + const prefixCls = mergedPrefixCls.value + return normalizeClass({ + [`${props.className}`]: !!props.className, + [prefixCls]: true, + [`${prefixCls}-disabled`]: isDisabled.value, + [`${prefixCls}-borderless`]: props.borderless, + [`${prefixCls}-readonly`]: props.readonly, + [`${prefixCls}-focused`]: props.focused, + [`${prefixCls}-${props.size}`]: props.size, + }) + }) + + const handleClick = (evt: Event) => { + if (isDisabled.value) { + return + } + + callEmit(props.onClick, evt) + } + + const handleKeyDown = (evt: KeyboardEvent) => { + if (isDisabled.value) { + return + } + + callEmit(props.onKeyDown, evt) + } + + const handleClear = (evt: MouseEvent) => { + if (isDisabled.value) { + return + } + + evt.stopPropagation() + callEmit(props.onClear, evt) + } + const renderSuffix = () => { + if (!(slots.suffix || props.suffix)) { + return null + } + + return ( +
+ {slots.suffix?.() ?? (props.suffix && )} +
+ ) + } + const renderClearIcon = () => { + if (!props.clearable || isDisabled.value || (!props.clearIcon && !slots.clearIcon)) { + return null + } + + return ( + + {slots.clearIcon ? slots.clearIcon() : props.clearIcon && } + + ) + } + + return () => ( +
+ {slots.default?.()} + {renderSuffix()} + {renderClearIcon()} +
+ ) + }, +}) diff --git a/packages/components/_private/trigger/src/types.ts b/packages/components/_private/trigger/src/types.ts new file mode 100644 index 000000000..f8682fd36 --- /dev/null +++ b/packages/components/_private/trigger/src/types.ts @@ -0,0 +1,33 @@ +/** + * @license + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://github.com/IDuxFE/idux/blob/main/LICENSE + */ + +import type { FormSize } from '@idux/components/form' +import type { DefineComponent, HTMLAttributes, PropType } from 'vue' + +import { ExtractInnerPropTypes, ExtractPublicPropTypes, MaybeArray } from '@idux/cdk/utils' + +export const triggerProps = { + borderless: Boolean as PropType, + clearable: Boolean as PropType, + clearIcon: String as PropType, + className: String as PropType, + disabled: Boolean as PropType, + focused: Boolean as PropType, + readonly: Boolean as PropType, + size: String as PropType, + suffix: String as PropType, + onClick: [Array, Function] as PropType void>>, + onClear: [Array, Function] as PropType void>>, + onFocus: [Array, Function] as PropType void>>, + onBlur: [Array, Function] as PropType void>>, + onKeyDown: [Array, Function] as PropType void>>, +} + +export type TriggerProps = ExtractInnerPropTypes +export type TriggerPublicProps = ExtractPublicPropTypes +export type TriggerComponent = DefineComponent & TriggerPublicProps> +export type TriggerInstance = InstanceType> diff --git a/packages/components/_private/trigger/style/index.less b/packages/components/_private/trigger/style/index.less new file mode 100644 index 000000000..0896877fa --- /dev/null +++ b/packages/components/_private/trigger/style/index.less @@ -0,0 +1,109 @@ +@import '../../../style/mixins/reset.less'; +@import '../../../style/mixins/borderless.less'; +@import '../../../style/mixins/placeholder.less'; + +.trigger-size(@height, @vertical-padding, @horizontal-padding, @font-size) { + @icon-padding: @trigger-icon-margin-left + @trigger-icon-margin-right; + + height: @height; + padding: @vertical-padding (@icon-padding + @font-size) @vertical-padding @horizontal-padding; + font-size: @font-size; +} + +.@{trigger-prefix} { + .reset-component(); + + position: relative; + width: 100%; + line-height: @trigger-line-height; + color: @trigger-color; + border: @trigger-border-width @trigger-border-style @trigger-border-color; + border-radius: @trigger-border-radius; + background-color: @trigger-background-color; + cursor: pointer; + transition: all @transition-duration-base @ease-in-out; + + @icon-padding: @trigger-icon-margin-left + @trigger-icon-margin-right; + + &-sm { + .trigger-size(@trigger-height-sm, @trigger-padding-vertical-sm, @trigger-padding-horizontal-sm, @trigger-font-size-sm); + } + + &-md { + .trigger-size(@trigger-height-md, @trigger-padding-vertical-md, @trigger-padding-horizontal-md, @trigger-font-size-md); + } + + &-lg { + .trigger-size(@trigger-height-lg, @trigger-padding-vertical-lg, @trigger-padding-horizontal-lg, @trigger-font-size-lg); + } + + &:hover:not(&-disabled):not(&-borderless) { + border-color: @trigger-hover-color; + } + + &-opened:not(&-disabled):not(&-borderless), + &-focused:not(&-disabled):not(&-borderless) { + border-color: @trigger-active-color; + box-shadow: @trigger-active-box-shadow; + } + + &-disabled { + color: @trigger-disabled-color; + background-color: @trigger-disabled-background-color; + cursor: not-allowed; + } + + &-disabled input { + cursor: not-allowed; + } + + &-borderless { + .borderless(); + } + + &-placeholder { + color: @trigger-placeholder-color; + } + + &-suffix, + &-clear-icon { + position: absolute; + top: 0; + right: @trigger-icon-margin-right; + display: flex; + align-items: center; + height: 100%; + transition: @transition-all-base; + } + + &-suffix { + color: @trigger-icon-color; + } + + &-clear-icon { + cursor: pointer; + color: @trigger-clear-icon-color; + background-color: @trigger-background-color; + opacity: 0; + &:hover { + color: @trigger-clear-icon-hover-color; + } + } + + &:not(&-disabled):hover &-clear-icon { + opacity: 1; + } + + & input { + width: 100%; + margin: 0; + padding: 0; + background: transparent; + border: none; + outline: none; + appearance: none; + color: inherit; + font-size: inherit; + .placeholder(@trigger-placeholder-color); + } +} \ No newline at end of file diff --git a/packages/components/_private/trigger/style/themes/default.less b/packages/components/_private/trigger/style/themes/default.less new file mode 100644 index 000000000..a9c9877eb --- /dev/null +++ b/packages/components/_private/trigger/style/themes/default.less @@ -0,0 +1,3 @@ +@import '../index.less'; +@import '../../../../form/style/themes/default.variable.less'; +@import './default.variable.less'; diff --git a/packages/components/_private/trigger/style/themes/default.ts b/packages/components/_private/trigger/style/themes/default.ts new file mode 100644 index 000000000..027ca3f89 --- /dev/null +++ b/packages/components/_private/trigger/style/themes/default.ts @@ -0,0 +1,4 @@ +// style dependencies +import '@idux/components/style/core/default' + +import './default.less' diff --git a/packages/components/_private/trigger/style/themes/default.variable.less b/packages/components/_private/trigger/style/themes/default.variable.less new file mode 100644 index 000000000..236a280f4 --- /dev/null +++ b/packages/components/_private/trigger/style/themes/default.variable.less @@ -0,0 +1,41 @@ +@import '../../../../style/themes/default.less'; + +@trigger-font-size-sm: @form-font-size-sm; +@trigger-font-size-md: @form-font-size-md; +@trigger-font-size-lg: @form-font-size-lg; +@trigger-line-height: @form-line-height; +@trigger-height-sm: @form-height-sm; +@trigger-height-md: @form-height-md; +@trigger-height-lg: @form-height-lg; +@trigger-padding-horizontal-sm: @form-padding-horizontal-sm; +@trigger-padding-horizontal-md: @form-padding-horizontal-md; +@trigger-padding-horizontal-lg: @form-padding-horizontal-lg; +@trigger-padding-vertical-sm: @form-padding-vertical-sm; +@trigger-padding-vertical-md: @form-padding-vertical-md; +@trigger-padding-vertical-lg: @form-padding-vertical-lg; + +@trigger-border-width: @form-border-width; +@trigger-border-style: @form-border-style; +@trigger-border-color: @form-border-color; +@trigger-border-radius: @border-radius-sm; + +@trigger-color: @form-color; +@trigger-disabled-color: @form-disabled-color; + +@trigger-color: @form-color; +@trigger-color-secondary: @form-color-secondary; +@trigger-background-color: @form-background-color; +@trigger-placeholder-color: @form-placeholder-color; +@trigger-hover-color: @form-hover-color; +@trigger-active-color: @form-active-color; +@trigger-active-box-shadow: @form-active-box-shadow; +@trigger-disabled-color: @form-disabled-color; +@trigger-disabled-background-color: @form-disabled-background-color; + +@trigger-icon-font-size: @font-size-sm; +@trigger-icon-margin-left: @spacing-xs; +@trigger-icon-margin-right: @spacing-xs; +@trigger-icon-color: @trigger-placeholder-color; +@trigger-clear-icon-color: @trigger-color-secondary; +@trigger-clear-icon-hover-color: @trigger-color; +@trigger-icon-background-color: @trigger-background-color; diff --git a/packages/components/config/src/defaultConfig.ts b/packages/components/config/src/defaultConfig.ts index 346431fc7..d9a7ea510 100644 --- a/packages/components/config/src/defaultConfig.ts +++ b/packages/components/config/src/defaultConfig.ts @@ -68,16 +68,13 @@ export const defaultConfig: GlobalConfig = { ghost: false, }, datePicker: { - allowInput: true, + allowInput: false, borderless: false, clearable: false, clearIcon: 'close-circle', size: 'md', suffix: 'calendar', }, - dateRangePicker: { - separator: 'swap-right', - }, divider: { dashed: false, labelPlacement: 'center', @@ -289,7 +286,7 @@ export const defaultConfig: GlobalConfig = { clearIcon: 'close-circle', size: 'md', suffix: 'clock-circle', - allowInput: true, + allowInput: false, format: 'HH:mm:ss', }, timeRangePicker: { @@ -298,7 +295,7 @@ export const defaultConfig: GlobalConfig = { clearIcon: 'close-circle', size: 'md', suffix: 'clock-circle', - allowInput: true, + allowInput: false, format: 'HH:mm:ss', }, transfer: { diff --git a/packages/components/config/src/types.ts b/packages/components/config/src/types.ts index 00d966343..38a9b0992 100644 --- a/packages/components/config/src/types.ts +++ b/packages/components/config/src/types.ts @@ -48,7 +48,6 @@ export interface GlobalConfig { checkbox: CheckboxConfig collapse: CollapseConfig datePicker: DatePickerConfig - dateRangePicker: DateRangePickerConfig divider: DividerConfig drawer: DrawerConfig dropdown: DropdownConfig @@ -164,10 +163,6 @@ export interface DatePickerConfig { target?: PortalTargetType } -export interface DateRangePickerConfig { - separator: string | VNode -} - export interface DividerConfig { dashed: boolean plain: boolean diff --git a/packages/components/date-picker/__tests__/__snapshots__/datePicker.spec.ts.snap b/packages/components/date-picker/__tests__/__snapshots__/datePicker.spec.ts.snap index 8860b19c2..aa6782057 100644 --- a/packages/components/date-picker/__tests__/__snapshots__/datePicker.spec.ts.snap +++ b/packages/components/date-picker/__tests__/__snapshots__/datePicker.spec.ts.snap @@ -1,19 +1,10 @@ // Vitest Snapshot v1 exports[`DatePicker > render work 1`] = ` -"
-
- -
-
-" -`; - -exports[`DatePicker > v-model:value work 1`] = ` -"
-
- -
+"
+
+
+
" `; diff --git a/packages/components/date-picker/__tests__/__snapshots__/dateRangePicker.spec.ts.snap b/packages/components/date-picker/__tests__/__snapshots__/dateRangePicker.spec.ts.snap new file mode 100644 index 000000000..49514c9ec --- /dev/null +++ b/packages/components/date-picker/__tests__/__snapshots__/dateRangePicker.spec.ts.snap @@ -0,0 +1,10 @@ +// Vitest Snapshot v1 + +exports[`DateRangePicker > render work 1`] = ` +"
+
+
+ +
+" +`; diff --git a/packages/components/date-picker/__tests__/datePicker.spec.ts b/packages/components/date-picker/__tests__/datePicker.spec.ts index 513011fb7..e501cab93 100644 --- a/packages/components/date-picker/__tests__/datePicker.spec.ts +++ b/packages/components/date-picker/__tests__/datePicker.spec.ts @@ -1,13 +1,40 @@ -import { MountingOptions, mount } from '@vue/test-utils' +import { MountingOptions, VueWrapper, mount } from '@vue/test-utils' import { renderWork } from '@tests' +import { parse } from 'date-fns' +import DatePanel from '../../_private/date-panel/src/DatePanel' +import DatePanelCell from '../../_private/date-panel/src/panel-body/PanelCell' +import { ɵTimePanelInstance } from '../../_private/time-panel' +import TimePanel from '../../_private/time-panel/src/TimePanel' +import TimePanelCell from '../../_private/time-panel/src/TimePanelCell' +import TimePanelColumn from '../../_private/time-panel/src/TimePanelColumn' import DatePicker from '../src/DatePicker' -import { DatePickerProps } from '../src/types' +import Content from '../src/content/Content' +import { DatePickerInstance, DatePickerProps } from '../src/types' describe('DatePicker', () => { - const DatePickerMount = (options?: MountingOptions>) => - mount(DatePicker, { ...(options as MountingOptions) }) + const DatePickerMount = (options?: MountingOptions>) => { + const { props, ...rest } = options || {} + return mount(DatePicker, { + props: { open: true, ...props }, + ...(rest as MountingOptions), + attachTo: 'body', + }) as VueWrapper + } + + const findCell = (wrapper: VueWrapper, cellLabel: string) => + wrapper.findAllComponents(DatePanelCell).find(cell => cell.find('.ix-date-panel-cell-trigger').text() === cellLabel) + + const findTimePanelColumns = (wrapper: VueWrapper<ɵTimePanelInstance>) => wrapper.findAllComponents(TimePanelColumn) + const findTimePanelColumn = (wrapper: VueWrapper<ɵTimePanelInstance>, idx: number) => + findTimePanelColumns(wrapper)?.[idx] + + const findTimePanelCellWithValue = (wrapper: ReturnType, value: string | number) => + wrapper.findAllComponents(TimePanelCell).find(comp => comp.props().value === value) + + const findTimePanelCell = (wrapper: VueWrapper<ɵTimePanelInstance>, idx: number, value: string | number) => + findTimePanelCellWithValue(findTimePanelColumn(wrapper, idx), value) renderWork(DatePicker, { props: {}, @@ -16,10 +43,233 @@ describe('DatePicker', () => { test('v-model:value work', async () => { const onUpdateValue = vi.fn() const onChange = vi.fn() + const onUpdateVisible = vi.fn() + const wrapper = DatePickerMount({ + props: { + value: new Date('2021-10-01'), + 'onUpdate:value': onUpdateValue, + onChange, + 'onUpdate:open': onUpdateVisible, + }, + }) + + expect(findCell(wrapper, '1')?.classes()).toContain('ix-date-panel-cell-selected') + + await findCell(wrapper, '11')?.trigger('click') + expect(onUpdateValue).toBeCalledWith(new Date('2021-10-11')) + expect(onChange).toBeCalledWith(new Date('2021-10-11'), new Date('2021-10-01')) + expect(onUpdateVisible).toBeCalledWith(false) + + await wrapper.setProps({ value: new Date('2021-10-22') }) + expect(findCell(wrapper, '1')?.classes()).not.toContain('ix-date-panel-cell-selected') + expect(findCell(wrapper, '22')?.classes()).toContain('ix-date-panel-cell-selected') + }) + + test('v-model:open work', async () => { + const onUpdateOpen = vi.fn() + const wrapper = DatePickerMount({ props: { open: false, 'onUpdate:open': onUpdateOpen } }) + + expect(wrapper.findComponent(Content).exists()).toBeFalsy() + + await wrapper.find('.ix-date-picker').trigger('click') + expect(onUpdateOpen).toBeCalledWith(true) + + await wrapper.setProps({ open: true }) + expect(wrapper.findComponent(Content).isVisible()).toBeTruthy() + }) + + test('autoFocus work', async () => { + const onUpdateOpen = vi.fn() + DatePickerMount({ props: { open: false, autofocus: true, 'onUpdate:open': onUpdateOpen } }) + + expect(onUpdateOpen).toBeCalledWith(true) + }) + + test('disabled work', async () => { + const onUpdateOpen = vi.fn() + const onInput = vi.fn() + const wrapper = DatePickerMount({ props: { open: false, disabled: true, 'onUpdate:open': onUpdateOpen, onInput } }) + + await wrapper.find('.ix-date-picker').trigger('click') + expect(onUpdateOpen).not.toBeCalled() + + await wrapper.find('.ix-date-picker').find('input').setValue('2021-03-01') + expect(onInput).not.toBeCalled() + }) + + test('clear work', async () => { + const onUpdateValue = vi.fn() + const onChange = vi.fn() + const onClear = vi.fn() + const wrapper = DatePickerMount({ + props: { + clearable: true, + value: new Date('2021-10-01'), + 'onUpdate:value': onUpdateValue, + onChange, + onClear, + }, + }) + + await wrapper.find('.ix-trigger-clear-icon').trigger('click') + expect(onClear).toBeCalled() + expect(onChange).toBeCalledWith(undefined, new Date('2021-10-01')) + expect(onUpdateValue).toBeCalledWith(undefined) + }) + + test('format work', async () => { + const wrapper = DatePickerMount({ props: { value: new Date('2021-10-01'), format: 'dd/MM/yyyy' } }) + + expect(wrapper.find('.ix-date-picker').find('input').element.value).toBe('01/10/2021') + }) + + test('input work', async () => { + const onInput = vi.fn() + const onChange = vi.fn() + const onUpdateValue = vi.fn() + const wrapper = DatePickerMount({ + props: { + value: new Date('2021-10-01'), + format: 'yyyy-MM-dd', + onInput, + onChange, + 'onUpdate:value': onUpdateValue, + }, + }) + + await wrapper.find('.ix-date-picker').find('input').setValue('2021-10-11') + expect(onInput).toBeCalled() + + const newDate = parse('2021-10-11', 'yyyy-MM-dd', new Date()) + expect(onChange).toBeCalledWith(newDate, new Date('2021-10-01')) + expect(onUpdateValue).toBeCalledWith(newDate) + }) + + test('defaultOpenValue work', async () => { + const wrapper = DatePickerMount({ + props: { + value: undefined, + defaultOpenValue: new Date('2021-10-11'), + }, + }) + + const headerContentBtns = wrapper.findComponent(DatePanel).find('.ix-date-panel-header-content').findAll('button') + expect(headerContentBtns.some(btn => btn.text().indexOf('2021') > -1)).toBeTruthy() + expect(headerContentBtns.some(btn => btn.text().indexOf('10') > -1)).toBeTruthy() + }) + + test('datetime panel switch work', async () => { + const wrapper = DatePickerMount({ + props: { + type: 'datetime', + value: new Date('2021-10-11'), + }, + }) + + expect(wrapper.findComponent(DatePanel).isVisible()).toBeTruthy() + expect(wrapper.findComponent(TimePanel).isVisible()).toBeFalsy() + + await wrapper.findComponent(Content).find('.ix-date-picker-board-time-input').trigger('focus') + expect(wrapper.findComponent(DatePanel).isVisible()).toBeFalsy() + expect(wrapper.findComponent(TimePanel).isVisible()).toBeTruthy() + + await wrapper.findComponent(Content).find('.ix-date-picker-board-date-input').trigger('focus') + expect(wrapper.findComponent(DatePanel).isVisible()).toBeTruthy() + expect(wrapper.findComponent(TimePanel).isVisible()).toBeFalsy() + + await wrapper.findComponent(Content).find('.ix-date-picker-board-time-input').trigger('input') + expect(wrapper.findComponent(DatePanel).isVisible()).toBeFalsy() + expect(wrapper.findComponent(TimePanel).isVisible()).toBeTruthy() + + await wrapper.findComponent(Content).find('.ix-date-picker-board-date-input').trigger('input') + expect(wrapper.findComponent(DatePanel).isVisible()).toBeTruthy() + expect(wrapper.findComponent(TimePanel).isVisible()).toBeFalsy() + }) + + test('datetime time select work', async () => { + const onChange = vi.fn() + const onUpdateValue = vi.fn() + const wrapper = DatePickerMount({ + props: { + type: 'datetime', + value: new Date('2021-10-11 00:00:00'), + onChange, + 'onUpdate:value': onUpdateValue, + }, + }) + + await wrapper.findComponent(Content).find('.ix-date-picker-board-time-input').trigger('focus') + + const timePanel = wrapper.findComponent(TimePanel) as VueWrapper<ɵTimePanelInstance> + + await findTimePanelCell(timePanel, 0, 13)?.trigger('click') + expect(onUpdateValue).toBeCalledWith(new Date('2021-10-11 13:00:00')) + expect(onChange).toBeCalledWith(new Date('2021-10-11 13:00:00'), new Date('2021-10-11 00:00:00')) + + onUpdateValue.mockClear() + onChange.mockClear() + + await findTimePanelCell(timePanel, 1, 3)?.trigger('click') + expect(onUpdateValue).toBeCalledWith(new Date('2021-10-11 00:03:00')) + expect(onChange).toBeCalledWith(new Date('2021-10-11 00:03:00'), new Date('2021-10-11 00:00:00')) + + onUpdateValue.mockClear() + onChange.mockClear() + + await findTimePanelCell(timePanel, 2, 4)?.trigger('click') + expect(onUpdateValue).toBeCalledWith(new Date('2021-10-11 00:00:04')) + expect(onChange).toBeCalledWith(new Date('2021-10-11 00:00:04'), new Date('2021-10-11 00:00:00')) + }) + + test('datetime input work', async () => { + const onChange = vi.fn() + const onUpdateValue = vi.fn() + const wrapper = DatePickerMount({ + props: { + type: 'datetime', + value: new Date('2021-10-11 00:00:00'), + format: 'yyyy-MM-dd HH:mm:ss', + onChange, + 'onUpdate:value': onUpdateValue, + }, + }) + + await wrapper.findComponent(Content).find('.ix-date-picker-board-date-input').find('input').setValue('2021-11-22') + expect(onUpdateValue).toBeCalledWith(new Date('2021-11-22 00:00:00')) + expect(onChange).toBeCalledWith(new Date('2021-11-22 00:00:00'), new Date('2021-10-11 00:00:00')) + + onUpdateValue.mockClear() + onChange.mockClear() + + await wrapper.findComponent(Content).find('.ix-date-picker-board-time-input').find('input').setValue('13:03:04') + expect(onUpdateValue).toBeCalledWith(new Date('2021-10-11 13:03:04')) + expect(onChange).toBeCalledWith(new Date('2021-10-11 13:03:04'), new Date('2021-10-11 00:00:00')) + + onUpdateValue.mockClear() + onChange.mockClear() + + await wrapper.find('.ix-date-picker').find('input').setValue('2021-11-22 13:03:04') + expect(onUpdateValue).toBeCalledWith(new Date('2021-11-22 13:03:04')) + expect(onChange).toBeCalledWith(new Date('2021-11-22 13:03:04'), new Date('2021-10-11 00:00:00')) + }) + + test('datetime format work', async () => { const wrapper = DatePickerMount({ - props: { value: new Date('2021-10-01'), 'onUpdate:value': onUpdateValue, onChange }, + props: { + type: 'datetime', + value: new Date('2021-10-11 13:03:04'), + format: 'yyyy-MM-dd HH/mm/ss', + dateFormat: 'yyyy年MM月dd日', + timeFormat: 'HH时mm分ss秒', + }, }) - expect(wrapper.html()).toMatchSnapshot() + expect(wrapper.find('.ix-date-picker').find('input').element.value).toBe('2021-10-11 13/03/04') + expect(wrapper.findComponent(Content).find('.ix-date-picker-board-date-input').find('input').element.value).toBe( + '2021年10月11日', + ) + expect(wrapper.findComponent(Content).find('.ix-date-picker-board-time-input').find('input').element.value).toBe( + '13时03分04秒', + ) }) }) diff --git a/packages/components/date-picker/__tests__/dateRangePicker.spec.ts b/packages/components/date-picker/__tests__/dateRangePicker.spec.ts new file mode 100644 index 000000000..1e4193cc0 --- /dev/null +++ b/packages/components/date-picker/__tests__/dateRangePicker.spec.ts @@ -0,0 +1,284 @@ +import { MountingOptions, VueWrapper, mount } from '@vue/test-utils' + +import { renderWork } from '@tests' +import { parse } from 'date-fns' + +import DatePanel from '@idux/components/_private/date-panel/src/DatePanel' + +import DatePanelCell from '../../_private/date-panel/src/panel-body/PanelCell' +import { ɵTimePanelInstance } from '../../_private/time-panel' +import TimePanel from '../../_private/time-panel/src/TimePanel' +import TimePanelCell from '../../_private/time-panel/src/TimePanelCell' +import TimePanelColumn from '../../_private/time-panel/src/TimePanelColumn' +import DateRangePicker from '../src/DateRangePicker' +import RangeContent from '../src/content/RangeContent' +import { DateRangePickerInstance, DateRangePickerProps } from '../src/types' + +describe('DateRangePicker', () => { + const DateRangePickerMount = (options?: MountingOptions>) => { + const { props, ...rest } = options || {} + return mount(DateRangePicker, { + props: { open: true, ...props }, + ...(rest as MountingOptions), + attachTo: 'body', + }) as VueWrapper + } + + renderWork(DateRangePicker, { + props: {}, + }) + + const findTimePanelColumns = (wrapper: VueWrapper<ɵTimePanelInstance>) => wrapper.findAllComponents(TimePanelColumn) + const findTimePanelColumn = (wrapper: VueWrapper<ɵTimePanelInstance>, idx: number) => + findTimePanelColumns(wrapper)?.[idx] + + const findTimePanelCellWithValue = (wrapper: ReturnType, value: string | number) => + wrapper.findAllComponents(TimePanelCell).find(comp => comp.props().value === value) + + const findTimePanelCell = (wrapper: VueWrapper<ɵTimePanelInstance>, idx: number, value: string | number) => + findTimePanelCellWithValue(findTimePanelColumn(wrapper, idx), value) + + const triggerConfirm = (wrapper: VueWrapper) => + wrapper + .findComponent(RangeContent) + .findAll('.ix-date-range-picker-overlay-footer .ix-button') + .find(btn => btn.text() === '确定') + ?.trigger('click') + + const findCell = (wrapper: VueWrapper, type: 'from' | 'to', cellLabel: string) => + wrapper + .findAllComponents(DatePanel) + [type === 'from' ? 0 : 1].findAllComponents(DatePanelCell) + .find(cell => cell.find('.ix-date-panel-cell-trigger').text() === cellLabel) + + test('v-model:value work', async () => { + const onUpdateValue = vi.fn() + const onChange = vi.fn() + const onUpdateVisible = vi.fn() + const wrapper = DateRangePickerMount({ + props: { + value: [new Date('2021-10-01 00:00:00'), new Date('2021-11-11 00:00:00')], + 'onUpdate:value': onUpdateValue, + onChange, + 'onUpdate:open': onUpdateVisible, + }, + }) + + expect(findCell(wrapper, 'from', '1')?.classes()).toContain('ix-date-panel-cell-selected') + expect(findCell(wrapper, 'to', '11')?.classes()).toContain('ix-date-panel-cell-selected') + + await findCell(wrapper, 'from', '11')?.trigger('click') + await findCell(wrapper, 'to', '21')?.trigger('click') + await triggerConfirm(wrapper) + + expect(onUpdateValue).toBeCalledWith([new Date('2021-10-11 00:00:00'), new Date('2021-11-21 00:00:00')]) + expect(onChange).toBeCalledWith( + [new Date('2021-10-11 00:00:00'), new Date('2021-11-21 00:00:00')], + [new Date('2021-10-01 00:00:00'), new Date('2021-11-11 00:00:00')], + ) + expect(onUpdateVisible).toBeCalledWith(false) + + await wrapper.setProps({ value: [new Date('2021-9-03 00:00:00'), new Date('2021-12-22 00:00:00')] }) + expect(findCell(wrapper, 'from', '3')?.classes()).toContain('ix-date-panel-cell-selected') + expect(findCell(wrapper, 'to', '22')?.classes()).toContain('ix-date-panel-cell-selected') + expect(findCell(wrapper, 'from', '11')?.classes()).toContain('ix-date-panel-cell-in-range') + expect(findCell(wrapper, 'to', '11')?.classes()).toContain('ix-date-panel-cell-in-range') + }) + + test('v-model:open work', async () => { + const onUpdateOpen = vi.fn() + const wrapper = DateRangePickerMount({ props: { open: false, 'onUpdate:open': onUpdateOpen } }) + + expect(wrapper.findComponent(RangeContent).exists()).toBeFalsy() + + await wrapper.find('.ix-date-range-picker').trigger('click') + expect(onUpdateOpen).toBeCalledWith(true) + + await wrapper.setProps({ open: true }) + expect(wrapper.findComponent(RangeContent).isVisible()).toBeTruthy() + }) + + test('autoFocus work', async () => { + const onUpdateOpen = vi.fn() + DateRangePickerMount({ props: { open: false, autofocus: true, 'onUpdate:open': onUpdateOpen } }) + + expect(onUpdateOpen).toBeCalledWith(true) + }) + + test('disabled work', async () => { + const onUpdateOpen = vi.fn() + const onInput = vi.fn() + const wrapper = DateRangePickerMount({ + props: { open: false, disabled: true, 'onUpdate:open': onUpdateOpen, onInput }, + }) + + await wrapper.find('.ix-date-range-picker').trigger('click') + expect(onUpdateOpen).not.toBeCalled() + + await wrapper.find('.ix-date-range-picker').find('input').setValue('2021-03-01') + expect(onInput).not.toBeCalled() + }) + + test('clear work', async () => { + const onUpdateValue = vi.fn() + const onChange = vi.fn() + const onClear = vi.fn() + const wrapper = DateRangePickerMount({ + props: { + clearable: true, + value: [new Date('2021-10-01 00:00:00'), new Date('2021-11-11 00:00:00')], + 'onUpdate:value': onUpdateValue, + onChange, + onClear, + }, + }) + + await wrapper.find('.ix-trigger-clear-icon').trigger('click') + expect(onClear).toBeCalled() + expect(onChange).toBeCalledWith(undefined, [new Date('2021-10-01 00:00:00'), new Date('2021-11-11 00:00:00')]) + expect(onUpdateValue).toBeCalledWith(undefined) + }) + + test('format work', async () => { + const wrapper = DateRangePickerMount({ + props: { value: [new Date('2021-10-01 00:00:00'), new Date('2021-11-11 00:00:00')], format: 'dd/MM/yyyy' }, + }) + + expect(wrapper.find('.ix-date-range-picker').findAll('input')[0].element.value).toBe('01/10/2021') + expect(wrapper.find('.ix-date-range-picker').findAll('input')[1].element.value).toBe('11/11/2021') + }) + + test('input work', async () => { + const onInput = vi.fn() + const onChange = vi.fn() + const onUpdateValue = vi.fn() + const wrapper = DateRangePickerMount({ + props: { + value: [new Date('2021-10-01 00:00:00'), new Date('2021-11-11 00:00:00')], + format: 'yyyy-MM-dd', + onInput, + onChange, + 'onUpdate:value': onUpdateValue, + }, + }) + + await wrapper.find('.ix-date-range-picker').findAll('input')[0].setValue('2021-10-11') + expect(onInput).toBeCalled() + + onInput.mockClear() + + await wrapper.find('.ix-date-range-picker').findAll('input')[1].setValue('2021-12-22') + expect(onInput).toBeCalled() + + await triggerConfirm(wrapper) + + const newFromDate = parse('2021-10-11', 'yyyy-MM-dd', new Date()) + const newToDate = parse('2021-12-22', 'yyyy-MM-dd', new Date()) + expect(onChange).toBeCalledWith( + [newFromDate, newToDate], + [new Date('2021-10-01 00:00:00'), new Date('2021-11-11 00:00:00')], + ) + expect(onUpdateValue).toBeCalledWith([newFromDate, newToDate]) + }) + + test('defaultOpenValue work', async () => { + const wrapper = DateRangePickerMount({ + props: { + value: undefined, + defaultOpenValue: [new Date('2021-10-11 00:00:00'), new Date('2021-11-11 00:00:00')], + }, + }) + + const [fromDatePanel, toDatePanel] = wrapper.findAllComponents(DatePanel) + const fromHeaderContentBtns = fromDatePanel.find('.ix-date-panel-header-content').findAll('button') + const toHeaderContentBtns = toDatePanel.find('.ix-date-panel-header-content').findAll('button') + expect(fromHeaderContentBtns.some(btn => btn.text().indexOf('2021') > -1)).toBeTruthy() + expect(fromHeaderContentBtns.some(btn => btn.text().indexOf('10') > -1)).toBeTruthy() + expect(toHeaderContentBtns.some(btn => btn.text().indexOf('2021') > -1)).toBeTruthy() + expect(toHeaderContentBtns.some(btn => btn.text().indexOf('11') > -1)).toBeTruthy() + }) + + test('datetime time select work', async () => { + const onChange = vi.fn() + const onUpdateValue = vi.fn() + const wrapper = DateRangePickerMount({ + props: { + type: 'datetime', + value: [new Date('2021-10-11 00:00:00'), new Date('2021-11-11 00:00:00')], + onChange, + 'onUpdate:value': onUpdateValue, + }, + }) + + await wrapper.findComponent(RangeContent).findAll('.ix-date-range-picker-board-time-input')[0].trigger('focus') + + const fromTimePanel = wrapper.findAllComponents(TimePanel)[0] as unknown as VueWrapper<ɵTimePanelInstance> + const toTimePanel = wrapper.findAllComponents(TimePanel)[1] as unknown as VueWrapper<ɵTimePanelInstance> + + await findTimePanelCell(fromTimePanel, 0, 13)?.trigger('click') + await findTimePanelCell(fromTimePanel, 1, 3)?.trigger('click') + await findTimePanelCell(fromTimePanel, 2, 4)?.trigger('click') + + await findTimePanelCell(toTimePanel, 0, 14)?.trigger('click') + await findTimePanelCell(toTimePanel, 1, 5)?.trigger('click') + await findTimePanelCell(toTimePanel, 2, 6)?.trigger('click') + + await triggerConfirm(wrapper) + + expect(onUpdateValue).toBeCalledWith([new Date('2021-10-11 13:03:04'), new Date('2021-11-11 14:05:06')]) + expect(onChange).toBeCalledWith( + [new Date('2021-10-11 13:03:04'), new Date('2021-11-11 14:05:06')], + [new Date('2021-10-11 00:00:00'), new Date('2021-11-11 00:00:00')], + ) + }) + + test('datetime input work', async () => { + const onChange = vi.fn() + const onUpdateValue = vi.fn() + const wrapper = DateRangePickerMount({ + props: { + type: 'datetime', + value: [new Date('2021-10-11 00:00:00'), new Date('2021-11-11 00:00:00')], + format: 'yyyy-MM-dd HH:mm:ss', + onChange, + 'onUpdate:value': onUpdateValue, + }, + }) + + const dateInputs = wrapper + .findComponent(RangeContent) + .findAll('.ix-date-range-picker-board-date-input') + .map(el => el.find('input')) + const timeInputs = wrapper + .findComponent(RangeContent) + .findAll('.ix-date-range-picker-board-time-input') + .map(el => el.find('input')) + await dateInputs[0].setValue('2021-11-22') + await dateInputs[1].setValue('2021-12-25') + await timeInputs[0].setValue('13:03:04') + await timeInputs[1].setValue('14:05:06') + + await triggerConfirm(wrapper) + + expect(onUpdateValue).toBeCalledWith([new Date('2021-11-22 13:03:04'), new Date('2021-12-25 14:05:06')]) + expect(onChange).toBeCalledWith( + [new Date('2021-11-22 13:03:04'), new Date('2021-12-25 14:05:06')], + [new Date('2021-10-11 00:00:00'), new Date('2021-11-11 00:00:00')], + ) + + onUpdateValue.mockClear() + onChange.mockClear() + + const triggerInputs = wrapper.find('.ix-date-range-picker').findAll('input') + await triggerInputs[0].setValue('2021-11-22 13:03:04') + await triggerInputs[1].setValue('2021-12-25 14:05:06') + + await triggerConfirm(wrapper) + + expect(onUpdateValue).toBeCalledWith([new Date('2021-11-22 13:03:04'), new Date('2021-12-25 14:05:06')]) + expect(onChange).toBeCalledWith( + [new Date('2021-11-22 13:03:04'), new Date('2021-12-25 14:05:06')], + [new Date('2021-10-11 00:00:00'), new Date('2021-11-11 00:00:00')], + ) + }) +}) diff --git a/packages/components/date-picker/demo/AllowInput.md b/packages/components/date-picker/demo/AllowInput.md new file mode 100644 index 000000000..8e0db4933 --- /dev/null +++ b/packages/components/date-picker/demo/AllowInput.md @@ -0,0 +1,14 @@ +--- +title: + zh: 可输入 + en: AllowInput +order: 1 +--- + +## zh + +设置 `allowInput` 为 `'overlay'`、`true`、`false` 开启输入 + +## en + +enable input in overlay by setting `allowInput` prop as `'overlay'`, `true` or `false`. diff --git a/packages/components/date-picker/demo/AllowInput.vue b/packages/components/date-picker/demo/AllowInput.vue new file mode 100644 index 000000000..82cc66e8f --- /dev/null +++ b/packages/components/date-picker/demo/AllowInput.vue @@ -0,0 +1,30 @@ + + + diff --git a/packages/components/date-picker/demo/Basic.vue b/packages/components/date-picker/demo/Basic.vue index b71e368c5..8adb4affe 100644 --- a/packages/components/date-picker/demo/Basic.vue +++ b/packages/components/date-picker/demo/Basic.vue @@ -5,6 +5,7 @@ + diff --git a/packages/components/date-picker/demo/Disabled.md b/packages/components/date-picker/demo/Disabled.md index 3a42440db..3f4de1823 100644 --- a/packages/components/date-picker/demo/Disabled.md +++ b/packages/components/date-picker/demo/Disabled.md @@ -2,7 +2,7 @@ title: zh: 禁用 en: Disabled -order: 6 +order: 5 --- ## zh diff --git a/packages/components/date-picker/demo/DisabledDate.vue b/packages/components/date-picker/demo/DisabledDate.vue index d0bba62ff..1e5e8bf2f 100644 --- a/packages/components/date-picker/demo/DisabledDate.vue +++ b/packages/components/date-picker/demo/DisabledDate.vue @@ -1,6 +1,6 @@ @@ -8,4 +8,9 @@ import { endOfMonth, isAfter, isBefore, startOfDay } from 'date-fns' const disabledDate = (date: Date) => isBefore(date, startOfDay(Date.now())) || isAfter(date, endOfMonth(Date.now())) +const cellTooltip = ({ disabled }: { disabled: boolean }): string | void => { + if (disabled) { + return 'cell disabled' + } +} diff --git a/packages/components/date-picker/demo/Format.md b/packages/components/date-picker/demo/Format.md index 6a0598d6f..ae31638a4 100644 --- a/packages/components/date-picker/demo/Format.md +++ b/packages/components/date-picker/demo/Format.md @@ -2,7 +2,7 @@ title: zh: 日期格式 en: Date format -order: 4 +order: 3 --- ## zh diff --git a/packages/components/date-picker/demo/FormatCustom.md b/packages/components/date-picker/demo/FormatCustom.md index 1acdc6108..e56373088 100644 --- a/packages/components/date-picker/demo/FormatCustom.md +++ b/packages/components/date-picker/demo/FormatCustom.md @@ -2,7 +2,7 @@ title: zh: 高级格式 en: Advanced format -order: 5 +order: 4 --- ## zh diff --git a/packages/components/date-picker/demo/Range.md b/packages/components/date-picker/demo/Range.md new file mode 100644 index 000000000..bed4e29d2 --- /dev/null +++ b/packages/components/date-picker/demo/Range.md @@ -0,0 +1,14 @@ +--- +title: + zh: 日期范围 + en: Date range +order: 2 +--- + +## zh + +日期范围选择 + +## en + +Date range picker. diff --git a/packages/components/date-picker/demo/Range.vue b/packages/components/date-picker/demo/Range.vue new file mode 100644 index 000000000..d6aea50d7 --- /dev/null +++ b/packages/components/date-picker/demo/Range.vue @@ -0,0 +1,26 @@ + + + diff --git a/packages/components/date-picker/docs/Index.zh.md b/packages/components/date-picker/docs/Index.zh.md index 6fb22fd11..09950813d 100644 --- a/packages/components/date-picker/docs/Index.zh.md +++ b/packages/components/date-picker/docs/Index.zh.md @@ -14,8 +14,10 @@ order: 0 | 名称 | 说明 | 类型 | 默认值 | 全局配置 | 备注 | | --- | --- | --- | --- | --- | --- | -| `control` | 控件控制器 | `string \| number \| AbstractControl` | - | - | 配合 `@idux/cdk/forms` 使用, 参考 [Form](/components/form/zh) | | `v-model:open` | 日期面板是否展开 | `boolean` | - | - | - | +| `control` | 控件控制器 | `string \| number \| AbstractControl` | - | - | 配合 `@idux/cdk/forms` 使用, 参考 [Form](/components/form/zh) | +| `cellTooltip` | 日期节点的tooltip | `(cell: { value: Date, disabled: boolean }) => string | void` | - | - | - | +| `allowInput` | 允许输入模式 | `boolean \| 'overlay'` | `false` | - | `'overlay'` 时在浮层内输入 | | `autofocus` | 默认获取焦点 | `boolean` | `false` | - | - | | `borderless` | 是否无边框 | `boolean` | `false` | ✅ | - | | `clearable` | 是否显示清除图标 | `boolean` | `false` | ✅ | - | @@ -24,12 +26,12 @@ order: 0 | `disabledDate` | 不可选择的日期 | `(date: Date) => boolean` | - | - | - | | `format` | 展示的格式 | `string` | - | ✅ | 默认值参见 `defaultFormat`, 更多用法参考[date-fns](https://date-fns.org/v2.27.0/docs/format) | | `overlayClassName` | 日期面板的 `class` | `string` | - | - | - | -| `overlayRender` | 自定义日期面板内容的渲染 | `(children:VNode[]) => VNodeTypes` | - | - | - | +| `overlayRender` | 自定义日期面板内容的渲染 | `(children:VNode[]) => VNodeChild` | - | - | - | | `readonly` | 只读模式 | `boolean` | - | - | - | | `size` | 设置选择器大小 | `'sm' \| 'md' \| 'lg'` | `md` | ✅ | - | | `suffix` | 设置后缀图标 | `string \| #suffix` | `'calendar'` | ✅ | - | | `target` | 自定义浮层容器节点 | `string \| HTMLElement \| () => string \| HTMLElement` | - | ✅ | - | -| `type` | 设置选择器类型 | `'date' \| 'week' \| 'month' \| 'quarter' \| 'year'` | `'date'` | - | - | +| `type` | 设置选择器类型 | `'date' \| 'week' \| 'month' \| 'quarter' \| 'year' \| 'datetime'` | `'date'` | - | - | | `onClear` | 清除图标被点击后的回调 | `(evt: MouseEvent) => void` | - | - | - | | `onFocus` | 获取焦点后的回调 | `(evt: FocusEvent) => void` | - | - | - | | `onBlur` | 失去焦点后的回调 | `(evt: FocusEvent) => void` | - | - | - | @@ -41,6 +43,7 @@ const defaultFormat = { month: 'yyyy-MM', quarter: "yyyy-'Q'Q", year: 'yyyy', + datetime: 'yyyy-MM-dd HH:mm:ss', } ``` @@ -66,9 +69,23 @@ const defaultFormat = { | --- | --- | --- | --- | --- | --- | | `v-model:value` | 当前选中的日期 | `number \| string \| Date` | - | - | 如果传入 `string` 类型,会根据 `format` 解析成 `Date`,使用 `control` 时,此配置无效 | | `defaultOpenValue` | 打开面板时默认选中的值 | `number \| string \| Date` | - | - | `value` 为空时,高亮的值 | +| `footer` | 自定义底部按钮 | `boolean \| ButtonProps[] \| VNode \| #footer` | `false` | - | 默认会根据 `type` 的不同渲染相应的按钮,如果传入 `false` 则不显示 | | `placeholder` | 选择框默认文本 | `string \| #placeholder` | - | - | 可以通过国际化配置默认值 | -| `timePicker` | 是否显示时间选择器 | `boolean \| TimePickerProps` | `false` | - | 仅在 `type='date'` 时生效 | +| `timePanelOptions` | 时间选择面板配置 | `TimePanelOptions` | `false` | - | 仅在 `type='datetime'` 时生效 | | `onChange` | 值改变后的回调 | `(value: Date, oldValue: Date) => void` | - | - | - | +| `onInput` | 输入后的回调 | `(evt: Event) => void` | - | - | - | + +#### TimePanelOptions + +| 名称 | 说明 | 类型 | 默认值 | 全局配置 | 备注 | +| --- | --- | --- | --- | --- | --- | +| `disabledHours` | 禁用部分小时选项 | `()=>number[]` | ``() => []`` | - | - | +| `disabledMinutes` | 禁用部分分钟选项 | `(selectedHour: number)=>number[]` | `() => []` | - | - | +| `disabledSeconds` | 禁用部分秒选项 | `(selectedHour: number, selectedMinute: number)=>number[]` | `() => []` | - | - | +| `hideDisabledOptions` | 隐藏禁止选择的options |`boolean` |`false` | - | - | +| `hourStep` | 小时选项的间隔 | `number` | `1` | - | - | +| `minuteStep` | 分钟选项的间隔 | `number` | `1` | - | - | +| `secondStep` | 秒选项的间隔 | `number` | `1` | - | - | ### IxDateRangePicker @@ -78,69 +95,96 @@ const defaultFormat = { | --- | --- | --- | --- | --- | --- | | `v-model:value` | 当前选中的日期 | `Array` | - | - | 如果传入 `string` 类型,会根据 `format` 解析成 `Date`,使用 `control` 时,此配置无效 | | `defaultOpenValue` | 打开面板时默认选中的值 | `Array` | - | - | `value` 为空时,高亮的值 | +| `footer` | 自定义底部按钮 | `boolean \| ButtonProps[] \| VNode \| #footer` | `false` | - | 默认会根据 `type` 的不同渲染相应的按钮,如果传入 `false` 则不显示 | | `placeholder` | 选择框默认文本 | `string[] \| #placeholder=placement:'start'\|'end'` | - | - | 默认使用 `i18n` 配置 | -| `separator` | 自定义分隔符图标 | `string \| VNode \| #separator` | `'swap-right'` | ✅ | - | -| `timePicker` | 是否显示时间选择器 | `boolean \| TimePickerProps \| TimePickerProps[]` | `false` | - | 如果需要对前后的时间选择器配置不同的禁用条件,可以传入一个数组 | +| `separator` | 自定义分隔符图标 | `string \| VNode \| #separator` | - | ✅ | - | +| `timePanelOptions` | 时间选择面板配置 | `TimePanelOptions \| TimePanelOptions[]` | `false` | - | 如果需要对前后的时间选择器配置不同的禁用条件,可以传入一个数组 | | `onChange` | 值改变后的回调 | `(value: Date[], oldValue: Date[]) => void` | - | - | - | +| `onInput` | 输入后的回调 | `(isFrom: boolean, evt: Event) => void` | - | - | - | ## 主题变量 | 名称 | `default` | `dark` | 备注 | | --- | --- | --- | --- | -| `@date-picker-font-size-sm` | `@form-font-size-sm` | - | - | -| `@date-picker-font-size-md` | `@form-font-size-md` | - | - | -| `@date-picker-font-size-lg` | `@form-font-size-lg` | - | - | | `@date-picker-line-height` | `@form-line-height` | - | - | -| `@date-picker-height-sm` | `@form-height-sm` | - | - | -| `@date-picker-height-md` | `@form-height-md` | - | - | -| `@date-picker-height-lg` | `@form-height-lg` | - | - | -| `@date-picker-padding-horizontal-sm` | `@form-padding-horizontal-sm` | - | - | -| `@date-picker-padding-horizontal-md` | `@form-padding-horizontal-md` | - | - | -| `@date-picker-padding-horizontal-lg` | `@form-padding-horizontal-lg` | - | - | -| `@date-picker-padding-vertical-sm` | `@form-padding-vertical-sm` | - | - | -| `@date-picker-padding-vertical-md` | `@form-padding-vertical-md` | - | - | -| `@date-picker-padding-vertical-lg` | `@form-padding-vertical-lg` | - | - | -| `@date-picker-border-width` | `@form-border-width` | - | - | -| `@date-picker-border-style` | `@form-border-style` | - | - | -| `@date-picker-border-color` | `@form-border-color` | - | - | -| `@date-picker-border-radius` | `@border-radius-md` | - | - | | `@date-picker-color` | `@form-color` | - | - | -| `@date-picker-color-secondary` | `@form-color-secondary` | - | - | | `@date-picker-background-color` | `@form-background-color` | - | - | -| `@date-picker-placeholder-color` | `@form-placeholder-color` | - | - | -| `@date-picker-hover-color` | `@form-hover-color` | - | - | -| `@date-picker-active-color` | `@form-active-color` | - | - | -| `@date-picker-active-box-shadow` | `@form-active-box-shadow` | - | - | -| `@date-picker-disabled-color` | `@form-disabled-color` | - | - | | `@date-picker-disabled-background-color` | `@form-disabled-background-color` | - | - | +| `@date-range-picker-trigger-separator-margin` | `@spacing-xl` | - | - | | `@date-picker-panel-font-size` | `@font-size-md` | - | - | | `@date-picker-panel-color` | `@text-color` | - | - | | `@date-picker-panel-color-inverse` | `@text-color-inverse` | - | - | | `@date-picker-panel-active-color` | `@color-primary` | - | - | +| `@date-picker-panel-in-range-color` | `@color-blue-l50` | - | - | | `@date-picker-panel-disabled-color` | `@text-color-disabled` | - | - | -| `@date-picker-panel-disabled-background-color` | `@background-color-disabled` | - | - | +| `@date-picker-panel-disabled-background-color` | `@color-graphite-l50` | - | - | | `@date-picker-panel-background-color` | `@background-color-component` | - | - | | `@date-picker-panel-border-width` | `@border-width-sm` | - | - | | `@date-picker-panel-border-style` | `@border-style` | - | - | | `@date-picker-panel-border-color` | `@border-color` | - | - | -| `@date-picker-panel-header-padding` | `0 @spacing-lg` | - | - | -| `@date-picker-panel-header-height` | `@height-lg` | - | - | +| `@date-picker-panel-header-padding` | `0 0 @spacing-xs 0` | - | - | +| `@date-picker-panel-header-height` | `@height-md` | - | - | | `@date-picker-panel-header-item-padding` | `0 @spacing-xs` | - | - | +| `@date-picker-panel-header-font-size` | `@font-size-md` | - | - | | `@date-picker-panel-header-font-weight` | `@font-weight-lg` | - | - | -| `@date-picker-panel-body-padding` | `@spacing-sm @spacing-lg` | - | - | -| `@date-picker-panel-body-padding-lg` | `@spacing-sm @spacing-lg` | - | - | +| `@date-picker-panel-header-border-bottom` | `none` | - | - | +| `@date-picker-panel-header-button-font-size` | `@font-size-lg` | - | - | +| `@date-picker-panel-header-content-spacing` | `@spacing-lg` | - | - | +| `@date-picker-panel-header-padding-lg` | `0 0 @spacing-2xl` | - | - | +| `@date-picker-panel-body-padding` | `0` | - | - | +| `@date-picker-panel-body-padding-lg` | `0` | - | - | +| `@date-picker-panel-body-font-size` | `@font-size-md` | - | - | +| `@date-picker-panel-body-header-border-bottom` | `solid transparent @spacing-md` | - | - | | `@date-picker-panel-body-header-font-weight` | `@font-weight-md` | - | - | -| `@date-picker-panel-cell-size` | `24px` | - | - | -| `@date-picker-panel-cell-padding` | `@spacing-xs 0` | - | - | -| `@date-picker-panel-cell-padding-lg` | `@padding-sm 0` | - | - | -| `@date-picker-panel-cell-inner-padding-lg` | `0 @padding-lg` | - | - | +| `@date-picker-panel-body-header-background-color` | `@color-graphite-l50` | - | - | +| `@date-picker-panel-cell-width` | `28px` | - | - | +| `@date-picker-panel-cell-height` | `28px` | - | - | +| `@date-picker-panel-cell-width-lg` | `52px` | - | - | +| `@date-picker-panel-cell-height-lg` | `24px` | - | - | +| `@date-picker-panel-cell-padding` | `2px 0` | - | - | +| `@date-picker-panel-cell-inner-padding` | `4px` | - | - | +| `@date-picker-panel-cell-padding-lg` | `@spacing-lg 0` | - | - | +| `@date-picker-panel-cell-inner-padding-lg` | `0` | - | - | +| `@date-picker-panel-cell-trigger-width` | `20px` | - | - | +| `@date-picker-panel-cell-trigger-height` | `20px` | - | - | +| `@date-picker-panel-cell-trigger-width-lg` | `52px` | - | - | +| `@date-picker-panel-cell-trigger-height-lg` | `24px` | - | - | | `@date-picker-panel-cell-border-radius` | `@border-radius-full` | - | - | | `@date-picker-panel-cell-border-radius-lg` | `@border-radius-md` | - | - | -| `@date-picker-panel-cell-hover-background-color` | `@background-color-hover` | - | - | -| `@date-picker-panel-footer-padding` | `0 @spacing-sm` | - | - | +| `@date-picker-panel-cell-hover-background-color` | `@color-graphite-l50` | - | - | +| `@date-picker-panel-cell-hover-color` | `@color-primary` | - | - | +| `@date-picker-panel-cell-today-mark-width` | `24px` | - | - | +| `@date-picker-panel-cell-today-mark-height` | `24px` | - | - | +| `@date-picker-panel-cell-today-border-color` | `@color-blue-l40` | - | - | +| `@date-picker-panel-cell-today-color` | `@color-primary` | - | - | +| `@date-picker-overlay-footer-border-width` | `@form-border-width` | - | - | +| `@date-picker-overlay-footer-border-style` | `@form-border-style` | - | - | +| `@date-picker-overlay-footer-border-color` | `@form-border-color` | - | - | +| `@date-picker-overlay-footer-padding` | `@spacing-sm @spacing-lg` | - | - | +| `@date-picker-overlay-footer-button-margin-left` | `@spacing-sm` | - | - | | `@date-picker-overlay-zindex` | `@zindex-l4-3` | - | - | -| `@date-picker-overlay-width` | `256px` | - | - | +| `@date-picker-overlay-width` | `252px` | - | - | | `@date-picker-overlay-border-radius` | `@border-radius-sm` | - | - | | `@date-picker-overlay-box-shadow` | `@shadow-bottom-md` | - | - | +| `@date-picker-overlay-date-input-width` | `120px` | - | - | +| `@date-picker-overlay-time-input-width` | `96px` | - | - | +| `@date-picker-overlay-input-gap` | `@spacing-xs` | - | - | +| `@date-picker-overlay-padding` | `@spacing-lg @spacing-lg @spacing-lg @spacing-lg` | - | - | +| `@date-picker-overlay-inputs-margin-bottom` | `@spacing-sm` | - | - | +| `@date-range-picker-overlay-padding` | `@spacing-lg @spacing-lg 0 @spacing-lg` | - | - | +| `@date-range-picker-overlay-content-padding` | `0 0 @spacing-sm 0` | - | - | +| `@date-range-picker-overlay-gap-width` | `@spacing-2xl` | - | - | +| `@date-range-picker-overlay-gap-padding` | `1px 0 0 0` | - | - | +| `@date-range-picker-overlay-footer-border-width` | `@date-picker-overlay-footer-border-width` | - | - | +| `@date-range-picker-overlay-footer-border-style` | `@date-picker-overlay-footer-border-style` | - | - | +| `@date-range-picker-overlay-footer-border-color` | `@color-graphite-l30` | - | - | +| `@date-range-picker-overlay-footer-padding` | `@spacing-sm 0` | - | - | +| `@date-range-picker-overlay-footer-button-margin-left` | `@date-picker-overlay-footer-button-margin-left` | - | - | +| `@date-range-picker-panel-width` | `220px` | - | - | +| `@date-range-picker-panel-max-height` | `260px` | - | - | +| `@date-range-picker-panel-border-width` | `@form-border-width` | - | - | +| `@date-range-picker-panel-border-style` | `@form-border-style` | - | - | +| `@date-range-picker-panel-border-color` | `@form-border-color` | - | - | +| `@date-range-picker-panel-border-radius` | `2px` | - | - | \ No newline at end of file diff --git a/packages/components/date-picker/index.ts b/packages/components/date-picker/index.ts index 38e63259e..10bcf05f7 100644 --- a/packages/components/date-picker/index.ts +++ b/packages/components/date-picker/index.ts @@ -5,12 +5,21 @@ * found in the LICENSE file at https://github.com/IDuxFE/idux/blob/main/LICENSE */ -import type { DatePickerComponent } from './src/types' +import type { DatePickerComponent, DateRangePickerComponent } from './src/types' import DatePicker from './src/DatePicker' +import DateRangePicker from './src/DateRangePicker' const IxDatePicker = DatePicker as unknown as DatePickerComponent +const IxDateRangePicker = DateRangePicker as unknown as DateRangePickerComponent -export { IxDatePicker } +export { IxDatePicker, IxDateRangePicker } -export type { DatePickerInstance, DatePickerComponent, DatePickerPublicProps as DatePickerProps } from './src/types' +export type { + DatePickerInstance, + DatePickerComponent, + DatePickerPublicProps as DatePickerProps, + DateRangePickerInstance, + DateRangePickerComponent, + DateRangePickerPublicProps as DateRangePickerProps, +} from './src/types' diff --git a/packages/components/date-picker/src/DatePicker.tsx b/packages/components/date-picker/src/DatePicker.tsx index 2dfb73507..b459aa3fd 100644 --- a/packages/components/date-picker/src/DatePicker.tsx +++ b/packages/components/date-picker/src/DatePicker.tsx @@ -5,18 +5,18 @@ * found in the LICENSE file at https://github.com/IDuxFE/idux/blob/main/LICENSE */ -import { computed, defineComponent, normalizeClass, provide, toRaw, watch } from 'vue' +import { computed, defineComponent, nextTick, normalizeClass, provide, watch } from 'vue' -import { useSharedFocusMonitor } from '@idux/cdk/a11y' -import { callEmit } from '@idux/cdk/utils' import { ɵOverlay } from '@idux/components/_private/overlay' import { useDateConfig, useGlobalConfig } from '@idux/components/config' -import { useFormAccessor, useFormElement } from '@idux/components/utils' +import { useFormElement } from '@idux/components/utils' +import { useControl } from './composables/useControl' import { useFormat } from './composables/useFormat' -import { useInputState } from './composables/useInputState' +import { useInputEnableStatus } from './composables/useInputEnableStatus' +import { useKeyboardEvents } from './composables/useKeyboardEvents' import { useOverlayState } from './composables/useOverlayState' -import { usePanelState } from './composables/usePanelState' +import { usePickerState } from './composables/usePickerState' import Content from './content/Content' import { datePickerToken } from './token' import Trigger from './trigger/Trigger' @@ -35,26 +35,19 @@ export default defineComponent({ const config = useGlobalConfig('datePicker') const dateConfig = useDateConfig() - const focusMonitor = useSharedFocusMonitor() const { elementRef: inputRef, focus, blur } = useFormElement() expose({ focus, blur }) - const { overlayOpened, setOverlayOpened } = useOverlayState(props) - const accessor = useFormAccessor() - const format = useFormat(props, config) - const inputStateContext = useInputState(props, dateConfig, accessor, format) - const { inputValue } = inputStateContext - const { panelDate, setPanelDate } = usePanelState(props, dateConfig, accessor, format) + const inputEnableStatus = useInputEnableStatus(props, config) + const formatContext = useFormat(props, config) + const pickerStateContext = usePickerState(props, dateConfig, formatContext.formatRef) - const handlePanelCellClick = (date: Date) => { - const oldDate = toRaw(accessor.valueRef.value) - if (!oldDate || !dateConfig.isSame(date, dateConfig.convert(oldDate, format.value), props.type)) { - setOverlayOpened(false) - accessor.setValue(date) - callEmit(props.onChange, date, oldDate) - } - } + const { accessor, handleChange } = pickerStateContext + + const controlContext = useControl(dateConfig, formatContext, inputEnableStatus, accessor.valueRef, handleChange) + const { overlayOpened, setOverlayOpened } = useOverlayState(props, controlContext) + const handleKeyDown = useKeyboardEvents(setOverlayOpened) provide(datePickerToken, { props, @@ -63,55 +56,44 @@ export default defineComponent({ config, mergedPrefixCls, dateConfig, - focusMonitor, inputRef, + inputEnableStatus, overlayOpened, setOverlayOpened, - accessor, - format, - ...inputStateContext, - panelDate, - setPanelDate, - handlePanelCellClick, + handleKeyDown, + controlContext, + ...formatContext, + ...pickerStateContext, }) watch(overlayOpened, opened => { - if (!opened && inputValue.value) { - // changeSelected(inputValue.value) + if (!opened) { + controlContext.init(true) } - opened ? focus() : blur() - }) - - const classes = computed(() => { - const { overlayClassName } = props - const prefixCls = mergedPrefixCls.value - return normalizeClass({ - [`${prefixCls}-overlay`]: true, - [overlayClassName || '']: !!overlayClassName, + nextTick(() => { + opened ? focus() : blur() }) }) const target = computed(() => props.target ?? config.target ?? `${mergedPrefixCls.value}-overlay-container`) + const renderTrigger = () => + const renderContent = () => + const overlayProps = { triggerId: attrs.id, 'onUpdate:visible': setOverlayOpened } - return () => { - const renderTrigger = () => - const renderContent = () => - const overlayProps = { triggerId: attrs.id, 'onUpdate:visible': setOverlayOpened } - return ( - <ɵOverlay - {...overlayProps} - visible={overlayOpened.value} - v-slots={{ default: renderTrigger, content: renderContent }} - class={classes.value} - clickOutside - disabled={accessor.disabled.value || props.readonly} - offset={defaultOffset} - placement="bottomStart" - target={target.value} - trigger="manual" - /> - ) - } + return () => ( + <ɵOverlay + {...overlayProps} + visible={overlayOpened.value} + v-slots={{ default: renderTrigger, content: renderContent }} + class={normalizeClass(props.overlayClassName)} + clickOutside + disabled={accessor.disabled.value || props.readonly} + offset={defaultOffset} + placement="bottomStart" + target={target.value} + trigger="manual" + /> + ) }, }) diff --git a/packages/components/date-picker/src/DateRangePicker.tsx b/packages/components/date-picker/src/DateRangePicker.tsx new file mode 100644 index 000000000..6a40653fe --- /dev/null +++ b/packages/components/date-picker/src/DateRangePicker.tsx @@ -0,0 +1,110 @@ +/** + * @license + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://github.com/IDuxFE/idux/blob/main/LICENSE + */ + +import { computed, defineComponent, normalizeClass, provide, watch } from 'vue' + +import { ɵOverlay } from '@idux/components/_private/overlay' +import { useDateConfig, useGlobalConfig } from '@idux/components/config' +import { useFormElement } from '@idux/components/utils' + +import { useFormat } from './composables/useFormat' +import { useInputEnableStatus } from './composables/useInputEnableStatus' +import { useRangeKeyboardEvents } from './composables/useKeyboardEvents' +import { useOverlayState } from './composables/useOverlayState' +import { usePickerState } from './composables/usePickerState' +import { useRangeControl } from './composables/useRangeControl' +import RangeContent from './content/RangeContent' +import { dateRangePickerToken } from './token' +import RangeTrigger from './trigger/RangeTrigger' +import { dateRangePickerProps } from './types' + +const defaultOffset: [number, number] = [0, 8] + +export default defineComponent({ + name: 'IxDateRangePicker', + inheritAttrs: false, + props: dateRangePickerProps, + setup(props, { attrs, expose, slots }) { + const common = useGlobalConfig('common') + const locale = useGlobalConfig('locale') + const mergedPrefixCls = computed(() => `${common.prefixCls}-date-range-picker`) + const config = useGlobalConfig('datePicker') + const dateConfig = useDateConfig() + + const { elementRef: inputRef, focus, blur } = useFormElement() + + expose({ focus, blur }) + + const showFooter = computed(() => !!props.footer || !!slots.footer) + + const inputEnableStatus = useInputEnableStatus(props, config) + const formatContext = useFormat(props, config) + const pickerStateContext = usePickerState(props, dateConfig, formatContext.formatRef) + + const { accessor, handleChange } = pickerStateContext + + const rangeControlContext = useRangeControl(dateConfig, formatContext, inputEnableStatus, accessor.valueRef) + const { overlayOpened, setOverlayOpened } = useOverlayState(props, rangeControlContext) + const handleKeyDown = useRangeKeyboardEvents(rangeControlContext, showFooter, setOverlayOpened, handleChange) + + const renderSeparator = () => slots.separator?.() ?? props.separator ?? locale.dateRangePicker.separator + + provide(dateRangePickerToken, { + props, + slots, + locale, + config, + mergedPrefixCls, + dateConfig, + inputRef, + inputEnableStatus, + overlayOpened, + setOverlayOpened, + rangeControlContext, + renderSeparator, + handleKeyDown, + ...formatContext, + ...pickerStateContext, + }) + + watch(overlayOpened, opened => { + if (!opened) { + rangeControlContext.init(true) + } + + opened ? focus() : blur() + }) + + watch(rangeControlContext.buffer, value => { + if (!showFooter.value) { + handleChange(value) + } + }) + + const target = computed(() => props.target ?? config.target ?? `${mergedPrefixCls.value}-overlay-container`) + const renderTrigger = () => + const renderContent = () => + const overlayProps = { 'onUpdate:visible': setOverlayOpened } + + return () => { + return ( + <ɵOverlay + {...overlayProps} + visible={overlayOpened.value} + v-slots={{ default: renderTrigger, content: renderContent }} + class={normalizeClass(props.overlayClassName)} + clickOutside + disabled={accessor.disabled.value || props.readonly} + offset={defaultOffset} + placement="bottomStart" + target={target.value} + trigger="manual" + /> + ) + } + }, +}) diff --git a/packages/components/date-picker/src/composables/useActiveDate.ts b/packages/components/date-picker/src/composables/useActiveDate.ts new file mode 100644 index 000000000..e609c1abe --- /dev/null +++ b/packages/components/date-picker/src/composables/useActiveDate.ts @@ -0,0 +1,149 @@ +/** + * @license + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://github.com/IDuxFE/idux/blob/main/LICENSE + */ + +import type { DatePickerProps, DatePickerType, DateRangePickerProps } from '../types' +import type { DateConfig } from '@idux/components/config' + +import { type ComputedRef, computed, watch } from 'vue' + +import { convertArray, useState } from '@idux/cdk/utils' + +import { convertToDate, sortRangeValue } from '../utils' + +export interface ActiveDateContext { + activeDate: ComputedRef + setActiveDate: (value: Date) => void +} + +export interface RangeActiveDateContext { + fromActiveDate: ComputedRef + toActiveDate: ComputedRef + setFromActiveDate: (value: Date) => void + setToActiveDate: (value: Date) => void +} + +export function useActiveDate( + dateConfig: DateConfig, + props: DatePickerProps, + valueRef: ComputedRef, + formatRef: ComputedRef, +): ActiveDateContext { + const defaultOpenValue = computed( + () => convertToDate(dateConfig, props.defaultOpenValue ?? dateConfig.now(), formatRef.value)!, + ) + const [activeDate, setActiveDate] = useState(valueRef.value ?? defaultOpenValue.value) + + watch(valueRef, value => setActiveDate(value ?? defaultOpenValue.value)) + + return { + activeDate, + setActiveDate, + } +} + +const viewTypeMap: Record = { + date: 'month', + datetime: 'month', + week: 'month', + month: 'year', + quarter: 'year', + year: 'year', +} + +export function useRangeActiveDate( + dateConfig: DateConfig, + props: DateRangePickerProps, + valuesRef: ComputedRef<(Date | undefined)[] | undefined>, + isSelecting: ComputedRef, + formatRef: ComputedRef, +): RangeActiveDateContext { + const { set, get } = dateConfig + const now = dateConfig.now() + + const fromPanelValue = computed(() => valuesRef.value?.[0]) + const toPanelValue = computed(() => valuesRef.value?.[1]) + + const calcValidActiveDate = (from: Date | undefined, to: Date | undefined, type: 'from' | 'to') => { + const viewType = viewTypeMap[props.type] + const viewSpan = props.type === 'year' ? 12 : 1 + const getViewDate = (value: Date) => + set(value, get(value, viewType) + (type === 'from' ? -viewSpan : viewSpan), viewType) + + if (!from) { + return type === 'from' ? now : getViewDate(now) + } + + if (!to) { + return type === 'from' ? from : getViewDate(from) + } + + const fromViewValue = get(from, viewType) + const toViewValue = get(to, viewType) + + /* eslint-disable indent */ + const valid = + props.type === 'year' + ? fromViewValue < toViewValue - viewSpan + : (() => { + const fromViewYearValue = get(from, 'year') + const toViewYearValue = get(to, 'year') + + return fromViewValue < toViewValue || fromViewYearValue < toViewYearValue + })() + /* eslint-disable indent */ + + if (type === 'from') { + return valid ? from : getViewDate(to) + } + + return valid ? to : getViewDate(from) + } + + const defaultOpenValue = computed(() => { + const convertedValues = sortRangeValue([ + ...convertArray(props.defaultOpenValue).map(v => convertToDate(dateConfig, v, formatRef.value)), + ]) + + return [convertedValues[0] ?? now, convertedValues[1]] + }) + + const [fromActiveDate, setFromActiveDate] = useState(fromPanelValue.value ?? defaultOpenValue.value[0]!) + const [toActiveDate, setToActiveDate] = useState( + calcValidActiveDate( + fromPanelValue.value ?? defaultOpenValue.value[0], + toPanelValue.value ?? defaultOpenValue.value[1], + 'to', + ), + ) + watch(valuesRef, value => { + if (isSelecting.value) { + return + } + + const from = value?.[0] ?? defaultOpenValue.value[0]! + const to = calcValidActiveDate(from, value?.[1] ?? defaultOpenValue.value[1], 'to') + + setFromActiveDate(from) + setToActiveDate(to) + }) + + const handleFromActiveDateUpdate = (value: Date) => { + setFromActiveDate(value) + setToActiveDate(calcValidActiveDate(value, toPanelValue.value, 'to')) + } + const handleToActiveDateUpdate = (value: Date) => { + setToActiveDate(value) + setFromActiveDate(calcValidActiveDate(fromPanelValue.value, value, 'from')) + } + + return { + fromActiveDate, + toActiveDate, + setFromActiveDate: handleFromActiveDateUpdate, + setToActiveDate: handleToActiveDateUpdate, + } +} diff --git a/packages/components/date-picker/src/composables/useControl.ts b/packages/components/date-picker/src/composables/useControl.ts new file mode 100644 index 000000000..bbe399f05 --- /dev/null +++ b/packages/components/date-picker/src/composables/useControl.ts @@ -0,0 +1,212 @@ +/** + * @license + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://github.com/IDuxFE/idux/blob/main/LICENSE + */ + +import type { FormatContext } from './useFormat' +import type { InputEnableStatus } from './useInputEnableStatus' +import type { DateConfig } from '@idux/components/config' + +import { type ComputedRef, watch } from 'vue' + +import { useState } from '@idux/cdk/utils' + +import { applyDateTime, convertToDate } from '../utils' + +export interface PickerControlContext { + inputValue: ComputedRef + dateInputValue: ComputedRef + timeInputVaue: ComputedRef + panelValue: ComputedRef + + visiblePanel: ComputedRef<'datePanel' | 'timePanel'> + setVisiblePanel: (value: 'datePanel' | 'timePanel') => void + + init: (force?: boolean) => void + handleInput: (evt: Event) => void + handleDateInput: (evt: Event) => void + handleTimeInput: (evt: Event) => void + handleDateInputClear: () => void + handleTimeInputClear: () => void + handleDatePanelChange: (value: Date) => void + handleTimePanelChange: (value: Date) => void + handleDateInputFocus: () => void + handleTimeInputFocus: () => void +} + +export function useControl( + dateConfig: DateConfig, + formatContext: FormatContext, + inputEnableStatus: ComputedRef, + valueRef: ComputedRef, + handleChange: (value: Date | undefined) => void, +): PickerControlContext { + const { formatRef, dateFormatRef, timeFormatRef } = formatContext + + const [inputValue, setInputValue] = useState('') + const [dateInputValue, setDateInputValue] = useState('') + const [timeInputVaue, setTimeInputValue] = useState('') + const [panelValue, setPanelValue] = useState(undefined) + const [visiblePanel, setVisiblePanel] = useState<'datePanel' | 'timePanel'>('datePanel') + + function initInputValue(currValue: Date | undefined, force = false) { + if (!currValue) { + setInputValue('') + return + } + + const { parse, format } = dateConfig + + if (force || parse(inputValue.value, formatRef.value).valueOf() !== currValue.valueOf()) { + setInputValue(format(currValue, formatRef.value)) + } + } + function initDateInputValue(currValue: Date | undefined, force = false) { + if (!currValue) { + setDateInputValue('') + return + } + + const { isSame, parse, format } = dateConfig + const parsedValue = parse(dateInputValue.value, dateFormatRef.value) + + if ( + force || + !isSame(parsedValue, currValue, 'year') || + !isSame(parsedValue, currValue, 'month') || + !isSame(parsedValue, currValue, 'date') + ) { + setDateInputValue(format(currValue, dateFormatRef.value)) + } + } + function initTimeInputValue(currValue: Date | undefined, force = false) { + if (!currValue) { + setTimeInputValue('') + return + } + + const { parse, format, isSame } = dateConfig + const parsedValue = parse(timeInputVaue.value, timeFormatRef.value) + + if ( + force || + !isSame(parsedValue, currValue, 'hour') || + !isSame(parsedValue, currValue, 'minute') || + !isSame(parsedValue, currValue, 'second') + ) { + setTimeInputValue(format(currValue, timeFormatRef.value)) + } + } + + function init(force = false) { + const currDateValue = convertToDate(dateConfig, valueRef.value, formatRef.value) + + initInputValue(currDateValue, force) + inputEnableStatus.value.enableOverlayDateInput && initDateInputValue(currDateValue, force) + inputEnableStatus.value.enableOverlayTimeInput && initTimeInputValue(currDateValue, force) + + setPanelValue(currDateValue) + } + + watch(valueRef, () => init(), { immediate: true }) + watch(inputEnableStatus, () => init()) + + function parseInput(value: string, format: string) { + return value ? dateConfig.parse(value, format) : undefined + } + function checkInputValid(date: Date | undefined) { + return !date || dateConfig.isValid(date) + } + + function handleInput(evt: Event) { + const value = (evt.target as HTMLInputElement).value + + setInputValue(value) + const currDate = parseInput(value, formatRef.value) + if (checkInputValid(currDate)) { + handleChange(currDate) + } + } + function handleDateInput(evt: Event) { + const value = (evt.target as HTMLInputElement).value + + setDateInputValue(value) + let currDate = parseInput(value, dateFormatRef.value) + if (!checkInputValid(currDate)) { + return + } + + const accessorValue = convertToDate(dateConfig, valueRef.value, formatRef.value) + if (currDate && accessorValue) { + currDate = applyDateTime(dateConfig, accessorValue, currDate, ['hour', 'minute', 'second']) + } + + handleChange(currDate) + setVisiblePanel('datePanel') + } + function handleTimeInput(evt: Event) { + const value = (evt.target as HTMLInputElement).value + + setTimeInputValue(value) + let currDate = parseInput(value, timeFormatRef.value) + if (!checkInputValid(currDate)) { + return + } + + const accessorValue = convertToDate(dateConfig, valueRef.value, formatRef.value) + if (currDate && accessorValue) { + currDate = applyDateTime(dateConfig, accessorValue, currDate, ['year', 'month', 'date']) + } + + handleChange(currDate) + setVisiblePanel('timePanel') + } + + function handleDateInputClear() { + setDateInputValue('') + } + function handleTimeInputClear() { + setTimeInputValue('') + } + + function handleDatePanelChange(value: Date) { + handleChange( + panelValue.value ? applyDateTime(dateConfig, panelValue.value, value, ['hour', 'minute', 'second']) : value, + ) + } + function handleTimePanelChange(value: Date) { + handleChange( + panelValue.value ? applyDateTime(dateConfig, panelValue.value, value, ['year', 'month', 'date']) : value, + ) + } + + function handleDateInputFocus() { + setVisiblePanel('datePanel') + } + function handleTimeInputFocus() { + setVisiblePanel('timePanel') + } + + return { + inputValue, + dateInputValue, + timeInputVaue, + panelValue, + + visiblePanel, + setVisiblePanel, + + init, + handleInput, + handleDateInput, + handleTimeInput, + handleDateInputClear, + handleTimeInputClear, + handleDatePanelChange, + handleTimePanelChange, + handleDateInputFocus, + handleTimeInputFocus, + } +} diff --git a/packages/components/date-picker/src/composables/useFormat.ts b/packages/components/date-picker/src/composables/useFormat.ts index f5c08fecf..0a4a877fe 100644 --- a/packages/components/date-picker/src/composables/useFormat.ts +++ b/packages/components/date-picker/src/composables/useFormat.ts @@ -7,31 +7,50 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import type { DatePickerProps } from '../types' -import type { DatePickerConfig } from '@idux/components/config' +import type { DatePickerProps, DateRangePickerProps } from '../types' import type { ComputedRef } from 'vue' import { computed } from 'vue' +import { type DatePickerConfig, useGlobalConfig } from '@idux/components/config' + +export interface FormatContext { + formatRef: ComputedRef + dateFormatRef: ComputedRef + timeFormatRef: ComputedRef +} + const defaultFormat = { date: 'yyyy-MM-dd', week: 'RRRR-II', month: 'yyyy-MM', quarter: "yyyy-'Q'Q", year: 'yyyy', + datetime: 'yyyy-MM-dd HH:mm:ss', } as const -export function useFormat(props: DatePickerProps, config: DatePickerConfig): ComputedRef { - return computed(() => { - let format = props.format - if (format) { - return format - } - const formatConfig = config.format +export function useFormat(props: DatePickerProps | DateRangePickerProps, config: DatePickerConfig): FormatContext { + const timePickerConfig = useGlobalConfig('timePicker') + const formatRef = computed(() => { const type = props.type - if (formatConfig) { - format = formatConfig[type] + return props.format ?? config.format?.[type] ?? defaultFormat[type] + }) + + const dateFormatRef = computed(() => { + if (props.type !== 'datetime') { + return formatRef.value } - return format ?? defaultFormat[type] + + return props.dateFormat ?? defaultFormat.date }) + + const timeFormatRef = computed(() => { + return props.timeFormat ?? timePickerConfig.format + }) + + return { + formatRef, + dateFormatRef, + timeFormatRef, + } } diff --git a/packages/components/date-picker/src/composables/useInputEnableStatus.ts b/packages/components/date-picker/src/composables/useInputEnableStatus.ts new file mode 100644 index 000000000..77c6a3ef5 --- /dev/null +++ b/packages/components/date-picker/src/composables/useInputEnableStatus.ts @@ -0,0 +1,35 @@ +/** + * @license + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://github.com/IDuxFE/idux/blob/main/LICENSE + */ + +import type { DatePickerProps, DateRangePickerProps } from '../types' +import type { DatePickerConfig } from '@idux/components/config' +import type { ComputedRef } from 'vue' + +import { computed } from 'vue' + +export interface InputEnableStatus { + allowInput: boolean | 'overlay' + enableInput: boolean + enableOverlayDateInput: boolean + enableOverlayTimeInput: boolean +} + +export function useInputEnableStatus( + props: DatePickerProps | DateRangePickerProps, + config: DatePickerConfig, +): ComputedRef { + return computed(() => { + const allowInput = props.allowInput ?? config.allowInput + + return { + allowInput, + enableInput: allowInput === true, + enableOverlayDateInput: allowInput === 'overlay' || props.type === 'datetime', + enableOverlayTimeInput: props.type === 'datetime', + } + }) +} diff --git a/packages/components/date-picker/src/composables/useKeyboardEvents.ts b/packages/components/date-picker/src/composables/useKeyboardEvents.ts new file mode 100644 index 000000000..c61d44370 --- /dev/null +++ b/packages/components/date-picker/src/composables/useKeyboardEvents.ts @@ -0,0 +1,43 @@ +/** + * @license + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://github.com/IDuxFE/idux/blob/main/LICENSE + */ + +import type { PickerRangeControlContext } from './useRangeControl' +import type { ComputedRef } from 'vue' + +export function useKeyboardEvents(setOverlayOpened: (opened: boolean) => void): (evt: KeyboardEvent) => void { + return (evt: KeyboardEvent) => { + if (evt.code === 'Escape' || evt.code === 'Enter') { + setOverlayOpened(false) + } + } +} + +export function useRangeKeyboardEvents( + rangeControl: PickerRangeControlContext, + showFooter: ComputedRef, + setOverlayOpened: (opened: boolean) => void, + handleChange: (value: (Date | undefined)[] | undefined) => void, +): (evt: KeyboardEvent) => void { + const { isSelecting, bufferUpdated, buffer } = rangeControl + return (evt: KeyboardEvent) => { + switch (evt.code) { + case 'Escape': + setOverlayOpened(false) + break + + case 'Enter': + if (!isSelecting.value && bufferUpdated.value && showFooter.value) { + handleChange(buffer.value) + } + setOverlayOpened(false) + break + + default: + break + } + } +} diff --git a/packages/components/date-picker/src/composables/useOverlayState.ts b/packages/components/date-picker/src/composables/useOverlayState.ts index ddf6f433b..b817744b7 100644 --- a/packages/components/date-picker/src/composables/useOverlayState.ts +++ b/packages/components/date-picker/src/composables/useOverlayState.ts @@ -5,7 +5,9 @@ * found in the LICENSE file at https://github.com/IDuxFE/idux/blob/main/LICENSE */ -import type { DatePickerProps } from '../types' +import type { DatePickerProps, DateRangePickerProps } from '../types' +import type { PickerControlContext } from './useControl' +import type { PickerRangeControlContext } from './useRangeControl' import type { ComputedRef } from 'vue' import { onMounted } from 'vue' @@ -17,14 +19,24 @@ export interface OverlayStateContext { setOverlayOpened: (open: boolean) => void } -export function useOverlayState(props: DatePickerProps): OverlayStateContext { +export function useOverlayState( + props: DatePickerProps | DateRangePickerProps, + control: PickerControlContext | PickerRangeControlContext, +): OverlayStateContext { const [overlayOpened, setOverlayOpened] = useControlledProp(props, 'open', false) + const changeOpenedState = (open: boolean) => { + setOverlayOpened(open) + if (!open) { + control.init() + } + } + onMounted(() => { if (props.autofocus) { setOverlayOpened(true) } }) - return { overlayOpened, setOverlayOpened } + return { overlayOpened, setOverlayOpened: changeOpenedState } } diff --git a/packages/components/date-picker/src/composables/usePickerState.ts b/packages/components/date-picker/src/composables/usePickerState.ts new file mode 100644 index 000000000..82502d999 --- /dev/null +++ b/packages/components/date-picker/src/composables/usePickerState.ts @@ -0,0 +1,87 @@ +/** + * @license + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://github.com/IDuxFE/idux/blob/main/LICENSE + */ + +import type { DatePickerProps, DateRangePickerProps } from '../types' +import type { ValueAccessor } from '@idux/cdk/forms' +import type { DateConfig } from '@idux/components/config' + +import { type ComputedRef, toRaw } from 'vue' + +import { isArray } from 'lodash-es' + +import { callEmit, useState } from '@idux/cdk/utils' +import { useFormAccessor } from '@idux/components/utils' + +import { convertToDate, sortRangeValue } from '../utils' + +type StateValueType = T extends DatePickerProps + ? Date | undefined + : (Date | undefined)[] | undefined + +export interface PickerStateContext { + accessor: ValueAccessor + isFocused: ComputedRef + handleChange: (value: StateValueType) => void + handleClear: (evt: Event) => void + handleFocus: (evt: FocusEvent) => void + handleBlur: (evt: FocusEvent) => void +} + +export function usePickerState( + props: T, + dateConfig: DateConfig, + formatRef: ComputedRef, +): PickerStateContext { + const accessor = useFormAccessor() + + const [isFocused, setFocused] = useState(false) + + function handleChange(value: StateValueType) { + const newValue = (isArray(value) ? sortRangeValue(value) : value) as StateValueType + let oldValue = toRaw(accessor.valueRef.value) as StateValueType + oldValue = ( + isArray(oldValue) + ? oldValue.map(v => convertToDate(dateConfig, v, formatRef.value)) + : convertToDate(dateConfig, oldValue, formatRef.value) + ) as StateValueType + callEmit(props.onChange as (value: StateValueType, oldValue: StateValueType) => void, newValue, oldValue) + accessor.setValue(value) + } + + function handleClear(evt: Event) { + let oldValue = toRaw(accessor.valueRef.value) as StateValueType + oldValue = ( + isArray(oldValue) + ? oldValue.map(v => convertToDate(dateConfig, v, formatRef.value)) + : convertToDate(dateConfig, oldValue, formatRef.value) + ) as StateValueType + + callEmit(props.onClear, evt as MouseEvent) + callEmit(props.onChange as (value: StateValueType, oldValue: StateValueType) => void, undefined, oldValue) + accessor.setValue(undefined) + } + + function handleFocus(evt: FocusEvent) { + callEmit(props.onFocus, evt) + setFocused(true) + } + + function handleBlur(evt: FocusEvent) { + callEmit(props.onBlur, evt) + accessor.markAsBlurred() + setFocused(false) + } + + return { + accessor, + isFocused, + handleChange, + handleClear, + handleFocus, + handleBlur, + } +} diff --git a/packages/components/date-picker/src/composables/useRangeControl.ts b/packages/components/date-picker/src/composables/useRangeControl.ts new file mode 100644 index 000000000..30e70bc99 --- /dev/null +++ b/packages/components/date-picker/src/composables/useRangeControl.ts @@ -0,0 +1,136 @@ +/** + * @license + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://github.com/IDuxFE/idux/blob/main/LICENSE + */ + +import type { FormatContext } from './useFormat' +import type { InputEnableStatus } from './useInputEnableStatus' +import type { DateConfig } from '@idux/components/config' + +import { type ComputedRef, computed, watch } from 'vue' + +import { convertArray, useState } from '@idux/cdk/utils' + +import { convertToDate, sortRangeValue } from '../utils' +import { type PickerControlContext, useControl } from './useControl' + +export interface PickerRangeControlContext { + buffer: ComputedRef<(Date | undefined)[] | undefined> + panelValue: ComputedRef<(Date | undefined)[] | undefined> + isSelecting: ComputedRef + bufferUpdated: ComputedRef + + visiblePanel: ComputedRef<'datePanel' | 'timePanel'> + setVisiblePanel: (value: 'datePanel' | 'timePanel') => void + + fromControl: PickerControlContext + toControl: PickerControlContext + + init: (force?: boolean) => void + handleDatePanelCellClick: (value: Date) => void + handleDatePanelCellMouseenter: (value: Date) => void +} + +export function useRangeControl( + dateConfig: DateConfig, + formatContext: FormatContext, + inputEnableStatus: ComputedRef, + valueRef: ComputedRef<(string | number | Date | undefined)[] | undefined>, +): PickerRangeControlContext { + const { formatRef } = formatContext + const [buffer, setBuffer] = useState<(Date | undefined)[] | undefined>( + convertArray(valueRef.value).map(v => convertToDate(dateConfig, v, formatRef.value)), + ) + const [bufferUpdated, setBufferUpdated] = useState(false) + const handleBufferUpdate = (values: (string | number | Date | undefined)[] | undefined) => { + setBuffer(getRangeValue(dateConfig, values, formatRef.value)) + setBufferUpdated(true) + } + + const [selectingDate, setSelectingDate] = useState<(Date | undefined)[] | undefined>(buffer.value) + const [isSelecting, setIsSelecting] = useState(false) + const [visiblePanel, setVisiblePanel] = useState<'datePanel' | 'timePanel'>('datePanel') + + const handleVisiblePanelUpdate = (value: 'datePanel' | 'timePanel') => { + visiblePanel.value !== value && setVisiblePanel(value) + fromControl.visiblePanel.value !== value && fromControl.setVisiblePanel(value) + toControl.visiblePanel.value !== value && toControl.setVisiblePanel(value) + } + + const rangeValueRef = computed(() => convertArray(buffer.value)) + const fromDateRef = computed(() => rangeValueRef.value[0]) + const toDateRef = computed(() => rangeValueRef.value[1]) + + const panelValue = computed(() => { + if (isSelecting.value) { + return sortRangeValue([...convertArray(selectingDate.value)]) + } + + return sortRangeValue([...convertArray(buffer.value)]) + }) + + watch(valueRef, handleBufferUpdate) + + const fromControl = useControl(dateConfig, formatContext, inputEnableStatus, fromDateRef, value => { + handleBufferUpdate([value, buffer.value?.[1]]) + }) + const toControl = useControl(dateConfig, formatContext, inputEnableStatus, toDateRef, value => { + handleBufferUpdate([buffer.value?.[0], value]) + }) + + watch(fromControl.visiblePanel, handleVisiblePanelUpdate) + watch(toControl.visiblePanel, handleVisiblePanelUpdate) + + const init = (force = false) => { + handleBufferUpdate(valueRef.value) + handleVisiblePanelUpdate('datePanel') + setBufferUpdated(false) + fromControl.init(force) + toControl.init(force) + } + + const handleDatePanelCellClick = (value: Date) => { + if (!isSelecting.value) { + setIsSelecting(true) + setSelectingDate([value, undefined]) + } else { + setIsSelecting(false) + handleBufferUpdate([selectingDate.value?.[0], value]) + } + } + + const handleDatePanelCellMouseenter = (value: Date) => { + if (!isSelecting.value) { + return + } + + setSelectingDate([selectingDate.value?.[0], value]) + } + + return { + buffer, + panelValue, + isSelecting, + bufferUpdated, + + visiblePanel, + setVisiblePanel: handleVisiblePanelUpdate, + + fromControl, + toControl, + + init, + handleDatePanelCellClick, + handleDatePanelCellMouseenter, + } +} + +function getRangeValue( + dateConfig: DateConfig, + values: (string | number | Date | undefined)[] | undefined, + format: string, +) { + return convertArray(values).map(v => convertToDate(dateConfig, v, format)) +} diff --git a/packages/components/date-picker/src/composables/useTriggerProps.ts b/packages/components/date-picker/src/composables/useTriggerProps.ts new file mode 100644 index 000000000..c8f411f9e --- /dev/null +++ b/packages/components/date-picker/src/composables/useTriggerProps.ts @@ -0,0 +1,58 @@ +/** + * @license + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://github.com/IDuxFE/idux/blob/main/LICENSE + */ + +import type { DataPickerContext, DateRangePickerContext } from '../token' +import type { ɵTriggerProps } from '@idux/components/_private/trigger' +import type { FormContext } from '@idux/components/form' + +import { type ComputedRef, computed } from 'vue' + +export function useTriggerProps( + context: DataPickerContext | DateRangePickerContext, + formContext: FormContext | null, +): ComputedRef<ɵTriggerProps> { + const { + props, + config, + accessor, + isFocused, + handleFocus, + handleBlur, + handleClear, + handleKeyDown, + overlayOpened, + setOverlayOpened, + inputEnableStatus, + } = context + + const handleClick = () => { + const currOpened = overlayOpened.value + if (currOpened || accessor.disabled.value) { + return + } + + setOverlayOpened(!currOpened) + } + + return computed(() => { + return { + borderless: props.borderless, + clearable: !accessor.disabled.value && props.clearable && !!accessor.valueRef.value, + clearIcon: props.clearIcon ?? config.clearIcon, + disabled: accessor.disabled.value, + focused: isFocused.value || overlayOpened.value, + readonly: props.readonly || inputEnableStatus.value.enableInput === false, + size: props.size ?? formContext?.size.value ?? config.size, + suffix: props.suffix ?? config.suffix, + onClick: handleClick, + onClear: handleClear, + onFocus: handleFocus, + onBlur: handleBlur, + onKeyDown: handleKeyDown, + } + }) +} diff --git a/packages/components/date-picker/src/content/Content.tsx b/packages/components/date-picker/src/content/Content.tsx index ffc81334d..ef85b6de2 100644 --- a/packages/components/date-picker/src/content/Content.tsx +++ b/packages/components/date-picker/src/content/Content.tsx @@ -5,31 +5,159 @@ * found in the LICENSE file at https://github.com/IDuxFE/idux/blob/main/LICENSE */ -import { defineComponent, inject } from 'vue' +import { defineComponent, inject, onMounted, onUpdated, ref } from 'vue' import { ɵDatePanel } from '@idux/components/_private/date-panel' +import { ɵFooter } from '@idux/components/_private/footer' +import { ɵInput, type ɵInputInstance } from '@idux/components/_private/input' +import { ɵTimePanel } from '@idux/components/_private/time-panel' +import { useActiveDate } from '../composables/useActiveDate' import { datePickerToken } from '../token' export default defineComponent({ setup() { - const { props, slots, overlayOpened, panelDate, handlePanelCellClick } = inject(datePickerToken)! + const context = inject(datePickerToken)! + const { + props, + config, + dateConfig, + mergedPrefixCls, + formatRef, + dateFormatRef, + timeFormatRef, + slots, + overlayOpened, + inputEnableStatus, + inputRef, + controlContext: { + dateInputValue, + timeInputVaue, + visiblePanel, + panelValue, + handleDateInput, + handleTimeInput, + handleDateInputClear, + handleTimeInputClear, + handleDateInputFocus, + handleTimeInputFocus, + handleDatePanelChange, + handleTimePanelChange, + }, + setOverlayOpened, + handleKeyDown, + handleClear, + } = context - return () => { - const { overlayRender } = props - - const children = ( - <ɵDatePanel - v-slots={slots} - disabledDate={props.disabledDate} - type={props.type} - value={panelDate.value} - visible={overlayOpened.value} - onCellClick={handlePanelCellClick} - /> + const { activeDate, setActiveDate } = useActiveDate(dateConfig, props, panelValue, formatRef) + + const inputInstance = ref<ɵInputInstance>() + const setInputRef = () => { + if (inputEnableStatus.value.allowInput === 'overlay') { + inputRef.value = inputInstance.value?.getInputElement() + } + } + onMounted(setInputRef) + onUpdated(setInputRef) + + const handleDatePanelCellClick = (value: Date) => { + handleDatePanelChange(value) + + if (!inputEnableStatus.value.enableOverlayTimeInput) { + setOverlayOpened(false) + } + } + + const handleMouseDown = (e: MouseEvent) => { + if (!(e.target instanceof HTMLInputElement)) { + e.preventDefault() + } + } + + const renderInputs = (prefixCls: string) => { + if (!inputEnableStatus.value.enableOverlayDateInput) { + return + } + + return ( +
+ <ɵInput + ref={inputInstance} + class={`${prefixCls}-date-input`} + v-slots={slots} + value={dateInputValue.value} + size="sm" + clearable={props.clearable ?? config.clearable} + clearIcon={props.clearIcon ?? config.clearIcon} + clearVisible={!!dateInputValue.value} + placeholder={dateFormatRef.value} + onInput={handleDateInput} + onFocus={handleDateInputFocus} + onKeydown={handleKeyDown} + onClear={inputEnableStatus.value.enableOverlayTimeInput ? handleDateInputClear : handleClear} + /> + {inputEnableStatus.value.enableOverlayTimeInput && ( + <ɵInput + class={`${prefixCls}-time-input`} + v-slots={slots} + value={timeInputVaue.value} + size="sm" + clearable={props.clearable ?? config.clearable} + clearIcon={props.clearIcon ?? config.clearIcon} + clearVisible={!!timeInputVaue.value} + placeholder={timeFormatRef.value} + onInput={handleTimeInput} + onFocus={handleTimeInputFocus} + onKeydown={handleKeyDown} + onClear={handleTimeInputClear} + /> + )} +
) + } + + return () => { + const prefixCls = `${mergedPrefixCls.value}-overlay` + const boardPrefixCls = `${mergedPrefixCls.value}-board` + const datePanelType = props.type === 'datetime' ? 'date' : props.type + + const datePanelProps = { + cellTooltip: props.cellTooltip, + disabledDate: props.disabledDate, + type: datePanelType, + value: panelValue.value, + visible: overlayOpened.value, + activeDate: activeDate.value, + onCellClick: handleDatePanelCellClick, + 'onUpdate:activeDate': setActiveDate, + } - return overlayRender ? overlayRender([children]) :
{children}
+ const children = [ +
+ {renderInputs(boardPrefixCls)} +
+ <ɵDatePanel v-show={visiblePanel.value === 'datePanel'} v-slots={slots} {...datePanelProps} /> + {inputEnableStatus.value.enableOverlayTimeInput && ( + <ɵTimePanel + v-show={visiblePanel.value === 'timePanel'} + {...(props.timePanelOptions ?? {})} + value={panelValue.value} + visible={visiblePanel.value === 'timePanel'} + onChange={handleTimePanelChange} + /> + )} +
+
, + <ɵFooter v-slots={slots} class={`${prefixCls}-footer`} footer={props.footer} />, + ] + + return props.overlayRender ? ( + props.overlayRender(children) + ) : ( +
+ {children} +
+ ) } }, }) diff --git a/packages/components/date-picker/src/content/RangeContent.tsx b/packages/components/date-picker/src/content/RangeContent.tsx new file mode 100644 index 000000000..e21289129 --- /dev/null +++ b/packages/components/date-picker/src/content/RangeContent.tsx @@ -0,0 +1,189 @@ +/** + * @license + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://github.com/IDuxFE/idux/blob/main/LICENSE + */ + +import { computed, defineComponent, inject, onMounted, onUpdated, ref } from 'vue' + +import { convertArray } from '@idux/cdk/utils' +import { ɵDatePanel } from '@idux/components/_private/date-panel' +import { ɵFooter } from '@idux/components/_private/footer' +import { ɵInput, type ɵInputInstance } from '@idux/components/_private/input' +import { ɵTimePanel } from '@idux/components/_private/time-panel' + +import { useRangeActiveDate } from '../composables/useActiveDate' +import { dateRangePickerToken } from '../token' + +export default defineComponent({ + setup() { + const context = inject(dateRangePickerToken)! + const { + props, + config, + dateConfig, + locale, + slots, + overlayOpened, + formatRef, + dateFormatRef, + timeFormatRef, + rangeControlContext: { + buffer, + panelValue, + visiblePanel, + isSelecting, + fromControl, + toControl, + handleDatePanelCellClick, + handleDatePanelCellMouseenter, + }, + mergedPrefixCls, + inputEnableStatus, + inputRef, + handleChange, + handleKeyDown, + renderSeparator, + setOverlayOpened, + } = context + + const timePanelProps = computed(() => convertArray(context.props.timePanelOptions)) + const { fromActiveDate, toActiveDate, setFromActiveDate, setToActiveDate } = useRangeActiveDate( + dateConfig, + props, + panelValue, + isSelecting, + formatRef, + ) + + const inputInstance = ref<ɵInputInstance>() + onMounted(() => { + inputRef.value = inputInstance.value?.getInputElement() + }) + onUpdated(() => { + inputRef.value = inputInstance.value?.getInputElement() + }) + + const handleConfirm = () => { + handleChange(buffer.value) + setOverlayOpened(false) + } + + const handleMouseDown = (e: MouseEvent) => { + if (!(e.target instanceof HTMLInputElement)) { + e.preventDefault() + } + } + + const renderBoard = (prefixCls: string, isFrom: boolean) => { + const { enableOverlayDateInput, enableOverlayTimeInput } = inputEnableStatus.value + + const { + dateInputValue, + timeInputVaue, + handleDateInput, + handleTimeInput, + handleDateInputClear, + handleTimeInputClear, + handleDateInputFocus, + handleTimeInputFocus, + handleTimePanelChange, + } = isFrom ? fromControl : toControl + + const inputs = enableOverlayDateInput && ( +
+ <ɵInput + ref={inputInstance} + class={`${prefixCls}-date-input`} + v-slots={slots} + value={dateInputValue.value} + size="sm" + clearable={props.clearable ?? config.clearable} + clearIcon={props.clearIcon ?? config.clearIcon} + placeholder={dateFormatRef.value} + onInput={handleDateInput} + onFocus={handleDateInputFocus} + onKeydown={handleKeyDown} + onClear={handleDateInputClear} + /> + {enableOverlayTimeInput && ( + <ɵInput + class={`${prefixCls}-time-input`} + v-slots={slots} + value={timeInputVaue.value} + size="sm" + clearable={props.clearable ?? config.clearable} + clearIcon={props.clearIcon ?? config.clearIcon} + placeholder={timeFormatRef.value} + onInput={handleTimeInput} + onFocus={handleTimeInputFocus} + onKeydown={handleKeyDown} + onClear={handleTimeInputClear} + /> + )} +
+ ) + + const datePanelProps = { + cellTooltip: props.cellTooltip, + disabledDate: props.disabledDate, + type: props.type === 'datetime' ? 'date' : props.type, + value: panelValue.value, + visible: overlayOpened.value, + activeDate: isFrom ? fromActiveDate.value : toActiveDate.value, + onCellClick: handleDatePanelCellClick, + onCellMouseenter: handleDatePanelCellMouseenter, + 'onUpdate:activeDate': isFrom ? setFromActiveDate : setToActiveDate, + } + + return ( +
+ {inputs} +
+ <ɵDatePanel v-show={visiblePanel.value !== 'timePanel'} v-slots={slots} {...datePanelProps} /> + {inputEnableStatus.value.enableOverlayTimeInput && ( + <ɵTimePanel + v-show={visiblePanel.value === 'timePanel'} + {...(timePanelProps.value[isFrom ? 0 : 1] ?? {})} + value={panelValue.value?.[isFrom ? 0 : 1]} + visible={visiblePanel.value === 'timePanel'} + onChange={handleTimePanelChange} + /> + )} +
+
+ ) + } + + return () => { + const prefixCls = `${mergedPrefixCls.value}-overlay` + const boardPrefixCls = `${mergedPrefixCls.value}-board` + + const children = [ +
+ {renderBoard(boardPrefixCls, true)} +
{inputEnableStatus.value.enableOverlayDateInput && renderSeparator()}
+ {renderBoard(boardPrefixCls, false)} +
, + <ɵFooter + v-slots={slots} + class={`${prefixCls}-footer`} + footer={props.footer} + okText={locale.dateRangePicker.okText} + okButton={{ size: 'xs', mode: 'primary' }} + cancelVisible={false} + ok={handleConfirm} + />, + ] + + return props.overlayRender ? ( + props.overlayRender(children) + ) : ( +
+ {children} +
+ ) + } + }, +}) diff --git a/packages/components/date-picker/src/token.ts b/packages/components/date-picker/src/token.ts index 7481ddf15..d20053659 100644 --- a/packages/components/date-picker/src/token.ts +++ b/packages/components/date-picker/src/token.ts @@ -7,28 +7,48 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import type { InputStateContext } from './composables/useInputState' +// import type { InputStateContext } from './composables/useInputState' +import type { PickerControlContext } from './composables/useControl' +import type { FormatContext } from './composables/useFormat' +import type { InputEnableStatus } from './composables/useInputEnableStatus' import type { OverlayStateContext } from './composables/useOverlayState' -import type { PanelStateContext } from './composables/usePanelState' -import type { DatePickerProps } from './types' -import type { FocusMonitor } from '@idux/cdk/a11y' -import type { ValueAccessor } from '@idux/cdk/forms' +// import type { PanelStateContext } from './composables/usePanelState' +import type { PickerStateContext } from './composables/usePickerState' +import type { PickerRangeControlContext } from './composables/useRangeControl' +import type { DatePickerProps, DateRangePickerProps } from './types' import type { DateConfig, DatePickerConfig } from '@idux/components/config' import type { Locale } from '@idux/components/locales' -import type { ComputedRef, InjectionKey, Ref, Slots } from 'vue' +import type { ComputedRef, InjectionKey, Ref, Slots, VNodeTypes } from 'vue' -export interface DataPickerContext extends InputStateContext, OverlayStateContext, PanelStateContext { +export interface DataPickerContext extends OverlayStateContext, FormatContext, PickerStateContext { props: DatePickerProps slots: Slots locale: Locale config: DatePickerConfig mergedPrefixCls: ComputedRef dateConfig: DateConfig - focusMonitor: FocusMonitor inputRef: Ref - format: ComputedRef - accessor: ValueAccessor - handlePanelCellClick: (date: Date) => void + inputEnableStatus: ComputedRef + controlContext: PickerControlContext + handleKeyDown: (evt: KeyboardEvent) => void +} + +export interface DateRangePickerContext + extends OverlayStateContext, + FormatContext, + PickerStateContext { + props: DateRangePickerProps + slots: Slots + locale: Locale + config: DatePickerConfig + mergedPrefixCls: ComputedRef + dateConfig: DateConfig + inputRef: Ref + inputEnableStatus: ComputedRef + rangeControlContext: PickerRangeControlContext + renderSeparator: () => VNodeTypes + handleKeyDown: (evt: KeyboardEvent) => void } export const datePickerToken: InjectionKey = Symbol('datePickerToken') +export const dateRangePickerToken: InjectionKey = Symbol('dateRangePickerToken') diff --git a/packages/components/date-picker/src/trigger/RangeTrigger.tsx b/packages/components/date-picker/src/trigger/RangeTrigger.tsx new file mode 100644 index 000000000..941a5f696 --- /dev/null +++ b/packages/components/date-picker/src/trigger/RangeTrigger.tsx @@ -0,0 +1,86 @@ +/** + * @license + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://github.com/IDuxFE/idux/blob/main/LICENSE + */ + +import { computed, defineComponent, inject } from 'vue' + +import { callEmit } from '@idux/cdk/utils' +import { ɵTrigger } from '@idux/components/_private/trigger' +import { FORM_TOKEN } from '@idux/components/form' + +import { useTriggerProps } from '../composables/useTriggerProps' +import { dateRangePickerToken } from '../token' + +export default defineComponent({ + setup() { + const context = inject(dateRangePickerToken)! + const { + props, + slots, + locale, + rangeControlContext: { fromControl, toControl }, + mergedPrefixCls, + formatRef, + inputRef, + inputEnableStatus, + renderSeparator, + } = context + const formContext = inject(FORM_TOKEN, null) + + const placeholders = computed(() => [ + props.placeholder?.[0] ?? locale.dateRangePicker[`${props.type}Placeholder`][0], + props.placeholder?.[1] ?? locale.dateRangePicker[`${props.type}Placeholder`][1], + ]) + const inputSize = computed(() => Math.max(10, formatRef.value.length) + 2) + const triggerProps = useTriggerProps(context, formContext) + + const handleFromInput = (evt: Event) => { + fromControl.handleInput(evt) + callEmit(props.onInput, true, evt) + } + const handleToInput = (evt: Event) => { + toControl.handleInput(evt) + callEmit(props.onInput, false, evt) + } + + const renderSide = (isFrom: boolean) => { + const prefixCls = mergedPrefixCls.value + const { inputValue } = isFrom ? fromControl : toControl + const placeholder = placeholders.value[isFrom ? 0 : 1] + const handleInput = isFrom ? handleFromInput : handleToInput + + const { disabled, readonly } = triggerProps.value + + return ( + + ) + } + + return () => { + const prefixCls = mergedPrefixCls.value + + return ( + <ɵTrigger className={prefixCls} v-slots={slots} {...triggerProps.value}> +
+ {renderSide(true)} + {renderSeparator()} + {renderSide(false)} +
+ + ) + } + }, +}) diff --git a/packages/components/date-picker/src/trigger/Trigger.tsx b/packages/components/date-picker/src/trigger/Trigger.tsx index bb8facf2a..d5670df47 100644 --- a/packages/components/date-picker/src/trigger/Trigger.tsx +++ b/packages/components/date-picker/src/trigger/Trigger.tsx @@ -5,124 +5,60 @@ * found in the LICENSE file at https://github.com/IDuxFE/idux/blob/main/LICENSE */ -import { computed, defineComponent, inject, normalizeClass, onBeforeUnmount, onMounted, ref, watch } from 'vue' +import { computed, defineComponent, inject } from 'vue' +import { callEmit } from '@idux/cdk/utils' +import { ɵTrigger } from '@idux/components/_private/trigger' import { FORM_TOKEN } from '@idux/components/form' -import { IxIcon } from '@idux/components/icon' +import { useTriggerProps } from '../composables/useTriggerProps' import { datePickerToken } from '../token' export default defineComponent({ setup() { + const context = inject(datePickerToken)! const { props, slots, locale, - config, + controlContext: { inputValue, handleInput: _handleInput }, mergedPrefixCls, - accessor, - format, - focusMonitor, + formatRef, + inputEnableStatus, inputRef, - inputValue, - isFocused, - handleFocus, - handleBlur, - handleInput, - handleClear, - overlayOpened, - setOverlayOpened, - } = inject(datePickerToken)! + } = context const formContext = inject(FORM_TOKEN, null) const placeholder = computed(() => props.placeholder ?? locale.datePicker[`${props.type}Placeholder`]) - const inputSize = computed(() => Math.max(10, format.value.length) + 2) - const allowInput = computed(() => props.allowInput ?? config.allowInput) - const clearable = computed(() => !accessor.disabled.value && props.clearable && inputValue.value.length > 0) + const inputSize = computed(() => Math.max(10, formatRef.value.length) + 2) - const suffix = computed(() => props.suffix ?? config.suffix) + const triggerProps = useTriggerProps(context, formContext) - const classes = computed(() => { - const { borderless = config.borderless, size = formContext?.size.value ?? config.size } = props - const disabled = accessor.disabled.value - const prefixCls = mergedPrefixCls.value - return normalizeClass({ - [prefixCls]: true, - [`${prefixCls}-borderless`]: borderless, - [`${prefixCls}-clearable`]: clearable.value, - [`${prefixCls}-disabled`]: disabled, - [`${prefixCls}-focused`]: isFocused.value, - [`${prefixCls}-opened`]: overlayOpened.value, - [`${prefixCls}-with-suffix`]: slots.suffix || suffix.value, - [`${prefixCls}-${size}`]: true, - }) - }) - - const handleClick = () => { - const currOpened = overlayOpened.value - if (currOpened || accessor.disabled.value) { - return - } - - setOverlayOpened(!currOpened) + const handleInput = (evt: Event) => { + _handleInput(evt) + callEmit(props.onInput, evt) } - const handleKeyDown = (evt: KeyboardEvent) => { - switch (evt.code) { - case 'Enter': - evt.preventDefault() - break - case 'Escape': - evt.preventDefault() - setOverlayOpened(false) - break - } - } - - const triggerRef = ref() - onMounted(() => { - watch(focusMonitor.monitor(triggerRef.value!, true), evt => { - const { origin, event } = evt - if (event) { - if (origin) { - handleFocus(event) - } else { - handleBlur(event) - } - } - }) - }) - - onBeforeUnmount(() => focusMonitor.stopMonitoring(triggerRef.value!)) - return () => { - const { readonly } = props + const { readonly, disabled } = triggerProps.value const prefixCls = mergedPrefixCls.value return ( -
+ <ɵTrigger className={prefixCls} v-slots={slots} {...triggerProps.value}>
- - - - {clearable.value && ( - - - - )}
-
+ ) } }, diff --git a/packages/components/date-picker/src/types.ts b/packages/components/date-picker/src/types.ts index 581c91745..bf46b2836 100644 --- a/packages/components/date-picker/src/types.ts +++ b/packages/components/date-picker/src/types.ts @@ -5,43 +5,82 @@ * found in the LICENSE file at https://github.com/IDuxFE/idux/blob/main/LICENSE */ -/* eslint-disable @typescript-eslint/no-explicit-any */ - -import type { ExtractInnerPropTypes, ExtractPublicPropTypes } from '@idux/cdk/utils' +import type { ExtractInnerPropTypes, ExtractPublicPropTypes, MaybeArray } from '@idux/cdk/utils' import type { FormSize } from '@idux/components/form' -import type { TimePickerProps } from '@idux/components/time-picker' -import type { DefineComponent, HTMLAttributes, VNode, VNodeTypes } from 'vue' +import type { DefineComponent, HTMLAttributes, PropType, VNode, VNodeChild } from 'vue' import { controlPropDef } from '@idux/cdk/forms' import { ɵPortalTargetDef } from '@idux/cdk/portal' -import { IxPropTypes } from '@idux/cdk/utils' +import { ɵFooterTypeDef } from '@idux/components/_private/footer' + +export interface TimePanelOptions { + disabledHours?: (selectedAmPm: string) => number[] + disabledMinutes?: (selectedHour: number, selectedAmPm: string) => number[] + disabledSeconds?: (selectedHour: number, selectedMinute: number, selectedAmPm: string) => number[] + hideDisabledOptions?: boolean + hourStep?: number + minuteStep?: number + secondStep?: number + hourEnabled?: boolean + minuteEnabled?: boolean + secondEnabled?: boolean + use12Hours?: boolean +} const datePickerCommonProps = { control: controlPropDef, - open: IxPropTypes.bool, - - allowInput: IxPropTypes.oneOfType([Boolean, IxPropTypes.oneOf(['overlay'])]), - autofocus: IxPropTypes.bool.def(false), - borderless: IxPropTypes.bool, - clearable: IxPropTypes.bool, - clearIcon: IxPropTypes.string, - disabled: IxPropTypes.bool.def(false), - disabledDate: IxPropTypes.func<(date: Date) => boolean>(), - format: IxPropTypes.string, - overlayClassName: IxPropTypes.string, - overlayContainer: IxPropTypes.oneOfType([String, HTMLElement, IxPropTypes.func<() => string | HTMLElement>()]), - overlayRender: IxPropTypes.func<(children: VNode[]) => VNodeTypes>(), - readonly: IxPropTypes.bool.def(false), - size: IxPropTypes.oneOf(['sm', 'md', 'lg']), - suffix: IxPropTypes.string, + cellTooltip: Function as PropType<(cell: { value: Date; disabled: boolean }) => string | void>, + open: { + type: Boolean, + default: undefined, + }, + + allowInput: { + type: [Boolean, String] as PropType, + default: undefined, + }, + autofocus: { + type: Boolean, + default: false, + }, + borderless: { + type: Boolean, + default: undefined, + }, + clearable: { + type: Boolean, + default: undefined, + }, + clearIcon: String, + disabled: { + type: Boolean, + default: false, + }, + disabledDate: Function as PropType<(date: Date) => boolean>, + format: String, + dateFormat: String, + timeFormat: String, + overlayClassName: String, + // overlayContainer: IxPropTypes.oneOfType([String, HTMLElement, IxPropTypes.func<() => string | HTMLElement>()]), + overlayContainer: [String, HTMLElement, Function] as PropType string | HTMLElement)>, + overlayRender: Function as PropType<(children: VNode[]) => VNodeChild>, + readonly: { + type: Boolean as PropType, + default: false, + }, + size: String as PropType, + suffix: String, + type: { + type: String as PropType, + default: 'date', + }, target: ɵPortalTargetDef, - type: IxPropTypes.oneOf(['date', 'week', 'month', 'quarter', 'year']).def('date'), // events - 'onUpdate:open': IxPropTypes.emit<(isOpen: boolean) => void>(), - onClear: IxPropTypes.emit<(evt: MouseEvent) => void>(), - onFocus: IxPropTypes.emit<(evt: FocusEvent) => void>(), - onBlur: IxPropTypes.emit<(evt: FocusEvent) => void>(), + 'onUpdate:open': [Function, Array] as PropType void>>, + onClear: [Function, Array] as PropType void>>, + onFocus: [Function, Array] as PropType void>>, + onBlur: [Function, Array] as PropType void>>, } export type DatePickerCommonProps = ExtractInnerPropTypes @@ -53,14 +92,14 @@ export interface DatePickerCommonBindings { export const datePickerProps = { ...datePickerCommonProps, - value: IxPropTypes.oneOfType([Number, String, Date]), - defaultOpenValue: IxPropTypes.oneOfType([Number, String, Date]), - placeholder: IxPropTypes.string, - timePicker: IxPropTypes.oneOfType([Boolean, IxPropTypes.object()]), - - 'onUpdate:value': IxPropTypes.emit<(value: Date) => void>(), - onChange: IxPropTypes.emit<(value: Date, oldValue: Date) => void>(), - onInput: IxPropTypes.emit<(evt: Event) => void>(), + value: [String, Date, Number], + defaultOpenValue: [String, Date, Number], + footer: { type: ɵFooterTypeDef, default: false }, + placeholder: String, + timePanelOptions: Object as PropType, + 'onUpdate:value': [Function, Array] as PropType void>>, + onChange: [Function, Array] as PropType void>>, + onInput: [Function, Array] as PropType void>>, } export type DatePickerProps = ExtractInnerPropTypes @@ -74,17 +113,17 @@ export type DatePickerInstance = InstanceType(), - defaultOpenValue: IxPropTypes.array(), - placeholder: IxPropTypes.arrayOf(String), - separator: IxPropTypes.oneOfType([String, IxPropTypes.vNode]), - timePicker: IxPropTypes.oneOfType([ - Boolean, - IxPropTypes.object(), - IxPropTypes.array(), - ]), - - 'onUpdate:value': IxPropTypes.emit<(value: any[]) => void>(), + value: Array as PropType<(number | string | Date | undefined)[]>, + defaultOpenValue: Array as PropType<(number | string | Date)[]>, + footer: { type: ɵFooterTypeDef, default: true }, + placeholder: Array as PropType, + separator: [String, Object] as PropType, + timePanelOptions: [Object, Array] as PropType, + 'onUpdate:value': [Function, Array] as PropType void>>, + onChange: [Function, Array] as PropType< + MaybeArray<(value: Date[] | undefined, oldValue: Date[] | undefined) => void> + >, + onInput: [Function, Array] as PropType void>>, } export type DateRangePickerProps = ExtractInnerPropTypes @@ -95,16 +134,4 @@ export type DateRangePickerComponent = DefineComponent< > export type DateRangePickerInstance = InstanceType> -export type DatePickerType = 'date' | 'week' | 'month' | 'quarter' | 'year' - -// private - -export const panelRowProps = { - rowIndex: IxPropTypes.number.isRequired, -} - -export const panelCellProps = { - rowIndex: IxPropTypes.number.isRequired, - cellIndex: IxPropTypes.number.isRequired, - isWeek: IxPropTypes.bool, -} +export type DatePickerType = 'date' | 'week' | 'month' | 'quarter' | 'year' | 'datetime' diff --git a/packages/components/date-picker/src/utils.ts b/packages/components/date-picker/src/utils.ts new file mode 100644 index 000000000..5f0cec17c --- /dev/null +++ b/packages/components/date-picker/src/utils.ts @@ -0,0 +1,47 @@ +/** + * @license + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://github.com/IDuxFE/idux/blob/main/LICENSE + */ + +import type { DateConfig, DateConfigType, TimeConfigType } from '@idux/components/config' + +import { convertArray } from '@idux/cdk/utils' + +export function convertToDate( + dateConfig: DateConfig, + value: string | number | Date | undefined, + format: string, +): Date | undefined { + if (!value) { + return undefined + } + + return dateConfig.convert(value, format) +} + +export function applyDateTime( + dateConfig: DateConfig, + sourceDate: Date, + targetDate: Date, + types: DateConfigType | TimeConfigType | (DateConfigType | TimeConfigType)[], +): Date { + const typesArray = convertArray(types) + + return typesArray.reduce((date, type) => dateConfig.set(date, dateConfig.get(sourceDate, type), type), targetDate) +} + +export function sortRangeValue(values: (Date | undefined)[]): (Date | undefined)[] { + return values.sort((v1, v2) => { + if (!v1) { + return 1 + } + + if (!v2) { + return 0 + } + + return v1.valueOf() - v2.valueOf() + }) +} diff --git a/packages/components/date-picker/style/index.less b/packages/components/date-picker/style/index.less index 063ce6595..a87633e43 100644 --- a/packages/components/date-picker/style/index.less +++ b/packages/components/date-picker/style/index.less @@ -4,20 +4,124 @@ @import './input.less'; @import './panel.less'; -.@{date-picker-prefix} { +.@{date-picker-prefix}, +.@{date-range-picker-prefix} { display: inline-flex; width: 100%; line-height: @date-picker-line-height; background-color: @date-picker-background-color; - .date-picker-input(); + &-input { + width: 100%; + &-inner { + display: inline-block; + width: 100%; + min-width: 0; + } + } &-overlay { z-index: @date-picker-overlay-zindex; - width: @date-picker-overlay-width; border-radius: @date-picker-overlay-border-radius; box-shadow: @date-picker-overlay-box-shadow; + background-color: @date-picker-panel-background-color; + + &-content { + outline: none; + } + } + + &-board { + &-inputs { + display: flex; + justify-content: center; + margin-bottom: @date-picker-overlay-inputs-margin-bottom; + } + & &-date-input { + width: @date-picker-overlay-date-input-width; + } + & &-date-input:only-child { + width: 100%; + } + & &-time-input { + width: @date-picker-overlay-time-input-width; + margin-left: @date-picker-overlay-input-gap; + } + + &-panel { + max-height: @date-range-picker-panel-max-height; + .@{time-panel-prefix} { + height: @date-range-picker-panel-max-height; + } + } + } +} + +.@{date-picker-prefix} { + &-overlay { + width: @date-picker-overlay-width; + padding: @date-picker-overlay-padding; .date-picker-panel(); + + &-footer { + text-align: end; + border-top: @date-picker-overlay-footer-border-width @date-picker-overlay-footer-border-style + @date-picker-overlay-footer-border-color; + padding: @date-picker-overlay-footer-padding; + + .@{button-prefix} + .@{button-prefix} { + margin-left: @date-picker-overlay-footer-button-margin-left; + } + } + } +} + +.@{date-range-picker-prefix} { + &-input { + display: flex; + align-items: center; + &-separator { + margin: 0 @date-range-picker-trigger-separator-margin; + } + } + + &-overlay { + padding: @date-range-picker-overlay-padding; + .date-picker-panel(); + &-content { + display: flex; + padding: @date-range-picker-overlay-content-padding; + } + &-gap { + display: flex; + justify-content: center; + align-items: flex-start; + width: @date-range-picker-overlay-gap-width; + padding: @date-range-picker-overlay-gap-padding; + text-align: center; + color: @date-picker-color; + } + + &-footer { + text-align: end; + border-top: @date-range-picker-overlay-footer-border-width @date-range-picker-overlay-footer-border-style + @date-range-picker-overlay-footer-border-color; + padding: @date-range-picker-overlay-footer-padding; + + .@{button-prefix} + .@{button-prefix} { + margin-left: @date-range-picker-overlay-footer-button-margin-left; + } + } + } + + &-board { + width: @date-range-picker-panel-width; + + &-panel .@{time-panel-prefix} { + border: @date-range-picker-panel-border-width @date-range-picker-panel-border-style + @date-range-picker-panel-border-color; + border-radius: @date-range-picker-panel-border-radius; + } } } diff --git a/packages/components/date-picker/style/input.less b/packages/components/date-picker/style/input.less index 73450be97..98c92dcb0 100644 --- a/packages/components/date-picker/style/input.less +++ b/packages/components/date-picker/style/input.less @@ -1,27 +1,27 @@ -.date-picker-input-size(@font-size; @padding-vertical; @padding-horizontal; @height) { - font-size: @font-size; +// .date-picker-input-size(@font-size; @padding-vertical; @padding-horizontal; @height) { +// font-size: @font-size; - .@{date-picker-prefix}-input { - padding: @padding-vertical @padding-horizontal; - } +// .@{date-picker-prefix}-input { +// padding: @padding-vertical @padding-horizontal; +// } - .@{date-picker-prefix}-clear { - right: @padding-horizontal; - } -} +// .@{date-picker-prefix}-clear { +// right: @padding-horizontal; +// } +// } -.date-picker-input-inner() { - display: inline-block; - width: 100%; - min-width: 0; - outline: 0; +// .date-picker-input-inner() { +// display: inline-block; +// width: 100%; +// min-width: 0; +// outline: 0; - &[disabled] { - cursor: not-allowed; - } +// &[disabled] { +// cursor: not-allowed; +// } - .placeholder(@date-picker-placeholder-color); -} +// .placeholder(@date-picker-placeholder-color); +// } .date-picker-input() { @@ -38,85 +38,85 @@ border-color: @date-picker-hover-color; } - &-inner { - .date-picker-input-inner(); - } - } - - &-suffix, - &-clear { - color: @date-picker-placeholder-color; - transition: color @transition-duration-base; - cursor: pointer; - - &:hover { - color: @date-picker-color-secondary; - } - } - - &-clear { - position: absolute; - z-index: 1; - opacity: 0; - background-color: @date-picker-background-color; - transition: opacity @transition-duration-base; - } - - &:hover &-clear { - opacity: 1; - } - - &-sm { - .date-picker-input-size(@date-picker-font-size-sm; @date-picker-padding-vertical-sm; @date-picker-padding-horizontal-sm; @date-picker-height-sm); - } - - &-md { - .date-picker-input-size(@date-picker-font-size-md; @date-picker-padding-vertical-md; @date-picker-padding-horizontal-md; @date-picker-height-md); - } - - &-lg { - .date-picker-input-size(@date-picker-font-size-lg; @date-picker-padding-vertical-lg; @date-picker-padding-horizontal-lg; @date-picker-height-lg); - } - - &-opened, - &-focused { - .@{date-picker-prefix}-input { - border-color: @date-picker-active-color; - box-shadow: @date-picker-active-box-shadow; - } - } - - &-disabled { - color: @date-picker-disabled-color; - background-color: @date-picker-disabled-background-color; - cursor: not-allowed; - opacity: 1; - - .@{date-picker-prefix}-input { - - &:hover { - border-color: @date-picker-border-color; - } - - .@{input-prefix}-suffix { - cursor: not-allowed; - - &:hover { - color: @date-picker-disabled-color; - } - } - } + // &-inner { + // .date-picker-input-inner(); + // } } - &-borderless { - - &, - &:hover, - &-focused, - &-disabled { - .@{date-picker-prefix}-input { - .borderless(); - } - } - } + // &-suffix, + // &-clear { + // color: @date-picker-placeholder-color; + // transition: color @transition-duration-base; + // cursor: pointer; + + // &:hover { + // color: @date-picker-color-secondary; + // } + // } + + // &-clear { + // position: absolute; + // z-index: 1; + // opacity: 0; + // background-color: @date-picker-background-color; + // transition: opacity @transition-duration-base; + // } + + // &:hover &-clear { + // opacity: 1; + // } + + // &-sm { + // .date-picker-input-size(@date-picker-font-size-sm; @date-picker-padding-vertical-sm; @date-picker-padding-horizontal-sm; @date-picker-height-sm); + // } + + // &-md { + // .date-picker-input-size(@date-picker-font-size-md; @date-picker-padding-vertical-md; @date-picker-padding-horizontal-md; @date-picker-height-md); + // } + + // &-lg { + // .date-picker-input-size(@date-picker-font-size-lg; @date-picker-padding-vertical-lg; @date-picker-padding-horizontal-lg; @date-picker-height-lg); + // } + + // &-opened, + // &-focused { + // .@{date-picker-prefix}-input { + // border-color: @date-picker-active-color; + // box-shadow: @date-picker-active-box-shadow; + // } + // } + + // &-disabled { + // color: @date-picker-disabled-color; + // background-color: @date-picker-disabled-background-color; + // cursor: not-allowed; + // opacity: 1; + + // .@{date-picker-prefix}-input { + + // &:hover { + // border-color: @date-picker-border-color; + // } + + // .@{input-prefix}-suffix { + // cursor: not-allowed; + + // &:hover { + // color: @date-picker-disabled-color; + // } + // } + // } + // } + + // &-borderless { + + // &, + // &:hover, + // &-focused, + // &-disabled { + // .@{date-picker-prefix}-input { + // .borderless(); + // } + // } + // } } diff --git a/packages/components/date-picker/style/panel.less b/packages/components/date-picker/style/panel.less index b48f9b4bf..c5f61ce35 100644 --- a/packages/components/date-picker/style/panel.less +++ b/packages/components/date-picker/style/panel.less @@ -11,10 +11,11 @@ &-header { display: flex; + font-size: @date-picker-panel-header-font-size; font-weight: @date-picker-panel-header-font-weight; padding: @date-picker-panel-header-padding; line-height: @date-picker-panel-header-height - @date-picker-panel-border-width; - border-bottom: @date-picker-panel-border-width @date-picker-panel-border-style @date-picker-panel-border-color; + border-bottom: @date-picker-panel-header-border-bottom; color: @date-picker-panel-color; transition: color @transition-duration-base; @@ -23,6 +24,7 @@ } button { + font-size: @date-picker-panel-header-button-font-size; transition: color @transition-duration-base; &:hover { @@ -47,11 +49,8 @@ &-content { flex: auto; - button { - - &:not(:first-child) { - margin-left: @spacing-xs; - } + button:not(:first-child) { + margin-left: @date-picker-panel-header-content-spacing; } } } @@ -60,109 +59,211 @@ &-body { padding: @date-picker-panel-body-padding; + font-size: @date-picker-panel-body-font-size; table { width: 100%; table-layout: fixed; border-collapse: collapse; + overflow: hidden; + } + + thead { + background-color: @date-picker-panel-body-header-background-color; + border-bottom: @date-picker-panel-body-header-border-bottom; } th, td { position: relative; + z-index: 1; } th { font-weight: @date-picker-panel-body-header-font-weight; - min-width: @date-picker-panel-cell-size; - height: @date-picker-panel-cell-size; - line-height: @date-picker-panel-cell-size; + min-width: @date-picker-panel-cell-width; + height: @date-picker-panel-cell-height; + line-height: @date-picker-panel-cell-height; } } + &-row:first-child .@{date-panel-prefix}-cell { + padding-top: 0; + } + &-row:last-child .@{date-panel-prefix}-cell { + padding-bottom: 0; + } &-cell { padding: @date-picker-panel-cell-padding; color: @date-picker-panel-color; cursor: pointer; - &::before { - position: absolute; - top: 50%; - right: 0; - left: 0; - z-index: 1; - height: @date-picker-panel-cell-size; - transform: translateY(-50%); - transition: @transition-all-base; - content: ''; - } - &-out-view { color: @date-picker-panel-disabled-color; } &-inner { - display: inline-block; - position: relative; - z-index: 2; - min-width: @date-picker-panel-cell-size; - height: @date-picker-panel-cell-size; - line-height: @date-picker-panel-cell-size; + display: flex; + align-items: center; + justify-content: center; + min-width: @date-picker-panel-cell-width; + height: @date-picker-panel-cell-height; + line-height: @date-picker-panel-cell-height; + padding: @date-picker-panel-cell-inner-padding; border-radius: @date-picker-panel-cell-border-radius; - transition: background @transition-duration-base, border @transition-duration-base; } - &:hover:not(&-selected) { - .@{date-panel-prefix}-cell-inner { - background: @date-picker-panel-cell-hover-background-color; - } + &-trigger { + width: @date-picker-panel-cell-trigger-width; + height: @date-picker-panel-cell-trigger-height; + display: flex; + align-items: center; + justify-content: center; + border-radius: @date-picker-panel-cell-border-radius; + transition: background @transition-duration-base, border @transition-duration-base; } &-today { - .@{date-panel-prefix}-cell-inner { - - &::before { - position: absolute; - top: 0; - right: 0; - bottom: 0; - left: 0; - z-index: 1; - border: @date-picker-panel-border-width @date-picker-panel-border-style @date-picker-panel-active-color; - border-radius: @date-picker-panel-cell-border-radius; - content: ''; - } + position: relative; + &::after { + content: ''; + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; + margin: auto; + width: @date-picker-panel-cell-today-mark-width; + height: @date-picker-panel-cell-today-mark-height; + border: @date-picker-panel-border-width @date-picker-panel-border-style @date-picker-panel-cell-today-border-color; + border-radius: @date-picker-panel-cell-border-radius; + color: @date-picker-panel-cell-today-color; } } - &-selected { + &-selected .@{date-panel-prefix}-cell-trigger { + color: @date-picker-panel-color-inverse; + background: @date-picker-panel-active-color; + } + + &-in-range { .@{date-panel-prefix}-cell-inner { - color: @date-picker-panel-color-inverse; - background: @date-picker-panel-active-color; + background: @date-picker-panel-in-range-color; + border-radius: 0; + } + &:not(.@{date-panel-prefix}-cell-selected) .@{date-panel-prefix}-cell-inner { + width: 100%; } } &-disabled { color: @date-picker-panel-disabled-color; - pointer-events: none; + cursor: not-allowed; - &::before { + .@{date-panel-prefix}-cell-inner { + border-radius: 0; background: @date-picker-panel-disabled-background-color; } } &-disabled&-today { - .@{date-panel-prefix}-cell-inner::before { + .@{date-panel-prefix}-cell-trigger { border-color: @date-picker-panel-disabled-color; } } } + &:not(&-week) { + .@{date-panel-prefix}-cell { + &:hover:not(&-selected):not(&-disabled) { + .@{date-panel-prefix}-cell-trigger { + background: @date-picker-panel-cell-hover-background-color; + color: @date-picker-panel-cell-hover-color; + } + } + } + } + &-week { + .@{date-panel-prefix}-row { + &:hover { + .@{date-panel-prefix}-cell-inner { + border-radius: 0; + background: @date-picker-panel-cell-hover-background-color; + color: @date-picker-panel-cell-hover-color; + } + .@{date-panel-prefix}-cell:first-child { + .@{date-panel-prefix}-cell-inner { + border-top-left-radius: @date-picker-panel-cell-border-radius; + border-bottom-left-radius: @date-picker-panel-cell-border-radius; + } + } + .@{date-panel-prefix}-cell:last-child { + .@{date-panel-prefix}-cell-inner { + border-top-right-radius: @date-picker-panel-cell-border-radius; + border-bottom-right-radius: @date-picker-panel-cell-border-radius; + } + } + } + } + } + + &-date, + &-week { + .@{date-panel-prefix}-cell-selected { + z-index: 0; + .@{date-panel-prefix}-cell-trigger { + color: @date-picker-panel-color-inverse; + background: @date-picker-panel-active-color; + } + &.@{date-panel-prefix}-cell-in-range { + @edge-bg-width: @date-picker-panel-cell-width * 1.5; + @translate-x: @edge-bg-width * 0.5 - @date-picker-panel-cell-width * 0.5; + + &.@{date-panel-prefix}-cell-start .@{date-panel-prefix}-cell-inner, + &.@{date-panel-prefix}-cell-end .@{date-panel-prefix}-cell-inner { + background: none; + &::before { + z-index: 0; + position: absolute; + background: @date-picker-panel-in-range-color; + width: @date-picker-panel-cell-width * 1.5; + height: @date-picker-panel-cell-height; + content: ''; + } + .@{date-panel-prefix}-cell-trigger { + position: relative; + z-index: 1; + } + } + &.@{date-panel-prefix}-cell-start .@{date-panel-prefix}-cell-inner::before { + transform: ~'translateX(@{translate-x})'; + border-top-left-radius: @date-picker-panel-cell-border-radius; + border-bottom-left-radius: @date-picker-panel-cell-border-radius; + } + &.@{date-panel-prefix}-cell-end .@{date-panel-prefix}-cell-inner::before { + transform: ~'translateX(-@{translate-x})'; + border-top-right-radius: @date-picker-panel-cell-border-radius; + border-bottom-right-radius: @date-picker-panel-cell-border-radius; + } + &.@{date-panel-prefix}-cell-start + .@{date-panel-prefix}-cell-end, + &.@{date-panel-prefix}-cell-start.@{date-panel-prefix}-cell-end { + .@{date-panel-prefix}-cell-inner::before { + width: @date-picker-panel-cell-width; + transform: none; + } + } + } + } + } + &-year, &-quarter, &-month { .@{date-panel-prefix} { - + &-header { + padding: @date-picker-panel-header-padding-lg; + } + &-body { padding: @date-picker-panel-body-padding-lg; } @@ -170,37 +271,45 @@ &-cell { padding: @date-picker-panel-cell-padding-lg; + &:first-child .@{date-panel-prefix}-cell-inner { + justify-content: flex-start; + } + &:last-child .@{date-panel-prefix}-cell-inner { + justify-content: flex-end; + } + &-inner { + min-width: @date-picker-panel-cell-width-lg; + height: @date-picker-panel-cell-height-lg; padding: @date-picker-panel-cell-inner-padding-lg; border-radius: @date-picker-panel-cell-border-radius-lg; } - } - } - } - - &-week { - .@{date-panel-prefix} { - - &-cell-selected { - color: @date-picker-panel-color-inverse; - background: @date-picker-panel-active-color; - } + &-trigger { + width: @date-picker-panel-cell-trigger-width-lg; + height: @date-picker-panel-cell-trigger-height-lg; + border-radius: @date-picker-panel-cell-border-radius-lg; + } - &-cell-inner { - transition: none; + &-selected { + &.@{date-panel-prefix}-cell-in-range { + &.@{date-panel-prefix}-cell-start .@{date-panel-prefix}-cell-inner { + border-top-left-radius: @date-picker-panel-cell-border-radius-lg; + border-bottom-left-radius: @date-picker-panel-cell-border-radius-lg; + } + &.@{date-panel-prefix}-cell-end .@{date-panel-prefix}-cell-inner { + border-top-right-radius: @date-picker-panel-cell-border-radius-lg; + border-bottom-right-radius: @date-picker-panel-cell-border-radius-lg; + } + } + } + &-in-range { + .@{date-panel-prefix}-cell-inner { + background: @date-picker-panel-in-range-color; + border-radius: 0; + } + } } } } - - // ======================== Footer ======================== - - &-footer { - width: 100%; - text-align: end; - padding: @date-picker-panel-footer-padding; - line-height: @date-picker-panel-header-height - 2 * @date-picker-panel-border-width; - border-top: @date-picker-panel-border-width @date-picker-panel-border-style @date-picker-panel-border-color; - border-bottom: @date-picker-panel-border-width @date-picker-panel-border-style transparent; - } } } diff --git a/packages/components/date-picker/style/themes/default.variable.less b/packages/components/date-picker/style/themes/default.variable.less index 7d2e7bc2d..1d16a1881 100644 --- a/packages/components/date-picker/style/themes/default.variable.less +++ b/packages/components/date-picker/style/themes/default.variable.less @@ -1,65 +1,99 @@ // Trigger -@date-picker-font-size-sm: @form-font-size-sm; -@date-picker-font-size-md: @form-font-size-md; -@date-picker-font-size-lg: @form-font-size-lg; @date-picker-line-height: @form-line-height; -@date-picker-height-sm: @form-height-sm; -@date-picker-height-md: @form-height-md; -@date-picker-height-lg: @form-height-lg; -@date-picker-padding-horizontal-sm: @form-padding-horizontal-sm; -@date-picker-padding-horizontal-md: @form-padding-horizontal-md; -@date-picker-padding-horizontal-lg: @form-padding-horizontal-lg; -@date-picker-padding-vertical-sm: @form-padding-vertical-sm; -@date-picker-padding-vertical-md: @form-padding-vertical-md; -@date-picker-padding-vertical-lg: @form-padding-vertical-lg; - -@date-picker-border-width: @form-border-width; -@date-picker-border-style: @form-border-style; -@date-picker-border-color: @form-border-color; -@date-picker-border-radius: @border-radius-md; - @date-picker-color: @form-color; -@date-picker-color-secondary: @form-color-secondary; @date-picker-background-color: @form-background-color; -@date-picker-placeholder-color: @form-placeholder-color; -@date-picker-hover-color: @form-hover-color; -@date-picker-active-color: @form-active-color; -@date-picker-active-box-shadow: @form-active-box-shadow; -@date-picker-disabled-color: @form-disabled-color; @date-picker-disabled-background-color: @form-disabled-background-color; +// Range Trigger +@date-range-picker-trigger-separator-margin: @spacing-xl; + // Panel @date-picker-panel-font-size: @font-size-md; @date-picker-panel-color: @text-color; @date-picker-panel-color-inverse: @text-color-inverse; @date-picker-panel-active-color: @color-primary; +@date-picker-panel-in-range-color: @color-blue-l50; @date-picker-panel-disabled-color: @text-color-disabled; -@date-picker-panel-disabled-background-color: @background-color-disabled; +@date-picker-panel-disabled-background-color: @color-graphite-l50; @date-picker-panel-background-color: @background-color-component; @date-picker-panel-border-width: @border-width-sm; @date-picker-panel-border-style: @border-style; @date-picker-panel-border-color: @border-color; -@date-picker-panel-header-padding: 0 @spacing-lg; -@date-picker-panel-header-height: @height-lg; +@date-picker-panel-header-padding: 0 0 @spacing-xs 0; +@date-picker-panel-header-height: @height-md; @date-picker-panel-header-item-padding: 0 @spacing-xs; +@date-picker-panel-header-font-size: @font-size-md; @date-picker-panel-header-font-weight: @font-weight-lg; +@date-picker-panel-header-border-bottom: none; +@date-picker-panel-header-button-font-size: @font-size-lg; +@date-picker-panel-header-content-spacing: @spacing-lg; +@date-picker-panel-header-padding-lg: 0 0 @spacing-2xl; -@date-picker-panel-body-padding: @spacing-sm @spacing-lg; -@date-picker-panel-body-padding-lg: @spacing-sm @spacing-lg; +@date-picker-panel-body-padding: 0; +@date-picker-panel-body-padding-lg: 0; +@date-picker-panel-body-font-size: @font-size-md; +@date-picker-panel-body-header-border-bottom: solid transparent @spacing-md; @date-picker-panel-body-header-font-weight: @font-weight-md; -@date-picker-panel-cell-size: 24px; -@date-picker-panel-cell-padding: @spacing-xs 0; -@date-picker-panel-cell-padding-lg: @padding-sm 0; -@date-picker-panel-cell-inner-padding-lg: 0 @padding-lg; +@date-picker-panel-body-header-background-color: @color-graphite-l50; + +@date-picker-panel-cell-width: 28px; +@date-picker-panel-cell-height: 28px; +@date-picker-panel-cell-width-lg: 52px; +@date-picker-panel-cell-height-lg: 24px; + +@date-picker-panel-cell-padding: 2px 0; +@date-picker-panel-cell-inner-padding: 4px; +@date-picker-panel-cell-padding-lg: @spacing-lg 0; +@date-picker-panel-cell-inner-padding-lg: 0; + +@date-picker-panel-cell-trigger-width: 20px; +@date-picker-panel-cell-trigger-height: 20px; +@date-picker-panel-cell-trigger-width-lg: 52px; +@date-picker-panel-cell-trigger-height-lg: 24px; + @date-picker-panel-cell-border-radius: @border-radius-full; @date-picker-panel-cell-border-radius-lg: @border-radius-md; -@date-picker-panel-cell-hover-background-color: @background-color-hover; +@date-picker-panel-cell-hover-background-color: @color-graphite-l50; +@date-picker-panel-cell-hover-color: @color-primary; + +@date-picker-panel-cell-today-mark-width: 24px; +@date-picker-panel-cell-today-mark-height: 24px; +@date-picker-panel-cell-today-border-color: @color-blue-l40; +@date-picker-panel-cell-today-color: @color-primary; -@date-picker-panel-footer-padding: 0 @spacing-sm; +@date-picker-overlay-footer-border-width: @form-border-width; +@date-picker-overlay-footer-border-style: @form-border-style; +@date-picker-overlay-footer-border-color: @form-border-color; +@date-picker-overlay-footer-padding: @spacing-sm @spacing-lg; +@date-picker-overlay-footer-button-margin-left: @spacing-sm; // Overlay @date-picker-overlay-zindex: @zindex-l4-3; -@date-picker-overlay-width: 256px; +@date-picker-overlay-width: 252px; @date-picker-overlay-border-radius: @border-radius-sm; @date-picker-overlay-box-shadow: @shadow-bottom-md; + +@date-picker-overlay-date-input-width: 120px; +@date-picker-overlay-time-input-width: 96px; +@date-picker-overlay-input-gap: @spacing-xs; +@date-picker-overlay-padding: @spacing-lg @spacing-lg @spacing-lg @spacing-lg; +@date-picker-overlay-inputs-margin-bottom: @spacing-sm; + +@date-range-picker-overlay-padding: @spacing-lg @spacing-lg 0 @spacing-lg; +@date-range-picker-overlay-content-padding: 0 0 @spacing-sm 0; +@date-range-picker-overlay-gap-width: @spacing-2xl; +@date-range-picker-overlay-gap-padding: 1px 0 0 0; + +@date-range-picker-overlay-footer-border-width: @date-picker-overlay-footer-border-width; +@date-range-picker-overlay-footer-border-style: @date-picker-overlay-footer-border-style; +@date-range-picker-overlay-footer-border-color: @color-graphite-l30; +@date-range-picker-overlay-footer-padding: @spacing-sm 0; +@date-range-picker-overlay-footer-button-margin-left: @date-picker-overlay-footer-button-margin-left; + +@date-range-picker-panel-width: 220px; +@date-range-picker-panel-max-height: 260px; +@date-range-picker-panel-border-width: @form-border-width; +@date-range-picker-panel-border-style: @form-border-style; +@date-range-picker-panel-border-color: @form-border-color; +@date-range-picker-panel-border-radius: 2px; \ No newline at end of file diff --git a/packages/components/default.less b/packages/components/default.less index 02056c9de..cfe11e793 100644 --- a/packages/components/default.less +++ b/packages/components/default.less @@ -67,3 +67,5 @@ @import './tree-select/style/themes/default.less'; @import './typography/style/themes/default.less'; @import './upload/style/themes/default.less'; + +@import './_private/trigger/style/themes/default.less'; \ No newline at end of file diff --git a/packages/components/form/docs/Index.zh.md b/packages/components/form/docs/Index.zh.md index 7284742a6..6adc5bade 100644 --- a/packages/components/form/docs/Index.zh.md +++ b/packages/components/form/docs/Index.zh.md @@ -1,114 +1,114 @@ ---- -category: components -type: 数据录入 -title: Form -subtitle: 表单 -order: 0 ---- - -## API - -### IxForm - -#### FormProps - -| 名称 | 说明 | 类型 | 默认值 | 全局配置 | 备注 | -| --- | --- | --- | --- | --- | --- | -| `colonless` | 配置 `IxFormItem` 的 `colon` 默认值 | `boolean` | `false` | ✅ | - | -| `control` | 表单的控制器 | `string \| number \| AbstractControl` | - | - | 通常是配合 `useFormGroup` 使用 | -| `controlCol` | 配置 `IxFormItem` 的 `controlCol` 默认值 | `number \| ColProps` | - | - | - | -| `labelAlign` | 配置 `IxFormItem` 的 `labelAlign` 默认值 | `'start' \| 'end'` | `'end'` | ✅ | - | -| `labelCol` | 配置 `IxFormItem` 的 `labelCol` 默认值 | `number \| ColProps` | - | - | - | -| `layout` | 表单布局 | `'horizontal' \| 'vertical' \| 'inline'` | `'horizontal'` | ✅ | - | -| `size` | 表单大小 | `'sm' \| 'md' \| 'lg'` | `'md'` | ✅ | - | -| `statusIcon` | 配置 `IxFormItem` 的 `statusIcon` 默认值 | `boolean \| Record` | `false` | - | - | - -### IxFormItem - -表单项组件,用于控制器的绑定、校验、布局等。 - -#### FormItemProps - -> 除以下表格之外还支持 `IxRow` 组件的[所有属性](/components/grid/zh#IxRow) - -| 名称 | 说明 | 类型 | 默认值 | 全局配置 | 备注 | -| --- | --- | --- | --- | --- | --- | -| `colonless` | 是否不显示 `label` 后面的冒号 | `boolean` | - | - | - | -| `control` | 表单控件的控制器 | `string \| number \| AbstractControl` | - | - | 默认取第 1 个子输入控件的 control,如果存在多个输入控件,建议手动指定,参考示例中的 `Phone Number`| -| `controlCol` | 配置表单控件的布局配置,可参考 `IxCol` 组件 | `number \| ColProps` | - | - | 传入 `string` 或者 `number` 时,为 `IxCol` 的 `span` 配置 | -| `controlTooltip` | 配置表单控件的提示信息 | `string \| #controlTooltip` | - | - | 通常用于对输入规则的详细说明 | -| `extraMessage` | 额外的提示信息 | `string \| #extraMessage` | - | - | 当需要错误信息和提示文案同时出现时使用 | -| `label` | `label` 标签的文本| `string \| #label` | - | - | - | -| `labelAlign` | `label` 标签文本对齐方式 | `'start' \| 'end'` | - | - | - | -| `labelCol` | `label` 标签布局配置,可参考 `IxCol` 组件 | `number \| ColProps` | - | - | 传入 `string` 或者 `number` 时,为 `IxCol` 的 `span` 配置 | -| `labelFor` | `label` 标签的 `for` 属性 | `string` | - | - | - | -| `labelTooltip` | 配置表单文本的提示信息 | `string \| #labelTooltip` | - | - | 通常用于对表单本文的解释说名 | -| `required` | 必填样式设置 | `boolean` | `false` | - | 仅控制样式 | -| `message` | 手动指定表单项的校验提示 | `string \| (control?: AbstractControl) => string \| FormValidateMessage` | - | - | 传入 `string` 时,为 `invalid` 状态的提示 | -| `status` | 手动指定表单项的校验状态 | `valid \| invalid \| validating` | - | - | - | -| `statusIcon` | 自定义校验状态图标 | `boolean \| Record \| #statusIcon={status}` | - | - | 为 `true` 时,显示默认的图标 | - -### IxFormWrapper - -用于嵌套表单时, 简于子组件的 `control` 路径 - -#### FormWrapperProps - -| 名称 | 说明 | 类型 | 默认值 | 全局配置 | 备注 | -| --- | --- | --- | --- | --- | --- | -| `control` | 表单控件的控制器 | `string \| number \| AbstractControl` | - | - | - | - -## FAQ - -### 自定义表单组件 - -参考下列代码来自定义表单组件,实现 `control` 并与 `IxFormItem` 配合使用。 - -```html - - - -``` - +--- +category: components +type: 数据录入 +title: Form +subtitle: 表单 +order: 0 +--- + +## API + +### IxForm + +#### FormProps + +| 名称 | 说明 | 类型 | 默认值 | 全局配置 | 备注 | +| --- | --- | --- | --- | --- | --- | +| `colonless` | 配置 `IxFormItem` 的 `colon` 默认值 | `boolean` | `false` | ✅ | - | +| `control` | 表单的控制器 | `string \| number \| AbstractControl` | - | - | 通常是配合 `useFormGroup` 使用 | +| `controlCol` | 配置 `IxFormItem` 的 `controlCol` 默认值 | `number \| ColProps` | - | - | - | +| `labelAlign` | 配置 `IxFormItem` 的 `labelAlign` 默认值 | `'start' \| 'end'` | `'end'` | ✅ | - | +| `labelCol` | 配置 `IxFormItem` 的 `labelCol` 默认值 | `number \| ColProps` | - | - | - | +| `layout` | 表单布局 | `'horizontal' \| 'vertical' \| 'inline'` | `'horizontal'` | ✅ | - | +| `size` | 表单大小 | `'sm' \| 'md' \| 'lg'` | `'md'` | ✅ | - | +| `statusIcon` | 配置 `IxFormItem` 的 `statusIcon` 默认值 | `boolean \| Record` | `false` | - | - | + +### IxFormItem + +表单项组件,用于控制器的绑定、校验、布局等。 + +#### FormItemProps + +> 除以下表格之外还支持 `IxRow` 组件的[所有属性](/components/grid/zh#IxRow) + +| 名称 | 说明 | 类型 | 默认值 | 全局配置 | 备注 | +| --- | --- | --- | --- | --- | --- | +| `colonless` | 是否不显示 `label` 后面的冒号 | `boolean` | - | - | - | +| `control` | 表单控件的控制器 | `string \| number \| AbstractControl` | - | - | 默认取第 1 个子输入控件的 control,如果存在多个输入控件,建议手动指定,参考示例中的 `Phone Number`| +| `controlCol` | 配置表单控件的布局配置,可参考 `IxCol` 组件 | `number \| ColProps` | - | - | 传入 `string` 或者 `number` 时,为 `IxCol` 的 `span` 配置 | +| `controlTooltip` | 配置表单控件的提示信息 | `string \| #controlTooltip` | - | - | 通常用于对输入规则的详细说明 | +| `extraMessage` | 额外的提示信息 | `string \| #extraMessage` | - | - | 当需要错误信息和提示文案同时出现时使用 | +| `label` | `label` 标签的文本| `string \| #label` | - | - | - | +| `labelAlign` | `label` 标签文本对齐方式 | `'start' \| 'end'` | - | - | - | +| `labelCol` | `label` 标签布局配置,可参考 `IxCol` 组件 | `number \| ColProps` | - | - | 传入 `string` 或者 `number` 时,为 `IxCol` 的 `span` 配置 | +| `labelFor` | `label` 标签的 `for` 属性 | `string` | - | - | - | +| `labelTooltip` | 配置表单文本的提示信息 | `string \| #labelTooltip` | - | - | 通常用于对表单本文的解释说名 | +| `required` | 必填样式设置 | `boolean` | `false` | - | 仅控制样式 | +| `message` | 手动指定表单项的校验提示 | `string \| (control?: AbstractControl) => string \| FormValidateMessage` | - | - | 传入 `string` 时,为 `invalid` 状态的提示 | +| `status` | 手动指定表单项的校验状态 | `valid \| invalid \| validating` | - | - | - | +| `statusIcon` | 自定义校验状态图标 | `boolean \| Record \| #statusIcon={status}` | - | - | 为 `true` 时,显示默认的图标 | + +### IxFormWrapper + +用于嵌套表单时, 简于子组件的 `control` 路径 + +#### FormWrapperProps + +| 名称 | 说明 | 类型 | 默认值 | 全局配置 | 备注 | +| --- | --- | --- | --- | --- | --- | +| `control` | 表单控件的控制器 | `string \| number \| AbstractControl` | - | - | - | + +## FAQ + +### 自定义表单组件 + +参考下列代码来自定义表单组件,实现 `control` 并与 `IxFormItem` 配合使用。 + +```html + + + +``` + ## 主题变量 @@ -126,9 +126,9 @@ export default defineComponent({ | `@form-padding-horizontal-sm` | `@spacing-sm - 2px` | - | - | | `@form-padding-horizontal-md` | `@spacing-sm` | - | - | | `@form-padding-horizontal-lg` | `@spacing-sm + 2px` | - | - | -| `@form-padding-vertical-sm` | `max( (round(((@form-height-sm - @form-font-size-sm * @form-line-height) / 2) * 10) / 10) - @form-border-width, 0)` | - | - | -| `@form-padding-vertical-md` | `max( (round(((@form-height-md - @form-font-size-md * @form-line-height) / 2) * 10) / 10) - @form-border-width, 2px)` | - | - | -| `@form-padding-vertical-lg` | `(ceil(((@form-height-lg - @form-font-size-lg * @form-line-height) / 2) * 10) / 10) - @form-border-width` | - | - | +| `@form-padding-vertical-sm` | `max( (round(((@form-height-sm - @form-font-size-sm * @form-line-height) / 2) * 10) / 10) - @form-border-width, 0 )` | - | - | +| `@form-padding-vertical-md` | `max( (round(((@form-height-md - @form-font-size-md * @form-line-height) / 2) * 10) / 10) - @form-border-width, 2px )` | - | - | +| `@form-padding-vertical-lg` | `(ceil(((@form-height-lg - @form-font-size-lg * @form-line-height) / 2) * 10) / 10) - @form-border-width` | - | - | | `@form-border-width` | `@border-width-sm` | - | - | | `@form-border-style` | `@border-style` | - | - | | `@form-border-color` | `@border-color` | - | - | @@ -159,4 +159,4 @@ export default defineComponent({ | `@form-item-label-color` | `@color-black` | - | - | | `@form-item-label-colon-margin-right` | `8px` | - | - | | `@form-item-label-colon-margin-left` | `2px` | - | - | - + diff --git a/packages/components/index.ts b/packages/components/index.ts index 429f08a40..767ccbd76 100644 --- a/packages/components/index.ts +++ b/packages/components/index.ts @@ -20,7 +20,7 @@ import { IxCarousel } from '@idux/components/carousel' import { IxCheckbox, IxCheckboxGroup } from '@idux/components/checkbox' import { IxCollapse, IxCollapsePanel } from '@idux/components/collapse' import { IxComment } from '@idux/components/comment' -import { IxDatePicker } from '@idux/components/date-picker' +import { IxDatePicker, IxDateRangePicker } from '@idux/components/date-picker' import { IxDivider } from '@idux/components/divider' import { IxDrawer, IxDrawerProvider } from '@idux/components/drawer' import { IxDropdown } from '@idux/components/dropdown' @@ -88,6 +88,7 @@ const components = [ IxCollapse, IxCollapsePanel, IxDatePicker, + IxDateRangePicker, IxDivider, IxDrawer, IxDrawerProvider, diff --git a/packages/components/locales/src/langs/en-US.ts b/packages/components/locales/src/langs/en-US.ts index 3312e5068..14b62a6cc 100644 --- a/packages/components/locales/src/langs/en-US.ts +++ b/packages/components/locales/src/langs/en-US.ts @@ -33,6 +33,7 @@ const enUS: Locale = { monthPlaceholder: 'Select month', quarterPlaceholder: 'Select quarter', yearPlaceholder: 'Select year', + datetimePlaceholder: 'Select date time', }, dateRangePicker: { datePlaceholder: ['Start date', 'End date'], @@ -40,6 +41,10 @@ const enUS: Locale = { monthPlaceholder: ['Start month', 'End month'], quarterPlaceholder: ['Start quarter', 'End quarter'], yearPlaceholder: ['Start year', 'End year'], + datetimePlaceholder: ['Start date time', 'End date time'], + separator: 'To', + okText: 'OK', + cancelText: 'Cancel', }, empty: { description: 'No Data', diff --git a/packages/components/locales/src/langs/zh-CN.ts b/packages/components/locales/src/langs/zh-CN.ts index 901e28338..2c5a38864 100755 --- a/packages/components/locales/src/langs/zh-CN.ts +++ b/packages/components/locales/src/langs/zh-CN.ts @@ -33,6 +33,7 @@ const zhCN: Locale = { quarterPlaceholder: '请选择季度', monthPlaceholder: '请选择月份', weekPlaceholder: '请选择周', + datetimePlaceholder: '请选择日期时间', }, dateRangePicker: { datePlaceholder: ['开始日期', '结束日期'], @@ -40,6 +41,10 @@ const zhCN: Locale = { monthPlaceholder: ['开始月份', '结束月份'], quarterPlaceholder: ['开始季度', '结束季度'], yearPlaceholder: ['开始年份', '结束年份'], + datetimePlaceholder: ['开始时间', '结束时间'], + separator: '至', + okText: '确定', + cancelText: '取消', }, empty: { description: '暂无数据', diff --git a/packages/components/locales/src/types.ts b/packages/components/locales/src/types.ts index 68363f702..d83963b84 100644 --- a/packages/components/locales/src/types.ts +++ b/packages/components/locales/src/types.ts @@ -30,6 +30,7 @@ export interface DatePickerLocale { monthPlaceholder: string quarterPlaceholder: string yearPlaceholder: string + datetimePlaceholder: string } export interface DateRangePickerLocale { @@ -38,6 +39,10 @@ export interface DateRangePickerLocale { monthPlaceholder: [string, string] quarterPlaceholder: [string, string] yearPlaceholder: [string, string] + datetimePlaceholder: [string, string] + separator: string + okText: string + cancelText: string } export interface EmptyLocale { diff --git a/packages/components/style/variable/prefix.less b/packages/components/style/variable/prefix.less index fc68e3b69..e6ec4132b 100644 --- a/packages/components/style/variable/prefix.less +++ b/packages/components/style/variable/prefix.less @@ -53,6 +53,7 @@ @checkbox-prefix: ~'@{idux-prefix}-checkbox'; @checkbox-group-prefix: ~'@{idux-prefix}-checkbox-group'; @date-picker-prefix: ~'@{idux-prefix}-date-picker'; +@date-range-picker-prefix: ~'@{idux-prefix}-date-range-picker'; @input-prefix: ~'@{idux-prefix}-input'; @input-number-prefix: ~'@{idux-prefix}-input-number'; @radio-prefix: ~'@{idux-prefix}-radio'; @@ -100,3 +101,4 @@ @loading-prefix: ~'@{idux-prefix}-loading'; @overflow-prefix: ~'@{idux-prefix}-overflow'; @selector-prefix: ~'@{idux-prefix}-selector'; +@trigger-prefix: ~'@{idux-prefix}-trigger'; diff --git a/packages/components/table/docs/Index.zh.md b/packages/components/table/docs/Index.zh.md index 08864cc87..3f2fc7881 100644 --- a/packages/components/table/docs/Index.zh.md +++ b/packages/components/table/docs/Index.zh.md @@ -235,4 +235,6 @@ export type TablePaginationPosition = 'topStart' | 'top' | 'topEnd' | 'bottomSta | `@table-icon-margin` | `@spacing-xs` | - | - | | `@table-expandable-icon-size` | `@font-size-md` | - | - | | `@table-expandable-icon-color` | `@color-black` | - | - | +| `@table-sticky-scroll-bar-background` | `fade(#000, 35%)` | - | - | +| `@table-sticky-scroll-bar-radius` | `4px` | - | - | \ No newline at end of file diff --git a/packages/components/table/src/main/body/Body.tsx b/packages/components/table/src/main/body/Body.tsx index 312f9abf6..d3677247a 100644 --- a/packages/components/table/src/main/body/Body.tsx +++ b/packages/components/table/src/main/body/Body.tsx @@ -5,7 +5,7 @@ * found in the LICENSE file at https://github.com/IDuxFE/idux/blob/main/LICENSE */ -import { type VNodeTypes, computed, defineComponent, inject } from 'vue' +import { VNodeChild, computed, defineComponent, inject } from 'vue' import { convertArray } from '@idux/cdk/utils' import { ɵEmpty } from '@idux/components/_private/empty' @@ -31,7 +31,7 @@ export default defineComponent({ const showMeasure = computed(() => scrollWidth.value || scrollHeight.value || isSticky.value) return () => { - const children: VNodeTypes[] = [] + const children: VNodeChild[] = [] if (tableSlots.alert) { children.push({tableSlots.alert()}) } diff --git a/packages/components/time-picker/docs/Index.zh.md b/packages/components/time-picker/docs/Index.zh.md index 70ef1ef45..07ad11f73 100644 --- a/packages/components/time-picker/docs/Index.zh.md +++ b/packages/components/time-picker/docs/Index.zh.md @@ -102,6 +102,7 @@ order: 0 | `@time-picker-overlay-width` | `200px` | - | - | | `@time-picker-overlay-padding` | `@spacing-sm` | - | - | | `@time-picker-overlay-box-shadow` | `@shadow-bottom-md` | - | - | +| `@time-picker-overlay-font-size` | `@font-size-md` | - | - | | `@time-picker-overlay-background-color` | `@form-background-color` | - | - | | `@time-picker-footer-padding` | `@spacing-sm 0` | - | - | | `@time-picker-footer-margin` | `0 @spacing-lg` | - | - | @@ -111,7 +112,7 @@ order: 0 | `@time-range-picker-overlay-gap-padding` | `5px 8px` | - | - | | `@time-range-picker-panel-border-width` | `@time-picker-border-width` | - | - | | `@time-range-picker-panel-border-style` | `@time-picker-border-style` | - | - | -| `@time-range-picker-panel-border-color` | `@color-graphite-l30` | - | - | +| `@time-range-picker-panel-border-color` | `@time-picker-border-color` | - | - | | `@time-range-picker-panel-border-radius` | `@time-picker-border-radius` | - | - | | `@time-picker-input-margin` | `@spacing-sm` | - | - | | `@time-picker-color` | `@form-color` | - | - | diff --git a/packages/components/time-picker/src/overlay/Overlay.tsx b/packages/components/time-picker/src/overlay/Overlay.tsx index 275905f25..54cd911ca 100644 --- a/packages/components/time-picker/src/overlay/Overlay.tsx +++ b/packages/components/time-picker/src/overlay/Overlay.tsx @@ -26,7 +26,6 @@ export default defineComponent({ config, format, dateConfig, - formContext, inputEnableStatus, mergedPrefixCls, overlayOpened, @@ -40,7 +39,7 @@ export default defineComponent({ setInputValue('') } - const inputProps = useCommonInputProps(props, config, formContext) + const inputProps = useCommonInputProps(props, config) const panelProps = useCommonPanelProps(props, config) const inputSlots = { diff --git a/packages/components/transfer/docs/Index.zh.md b/packages/components/transfer/docs/Index.zh.md index b8997545e..07db31466 100644 --- a/packages/components/transfer/docs/Index.zh.md +++ b/packages/components/transfer/docs/Index.zh.md @@ -188,6 +188,7 @@ export interface TransferOperationsSlotParams { | `@transfer-list-border-color` | `@form-border-color` | - | - | | `@transfer-list-border-radius` | `2px` | - | - | | `@transfer-list-background-color` | `@form-background-color` | - | - | +| `@transfer-font-size` | `@font-size-md` | - | - | | `@transfer-color` | `@text-color` | - | - | | `@transfer-disabled-color` | `@text-color-disabled` | - | - | | `@transfer-list-header-height` | `@height-lg` | - | - | @@ -202,7 +203,9 @@ export interface TransferOperationsSlotParams { | `@transfer-operations-btn-gap` | `@spacing-sm` | - | - | | `@transer-search-width` | `110px` | - | - | | `@transfer-clear-icon-font-size` | `@font-size-md` | - | - | -| `@transfer-icon-color` | `@color-graphite-d20` | - | - | -| `@transfer-icon-hover-color` | `@color-primary` | - | - | -| `@transfer-icon-active-color` | `@color-primary` | - | - | +| `@transfer-clear-icon-color` | `@color-graphite-d20` | - | - | +| `@transfer-clear-icon-hover-color` | `@color-primary` | - | - | +| `@transfer-clear-icon-active-color` | `@color-primary` | - | - | +| `@transfer-search-icon-font-size` | `@font-size-md` | - | - | +| `@transfer-search-icon-color` | `@color-graphite` | - | - | diff --git a/packages/components/tree-select/docs/Index.zh.md b/packages/components/tree-select/docs/Index.zh.md index 88c3ce717..9852db962 100644 --- a/packages/components/tree-select/docs/Index.zh.md +++ b/packages/components/tree-select/docs/Index.zh.md @@ -112,32 +112,6 @@ order: 0 | 名称 | `default` | `dark` | 备注 | | --- | --- | --- | --- | -| `@tree-select-font-size-sm` | `@form-font-size-sm` | - | - | -| `@tree-select-font-size-md` | `@form-font-size-md` | - | - | -| `@tree-select-font-size-lg` | `@form-font-size-lg` | - | - | -| `@tree-select-line-height` | `@form-line-height` | - | - | -| `@tree-select-height-sm` | `@form-height-sm` | - | - | -| `@tree-select-height-md` | `@form-height-md` | - | - | -| `@tree-select-height-lg` | `@form-height-lg` | - | - | -| `@tree-select-padding-horizontal-sm` | `@form-padding-horizontal-sm` | - | - | -| `@tree-select-padding-horizontal-md` | `@form-padding-horizontal-md` | - | - | -| `@tree-select-padding-horizontal-lg` | `@form-padding-horizontal-lg` | - | - | -| `@tree-select-padding-vertical-sm` | `@form-padding-vertical-sm` | - | - | -| `@tree-select-padding-vertical-md` | `@form-padding-vertical-md` | - | - | -| `@tree-select-padding-vertical-lg` | `@form-padding-vertical-lg` | - | - | -| `@tree-select-border-width` | `@form-border-width` | - | - | -| `@tree-select-border-style` | `@form-border-style` | - | - | -| `@tree-select-border-color` | `@form-border-color` | - | - | -| `@tree-select-border-radius` | `@border-radius-md` | - | - | -| `@tree-select-color` | `@form-color` | - | - | -| `@tree-select-color-secondary` | `@form-color-secondary` | - | - | -| `@tree-select-background-color` | `@form-background-color` | - | - | -| `@tree-select-placeholder-color` | `@form-placeholder-color` | - | - | -| `@tree-select-hover-color` | `@form-hover-color` | - | - | -| `@tree-select-active-color` | `@form-active-color` | - | - | -| `@tree-select-active-box-shadow` | `@form-active-box-shadow` | - | - | -| `@tree-select-disabled-color` | `@form-disabled-color` | - | - | -| `@tree-select-disabled-background-color` | `@form-disabled-background-color` | - | - | | `@tree-select-option-font-size` | `@font-size-md` | - | - | | `@tree-select-option-height` | `@height-md` | - | - | | `@tree-select-option-margin-left` | `@spacing-md` | - | - | @@ -154,19 +128,4 @@ order: 0 | `@tree-select-option-container-background-color` | `@background-color-component` | - | - | | `@tree-select-option-container-border-radius` | `@border-radius-sm` | - | - | | `@tree-select-option-container-box-shadow` | `@shadow-bottom-md` | - | - | -| `@tree-select-icon-font-size` | `@font-size-sm` | - | - | -| `@tree-select-icon-margin-right` | `@spacing-xs` | - | - | -| `@tree-select-icon-color` | `@tree-select-placeholder-color` | - | - | -| `@tree-select-icon-hover-color` | `@tree-select-color-secondary` | - | - | -| `@tree-select-icon-background-color` | `@tree-select-background-color` | - | - | -| `@tree-select-multiple-padding` | `@tree-select-padding-vertical-md` | - | - | -| `@tree-select-multiple-item-padding` | `0 @spacing-xs` | - | - | -| `@tree-select-multiple-item-background-color` | `@background-color-base` | - | - | -| `@tree-select-multiple-item-disabled-color` | `@tree-select-disabled-color` | - | - | -| `@tree-select-multiple-item-disabled-border-color` | `@tree-select-border-color` | - | - | -| `@tree-select-multiple-item-border-width` | `@border-width-sm` | - | - | -| `@tree-select-multiple-item-border` | `@tree-select-multiple-item-border-width @border-style @border-color-split` | - | - | -| `@tree-select-multiple-item-border-radius` | `@tree-select-border-radius` | - | - | -| `@tree-select-multiple-item-label-margin-left` | `@spacing-xs` | - | - | -| `@tree-select-multiple-item-remove-icon-font-size` | `@font-size-xs` | - | - | diff --git a/packages/components/types.d.ts b/packages/components/types.d.ts index 9125fba86..7d16016c0 100644 --- a/packages/components/types.d.ts +++ b/packages/components/types.d.ts @@ -16,7 +16,7 @@ import type { CardComponent, CardGridComponent } from '@idux/components/card' import type { CarouselComponent } from '@idux/components/carousel' import type { CheckboxComponent, CheckboxGroupComponent } from '@idux/components/checkbox' import type { CollapseComponent, CollapsePanelComponent } from '@idux/components/collapse' -import type { DatePickerComponent } from '@idux/components/date-picker' +import type { DatePickerComponent, DateRangePickerComponent } from '@idux/components/date-picker' import type { DividerComponent } from '@idux/components/divider' import type { DrawerComponent, DrawerProviderComponent } from '@idux/components/drawer' import type { DropdownComponent } from '@idux/components/dropdown' @@ -93,6 +93,7 @@ declare module 'vue' { IxCollapse: CollapseComponent IxCollapsePanel: CollapsePanelComponent IxDatePicker: DatePickerComponent + IxDateRangePicker: DateRangePickerComponent IxDivider: DividerComponent IxDrawer: DrawerComponent IxDrawerProvider: DrawerProviderComponent diff --git a/packages/pro/transfer/docs/Index.zh.md b/packages/pro/transfer/docs/Index.zh.md index 913f22536..2d4846cb9 100644 --- a/packages/pro/transfer/docs/Index.zh.md +++ b/packages/pro/transfer/docs/Index.zh.md @@ -109,3 +109,18 @@ export interface ProTransferTreeProps { | 名称 | 说明 | 参数类型 | 备注 | | --- | --- | --- | --- | | `scrollTo` | 滚动到指定位置 | `(isSource: boolean, option?: number \| VirtualScrollToOptions) => void` | 仅在开启 `virtual` 时可用 | + + +## 主题变量 + +| 名称 | `default` | `dark` | 备注 | +| --- | --- | --- | --- | +| `@pro-transfer-list-min-width` | `260px` | - | - | +| `@pro-transfer-list-min-height` | `290px` | - | - | +| `@pro-transfer-table-close-icon-padding` | `0 0 0 @spacing-md` | - | - | +| `@pro-transfer-tree-close-icon-padding` | `0 @spacing-md` | - | - | +| `@pro-transfer-list-close-icon-font-size` | `@font-size-md` | - | - | +| `@pro-transfer-list-close-icon-color` | `@color-graphite-d20` | - | - | +| `@pro-transfer-list-close-icon-hover-color` | `@color-primary` | - | - | +| `@pro-transfer-list-close-icon-active-color` | `@color-primary` | - | - | + \ No newline at end of file