From 8d2133b860c105a7cfa78cdc6f967eeed1f6ed78 Mon Sep 17 00:00:00 2001 From: saller Date: Mon, 9 May 2022 09:08:27 +0800 Subject: [PATCH] fix(comp:time-picker,date-picker): refactor code and fix z-index (#886) 1. refactor DateRangePicker activeDate control logic 2. refactor DateRangePicker input logic 3. unify DatePicker and TimePicker style variables 4. unify DatePicker and TimePicker code structure feat(comp:time-panel): add activeValue prop fix(comp:date-picker,time-picker): fix overlay z-index error --- .../date-panel/src/panel-body/PanelCell.tsx | 4 - .../__snapshots__/timeSelector.spec.ts.snap | 1224 +++++++++-------- .../timeSelectorColumn.spec.ts.snap | 16 +- .../__tests__/useSelectorScroll.spec.ts | 6 +- .../_private/time-panel/src/TimePanelCell.tsx | 4 +- .../time-panel/src/TimePanelColumn.tsx | 37 +- .../time-panel/src/composables/useOptions.ts | 61 +- .../src/composables/usePanelScroll.ts | 39 +- .../_private/time-panel/src/types.ts | 118 +- .../_private/time-panel/style/index.less | 55 +- .../components/config/src/defaultConfig.ts | 9 - packages/components/config/src/types.ts | 7 +- .../__tests__/dateRangePicker.spec.ts | 1 + .../components/date-picker/docs/Index.zh.md | 11 +- .../components/date-picker/src/DatePicker.tsx | 25 +- .../date-picker/src/DateRangePicker.tsx | 35 +- .../src/composables/useActiveDate.ts | 60 +- .../date-picker/src/composables/useControl.ts | 20 +- .../src/composables/useInputProps.ts | 26 + .../src/composables/useKeyboardEvents.ts | 4 +- .../src/composables/useOverlayProps.ts | 29 + .../src/composables/usePickerState.ts | 2 +- .../src/composables/useRangeControl.ts | 26 +- .../src/composables/useTimePanelProps.ts | 54 + .../src/composables/useTriggerProps.ts | 10 +- .../date-picker/src/content/Content.tsx | 40 +- .../date-picker/src/content/RangeContent.tsx | 67 +- packages/components/date-picker/src/token.ts | 4 +- .../date-picker/src/trigger/RangeTrigger.tsx | 5 +- .../date-picker/src/trigger/Trigger.tsx | 5 +- packages/components/date-picker/src/types.ts | 14 +- packages/components/date-picker/src/utils.ts | 44 +- .../components/date-picker/style/index.less | 83 +- .../components/date-picker/style/mixin.less | 64 + .../components/date-picker/style/panel.less | 1 - .../style/themes/default.variable.less | 3 +- .../time-picker/demo/Borderless.vue | 2 +- .../components/time-picker/docs/Index.zh.md | 87 +- .../components/time-picker/src/TimePicker.tsx | 111 +- .../time-picker/src/TimeRangePicker.tsx | 125 +- .../time-picker/src/composables/useControl.ts | 116 ++ .../src/composables/useInputProps.ts | 26 + .../src/composables/useKeyboardEvents.ts | 41 + .../src/composables/useOverlayProps.ts | 30 + .../src/composables/useOverlayState.ts | 41 + .../src/composables/usePanelActiveValue.ts | 71 + .../src/composables/usePanelProps.ts | 53 + .../src/composables/usePickerControl.ts | 123 -- .../src/composables/usePickerState.ts | 88 ++ .../time-picker/src/composables/useProps.ts | 152 -- .../src/composables/useRangeControl.ts | 93 ++ .../useTimePickerCommonBindings.ts | 98 -- .../src/composables/useTriggerProps.ts | 55 + .../time-picker/src/content/Content.tsx | 105 ++ .../time-picker/src/content/RangeContent.tsx | 141 ++ .../time-picker/src/overlay/Overlay.tsx | 85 -- .../time-picker/src/overlay/RangeOverlay.tsx | 119 -- packages/components/time-picker/src/tokens.ts | 27 +- .../time-picker/src/trigger/BaseTrigger.tsx | 96 -- .../time-picker/src/trigger/RangeTrigger.tsx | 140 +- .../time-picker/src/trigger/Trigger.tsx | 93 +- packages/components/time-picker/src/types.ts | 141 +- packages/components/time-picker/src/utils.ts | 14 + .../components/time-picker/style/index.less | 168 +-- .../components/time-picker/style/mixin.less | 41 + .../components/time-picker/style/range.less | 57 - .../style/themes/default.variable.less | 75 +- .../style/themes/seer.variable.less | 6 +- scripts/gen/style-variable/update.ts | 4 +- 69 files changed, 2574 insertions(+), 2263 deletions(-) create mode 100644 packages/components/date-picker/src/composables/useInputProps.ts create mode 100644 packages/components/date-picker/src/composables/useOverlayProps.ts create mode 100644 packages/components/date-picker/src/composables/useTimePanelProps.ts create mode 100644 packages/components/date-picker/style/mixin.less create mode 100644 packages/components/time-picker/src/composables/useControl.ts create mode 100644 packages/components/time-picker/src/composables/useInputProps.ts create mode 100644 packages/components/time-picker/src/composables/useKeyboardEvents.ts create mode 100644 packages/components/time-picker/src/composables/useOverlayProps.ts create mode 100644 packages/components/time-picker/src/composables/useOverlayState.ts create mode 100644 packages/components/time-picker/src/composables/usePanelActiveValue.ts create mode 100644 packages/components/time-picker/src/composables/usePanelProps.ts delete mode 100644 packages/components/time-picker/src/composables/usePickerControl.ts create mode 100644 packages/components/time-picker/src/composables/usePickerState.ts delete mode 100644 packages/components/time-picker/src/composables/useProps.ts create mode 100644 packages/components/time-picker/src/composables/useRangeControl.ts delete mode 100644 packages/components/time-picker/src/composables/useTimePickerCommonBindings.ts create mode 100644 packages/components/time-picker/src/composables/useTriggerProps.ts create mode 100644 packages/components/time-picker/src/content/Content.tsx create mode 100644 packages/components/time-picker/src/content/RangeContent.tsx delete mode 100644 packages/components/time-picker/src/overlay/Overlay.tsx delete mode 100644 packages/components/time-picker/src/overlay/RangeOverlay.tsx delete mode 100644 packages/components/time-picker/src/trigger/BaseTrigger.tsx create mode 100644 packages/components/time-picker/style/mixin.less delete mode 100644 packages/components/time-picker/style/range.less 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 6e535d490..46fc74fde 100644 --- a/packages/components/_private/date-panel/src/panel-body/PanelCell.tsx +++ b/packages/components/_private/date-panel/src/panel-body/PanelCell.tsx @@ -93,10 +93,6 @@ export default defineComponent({ ) }) const isInRange = computed(() => { - if (outView.value) { - return false - } - const compareType = dayTypes.includes(activeType.value) ? 'date' : activeType.value const cellDateValue = dateConfig.startOf(cellDate.value, compareType).valueOf() diff --git a/packages/components/_private/time-panel/__tests__/__snapshots__/timeSelector.spec.ts.snap b/packages/components/_private/time-panel/__tests__/__snapshots__/timeSelector.spec.ts.snap index 66110f598..e18c40ee6 100644 --- a/packages/components/_private/time-panel/__tests__/__snapshots__/timeSelector.spec.ts.snap +++ b/packages/components/_private/time-panel/__tests__/__snapshots__/timeSelector.spec.ts.snap @@ -2,620 +2,644 @@ exports[`TimePanel > disableMinutes work 1`] = ` "
- - - +
+
    +
  • 00
  • +
  • 01
  • +
  • 02
  • +
  • 03
  • +
  • 04
  • +
  • 05
  • +
  • 06
  • +
  • 07
  • +
  • 08
  • +
  • 09
  • +
  • 10
  • +
  • 11
  • +
  • 12
  • +
  • 13
  • +
  • 14
  • +
  • 15
  • +
  • 16
  • +
  • 17
  • +
  • 18
  • +
  • 19
  • +
  • 20
  • +
  • 21
  • +
  • 22
  • +
  • 23
  • +
+
+
+
    +
  • 00
  • +
  • 01
  • +
  • 02
  • +
  • 03
  • +
  • 04
  • +
  • 05
  • +
  • 06
  • +
  • 07
  • +
  • 08
  • +
  • 09
  • +
  • 10
  • +
  • 11
  • +
  • 12
  • +
  • 13
  • +
  • 14
  • +
  • 15
  • +
  • 16
  • +
  • 17
  • +
  • 18
  • +
  • 19
  • +
  • 20
  • +
  • 21
  • +
  • 22
  • +
  • 23
  • +
  • 24
  • +
  • 25
  • +
  • 26
  • +
  • 27
  • +
  • 28
  • +
  • 29
  • +
  • 30
  • +
  • 31
  • +
  • 32
  • +
  • 33
  • +
  • 34
  • +
  • 35
  • +
  • 36
  • +
  • 37
  • +
  • 38
  • +
  • 39
  • +
  • 40
  • +
  • 41
  • +
  • 42
  • +
  • 43
  • +
  • 44
  • +
  • 45
  • +
  • 46
  • +
  • 47
  • +
  • 48
  • +
  • 49
  • +
  • 50
  • +
  • 51
  • +
  • 52
  • +
  • 53
  • +
  • 54
  • +
  • 55
  • +
  • 56
  • +
  • 57
  • +
  • 58
  • +
  • 59
  • +
+
+
+
    +
  • 00
  • +
  • 01
  • +
  • 02
  • +
  • 03
  • +
  • 04
  • +
  • 05
  • +
  • 06
  • +
  • 07
  • +
  • 08
  • +
  • 09
  • +
  • 10
  • +
  • 11
  • +
  • 12
  • +
  • 13
  • +
  • 14
  • +
  • 15
  • +
  • 16
  • +
  • 17
  • +
  • 18
  • +
  • 19
  • +
  • 20
  • +
  • 21
  • +
  • 22
  • +
  • 23
  • +
  • 24
  • +
  • 25
  • +
  • 26
  • +
  • 27
  • +
  • 28
  • +
  • 29
  • +
  • 30
  • +
  • 31
  • +
  • 32
  • +
  • 33
  • +
  • 34
  • +
  • 35
  • +
  • 36
  • +
  • 37
  • +
  • 38
  • +
  • 39
  • +
  • 40
  • +
  • 41
  • +
  • 42
  • +
  • 43
  • +
  • 44
  • +
  • 45
  • +
  • 46
  • +
  • 47
  • +
  • 48
  • +
  • 49
  • +
  • 50
  • +
  • 51
  • +
  • 52
  • +
  • 53
  • +
  • 54
  • +
  • 55
  • +
  • 56
  • +
  • 57
  • +
  • 58
  • +
  • 59
  • +
+
" `; exports[`TimePanel > disableSeconds work 1`] = ` "
- - - +
+
    +
  • 00
  • +
  • 01
  • +
  • 02
  • +
  • 03
  • +
  • 04
  • +
  • 05
  • +
  • 06
  • +
  • 07
  • +
  • 08
  • +
  • 09
  • +
  • 10
  • +
  • 11
  • +
  • 12
  • +
  • 13
  • +
  • 14
  • +
  • 15
  • +
  • 16
  • +
  • 17
  • +
  • 18
  • +
  • 19
  • +
  • 20
  • +
  • 21
  • +
  • 22
  • +
  • 23
  • +
+
+
+
    +
  • 00
  • +
  • 01
  • +
  • 02
  • +
  • 03
  • +
  • 04
  • +
  • 05
  • +
  • 06
  • +
  • 07
  • +
  • 08
  • +
  • 09
  • +
  • 10
  • +
  • 11
  • +
  • 12
  • +
  • 13
  • +
  • 14
  • +
  • 15
  • +
  • 16
  • +
  • 17
  • +
  • 18
  • +
  • 19
  • +
  • 20
  • +
  • 21
  • +
  • 22
  • +
  • 23
  • +
  • 24
  • +
  • 25
  • +
  • 26
  • +
  • 27
  • +
  • 28
  • +
  • 29
  • +
  • 30
  • +
  • 31
  • +
  • 32
  • +
  • 33
  • +
  • 34
  • +
  • 35
  • +
  • 36
  • +
  • 37
  • +
  • 38
  • +
  • 39
  • +
  • 40
  • +
  • 41
  • +
  • 42
  • +
  • 43
  • +
  • 44
  • +
  • 45
  • +
  • 46
  • +
  • 47
  • +
  • 48
  • +
  • 49
  • +
  • 50
  • +
  • 51
  • +
  • 52
  • +
  • 53
  • +
  • 54
  • +
  • 55
  • +
  • 56
  • +
  • 57
  • +
  • 58
  • +
  • 59
  • +
+
+
+
    +
  • 00
  • +
  • 01
  • +
  • 02
  • +
  • 03
  • +
  • 04
  • +
  • 05
  • +
  • 06
  • +
  • 07
  • +
  • 08
  • +
  • 09
  • +
  • 10
  • +
  • 11
  • +
  • 12
  • +
  • 13
  • +
  • 14
  • +
  • 15
  • +
  • 16
  • +
  • 17
  • +
  • 18
  • +
  • 19
  • +
  • 20
  • +
  • 21
  • +
  • 22
  • +
  • 23
  • +
  • 24
  • +
  • 25
  • +
  • 26
  • +
  • 27
  • +
  • 28
  • +
  • 29
  • +
  • 30
  • +
  • 31
  • +
  • 32
  • +
  • 33
  • +
  • 34
  • +
  • 35
  • +
  • 36
  • +
  • 37
  • +
  • 38
  • +
  • 39
  • +
  • 40
  • +
  • 41
  • +
  • 42
  • +
  • 43
  • +
  • 44
  • +
  • 45
  • +
  • 46
  • +
  • 47
  • +
  • 48
  • +
  • 49
  • +
  • 50
  • +
  • 51
  • +
  • 52
  • +
  • 53
  • +
  • 54
  • +
  • 55
  • +
  • 56
  • +
  • 57
  • +
  • 58
  • +
  • 59
  • +
+
" `; exports[`TimePanel > disabledHours work 1`] = ` "
- - - +
+
    +
  • 00
  • +
  • 01
  • +
  • 02
  • +
  • 03
  • +
  • 04
  • +
  • 05
  • +
  • 06
  • +
  • 07
  • +
  • 08
  • +
  • 09
  • +
  • 10
  • +
  • 11
  • +
  • 12
  • +
  • 13
  • +
  • 14
  • +
  • 15
  • +
  • 16
  • +
  • 17
  • +
  • 18
  • +
  • 19
  • +
  • 20
  • +
  • 21
  • +
  • 22
  • +
  • 23
  • +
+
+
+
    +
  • 00
  • +
  • 01
  • +
  • 02
  • +
  • 03
  • +
  • 04
  • +
  • 05
  • +
  • 06
  • +
  • 07
  • +
  • 08
  • +
  • 09
  • +
  • 10
  • +
  • 11
  • +
  • 12
  • +
  • 13
  • +
  • 14
  • +
  • 15
  • +
  • 16
  • +
  • 17
  • +
  • 18
  • +
  • 19
  • +
  • 20
  • +
  • 21
  • +
  • 22
  • +
  • 23
  • +
  • 24
  • +
  • 25
  • +
  • 26
  • +
  • 27
  • +
  • 28
  • +
  • 29
  • +
  • 30
  • +
  • 31
  • +
  • 32
  • +
  • 33
  • +
  • 34
  • +
  • 35
  • +
  • 36
  • +
  • 37
  • +
  • 38
  • +
  • 39
  • +
  • 40
  • +
  • 41
  • +
  • 42
  • +
  • 43
  • +
  • 44
  • +
  • 45
  • +
  • 46
  • +
  • 47
  • +
  • 48
  • +
  • 49
  • +
  • 50
  • +
  • 51
  • +
  • 52
  • +
  • 53
  • +
  • 54
  • +
  • 55
  • +
  • 56
  • +
  • 57
  • +
  • 58
  • +
  • 59
  • +
+
+
+
    +
  • 00
  • +
  • 01
  • +
  • 02
  • +
  • 03
  • +
  • 04
  • +
  • 05
  • +
  • 06
  • +
  • 07
  • +
  • 08
  • +
  • 09
  • +
  • 10
  • +
  • 11
  • +
  • 12
  • +
  • 13
  • +
  • 14
  • +
  • 15
  • +
  • 16
  • +
  • 17
  • +
  • 18
  • +
  • 19
  • +
  • 20
  • +
  • 21
  • +
  • 22
  • +
  • 23
  • +
  • 24
  • +
  • 25
  • +
  • 26
  • +
  • 27
  • +
  • 28
  • +
  • 29
  • +
  • 30
  • +
  • 31
  • +
  • 32
  • +
  • 33
  • +
  • 34
  • +
  • 35
  • +
  • 36
  • +
  • 37
  • +
  • 38
  • +
  • 39
  • +
  • 40
  • +
  • 41
  • +
  • 42
  • +
  • 43
  • +
  • 44
  • +
  • 45
  • +
  • 46
  • +
  • 47
  • +
  • 48
  • +
  • 49
  • +
  • 50
  • +
  • 51
  • +
  • 52
  • +
  • 53
  • +
  • 54
  • +
  • 55
  • +
  • 56
  • +
  • 57
  • +
  • 58
  • +
  • 59
  • +
+
" `; exports[`TimePanel > render work 1`] = ` "
- - - +
+
    +
  • 00
  • +
  • 01
  • +
  • 02
  • +
  • 03
  • +
  • 04
  • +
  • 05
  • +
  • 06
  • +
  • 07
  • +
  • 08
  • +
  • 09
  • +
  • 10
  • +
  • 11
  • +
  • 12
  • +
  • 13
  • +
  • 14
  • +
  • 15
  • +
  • 16
  • +
  • 17
  • +
  • 18
  • +
  • 19
  • +
  • 20
  • +
  • 21
  • +
  • 22
  • +
  • 23
  • +
+
+
+
    +
  • 00
  • +
  • 01
  • +
  • 02
  • +
  • 03
  • +
  • 04
  • +
  • 05
  • +
  • 06
  • +
  • 07
  • +
  • 08
  • +
  • 09
  • +
  • 10
  • +
  • 11
  • +
  • 12
  • +
  • 13
  • +
  • 14
  • +
  • 15
  • +
  • 16
  • +
  • 17
  • +
  • 18
  • +
  • 19
  • +
  • 20
  • +
  • 21
  • +
  • 22
  • +
  • 23
  • +
  • 24
  • +
  • 25
  • +
  • 26
  • +
  • 27
  • +
  • 28
  • +
  • 29
  • +
  • 30
  • +
  • 31
  • +
  • 32
  • +
  • 33
  • +
  • 34
  • +
  • 35
  • +
  • 36
  • +
  • 37
  • +
  • 38
  • +
  • 39
  • +
  • 40
  • +
  • 41
  • +
  • 42
  • +
  • 43
  • +
  • 44
  • +
  • 45
  • +
  • 46
  • +
  • 47
  • +
  • 48
  • +
  • 49
  • +
  • 50
  • +
  • 51
  • +
  • 52
  • +
  • 53
  • +
  • 54
  • +
  • 55
  • +
  • 56
  • +
  • 57
  • +
  • 58
  • +
  • 59
  • +
+
+
+
    +
  • 00
  • +
  • 01
  • +
  • 02
  • +
  • 03
  • +
  • 04
  • +
  • 05
  • +
  • 06
  • +
  • 07
  • +
  • 08
  • +
  • 09
  • +
  • 10
  • +
  • 11
  • +
  • 12
  • +
  • 13
  • +
  • 14
  • +
  • 15
  • +
  • 16
  • +
  • 17
  • +
  • 18
  • +
  • 19
  • +
  • 20
  • +
  • 21
  • +
  • 22
  • +
  • 23
  • +
  • 24
  • +
  • 25
  • +
  • 26
  • +
  • 27
  • +
  • 28
  • +
  • 29
  • +
  • 30
  • +
  • 31
  • +
  • 32
  • +
  • 33
  • +
  • 34
  • +
  • 35
  • +
  • 36
  • +
  • 37
  • +
  • 38
  • +
  • 39
  • +
  • 40
  • +
  • 41
  • +
  • 42
  • +
  • 43
  • +
  • 44
  • +
  • 45
  • +
  • 46
  • +
  • 47
  • +
  • 48
  • +
  • 49
  • +
  • 50
  • +
  • 51
  • +
  • 52
  • +
  • 53
  • +
  • 54
  • +
  • 55
  • +
  • 56
  • +
  • 57
  • +
  • 58
  • +
  • 59
  • +
+
" `; diff --git a/packages/components/_private/time-panel/__tests__/__snapshots__/timeSelectorColumn.spec.ts.snap b/packages/components/_private/time-panel/__tests__/__snapshots__/timeSelectorColumn.spec.ts.snap index 5133d3f7a..727e9d13e 100644 --- a/packages/components/_private/time-panel/__tests__/__snapshots__/timeSelectorColumn.spec.ts.snap +++ b/packages/components/_private/time-panel/__tests__/__snapshots__/timeSelectorColumn.spec.ts.snap @@ -1,11 +1,13 @@ // Vitest Snapshot v1 exports[`TimePanelColumn > render work 1`] = ` -"" +"
+ +
" `; diff --git a/packages/components/_private/time-panel/__tests__/useSelectorScroll.spec.ts b/packages/components/_private/time-panel/__tests__/useSelectorScroll.spec.ts index 623406958..b7cbcb0bf 100644 --- a/packages/components/_private/time-panel/__tests__/useSelectorScroll.spec.ts +++ b/packages/components/_private/time-panel/__tests__/useSelectorScroll.spec.ts @@ -70,7 +70,9 @@ describe('usePanelScroll', () => { }) const onChange = vi.fn() const props: TimePanelColumnProps = reactive({ + visible: true, selectedValue: selectedValue ?? 1, + activeValue: selectedValue ?? 1, options, onChange, }) @@ -120,11 +122,11 @@ describe('usePanelScroll', () => { await wait(400) expect(wrapper.element.scrollTop).toBe(cellHeight) - props.selectedValue = 3 + props.activeValue = 3 await wait(400) expect(wrapper.element.scrollTop).toBe(2 * cellHeight) - props.selectedValue = 1 + props.activeValue = 1 await wait(400) expect(wrapper.element.scrollTop).toBe(0) }) diff --git a/packages/components/_private/time-panel/src/TimePanelCell.tsx b/packages/components/_private/time-panel/src/TimePanelCell.tsx index 836fad691..3b44f1c46 100644 --- a/packages/components/_private/time-panel/src/TimePanelCell.tsx +++ b/packages/components/_private/time-panel/src/TimePanelCell.tsx @@ -27,11 +27,11 @@ export default defineComponent({ const onClick = () => { if (!props.disabled && !props.selected) { - callEmit(props.onChange, props.value) + callEmit(props.onChange, props.value!) } } - const displayValue = computed(() => displayFormat(props.value)) + const displayValue = computed(() => displayFormat(props.value!)) return () => (
  • diff --git a/packages/components/_private/time-panel/src/TimePanelColumn.tsx b/packages/components/_private/time-panel/src/TimePanelColumn.tsx index 2b990708c..2cab7b6e8 100644 --- a/packages/components/_private/time-panel/src/TimePanelColumn.tsx +++ b/packages/components/_private/time-panel/src/TimePanelColumn.tsx @@ -5,7 +5,7 @@ * found in the LICENSE file at https://github.com/IDuxFE/idux/blob/main/LICENSE */ -import { defineComponent, inject, nextTick, ref } from 'vue' +import { defineComponent, inject, ref } from 'vue' import { callEmit } from '@idux/cdk/utils' @@ -19,34 +19,35 @@ export default defineComponent({ setup(props) { const { mergedPrefixCls } = inject(timePanelContext)! const listRef = ref(null) - const { scrollToSelected, handleScroll, handleScrollAdjust } = usePanelScroll(props, listRef, mergedPrefixCls) + const { handleScroll, handleScrollAdjust } = usePanelScroll(props, listRef, mergedPrefixCls) function onChange(value: string | number) { callEmit(props.onChange, value) - nextTick(scrollToSelected) } return () => ( -
      - {props.options.map((item, index) => { - const { disabled, value } = item - return ( - - ) - })} -
    +
      + {props.options!.map((item, index) => { + const { disabled, value } = item + return ( + + ) + })} +
    + ) }, }) diff --git a/packages/components/_private/time-panel/src/composables/useOptions.ts b/packages/components/_private/time-panel/src/composables/useOptions.ts index b419d73dc..3933eb20b 100644 --- a/packages/components/_private/time-panel/src/composables/useOptions.ts +++ b/packages/components/_private/time-panel/src/composables/useOptions.ts @@ -11,7 +11,7 @@ import type { ComputedRef } from 'vue' import { computed } from 'vue' -import { callEmit } from '@idux/cdk/utils' +import { callEmit, useControlledProp } from '@idux/cdk/utils' import { calculateValue, calculateViewHour, normalizeAmPm } from '../utils' @@ -25,10 +25,13 @@ export function useOptions( amPmOptionsProps: ComputedRef } { const { get } = dateConfig - const defaultOpenValue = computed(() => props.defaultOpenValue ?? dateConfig.startOf(dateConfig.now(), 'date')) - const selectedValue = computed(() => props.value ?? defaultOpenValue.value) - const viewHours = computed(() => calculateViewHour(get(selectedValue.value, 'hour'), props.use12Hours)) - const ampm = computed(() => normalizeAmPm(get(selectedValue.value, 'hour'), props.use12Hours)) + const [activeValue, setActiveValue] = useControlledProp(props, 'activeValue', () => dateConfig.now()) + const [selectedValue, setSelectedValue] = useControlledProp(props, 'value') + + const viewHours = computed( + () => selectedValue.value && calculateViewHour(get(selectedValue.value, 'hour'), props.use12Hours), + ) + const ampm = computed(() => selectedValue.value && normalizeAmPm(get(selectedValue.value, 'hour'), props.use12Hours)) function getOptions(type: TimePanelColumnType): TimePanelCell[] { const getHourOptions = () => { @@ -54,7 +57,11 @@ export function useOptions( return generateNumericOptions( 60, props.secondStep, - props.disabledSeconds(viewHours.value, get(selectedValue.value, 'minute'), ampm.value), + props.disabledSeconds!( + viewHours.value, + selectedValue.value && get(selectedValue.value, 'minute'), + ampm.value, + ), props.hideDisabledOptions, ) case 'minute': @@ -71,34 +78,42 @@ export function useOptions( } } - const getHourValue = () => { - const value = ampm.value === 'pm' ? get(selectedValue.value, 'hour') % 12 : get(selectedValue.value, 'hour') + const getHourValue = (value: Date) => { + const hour = ampm.value === 'pm' ? get(value, 'hour') % 12 : get(value, 'hour') if (ampm.value) { - return value === 0 ? 12 : value + return hour === 0 ? 12 : hour } - return value + return hour } - function getSelectedValue(type: TimePanelColumnType) { + function getColumnValue(value: Date, type: TimePanelColumnType) { switch (type) { case 'AM/PM': return ampm.value case 'hour': default: - return getHourValue() + return getHourValue(value) case 'minute': - return get(selectedValue.value, 'minute') + return get(value, 'minute') case 'second': - return get(selectedValue.value, 'second') + return get(value, 'second') } } function getOnChange(type: TimePanelColumnType) { const onChange = (type: TimePanelColumnType, value: string | number) => { - const newValue = calculateValue(dateConfig, selectedValue.value, type, props.use12Hours, value) - callEmit(props['onUpdate:value'], newValue) + const newValue = calculateValue( + dateConfig, + selectedValue.value ?? activeValue.value, + type, + props.use12Hours, + value, + ) + + setSelectedValue(newValue) + setActiveValue(newValue) callEmit(props.onChange, newValue) } @@ -106,11 +121,15 @@ export function useOptions( } const getProps = (type: TimePanelColumnType) => { - return computed(() => ({ - selectedValue: getSelectedValue(type), - options: getOptions(type), - onChange: getOnChange(type), - })) + return computed( + () => + ({ + activeValue: getColumnValue(activeValue.value, type), + selectedValue: selectedValue.value && getColumnValue(selectedValue.value, type), + options: getOptions(type), + onChange: getOnChange(type), + } as TimePanelColumnProps), + ) } return { diff --git a/packages/components/_private/time-panel/src/composables/usePanelScroll.ts b/packages/components/_private/time-panel/src/composables/usePanelScroll.ts index bc80b3054..f6de3974d 100644 --- a/packages/components/_private/time-panel/src/composables/usePanelScroll.ts +++ b/packages/components/_private/time-panel/src/composables/usePanelScroll.ts @@ -17,7 +17,7 @@ import { callEmit } from '@idux/cdk/utils' export interface PanelColumnScroll { adjustPanel: (selectedIndex: number, duration?: number) => void - scrollToSelected: (duration?: number) => void + scrollToActive: (duration?: number) => void handleScrollAdjust: () => void handleScroll: () => void } @@ -28,6 +28,7 @@ export function usePanelScroll( mergedPrefixCls: ComputedRef, ): PanelColumnScroll { let scrollHandlerLocked = false + let scrollHandlerLockedTmr: null | number = null let isScrolling = false let scrollTargetIndex: number | undefined @@ -37,7 +38,7 @@ export function usePanelScroll( function adjustPanel(selectedIndex: number, duration = 200) { const target = listRef.value - if (!target || isScrolling || scrollHandlerLocked) { + if (!target || isScrolling) { return } @@ -47,36 +48,42 @@ export function usePanelScroll( } scrollHandlerLocked = true + + if (scrollHandlerLockedTmr) { + clearTimeout(scrollHandlerLockedTmr) + } + scrollToTop({ top, target, duration, callback: () => { - setTimeout(() => { + scrollHandlerLockedTmr = setTimeout(() => { scrollHandlerLocked = false - }, 100) + scrollHandlerLockedTmr = null + }, Math.max(duration, 200)) }, }) } - function scrollToSelected(duration?: number) { - const selectedIndex = props.options.findIndex(item => item.value === props.selectedValue) - adjustPanel(selectedIndex, duration) + function scrollToActive(duration?: number) { + const activeIndex = props.options!.findIndex(item => item.value === props.activeValue) + adjustPanel(activeIndex, duration) } function handleScrollAdjust() { - isNil(scrollTargetIndex) || scrollTargetIndex < 0 ? scrollToSelected() : adjustPanel(scrollTargetIndex) + isNil(scrollTargetIndex) || scrollTargetIndex < 0 ? scrollToActive() : adjustPanel(scrollTargetIndex) } function handleScroll() { const target = listRef.value - if (!target || scrollHandlerLocked) { + if (!target || isScrolling || scrollHandlerLocked) { return } isScrolling = true - scrollTargetIndex = Math.min(Math.round(getScroll(target).scrollTop / getCellHeight()), props.options.length - 1) - const targetItem = props.options[scrollTargetIndex] + scrollTargetIndex = Math.min(Math.round(getScroll(target).scrollTop / getCellHeight()), props.options!.length - 1) + const targetItem = props.options![scrollTargetIndex] if (!targetItem.disabled) { callEmit(props.onChange, targetItem.value) } @@ -87,23 +94,23 @@ export function usePanelScroll( watchEffect(() => { if (props.visible) { - nextTick(() => scrollToSelected(0)) + nextTick(() => scrollToActive(0)) } }) watch( - () => props.selectedValue, + () => props.activeValue, value => { - const newScrollTargetIndex = props.options.findIndex(item => item.value === value) + const newScrollTargetIndex = props.options!.findIndex(item => item.value === value) if (scrollTargetIndex !== newScrollTargetIndex) { scrollTargetIndex = newScrollTargetIndex - !isScrolling && nextTick(scrollToSelected) + !isScrolling && nextTick(scrollToActive) } }, ) return { adjustPanel, - scrollToSelected, + scrollToActive, handleScrollAdjust, handleScroll, } diff --git a/packages/components/_private/time-panel/src/types.ts b/packages/components/_private/time-panel/src/types.ts index 9502806bd..9b38df7dc 100644 --- a/packages/components/_private/time-panel/src/types.ts +++ b/packages/components/_private/time-panel/src/types.ts @@ -5,38 +5,81 @@ * found in the LICENSE file at https://github.com/IDuxFE/idux/blob/main/LICENSE */ +/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ + import type { ExtractInnerPropTypes, ExtractPublicPropTypes } from '@idux/cdk/utils' -import type { DefineComponent, HTMLAttributes } from 'vue' +import type { DefineComponent, HTMLAttributes, PropType } from 'vue' -import { IxPropTypes } from '@idux/cdk/utils' +import { MaybeArray } from '@idux/cdk/utils' export const baseTimePanelProps = { - disabledHours: IxPropTypes.func<(selectedAmPm: string) => number[]>().def(() => []), - disabledMinutes: IxPropTypes.func<(selectedHour: number, selectedAmPm: string) => number[]>().def(() => []), - disabledSeconds: IxPropTypes.func< - (selectedHour: number, selectedMinute: number, selectedAmPm: string) => number[] - >().def(() => []), + disabledHours: { + type: Function as PropType<(selectedAmPm: string | undefined) => number[]>, + default: () => [], + }, + disabledMinutes: { + type: Function as PropType<(selectedHour: number | undefined, selectedAmPm: string | undefined) => number[]>, + default: () => [], + }, + disabledSeconds: { + type: Function as PropType< + ( + selectedHour: number | undefined, + selectedMinute: number | undefined, + selectedAmPm: string | undefined, + ) => number[] + >, + default: () => [], + }, - hideDisabledOptions: IxPropTypes.bool.def(false), - hourStep: IxPropTypes.number.def(1), - minuteStep: IxPropTypes.number.def(1), - secondStep: IxPropTypes.number.def(1), + hideDisabledOptions: { + type: Boolean, + default: false, + }, + hourStep: { + type: Number, + default: 1, + }, + minuteStep: { + type: Number, + default: 1, + }, + secondStep: { + type: Number, + default: 1, + }, } export const timePanelProps = { ...baseTimePanelProps, - value: IxPropTypes.object(), - defaultOpenValue: IxPropTypes.object(), - visible: IxPropTypes.bool, - hourEnabled: IxPropTypes.bool.def(true), - minuteEnabled: IxPropTypes.bool.def(true), - secondEnabled: IxPropTypes.bool.def(true), - use12Hours: IxPropTypes.bool.def(false), + value: Date, + activeValue: Date, + visible: { + type: Boolean, + default: false, + }, + hourEnabled: { + type: Boolean, + default: true, + }, + minuteEnabled: { + type: Boolean, + default: true, + }, + secondEnabled: { + type: Boolean, + default: true, + }, + use12Hours: { + type: Boolean, + default: false, + }, // events - 'onUpdate:value': IxPropTypes.emit<(value: Date) => void>(), - onChange: IxPropTypes.emit<(value: Date) => void>(), + 'onUpdate:value': [Function, Array] as PropType void>>, + 'onUpdate:activeValue': [Function, Array] as PropType void>>, + onChange: [Function, Array] as PropType void>>, } export type TimePanelProps = ExtractInnerPropTypes @@ -53,21 +96,40 @@ export interface TimePanelCell { } export const timePanelColumnProps = { - selectedValue: IxPropTypes.oneOfType([IxPropTypes.number, IxPropTypes.string]).isRequired, - options: IxPropTypes.arrayOf(IxPropTypes.object()).isRequired, - visible: IxPropTypes.bool, + selectedValue: [Number, String], + activeValue: { + type: [Number, String], + required: true, + }, + options: { + type: Array as PropType, + require: true, + }, + visible: { + type: Boolean, + default: false, + }, // events - onChange: IxPropTypes.emit<(value: number | string) => void>(), + onChange: [Function, Array] as PropType void>>, } export const timePanelCellProps = { - disabled: IxPropTypes.bool.def(false), - selected: IxPropTypes.bool.def(false), - value: IxPropTypes.oneOfType([Number, String]).isRequired, + disabled: { + type: Boolean, + default: false, + }, + selected: { + type: Boolean, + default: false, + }, + value: { + type: [Number, String], + required: true, + }, // events - onChange: IxPropTypes.emit<(value: number | string) => void>(), + onChange: [Function, Array] as PropType void>>, } export type BaseTimePanelProps = ExtractInnerPropTypes export type TimePanelColumnProps = ExtractInnerPropTypes diff --git a/packages/components/_private/time-panel/style/index.less b/packages/components/_private/time-panel/style/index.less index 6f8ed501b..f962959f1 100644 --- a/packages/components/_private/time-panel/style/index.less +++ b/packages/components/_private/time-panel/style/index.less @@ -16,12 +16,18 @@ ::-webkit-scrollbar-thumb { border-radius: @time-panel-scrollbar-thumb-border-radius; background-color: @time-panel-scrollbar-thumb-background-color; + border: 0; + padding: 0; + margin: 0; } ::-webkit-scrollbar-track { border-radius: 0; background: @time-panel-scrollbar-track-background; transition: @transition-all-base; + border: 0; + padding: 0; + margin: 0; } &::before { @@ -33,16 +39,16 @@ margin-top: -(@time-panel-cell-height / 2); height: @time-panel-cell-height; border-top: @time-panel-window-border-width @time-panel-window-border-style @time-panel-window-border-color; - border-bottom: @time-panel-window-border-width @time-panel-window-border-style - @time-panel-window-border-color; + border-bottom: @time-panel-window-border-width @time-panel-window-border-style @time-panel-window-border-color; + pointer-events: none; } &-column { position: relative; + z-index: 1; height: 100%; - margin: 0; - list-style-type: none; - overflow-y: hidden; + flex: 1; + overflow: hidden; &::before, &::after { @@ -50,19 +56,58 @@ display: block; width: 100%; height: calc(50% - (@time-panel-cell-height / 2)); + flex-shrink: 0; } + &-inner { + position: relative; + width: 100%; + margin: 0; + list-style-type: none; + height: auto; + } &:hover { overflow-y: scroll; + + .@{time-panel-prefix}-column-inner { + right: -(@time-panel-scrollbar-width / 2); + } + } + + &:first-child { + &:hover .@{time-panel-prefix}-column-inner { + right: 0; + } + .@{time-panel-prefix}-cell { + justify-content: flex-start; + } + } + &:last-child { margin-right: -@time-panel-scrollbar-width; + padding-right: @time-panel-scrollbar-width; + + &:hover { + padding-right: 0; + padding-left: @time-panel-scrollbar-width; + } + + .@{time-panel-prefix}-column-inner { + right: 0; + } + + .@{time-panel-prefix}-cell { + justify-content: flex-end; + } } } &-cell { + flex-shrink: 0; display: flex; align-items: center; justify-content: center; + width: 100%; height: @time-panel-cell-height; margin: 0; cursor: pointer; diff --git a/packages/components/config/src/defaultConfig.ts b/packages/components/config/src/defaultConfig.ts index 814750155..5e1045799 100644 --- a/packages/components/config/src/defaultConfig.ts +++ b/packages/components/config/src/defaultConfig.ts @@ -309,15 +309,6 @@ export const defaultConfig: GlobalConfig = { allowInput: false, format: 'HH:mm:ss', }, - timeRangePicker: { - borderless: false, - clearable: true, - clearIcon: 'close-circle', - size: 'md', - suffix: 'clock-circle', - allowInput: false, - format: 'HH:mm:ss', - }, transfer: { getKey: 'key', clearable: true, diff --git a/packages/components/config/src/types.ts b/packages/components/config/src/types.ts index 99469b971..d09670b5a 100644 --- a/packages/components/config/src/types.ts +++ b/packages/components/config/src/types.ts @@ -88,7 +88,6 @@ export interface GlobalConfig { tagGroup: TagGroupConfig textarea: TextareaConfig timePicker: TimePickerConfig - timeRangePicker: TimeRangePickerConfig transfer: TransferConfig tooltip: TooltipConfig tree: TreeConfig @@ -180,7 +179,7 @@ export interface DatePickerConfig { format?: Partial> size: FormSize suffix: string - target?: PortalTargetType + overlayContainer?: PortalTargetType } export interface DividerConfig { @@ -485,13 +484,11 @@ export interface TimePickerConfig { clearIcon: string size: FormSize suffix: string - target?: PortalTargetType + overlayContainer?: PortalTargetType allowInput: boolean | 'overlay' format: string } -export type TimeRangePickerConfig = TimePickerConfig - export interface TransferConfig { getKey: string searchable?: boolean | { source: boolean; target: boolean } diff --git a/packages/components/date-picker/__tests__/dateRangePicker.spec.ts b/packages/components/date-picker/__tests__/dateRangePicker.spec.ts index 1e4193cc0..4fee92e59 100644 --- a/packages/components/date-picker/__tests__/dateRangePicker.spec.ts +++ b/packages/components/date-picker/__tests__/dateRangePicker.spec.ts @@ -253,6 +253,7 @@ describe('DateRangePicker', () => { .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') diff --git a/packages/components/date-picker/docs/Index.zh.md b/packages/components/date-picker/docs/Index.zh.md index a6119d19a..b45afc720 100644 --- a/packages/components/date-picker/docs/Index.zh.md +++ b/packages/components/date-picker/docs/Index.zh.md @@ -26,11 +26,11 @@ order: 0 | `disabledDate` | 不可选择的日期 | `(date: Date) => boolean` | - | - | - | | `format` | 展示的格式 | `string` | - | ✅ | 默认值参见 `defaultFormat`, 更多用法参考[date-fns](https://date-fns.org/v2.27.0/docs/format) | | `overlayClassName` | 日期面板的 `class` | `string` | - | - | - | +| `overlayContainer` | 自定义浮层容器节点 | `string \| HTMLElement \| () => string \| HTMLElement` | - | ✅ | - | | `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' \| 'datetime'` | `'date'` | - | - | | `onClear` | 清除图标被点击后的回调 | `(evt: MouseEvent) => void` | - | - | - | | `onFocus` | 获取焦点后的回调 | `(evt: FocusEvent) => void` | - | - | - | @@ -79,9 +79,9 @@ const defaultFormat = { | 名称 | 说明 | 类型 | 默认值 | 全局配置 | 备注 | | --- | --- | --- | --- | --- | --- | -| `disabledHours` | 禁用部分小时选项 | `()=>number[]` | ``() => []`` | - | - | -| `disabledMinutes` | 禁用部分分钟选项 | `(selectedHour: number)=>number[]` | `() => []` | - | - | -| `disabledSeconds` | 禁用部分秒选项 | `(selectedHour: number, selectedMinute: number)=>number[]` | `() => []` | - | - | +| `disabledHours` | 禁用部分小时选项 | `(selectedAmPm: string | undefined) => number[]` | ``() => []`` | - | - | +| `disabledMinutes` | 禁用部分分钟选项 | `(selectedHour: number | undefined, selectedAmPm: string | undefined) => number[]` | `() => []` | - | - | +| `disabledSeconds` | 禁用部分秒选项 | `(selectedHour: number | undefined, selectedMinute: number | undefined, selectedAmPm: string | undefined)=>number[]` | `() => []` | - | - | | `hideDisabledOptions` | 隐藏禁止选择的options |`boolean` |`false` | - | - | | `hourStep` | 小时选项的间隔 | `number` | `1` | - | - | | `minuteStep` | 分钟选项的间隔 | `number` | `1` | - | - | @@ -166,9 +166,10 @@ const defaultFormat = { | `@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-body-padding` | `0` | - | - | | `@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-body-padding` | `0 0 @spacing-sm 0` | - | - | | `@date-range-picker-overlay-separator-width` | `@spacing-2xl` | - | - | | `@date-range-picker-overlay-separator-padding` | `1px 0 0 0` | - | - | | `@date-range-picker-overlay-separator-font-size` | `@font-size-md` | - | - | diff --git a/packages/components/date-picker/src/DatePicker.tsx b/packages/components/date-picker/src/DatePicker.tsx index 1bc1ead21..25f360b94 100644 --- a/packages/components/date-picker/src/DatePicker.tsx +++ b/packages/components/date-picker/src/DatePicker.tsx @@ -15,6 +15,7 @@ import { useControl } from './composables/useControl' import { useFormat } from './composables/useFormat' import { useInputEnableStatus } from './composables/useInputEnableStatus' import { useKeyboardEvents } from './composables/useKeyboardEvents' +import { useOverlayProps } from './composables/useOverlayProps' import { useOverlayState } from './composables/useOverlayState' import { usePickerState } from './composables/usePickerState' import Content from './content/Content' @@ -22,8 +23,6 @@ import { datePickerToken } from './token' import Trigger from './trigger/Trigger' import { datePickerProps } from './types' -const defaultOffset: [number, number] = [0, 8] - export default defineComponent({ name: 'IxDatePicker', inheritAttrs: false, @@ -49,7 +48,7 @@ export default defineComponent({ const { overlayOpened, setOverlayOpened } = useOverlayState(props, controlContext) const handleKeyDown = useKeyboardEvents(setOverlayOpened) - provide(datePickerToken, { + const context = { props, slots, locale, @@ -64,7 +63,9 @@ export default defineComponent({ controlContext, ...formatContext, ...pickerStateContext, - }) + } + + provide(datePickerToken, context) watch(overlayOpened, opened => { nextTick(() => { @@ -78,23 +79,17 @@ export default defineComponent({ }) }) - const target = computed(() => props.target ?? config.target ?? `${mergedPrefixCls.value}-overlay-container`) const renderTrigger = () => const renderContent = () => - const overlayProps = { triggerId: attrs.id, 'onUpdate:visible': setOverlayOpened } + const overlayProps = useOverlayProps(context) + const overlayClass = computed(() => normalizeClass([`${mergedPrefixCls.value}-overlay`, props.overlayClassName])) return () => ( <ɵOverlay - {...overlayProps} - visible={overlayOpened.value} + {...overlayProps.value} + class={overlayClass.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" + triggerId={attrs.id} /> ) }, diff --git a/packages/components/date-picker/src/DateRangePicker.tsx b/packages/components/date-picker/src/DateRangePicker.tsx index 013b38a4d..90eb38bcd 100644 --- a/packages/components/date-picker/src/DateRangePicker.tsx +++ b/packages/components/date-picker/src/DateRangePicker.tsx @@ -14,6 +14,7 @@ import { useFormElement } from '@idux/components/form' import { useFormat } from './composables/useFormat' import { useInputEnableStatus } from './composables/useInputEnableStatus' import { useRangeKeyboardEvents } from './composables/useKeyboardEvents' +import { useOverlayProps } from './composables/useOverlayProps' import { useOverlayState } from './composables/useOverlayState' import { usePickerState } from './composables/usePickerState' import { useRangeControl } from './composables/useRangeControl' @@ -22,8 +23,6 @@ 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, @@ -39,8 +38,6 @@ export default defineComponent({ 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) @@ -49,11 +46,11 @@ export default defineComponent({ const rangeControlContext = useRangeControl(dateConfig, formatContext, inputEnableStatus, accessor.valueRef) const { overlayOpened, setOverlayOpened } = useOverlayState(props, rangeControlContext) - const handleKeyDown = useRangeKeyboardEvents(rangeControlContext, showFooter, setOverlayOpened, handleChange) + const handleKeyDown = useRangeKeyboardEvents(rangeControlContext, setOverlayOpened, handleChange) const renderSeparator = () => slots.separator?.() ?? props.separator ?? locale.dateRangePicker.separator - provide(dateRangePickerToken, { + const context = { props, slots, locale, @@ -69,7 +66,9 @@ export default defineComponent({ handleKeyDown, ...formatContext, ...pickerStateContext, - }) + } + + provide(dateRangePickerToken, context) watch(overlayOpened, opened => { nextTick(() => { @@ -83,30 +82,18 @@ export default defineComponent({ }) }) - 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 } + const overlayProps = useOverlayProps(context) + const overlayClass = computed(() => normalizeClass([`${mergedPrefixCls.value}-overlay`, props.overlayClassName])) return () => { return ( <ɵOverlay - {...overlayProps} - visible={overlayOpened.value} + {...overlayProps.value} + class={overlayClass.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" + triggerId={attrs.id} /> ) } diff --git a/packages/components/date-picker/src/composables/useActiveDate.ts b/packages/components/date-picker/src/composables/useActiveDate.ts index 1010f93b8..bbc5d2483 100644 --- a/packages/components/date-picker/src/composables/useActiveDate.ts +++ b/packages/components/date-picker/src/composables/useActiveDate.ts @@ -59,6 +59,7 @@ export function useRangeActiveDate( props: DateRangePickerProps, valuesRef: ComputedRef<(Date | undefined)[] | undefined>, isSelecting: ComputedRef, + overlayOpened: ComputedRef, formatRef: ComputedRef, ): RangeActiveDateContext { const { set, get } = dateConfig @@ -67,6 +68,14 @@ export function useRangeActiveDate( const fromPanelValue = computed(() => valuesRef.value?.[0]) const toPanelValue = computed(() => valuesRef.value?.[1]) + const defaultOpenValue = computed(() => { + const convertedValues = sortRangeValue(dateConfig, [ + ...convertArray(props.defaultOpenValue).map(v => convertToDate(dateConfig, v, formatRef.value)), + ]) + + return [convertedValues[0] ?? now, convertedValues[1]] + }) + const calcValidActiveDate = (from: Date | undefined, to: Date | undefined, type: 'from' | 'to') => { const viewType = viewTypeMap[props.type] const viewSpan = props.type === 'year' ? 12 : 1 @@ -92,7 +101,7 @@ export function useRangeActiveDate( const fromViewYearValue = get(from, 'year') const toViewYearValue = get(to, 'year') - return fromViewValue < toViewValue || fromViewYearValue < toViewYearValue + return fromViewValue < toViewValue && fromViewYearValue <= toViewYearValue })() /* eslint-disable indent */ @@ -103,13 +112,36 @@ export function useRangeActiveDate( return valid ? to : getViewDate(from) } - const defaultOpenValue = computed(() => { - const convertedValues = sortRangeValue([ - ...convertArray(props.defaultOpenValue).map(v => convertToDate(dateConfig, v, formatRef.value)), - ]) + const initActiveDate = () => { + if (isSelecting.value) { + return + } - return [convertedValues[0] ?? now, convertedValues[1]] - }) + const fromValue = valuesRef.value?.[0] ?? defaultOpenValue.value[0]! + const toValue = valuesRef.value?.[1] ?? defaultOpenValue.value[1] + + const valueAlreadyInView = [fromValue, toValue].every(value => { + if (!value) { + return true + } + + const yearValue = get(value, 'year') + if (viewTypeMap[props.type] === 'year') { + return [fromActiveDate, toActiveDate].map(activeDate => get(activeDate.value, 'year')).includes(yearValue) + } + + const monthValue = get(value, 'month') + return [fromActiveDate, toActiveDate].some( + activeDate => yearValue === get(activeDate.value, 'year') && monthValue === get(activeDate.value, 'month'), + ) + }) + if (valueAlreadyInView) { + return + } + + setFromActiveDate(fromValue) + setToActiveDate(calcValidActiveDate(fromValue, toValue, 'to')) + } const [fromActiveDate, setFromActiveDate] = useState(fromPanelValue.value ?? defaultOpenValue.value[0]!) const [toActiveDate, setToActiveDate] = useState( @@ -119,16 +151,12 @@ export function useRangeActiveDate( '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) + watch(valuesRef, initActiveDate) + watch(overlayOpened, opened => { + if (!opened) { + initActiveDate() + } }) const handleFromActiveDateUpdate = (value: Date) => { diff --git a/packages/components/date-picker/src/composables/useControl.ts b/packages/components/date-picker/src/composables/useControl.ts index d4c881297..2361d260c 100644 --- a/packages/components/date-picker/src/composables/useControl.ts +++ b/packages/components/date-picker/src/composables/useControl.ts @@ -13,7 +13,7 @@ import { type ComputedRef, watch } from 'vue' import { useState } from '@idux/cdk/utils' -import { applyDateTime, convertToDate } from '../utils' +import { applyDateTime, convertToDate, isSameDateTime } from '../utils' export interface PickerControlContext { inputValue: ComputedRef @@ -75,15 +75,10 @@ export function useControl( return } - const { isSame, parse, format } = dateConfig + const { parse, format } = dateConfig const parsedValue = parse(dateInputValue.value, dateFormatRef.value) - if ( - force || - !isSame(parsedValue, currValue, 'year') || - !isSame(parsedValue, currValue, 'month') || - !isSame(parsedValue, currValue, 'date') - ) { + if (force || !isSameDateTime(dateConfig, parsedValue, currValue, ['year', 'month', 'date'])) { setDateInputValue(format(currValue, dateFormatRef.value)) } } @@ -93,15 +88,10 @@ export function useControl( return } - const { parse, format, isSame } = dateConfig + const { parse, format } = dateConfig const parsedValue = parse(timeInputVaue.value, timeFormatRef.value) - if ( - force || - !isSame(parsedValue, currValue, 'hour') || - !isSame(parsedValue, currValue, 'minute') || - !isSame(parsedValue, currValue, 'second') - ) { + if (force || !isSameDateTime(dateConfig, parsedValue, currValue, ['hour', 'minute', 'second'])) { setTimeInputValue(format(currValue, timeFormatRef.value)) } } diff --git a/packages/components/date-picker/src/composables/useInputProps.ts b/packages/components/date-picker/src/composables/useInputProps.ts new file mode 100644 index 000000000..9b5ce4f7b --- /dev/null +++ b/packages/components/date-picker/src/composables/useInputProps.ts @@ -0,0 +1,26 @@ +/** + * @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 { ɵInputProps } from '@idux/components/_private/input' + +import { type ComputedRef, computed } from 'vue' + +import { DatePickerContext, DateRangePickerContext } from '../token' + +export function useInputProps(context: DatePickerContext | DateRangePickerContext): ComputedRef<ɵInputProps> { + return computed(() => { + const { props, config, accessor } = context + + return { + borderless: false, + clearable: props.clearable ?? config.clearable, + clearIcon: props.clearIcon ?? config.clearIcon, + disabled: accessor.disabled.value, + size: 'sm', + } + }) +} diff --git a/packages/components/date-picker/src/composables/useKeyboardEvents.ts b/packages/components/date-picker/src/composables/useKeyboardEvents.ts index c61d44370..8d453766b 100644 --- a/packages/components/date-picker/src/composables/useKeyboardEvents.ts +++ b/packages/components/date-picker/src/composables/useKeyboardEvents.ts @@ -6,7 +6,6 @@ */ import type { PickerRangeControlContext } from './useRangeControl' -import type { ComputedRef } from 'vue' export function useKeyboardEvents(setOverlayOpened: (opened: boolean) => void): (evt: KeyboardEvent) => void { return (evt: KeyboardEvent) => { @@ -18,7 +17,6 @@ export function useKeyboardEvents(setOverlayOpened: (opened: boolean) => void): export function useRangeKeyboardEvents( rangeControl: PickerRangeControlContext, - showFooter: ComputedRef, setOverlayOpened: (opened: boolean) => void, handleChange: (value: (Date | undefined)[] | undefined) => void, ): (evt: KeyboardEvent) => void { @@ -30,7 +28,7 @@ export function useRangeKeyboardEvents( break case 'Enter': - if (!isSelecting.value && bufferUpdated.value && showFooter.value) { + if (!isSelecting.value && bufferUpdated.value) { handleChange(buffer.value) } setOverlayOpened(false) diff --git a/packages/components/date-picker/src/composables/useOverlayProps.ts b/packages/components/date-picker/src/composables/useOverlayProps.ts new file mode 100644 index 000000000..1a3d76d95 --- /dev/null +++ b/packages/components/date-picker/src/composables/useOverlayProps.ts @@ -0,0 +1,29 @@ +/** + * @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 { DatePickerContext, DateRangePickerContext } from '../token' +import type { ɵOverlayProps } from '@idux/components/_private/overlay' + +import { type ComputedRef, computed } from 'vue' + +const defaultOffset: [number, number] = [0, 8] +export function useOverlayProps(context: DatePickerContext | DateRangePickerContext): ComputedRef<ɵOverlayProps> { + return computed(() => { + const { props, config, accessor, mergedPrefixCls, overlayOpened, setOverlayOpened } = context + return { + clickOutside: true, + disabled: accessor.disabled.value || props.readonly, + offset: defaultOffset, + placement: 'bottomStart', + transitionName: 'ix-fade', + target: props.overlayContainer ?? config.overlayContainer ?? `${mergedPrefixCls.value}-overlay-container`, + trigger: 'manual', + visible: overlayOpened.value, + 'onUpdate:visible': setOverlayOpened, + } + }) +} diff --git a/packages/components/date-picker/src/composables/usePickerState.ts b/packages/components/date-picker/src/composables/usePickerState.ts index 490af65a7..ae4d4070f 100644 --- a/packages/components/date-picker/src/composables/usePickerState.ts +++ b/packages/components/date-picker/src/composables/usePickerState.ts @@ -41,7 +41,7 @@ export function usePickerState const [isFocused, setFocused] = useState(false) function handleChange(value: StateValueType) { - const newValue = (isArray(value) ? sortRangeValue(value) : value) as StateValueType + const newValue = (isArray(value) ? sortRangeValue(dateConfig, value) : value) as StateValueType let oldValue = toRaw(accessor.valueRef.value) as StateValueType oldValue = ( isArray(oldValue) diff --git a/packages/components/date-picker/src/composables/useRangeControl.ts b/packages/components/date-picker/src/composables/useRangeControl.ts index 30e70bc99..dc242746b 100644 --- a/packages/components/date-picker/src/composables/useRangeControl.ts +++ b/packages/components/date-picker/src/composables/useRangeControl.ts @@ -13,7 +13,7 @@ import { type ComputedRef, computed, watch } from 'vue' import { convertArray, useState } from '@idux/cdk/utils' -import { convertToDate, sortRangeValue } from '../utils' +import { compareDateTime, convertToDate, sortRangeValue } from '../utils' import { type PickerControlContext, useControl } from './useControl' export interface PickerRangeControlContext { @@ -45,7 +45,7 @@ export function useRangeControl( ) const [bufferUpdated, setBufferUpdated] = useState(false) const handleBufferUpdate = (values: (string | number | Date | undefined)[] | undefined) => { - setBuffer(getRangeValue(dateConfig, values, formatRef.value)) + setBuffer(sortRangeValue(dateConfig, getRangeValue(dateConfig, values, formatRef.value), 'date')) setBufferUpdated(true) } @@ -65,19 +65,33 @@ export function useRangeControl( const panelValue = computed(() => { if (isSelecting.value) { - return sortRangeValue([...convertArray(selectingDate.value)]) + return sortRangeValue(dateConfig, [...convertArray(selectingDate.value)], 'date') } - return sortRangeValue([...convertArray(buffer.value)]) + return convertArray(buffer.value) }) watch(valueRef, handleBufferUpdate) + const getValidBufferValue = (value: Date | undefined, isFrom: boolean) => { + if (isFrom) { + return compareDateTime(dateConfig, value, buffer.value?.[1], 'date') > 0 + ? [value, value] + : [value, buffer.value?.[1]] + } + + return compareDateTime(dateConfig, value, buffer.value?.[0], 'date') < 0 + ? [value, value] + : [buffer.value?.[0], value] + } + const fromControl = useControl(dateConfig, formatContext, inputEnableStatus, fromDateRef, value => { - handleBufferUpdate([value, buffer.value?.[1]]) + setBuffer(getValidBufferValue(value, true)) + setBufferUpdated(true) }) const toControl = useControl(dateConfig, formatContext, inputEnableStatus, toDateRef, value => { - handleBufferUpdate([buffer.value?.[0], value]) + setBuffer(getValidBufferValue(value, false)) + setBufferUpdated(true) }) watch(fromControl.visiblePanel, handleVisiblePanelUpdate) diff --git a/packages/components/date-picker/src/composables/useTimePanelProps.ts b/packages/components/date-picker/src/composables/useTimePanelProps.ts new file mode 100644 index 000000000..06dee6594 --- /dev/null +++ b/packages/components/date-picker/src/composables/useTimePanelProps.ts @@ -0,0 +1,54 @@ +/** + * @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, TimePanelOptions } from '../types' +import type { ɵTimePanelProps } from '@idux/components/_private/time-panel' + +import { type ComputedRef, computed } from 'vue' + +import { isArray } from 'lodash-es' + +export function useTimePanelProps( + props: DatePickerProps, + timeFormatRef: ComputedRef, +): ComputedRef<ɵTimePanelProps> { + return computed(() => getTimePanelProps(props.timePanelOptions ?? {}, timeFormatRef)) +} + +export function useRangeTimePanelProps( + props: DateRangePickerProps, + timeFormatRef: ComputedRef, +): ComputedRef<ɵTimePanelProps[]> { + const getOptions = (isFrom: boolean) => + (isArray(props.timePanelOptions) ? props.timePanelOptions[isFrom ? 0 : 1] : props.timePanelOptions) ?? {} + + const rangeTimePanelProps = computed(() => [ + getTimePanelProps(getOptions(true), timeFormatRef), + getTimePanelProps(getOptions(false), timeFormatRef), + ]) + + return rangeTimePanelProps +} + +function getTimePanelProps(timePanelOptions: TimePanelOptions, timeFormatRef: ComputedRef): ɵTimePanelProps { + const { disabledHours, disabledMinutes, disabledSeconds, hideDisabledOptions, hourStep, minuteStep, secondStep } = + timePanelOptions + + return { + disabledHours, + disabledMinutes, + disabledSeconds, + hideDisabledOptions, + hourStep, + minuteStep, + secondStep, + hourEnabled: /[hH]/.test(timeFormatRef.value), + minuteEnabled: /m/.test(timeFormatRef.value), + secondEnabled: /s/.test(timeFormatRef.value), + use12Hours: /[aA]/.test(timeFormatRef.value), + } +} diff --git a/packages/components/date-picker/src/composables/useTriggerProps.ts b/packages/components/date-picker/src/composables/useTriggerProps.ts index e653b0760..c02278e5e 100644 --- a/packages/components/date-picker/src/composables/useTriggerProps.ts +++ b/packages/components/date-picker/src/composables/useTriggerProps.ts @@ -5,14 +5,14 @@ * found in the LICENSE file at https://github.com/IDuxFE/idux/blob/main/LICENSE */ -import type { DataPickerContext, DateRangePickerContext } from '../token' +import type { DatePickerContext, 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, + context: DatePickerContext | DateRangePickerContext, formContext: FormContext | null, ): ComputedRef<ɵTriggerProps> { const { @@ -41,7 +41,11 @@ export function useTriggerProps( return computed(() => { return { borderless: props.borderless, - clearable: !accessor.disabled.value && props.clearable && !!accessor.valueRef.value, + clearable: + !props.readonly && + !accessor.disabled.value && + (props.clearable ?? config.clearable) && + !!accessor.valueRef.value, clearIcon: props.clearIcon ?? config.clearIcon, disabled: accessor.disabled.value, focused: isFocused.value, diff --git a/packages/components/date-picker/src/content/Content.tsx b/packages/components/date-picker/src/content/Content.tsx index 7589a6626..b16ed0bab 100644 --- a/packages/components/date-picker/src/content/Content.tsx +++ b/packages/components/date-picker/src/content/Content.tsx @@ -13,6 +13,8 @@ import { ɵInput, type ɵInputInstance } from '@idux/components/_private/input' import { ɵTimePanel } from '@idux/components/_private/time-panel' import { useActiveDate } from '../composables/useActiveDate' +import { useInputProps } from '../composables/useInputProps' +import { useTimePanelProps } from '../composables/useTimePanelProps' import { datePickerToken } from '../token' export default defineComponent({ @@ -20,7 +22,6 @@ export default defineComponent({ const context = inject(datePickerToken)! const { props, - config, dateConfig, mergedPrefixCls, formatRef, @@ -86,6 +87,9 @@ export default defineComponent({ handleDateInputClear() } + const inputProps = useInputProps(context) + const timePanelProps = useTimePanelProps(props, timeFormatRef) + const renderInputs = (prefixCls: string) => { if (!inputEnableStatus.value.enableOverlayDateInput) { return @@ -95,12 +99,10 @@ export default defineComponent({
    <ɵInput ref={inputInstance} + {...inputProps.value} 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} focused={dateInputFocused.value} placeholder={dateFormatRef.value} @@ -112,12 +114,10 @@ export default defineComponent({ /> {inputEnableStatus.value.enableOverlayTimeInput && ( <ɵInput + {...inputProps.value} 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} focused={timeInputFocused.value} @@ -147,33 +147,29 @@ export default defineComponent({ onCellClick: handleDatePanelCellClick, 'onUpdate:activeDate': setActiveDate, } + const _timePanelProps = { + ...timePanelProps.value, + activeValue: activeDate.value, + value: panelValue.value, + visible: visiblePanel.value === 'timePanel', + onChange: handleTimePanelChange, + 'onUpdate:activeValue': setActiveDate, + } 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} - /> + <ɵTimePanel v-show={visiblePanel.value === 'timePanel'} {..._timePanelProps} /> )}
    , <ɵFooter v-slots={slots} class={`${prefixCls}-footer`} footer={props.footer} />, ] - return props.overlayRender ? ( - props.overlayRender(children) - ) : ( -
    - {children} -
    - ) + 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 index 65594113b..2448ef18d 100644 --- a/packages/components/date-picker/src/content/RangeContent.tsx +++ b/packages/components/date-picker/src/content/RangeContent.tsx @@ -5,15 +5,16 @@ * found in the LICENSE file at https://github.com/IDuxFE/idux/blob/main/LICENSE */ -import { computed, defineComponent, inject, onMounted, onUpdated, ref } from 'vue' +import { 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 { useInputProps } from '../composables/useInputProps' +import { useRangeTimePanelProps } from '../composables/useTimePanelProps' import { dateRangePickerToken } from '../token' export default defineComponent({ @@ -21,7 +22,6 @@ export default defineComponent({ const context = inject(dateRangePickerToken)! const { props, - config, dateConfig, locale, slots, @@ -48,12 +48,12 @@ export default defineComponent({ setOverlayOpened, } = context - const timePanelProps = computed(() => convertArray(context.props.timePanelOptions)) const { fromActiveDate, toActiveDate, setFromActiveDate, setToActiveDate } = useRangeActiveDate( dateConfig, props, panelValue, isSelecting, + overlayOpened, formatRef, ) @@ -76,8 +76,12 @@ export default defineComponent({ } } - const renderBoard = (prefixCls: string, isFrom: boolean) => { + const inputProps = useInputProps(context) + const timePanelProps = useRangeTimePanelProps(props, timeFormatRef) + + const renderBoard = (isFrom: boolean) => { const { enableOverlayDateInput, enableOverlayTimeInput } = inputEnableStatus.value + const boardPrefixCls = `${mergedPrefixCls.value}-board` const { dateInputValue, @@ -96,15 +100,13 @@ export default defineComponent({ } = isFrom ? fromControl : toControl const inputs = enableOverlayDateInput && ( -
    +
    <ɵInput ref={isFrom ? inputInstance : undefined} - class={`${prefixCls}-date-input`} + {...inputProps.value} + class={`${boardPrefixCls}-date-input`} v-slots={slots} value={dateInputValue.value} - size="sm" - clearable={props.clearable ?? config.clearable} - clearIcon={props.clearIcon ?? config.clearIcon} clearVisible={!!dateInputValue.value} focused={dateInputFocused.value} placeholder={dateFormatRef.value} @@ -116,12 +118,10 @@ export default defineComponent({ /> {enableOverlayTimeInput && ( <ɵInput - class={`${prefixCls}-time-input`} + {...inputProps.value} + class={`${boardPrefixCls}-time-input`} v-slots={slots} value={timeInputVaue.value} - size="sm" - clearable={props.clearable ?? config.clearable} - clearIcon={props.clearIcon ?? config.clearIcon} clearVisible={!!timeInputVaue.value} focused={timeInputFocused.value} placeholder={timeFormatRef.value} @@ -135,31 +135,35 @@ export default defineComponent({
    ) + const timeValue = panelValue.value?.[isFrom ? 0 : 1] + const activeDate = isFrom ? fromActiveDate.value : toActiveDate.value 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, + activeDate, onCellClick: handleDatePanelCellClick, onCellMouseenter: handleDatePanelCellMouseenter, 'onUpdate:activeDate': isFrom ? setFromActiveDate : setToActiveDate, } + const _timePanelProps = { + ...timePanelProps.value[isFrom ? 0 : 1], + activeValue: timeValue ?? activeDate, + value: timeValue, + visible: visiblePanel.value === 'timePanel', + onChange: handleTimePanelChange, + 'onUpdate:activeValue': 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} - /> + <ɵTimePanel v-show={visiblePanel.value === 'timePanel'} {..._timePanelProps} /> )}
    @@ -168,15 +172,14 @@ export default defineComponent({ return () => { const prefixCls = `${mergedPrefixCls.value}-overlay` - const boardPrefixCls = `${mergedPrefixCls.value}-board` const children = [ -
    - {renderBoard(boardPrefixCls, true)} +
    + {renderBoard(true)}
    {inputEnableStatus.value.enableOverlayDateInput && renderSeparator()}
    - {renderBoard(boardPrefixCls, false)} + {renderBoard(false)}
    , <ɵFooter v-slots={slots} @@ -189,13 +192,7 @@ export default defineComponent({ />, ] - return props.overlayRender ? ( - props.overlayRender(children) - ) : ( -
    - {children} -
    - ) + 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 d20053659..79df89eb2 100644 --- a/packages/components/date-picker/src/token.ts +++ b/packages/components/date-picker/src/token.ts @@ -20,7 +20,7 @@ import type { DateConfig, DatePickerConfig } from '@idux/components/config' import type { Locale } from '@idux/components/locales' import type { ComputedRef, InjectionKey, Ref, Slots, VNodeTypes } from 'vue' -export interface DataPickerContext extends OverlayStateContext, FormatContext, PickerStateContext { +export interface DatePickerContext extends OverlayStateContext, FormatContext, PickerStateContext { props: DatePickerProps slots: Slots locale: Locale @@ -50,5 +50,5 @@ export interface DateRangePickerContext handleKeyDown: (evt: KeyboardEvent) => void } -export const datePickerToken: InjectionKey = Symbol('datePickerToken') +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 index 1fab0fad3..0caf99aa9 100644 --- a/packages/components/date-picker/src/trigger/RangeTrigger.tsx +++ b/packages/components/date-picker/src/trigger/RangeTrigger.tsx @@ -15,7 +15,8 @@ import { useTriggerProps } from '../composables/useTriggerProps' import { dateRangePickerToken } from '../token' export default defineComponent({ - setup() { + inheritAttrs: false, + setup(_, { attrs }) { const context = inject(dateRangePickerToken)! const { props, @@ -85,7 +86,7 @@ export default defineComponent({ clearIcon: slots.clearIcon, } - return <ɵTrigger className={prefixCls} v-slots={triggerSlots} {...triggerProps.value} /> + return <ɵTrigger className={prefixCls} v-slots={triggerSlots} {...triggerProps.value} {...attrs} /> } }, }) diff --git a/packages/components/date-picker/src/trigger/Trigger.tsx b/packages/components/date-picker/src/trigger/Trigger.tsx index e22c5e949..1205b7eef 100644 --- a/packages/components/date-picker/src/trigger/Trigger.tsx +++ b/packages/components/date-picker/src/trigger/Trigger.tsx @@ -15,7 +15,8 @@ import { useTriggerProps } from '../composables/useTriggerProps' import { datePickerToken } from '../token' export default defineComponent({ - setup() { + inheritAttrs: false, + setup(_, { attrs }) { const context = inject(datePickerToken)! const { props, @@ -67,7 +68,7 @@ export default defineComponent({ clearIcon: slots.clearIcon, } - return <ɵTrigger className={prefixCls} v-slots={triggerSlots} {...triggerProps.value} /> + return <ɵTrigger className={prefixCls} v-slots={triggerSlots} {...triggerProps.value} {...attrs} /> } }, }) diff --git a/packages/components/date-picker/src/types.ts b/packages/components/date-picker/src/types.ts index bf46b2836..682eb410f 100644 --- a/packages/components/date-picker/src/types.ts +++ b/packages/components/date-picker/src/types.ts @@ -14,9 +14,13 @@ import { ɵPortalTargetDef } from '@idux/cdk/portal' 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[] + disabledHours?: (selectedAmPm: string | undefined) => number[] + disabledMinutes?: (selectedHour: number | undefined, selectedAmPm: string | undefined) => number[] + disabledSeconds?: ( + selectedHour: number | undefined, + selectedMinute: number | undefined, + selectedAmPm: string | undefined, + ) => number[] hideDisabledOptions?: boolean hourStep?: number minuteStep?: number @@ -61,8 +65,7 @@ const datePickerCommonProps = { dateFormat: String, timeFormat: String, overlayClassName: String, - // overlayContainer: IxPropTypes.oneOfType([String, HTMLElement, IxPropTypes.func<() => string | HTMLElement>()]), - overlayContainer: [String, HTMLElement, Function] as PropType string | HTMLElement)>, + overlayContainer: ɵPortalTargetDef, overlayRender: Function as PropType<(children: VNode[]) => VNodeChild>, readonly: { type: Boolean as PropType, @@ -74,7 +77,6 @@ const datePickerCommonProps = { type: String as PropType, default: 'date', }, - target: ɵPortalTargetDef, // events 'onUpdate:open': [Function, Array] as PropType void>>, diff --git a/packages/components/date-picker/src/utils.ts b/packages/components/date-picker/src/utils.ts index 5f0cec17c..3331c88f2 100644 --- a/packages/components/date-picker/src/utils.ts +++ b/packages/components/date-picker/src/utils.ts @@ -32,16 +32,42 @@ export function applyDateTime( 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 - } +export function isSameDateTime( + dateConfig: DateConfig, + sourceDate: Date, + targetDate: Date, + types: DateConfigType | TimeConfigType | (DateConfigType | TimeConfigType)[], +): boolean { + const typesArray = convertArray(types) + + return typesArray.every(type => dateConfig.get(sourceDate, type) === dateConfig.get(targetDate, type)) +} + +export function compareDateTime( + dateConfig: DateConfig, + v1: Date | undefined, + v2: Date | undefined, + type: DateConfigType | 'time' = 'time', +): number { + if (!v1) { + return 1 + } - if (!v2) { - return 0 - } + if (!v2) { + return 0 + } + if (type === 'time') { return v1.valueOf() - v2.valueOf() - }) + } + + return dateConfig.startOf(v1, type).valueOf() - dateConfig.startOf(v2, type).valueOf() +} + +export function sortRangeValue( + dateConfig: DateConfig, + values: (Date | undefined)[], + type: DateConfigType | 'time' = 'time', +): (Date | undefined)[] { + return values.sort((v1, v2) => compareDateTime(dateConfig, v1, v2, type)) } diff --git a/packages/components/date-picker/style/index.less b/packages/components/date-picker/style/index.less index 04de99118..0a981dc1d 100644 --- a/packages/components/date-picker/style/index.less +++ b/packages/components/date-picker/style/index.less @@ -1,82 +1,23 @@ @import '../../style/mixins/borderless.less'; @import '../../style/mixins/placeholder.less'; @import '../../style/mixins/reset.less'; +@import './mixin.less'; @import './panel.less'; -.@{date-picker-prefix}, -.@{date-range-picker-prefix} { - display: inline-flex; - width: 100%; - line-height: @date-picker-line-height; - - &:not(.@{trigger-prefix}-disabled) { - color: @date-picker-color; - } - - &-input { - width: 100%; - &-inner { - display: inline-block; - width: 100%; - min-width: 0; - } - } - - &-overlay { - z-index: @date-picker-overlay-zindex; - 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; - color: @date-picker-color; - - & .@{input-prefix}-focused { - box-shadow: none; - } - } - & &-date-input { - width: @date-picker-overlay-date-input-width; - input { - color: @date-picker-color; - } - } - & &-date-input:only-child { - width: 100%; - } - & &-time-input { - width: @date-picker-overlay-time-input-width; - margin-left: @date-picker-overlay-input-gap; - input { - color: @date-picker-color; - } - } - - &-panel { - max-height: @date-range-picker-panel-max-height; - .@{time-panel-prefix} { - height: @date-range-picker-panel-max-height; - } - } - } -} - .@{date-picker-prefix} { + .date-picker-trigger(); + .date-picker-board(); + &-overlay { width: @date-picker-overlay-width; padding: @date-picker-overlay-padding; .date-picker-panel(); + &-body { + padding: @date-picker-overlay-body-padding; + } + &-footer { text-align: end; border-top: @date-picker-overlay-footer-border-width @date-picker-overlay-footer-border-style @@ -91,6 +32,9 @@ } .@{date-range-picker-prefix} { + .date-picker-trigger(); + .date-picker-board(); + &-input { display: flex; align-items: center; @@ -102,9 +46,10 @@ &-overlay { padding: @date-range-picker-overlay-padding; .date-picker-panel(); - &-content { + + &-body { display: flex; - padding: @date-range-picker-overlay-content-padding; + padding: @date-range-picker-overlay-body-padding; } &-separator { display: flex; diff --git a/packages/components/date-picker/style/mixin.less b/packages/components/date-picker/style/mixin.less new file mode 100644 index 000000000..d191c4313 --- /dev/null +++ b/packages/components/date-picker/style/mixin.less @@ -0,0 +1,64 @@ +.date-picker-trigger() { + display: inline-flex; + width: 100%; + line-height: @date-picker-line-height; + + &:not(.@{trigger-prefix}-disabled) { + color: @date-picker-color; + } + + &-input { + width: 100%; + &-inner { + display: inline-block; + width: 100%; + min-width: 0; + } + } + + &-overlay { + z-index: @date-picker-overlay-zindex; + border-radius: @date-picker-overlay-border-radius; + box-shadow: @date-picker-overlay-box-shadow; + background-color: @date-picker-panel-background-color; + + &-body { + outline: none; + } + } +} + +.date-picker-board() { + &-board { + & &-inputs { + display: flex; + justify-content: center; + margin-bottom: @date-picker-overlay-inputs-margin-bottom; + + input { + color: @date-picker-color; + } + + .@{input-prefix}-focused { + box-shadow: none; + } + } + & &-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; + } + } + } +} \ No newline at end of file diff --git a/packages/components/date-picker/style/panel.less b/packages/components/date-picker/style/panel.less index 061662113..401576776 100644 --- a/packages/components/date-picker/style/panel.less +++ b/packages/components/date-picker/style/panel.less @@ -234,7 +234,6 @@ 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; diff --git a/packages/components/date-picker/style/themes/default.variable.less b/packages/components/date-picker/style/themes/default.variable.less index b8f65b77b..b256b451b 100644 --- a/packages/components/date-picker/style/themes/default.variable.less +++ b/packages/components/date-picker/style/themes/default.variable.less @@ -73,10 +73,11 @@ @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-body-padding: 0; @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-body-padding: 0 0 @spacing-sm 0; @date-range-picker-overlay-separator-width: @spacing-2xl; @date-range-picker-overlay-separator-padding: 1px 0 0 0; @date-range-picker-overlay-separator-font-size: @font-size-md; diff --git a/packages/components/time-picker/demo/Borderless.vue b/packages/components/time-picker/demo/Borderless.vue index 8819c27ff..5fdd87375 100644 --- a/packages/components/time-picker/demo/Borderless.vue +++ b/packages/components/time-picker/demo/Borderless.vue @@ -5,6 +5,6 @@ diff --git a/packages/components/time-picker/docs/Index.zh.md b/packages/components/time-picker/docs/Index.zh.md index 183c4a88b..1b418d383 100644 --- a/packages/components/time-picker/docs/Index.zh.md +++ b/packages/components/time-picker/docs/Index.zh.md @@ -28,9 +28,9 @@ order: 0 | `clearIcon` | 清除按钮图标 |`string \| #clearIcon` | `close-circle` | ✅ | - | | `clearText` | hover到clearIcon上,显示的title |`string` | clear | ✅ | - | | `size` | 尺寸大小 | `lg \| md \| sm` | `md` | ✅ | - | - | `disabledHours` | 禁用部分小时选项 | `()=>number[]` | ``() => []`` | - | - | - | `disabledMinutes` | 禁用部分分钟选项 | `(selectedHour: number)=>number[]` | `() => []` | - | - | - | `disabledSeconds` | 禁用部分秒选项 | `(selectedHour: number, selectedMinute: number)=>number[]` | `() => []` | - | - | + | `disabledHours` | 禁用部分小时选项 | `(selectedAmPm: string | undefined) => number[]` | ``() => []`` | - | - | + | `disabledMinutes` | 禁用部分分钟选项 | `(selectedHour: number | undefined, selectedAmPm: string | undefined) => number[]` | `() => []` | - | - | + | `disabledSeconds` | 禁用部分秒选项 | `(selectedHour: number | undefined, selectedMinute: number | undefined, selectedAmPm: string | undefined)=>number[]` | `() => []` | - | - | | `hideDisabledOptions` | 隐藏禁止选择的options |`boolean` |`false` | - | - | | `hourStep` | 小时选项的间隔 | `number` | `1` | - | - | | `minuteStep` | 分钟选项的间隔 | `number` | `1` | - | - | @@ -57,19 +57,19 @@ order: 0 | `clearable` | 是否展示清除按钮 |`boolean` |`true` | ✅ | - | | `borderless` | 是否为无边框 |`boolean` |`false` | ✅ | - | | `suffix` | 后缀图标 |`string \| #suffix` | `clock-circle` | ✅ | - | - | `target` | 自定义浮层容器节点 | `string \| HTMLElement \| () => string \| HTMLElement` | - | ✅ | - | | `clearIcon` | 清除按钮图标 |`string \| #clearIcon` | `close-circle` | ✅ | - | | `clearText` | hover到clearIcon上,显示的title |`string` | clear | ✅ | - | | `size` | 尺寸大小 | `lg \| md \| sm` | `md` | ✅ | - | - | `disabledHours` | 禁用部分小时选项 | `()=>number[]` | ``() => []`` | - | - | - | `disabledMinutes` | 禁用部分分钟选项 | `(selectedHour: number)=>number[]` | `() => []` | - | - | - | `disabledSeconds` | 禁用部分秒选项 | `(selectedHour: number, selectedMinute: number)=>number[]` | `() => []` | - | - | + | `disabledHours` | 禁用部分小时选项 | `(selectedAmPm: string | undefined) => number[]` | ``() => []`` | - | - | + | `disabledMinutes` | 禁用部分分钟选项 | `(selectedHour: number | undefined, selectedAmPm: string | undefined) => number[]` | `() => []` | - | - | + | `disabledSeconds` | 禁用部分秒选项 | `(selectedHour: number | undefined, selectedMinute: number | undefined, selectedAmPm: string | undefined)=>number[]` | `() => []` | - | - | | `hideDisabledOptions` | 隐藏禁止选择的options |`boolean` |`false` | - | - | | `hourStep` | 小时选项的间隔 | `number` | `1` | - | - | | `minuteStep` | 分钟选项的间隔 | `number` | `1` | - | - | | `secondStep` | 秒选项的间隔 | `number` | `1` | - | - | | `defaultOpenValue` | 打开面板时默认高亮的值 | `[Date \| string \| number, Date \| string \| number]` | - | - | 如果value不为空,则高亮value的值 | | `overlayClassName` | 浮层的类名 |`string` | - | - | - | + | `overlayContainer` | 自定义浮层容器节点 | `string \| HTMLElement \| () => string \| HTMLElement` | - | ✅ | - | | `onChange` | 时间选择回调函数 |`(value: [Date \| undefined, Date \| undefined]) => void` | - | - | - | | `onClear` | 清除事件回调函数 |`(evt: MouseEvent) => void` | - | - | - | | `onFocus` | focus事件回调函数 |`(evt: FocusEvent) => void` | - | - | - | @@ -80,52 +80,35 @@ order: 0 | 名称 | default | seer | 备注 | | --- | --- | --- | --- | -| `@time-picker-font-size-sm` | `@form-font-size-sm` | - | - | -| `@time-picker-font-size-md` | `@form-font-size-md` | - | - | -| `@time-picker-font-size-lg` | `@form-font-size-lg` | - | - | -| `@time-picker-line-height` | `@form-line-height` | - | - | -| `@time-picker-height-sm` | `@form-height-sm` | - | - | -| `@time-picker-height-md` | `@form-height-md` | - | - | -| `@time-picker-height-lg` | `@form-height-lg` | - | - | -| `@time-picker-padding-horizontal-sm` | `@form-padding-horizontal-sm` | - | - | -| `@time-picker-padding-horizontal-md` | `@form-padding-horizontal-md` | - | - | -| `@time-picker-padding-horizontal-lg` | `@form-padding-horizontal-lg` | - | - | -| `@time-picker-padding-vertical-sm` | `@form-padding-vertical-sm` | - | - | -| `@time-picker-padding-vertical-md` | `@form-padding-vertical-md` | - | - | -| `@time-picker-padding-vertical-lg` | `@form-padding-vertical-lg` | - | - | -| `@time-picker-border-width` | `@form-border-width` | - | - | -| `@time-picker-border-style` | `@form-border-style` | - | - | -| `@time-picker-border-color` | `@form-border-color` | `@color-graphite-l30` | - | -| `@time-picker-border-radius` | `@border-radius-sm` | - | - | | `@time-picker-color` | `@form-color` | - | - | -| `@time-picker-disabled-color` | `@form-disabled-color` | - | - | +| `@time-picker-line-height` | `@form-line-height` | - | - | +| `@time-range-picker-trigger-separator-margin` | `@spacing-xl` | - | - | +| `@time-picker-overlay-zindex` | `@zindex-l4-3` | - | - | | `@time-picker-overlay-width` | `200px` | - | - | -| `@time-picker-overlay-padding` | `@spacing-sm` | - | - | +| `@time-picker-overlay-padding` | `@spacing-sm @spacing-sm 0 @spacing-sm` | - | - | +| `@time-picker-overlay-body-padding` | `0 0 @spacing-sm 0` | - | - | | `@time-picker-overlay-box-shadow` | `@shadow-bottom-md` | - | - | -| `@time-picker-overlay-font-size` | `@font-size-md` | `@font-size-sm` | - | | `@time-picker-overlay-background-color` | `@form-background-color` | - | - | -| `@time-picker-footer-padding` | `@spacing-sm 0` | - | - | -| `@time-picker-footer-margin` | `0 @spacing-lg` | - | - | -| `@time-range-picker-overlay-padding` | `@spacing-lg` | - | - | -| `@time-range-picker-trigger-separator-margin` | `@spacing-xl` | - | - | -| `@time-range-picker-overlay-side-width` | `184px` | - | - | -| `@time-range-picker-overlay-gap-padding` | `5px 8px` | `2px 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` | `@time-picker-border-color` | - | - | -| `@time-range-picker-panel-border-radius` | `@time-picker-border-radius` | - | - | -| `@time-picker-input-margin` | `@spacing-sm` | - | - | -| `@time-picker-color-secondary` | `@form-color-secondary` | - | - | -| `@time-picker-background-color` | `@form-background-color` | - | - | -| `@time-picker-placeholder-color` | `@form-placeholder-color` | - | - | -| `@time-picker-hover-color` | `@form-hover-color` | - | - | -| `@time-picker-active-color` | `@form-active-color` | - | - | -| `@time-picker-active-box-shadow` | `@form-active-box-shadow` | - | - | -| `@time-picker-disabled-background-color` | `@form-disabled-background-color` | - | - | -| `@time-picker-icon-font-size` | `@font-size-sm` | `@font-size-lg` | - | -| `@time-picker-icon-margin-left` | `@spacing-xs` | - | - | -| `@time-picker-icon-margin-right` | `@spacing-xs` | - | - | -| `@time-picker-icon-color` | `@time-picker-placeholder-color` | - | - | -| `@time-picker-clear-icon-color` | `@time-picker-color-secondary` | - | - | -| `@time-picker-icon-background-color` | `@time-picker-background-color` | - | - | - +| `@time-picker-overlay-input-margin-bottom` | `@spacing-sm` | - | - | +| `@time-range-picker-overlay-padding` | `@spacing-lg @spacing-lg 0 @spacing-lg` | - | - | +| `@time-range-picker-overlay-body-padding` | `0 0 @spacing-lg 0` | - | - | +| `@time-range-picker-overlay-board-width` | `184px` | - | - | +| `@time-range-picker-overlay-separator-width` | `@spacing-2xl` | - | - | +| `@time-range-picker-overlay-separator-padding` | `1px 0 0 0` | `2px 8px` | - | +| `@time-range-picker-overlay-separator-font-size` | `@font-size-md` | `@font-size-sm` | - | +| `@time-range-picker-board-width` | `184px` | - | - | +| `@time-range-picker-board-panel-border-width` | `@form-border-width` | - | - | +| `@time-range-picker-board-panel-border-style` | `@form-border-style` | - | - | +| `@time-range-picker-board-panel-border-color` | `@form-border-color` | - | - | +| `@time-range-picker-board-panel-border-radius` | `@border-radius-sm` | - | - | +| `@time-picker-overlay-footer-border-width` | `@form-border-width` | - | - | +| `@time-picker-overlay-footer-border-style` | `@form-border-style` | - | - | +| `@time-picker-overlay-footer-border-color` | `@form-border-color` | - | - | +| `@time-picker-overlay-footer-padding` | `@spacing-sm @spacing-lg` | - | - | +| `@time-picker-overlay-footer-button-margin-left` | `@spacing-sm` | - | - | +| `@time-range-picker-overlay-footer-border-width` | `@time-picker-overlay-footer-border-width` | - | - | +| `@time-range-picker-overlay-footer-border-style` | `@time-picker-overlay-footer-border-style` | - | - | +| `@time-range-picker-overlay-footer-border-color` | `@color-graphite-l30` | - | - | +| `@time-range-picker-overlay-footer-padding` | `@spacing-sm 0` | - | - | +| `@time-range-picker-overlay-footer-button-margin-left` | `@time-picker-overlay-footer-button-margin-left` | - | - | + \ No newline at end of file diff --git a/packages/components/time-picker/src/TimePicker.tsx b/packages/components/time-picker/src/TimePicker.tsx index e66df94ad..7447588fe 100644 --- a/packages/components/time-picker/src/TimePicker.tsx +++ b/packages/components/time-picker/src/TimePicker.tsx @@ -5,97 +5,92 @@ * found in the LICENSE file at https://github.com/IDuxFE/idux/blob/main/LICENSE */ -import { computed, defineComponent, inject, provide } from 'vue' +import { computed, defineComponent, inject, nextTick, normalizeClass, provide, watch } from 'vue' -import { useControlledProp } from '@idux/cdk/utils' import { ɵOverlay } from '@idux/components/_private/overlay' import { useDateConfig, useGlobalConfig } from '@idux/components/config' -import { FORM_TOKEN } from '@idux/components/form' +import { FORM_TOKEN, useFormElement } from '@idux/components/form' +import { usePickerControl } from './composables/useControl' import { useInputEnableStatus } from './composables/useInputEnableStatus' -import { usePickerControl } from './composables/usePickerControl' -import { useCommonOverlayProps } from './composables/useProps' -import { useTimePickerCommonBindings } from './composables/useTimePickerCommonBindings' -import Overlay from './overlay/Overlay' -import { timePickerContext, timePickerControl } from './tokens' +import { useKeyboardEvents } from './composables/useKeyboardEvents' +import { useOverlayProps } from './composables/useOverlayProps' +import { useOverlayState } from './composables/useOverlayState' +import { usePickerState } from './composables/usePickerState' +import Content from './content/Content' +import { timePickerContext } from './tokens' import Trigger from './trigger/Trigger' import { timePickerProps } from './types' -import { convertToDate } from './utils' export default defineComponent({ name: 'IxTimePicker', inheritAttrs: false, props: timePickerProps, - setup(props, { attrs, slots }) { + setup(props, { attrs, expose, slots }) { const common = useGlobalConfig('common') const mergedPrefixCls = computed(() => `${common.prefixCls}-time-picker`) const locale = useGlobalConfig('locale') const config = useGlobalConfig('timePicker') const dateConfig = useDateConfig() - const { isValid, parse } = dateConfig - const [visibility, setVisibility] = useControlledProp(props, 'open', false) - const format = computed(() => props.format ?? config.format) - const commonBindings = useTimePickerCommonBindings(props) - const { accessor, isDisabled, handleChange } = commonBindings - const pickerControl = usePickerControl( - accessor.valueRef, - dateConfig, - format, - [], - (value: string) => !value || isValid(parse(value, format.value)), - handleChange, - ) - const { init } = pickerControl - - const inputEnableStatus = useInputEnableStatus(props, config) + const { elementRef: inputRef, focus, blur } = useFormElement() - const changeVisible = (visible: boolean) => { - setVisibility(visible) - if (!visible) { - init() - } - } + const formatRef = computed(() => props.format ?? config.format) + const pickerState = usePickerState(props, dateConfig, formatRef) + const { accessor, handleChange } = pickerState + const pickerControl = usePickerControl(dateConfig, formatRef, handleChange, accessor.valueRef) + const { overlayOpened, setOverlayOpened } = useOverlayState(props, pickerControl) + const inputEnableStatus = useInputEnableStatus(props, config) const formContext = inject(FORM_TOKEN, null) + const handleKeyDown = useKeyboardEvents(setOverlayOpened) - provide(timePickerControl, pickerControl) - provide(timePickerContext, { + const context = { props, slots, dateConfig, locale, config, mergedPrefixCls, - format, + formatRef, formContext, - overlayOpened: visibility, + handleKeyDown, + inputRef, inputEnableStatus, - commonBindings, - setOverlayOpened: changeVisible, + overlayOpened, + setOverlayOpened, + controlContext: pickerControl, + ...pickerState, + } + provide(timePickerContext, context) + + expose({ focus, blur }) + + watch(overlayOpened, opened => { + nextTick(() => { + if (opened) { + focus() + inputRef.value?.dispatchEvent(new FocusEvent('focus')) + } else { + blur() + inputRef.value?.dispatchEvent(new FocusEvent('blur')) + } + }) }) - const overlayProps = useCommonOverlayProps(props, config, mergedPrefixCls, changeVisible) + const renderTrigger = () => + const renderContent = () => - return () => { - const renderTrigger = () => ( - - ) - const renderContent = () => + const overlayProps = useOverlayProps(context) + const overlayClass = computed(() => normalizeClass([`${mergedPrefixCls.value}-overlay`, props.overlayClassName])) - return ( - <ɵOverlay - {...overlayProps.value} - triggerId={attrs.id} - visible={visibility.value} - v-slots={{ default: renderTrigger, content: renderContent }} - disabled={isDisabled.value || props.readonly} - /> - ) - } + return () => ( + <ɵOverlay + {...overlayProps.value} + class={overlayClass.value} + triggerId={attrs.id} + v-slots={{ default: renderTrigger, content: renderContent }} + /> + ) }, }) diff --git a/packages/components/time-picker/src/TimeRangePicker.tsx b/packages/components/time-picker/src/TimeRangePicker.tsx index 1bc4ceab2..7bc4b7956 100644 --- a/packages/components/time-picker/src/TimeRangePicker.tsx +++ b/packages/components/time-picker/src/TimeRangePicker.tsx @@ -5,111 +5,98 @@ * found in the LICENSE file at https://github.com/IDuxFE/idux/blob/main/LICENSE */ -import type { ComputedRef } from 'vue' +import { computed, defineComponent, inject, nextTick, normalizeClass, provide, watch } from 'vue' -import { computed, defineComponent, inject, provide, watch } from 'vue' - -import { isArray } from 'lodash-es' - -import { useControlledProp, useState } from '@idux/cdk/utils' import { ɵOverlay } from '@idux/components/_private/overlay' import { useDateConfig, useGlobalConfig } from '@idux/components/config' -import { FORM_TOKEN } from '@idux/components/form' +import { FORM_TOKEN, useFormElement } from '@idux/components/form' import { useInputEnableStatus } from './composables/useInputEnableStatus' -import { useRangePickerControl } from './composables/usePickerControl' -import { useCommonOverlayProps } from './composables/useProps' -import { useTimePickerCommonBindings } from './composables/useTimePickerCommonBindings' -import RangeOverlay from './overlay/RangeOverlay' -import { timeRangePickerContext, timeRangePickerControl } from './tokens' +import { useRangeKeyboardEvents } from './composables/useKeyboardEvents' +import { useOverlayProps } from './composables/useOverlayProps' +import { useOverlayState } from './composables/useOverlayState' +import { usePickerState } from './composables/usePickerState' +import { useRangePickerControl } from './composables/useRangeControl' +import RangeOverlay from './content/RangeContent' +import { timeRangePickerContext } from './tokens' import RangeTrigger from './trigger/RangeTrigger' import { timeRangePickerProps } from './types' -import { convertToDate } from './utils' export default defineComponent({ name: 'IxTimeRangePicker', inheritAttrs: false, props: timeRangePickerProps, - setup(props, { attrs, slots }) { + setup(props, { attrs, expose, slots }) { const common = useGlobalConfig('common') const mergedPrefixCls = computed(() => `${common.prefixCls}-time-range-picker`) const locale = useGlobalConfig('locale') - const config = useGlobalConfig('timeRangePicker') + const config = useGlobalConfig('timePicker') const dateConfig = useDateConfig() - const { isValid, parse } = dateConfig - const [visibility, setVisibility] = useControlledProp(props, 'open', false) - const format = computed(() => props.format ?? config.format) + const { elementRef: inputRef, focus, blur } = useFormElement() - const commonBindings = useTimePickerCommonBindings(props) - const { accessor, isDisabled } = commonBindings + const formatRef = computed(() => props.format ?? config.format) - const accessorDateValue = computed(() => { - if (!isArray(accessor.valueRef.value)) { - return [undefined, undefined] - } + const pickerState = usePickerState(props, dateConfig, formatRef) + const { accessor, handleChange } = pickerState - return accessor.valueRef.value.map(v => convertToDate(dateConfig, v, format.value)) - }) as ComputedRef<[Date | undefined, Date | undefined]> - const [bufferValue, setBufferValue] = useState<[Date | undefined, Date | undefined]>(accessorDateValue.value) - watch(accessorDateValue, setBufferValue) - - const rangePickerControl = useRangePickerControl( - bufferValue, - dateConfig, - format, - [], - (value: string) => !value || isValid(parse(value, format.value)), - setBufferValue, - ) + const rangePickerControl = useRangePickerControl(dateConfig, formatRef, accessor.valueRef) + const { overlayOpened, setOverlayOpened } = useOverlayState(props, rangePickerControl) const inputEnableStatus = useInputEnableStatus(props, config) - const changeVisible = (visible: boolean) => { - setVisibility(visible) - if (!visible) { - setBufferValue(accessorDateValue.value) - } - } - const formContext = inject(FORM_TOKEN, null) + const handleKeyDown = useRangeKeyboardEvents(rangePickerControl, setOverlayOpened, handleChange) + const renderSeparator = () => slots.separator?.() ?? props.separator ?? locale.timeRangePicker.separator - provide(timeRangePickerControl, rangePickerControl) - provide(timeRangePickerContext, { + const context = { props, slots, dateConfig, locale, config, mergedPrefixCls, - format, + formatRef, formContext, - overlayOpened: visibility, + handleKeyDown, + inputRef, inputEnableStatus, - commonBindings, - bufferValue, - setOverlayOpened: changeVisible, + overlayOpened, + setOverlayOpened, renderSeparator, + rangeControlContext: rangePickerControl, + ...pickerState, + } + provide(timeRangePickerContext, context) + + expose({ focus, blur }) + + watch(overlayOpened, opened => { + nextTick(() => { + if (opened) { + focus() + inputRef.value?.dispatchEvent(new FocusEvent('focus')) + } else { + blur() + inputRef.value?.dispatchEvent(new FocusEvent('blur')) + } + }) }) - const overlayProps = useCommonOverlayProps(props, config, mergedPrefixCls, changeVisible) - - return () => { - const renderTrigger = () => ( - - ) - const renderContent = () => - - return ( - <ɵOverlay - {...overlayProps.value} - triggerId={attrs.id} - visible={visibility.value} - v-slots={{ default: renderTrigger, content: renderContent }} - disabled={isDisabled.value || props.readonly} - /> - ) - } + const renderTrigger = () => + const renderContent = () => + + const overlayProps = useOverlayProps(context) + const overlayClass = computed(() => normalizeClass([`${mergedPrefixCls.value}-overlay`, props.overlayClassName])) + + return () => ( + <ɵOverlay + {...overlayProps.value} + class={overlayClass.value} + triggerId={attrs.id} + v-slots={{ default: renderTrigger, content: renderContent }} + /> + ) }, }) diff --git a/packages/components/time-picker/src/composables/useControl.ts b/packages/components/time-picker/src/composables/useControl.ts new file mode 100644 index 000000000..bf75d9c3f --- /dev/null +++ b/packages/components/time-picker/src/composables/useControl.ts @@ -0,0 +1,116 @@ +/** + * @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 { TimePickerProps } from '../types' +import type { DateConfig } from '@idux/components/config' +import type { ComputedRef, Ref } from 'vue' + +import { computed, watch } from 'vue' + +import { isString } from 'lodash-es' + +import { useState } from '@idux/cdk/utils' + +import { convertToDate } from '../utils' + +export type OnPickerValueChange = (value: Date | undefined) => void + +type ValueType = TimePickerProps['value'] + +export interface PickerControlContext { + inputValue: ComputedRef + panelValue: ComputedRef + inputFocused: ComputedRef + setInputValue: (value: string | undefined) => void + setPanelValue: (value: Date | undefined) => void + + init: (force?: boolean) => void + handleInput: (evt: Event) => void + handleInputClear: () => void + handleInputFocus: () => void + handleInputBlur: () => void + handlePanelChange: (value: Date) => void +} + +export function usePickerControl( + dateConfig: DateConfig, + formatRef: ComputedRef, + handleChange: OnPickerValueChange, + valueProp: Ref, +): PickerControlContext { + const [inputValue, setInputValue] = useState(undefined) + const [inputFocused, setInputFocused] = useState(false) + const [panelValue, setPanelValue] = useState(undefined) + const { parse, format: formatDate } = dateConfig + + const dateValue = computed(() => convertToDate(dateConfig, valueProp.value, formatRef.value)) + const formatedDateValue = computed(() => + isString(valueProp.value) ? valueProp.value : dateValue.value ? formatDate(dateValue.value, formatRef.value) : '', + ) + + function init(force = false) { + if ( + force || + !inputValue.value || + parse(inputValue.value, formatRef.value).valueOf() !== dateValue.value?.valueOf() + ) { + setInputValue(formatedDateValue.value) + } + + setPanelValue(dateValue.value) + } + + watch(valueProp, () => init(), { immediate: true }) + + 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(inputValue.value ? currDate : undefined) + } + } + + function handleInputClear() { + setInputValue('') + } + + function handleInputFocus() { + setInputFocused(true) + } + + function handleInputBlur() { + setInputFocused(false) + } + + function handlePanelChange(value: Date) { + handleChange(value) + } + + return { + inputValue, + panelValue, + inputFocused, + setInputValue, + setPanelValue, + + init, + handleInput, + handleInputClear, + handleInputFocus, + handleInputBlur, + handlePanelChange, + } +} diff --git a/packages/components/time-picker/src/composables/useInputProps.ts b/packages/components/time-picker/src/composables/useInputProps.ts new file mode 100644 index 000000000..326b48042 --- /dev/null +++ b/packages/components/time-picker/src/composables/useInputProps.ts @@ -0,0 +1,26 @@ +/** + * @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 { ɵInputProps } from '@idux/components/_private/input' + +import { type ComputedRef, computed } from 'vue' + +import { TimePickerContext, TimeRangePickerContext } from '../tokens' + +export function useInputProps(context: TimePickerContext | TimeRangePickerContext): ComputedRef<ɵInputProps> { + return computed(() => { + const { props, config, accessor } = context + + return { + borderless: false, + clearable: props.clearable ?? config.clearable, + clearIcon: props.clearIcon ?? config.clearIcon, + disabled: accessor.disabled.value, + size: 'sm', + } + }) +} diff --git a/packages/components/time-picker/src/composables/useKeyboardEvents.ts b/packages/components/time-picker/src/composables/useKeyboardEvents.ts new file mode 100644 index 000000000..07d16ac31 --- /dev/null +++ b/packages/components/time-picker/src/composables/useKeyboardEvents.ts @@ -0,0 +1,41 @@ +/** + * @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' + +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, + setOverlayOpened: (opened: boolean) => void, + handleChange: (value: (Date | undefined)[] | undefined) => void, +): (evt: KeyboardEvent) => void { + const { bufferUpdated, buffer } = rangeControl + return (evt: KeyboardEvent) => { + switch (evt.code) { + case 'Escape': + setOverlayOpened(false) + break + + case 'Enter': + if (bufferUpdated.value) { + handleChange(buffer.value) + } + setOverlayOpened(false) + break + + default: + break + } + } +} diff --git a/packages/components/time-picker/src/composables/useOverlayProps.ts b/packages/components/time-picker/src/composables/useOverlayProps.ts new file mode 100644 index 000000000..31756db83 --- /dev/null +++ b/packages/components/time-picker/src/composables/useOverlayProps.ts @@ -0,0 +1,30 @@ +/** + * @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 { ɵOverlayProps } from '@idux/components/_private/overlay' + +import { type ComputedRef, computed } from 'vue' + +import { TimePickerContext, TimeRangePickerContext } from '../tokens' + +const defaultOffset: [number, number] = [0, 8] +export function useOverlayProps(context: TimePickerContext | TimeRangePickerContext): ComputedRef<ɵOverlayProps> { + return computed(() => { + const { props, config, accessor, mergedPrefixCls, overlayOpened, setOverlayOpened } = context + return { + clickOutside: true, + disabled: accessor.disabled.value || props.readonly, + offset: defaultOffset, + placement: 'bottomStart', + transitionName: 'ix-fade', + target: props.overlayContainer ?? config.overlayContainer ?? `${mergedPrefixCls.value}-overlay-container`, + trigger: 'manual', + visible: overlayOpened.value, + 'onUpdate:visible': setOverlayOpened, + } + }) +} diff --git a/packages/components/time-picker/src/composables/useOverlayState.ts b/packages/components/time-picker/src/composables/useOverlayState.ts new file mode 100644 index 000000000..2c241db1f --- /dev/null +++ b/packages/components/time-picker/src/composables/useOverlayState.ts @@ -0,0 +1,41 @@ +/** + * @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 { TimePickerProps, TimeRangePickerProps } from '../types' +import type { PickerControlContext } from './useControl' +import type { PickerRangeControlContext } from './useRangeControl' + +import { type ComputedRef, onMounted } from 'vue' + +import { useControlledProp } from '@idux/cdk/utils' + +export interface OverlayStateContext { + overlayOpened: ComputedRef + setOverlayOpened: (open: boolean) => void +} + +export function useOverlayState( + props: TimePickerProps | TimeRangePickerProps, + control: PickerControlContext | PickerRangeControlContext, +): OverlayStateContext { + const [overlayOpened, setOverlayOpened] = useControlledProp(props, 'open', false) + + const changeOpenedState = (open: boolean) => { + setOverlayOpened(open) + if (!open) { + control.init(true) + } + } + + onMounted(() => { + if (props.autofocus) { + setOverlayOpened(true) + } + }) + + return { overlayOpened, setOverlayOpened: changeOpenedState } +} diff --git a/packages/components/time-picker/src/composables/usePanelActiveValue.ts b/packages/components/time-picker/src/composables/usePanelActiveValue.ts new file mode 100644 index 000000000..c29e5189e --- /dev/null +++ b/packages/components/time-picker/src/composables/usePanelActiveValue.ts @@ -0,0 +1,71 @@ +/** + * @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 { TimePickerProps, TimeRangePickerProps } 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 } from '../utils' + +export interface ActiveValueContext { + activeValue: ComputedRef + setActiveValue: (value: Date | undefined) => void +} + +export interface RangeActiveValueContext { + fromActiveValue: ComputedRef + toActiveValue: ComputedRef + setFromActiveValue: (value: Date | undefined) => void + setToActiveValue: (value: Date | undefined) => void +} + +export function useActiveValue( + props: TimePickerProps, + dateConfig: DateConfig, + formatRef: ComputedRef, + valueRef: ComputedRef, +): ActiveValueContext { + const defaultOpenValue = computed(() => convertToDate(dateConfig, props.defaultOpenValue, formatRef.value)) + const [activeValue, setActiveValue] = useState(() => valueRef.value ?? defaultOpenValue.value) + + watch(valueRef, value => { + setActiveValue(value ?? defaultOpenValue.value) + }) + + return { + activeValue, + setActiveValue, + } +} + +export function useRangeActiveValue( + props: TimeRangePickerProps, + dateConfig: DateConfig, + formatRef: ComputedRef, + valuesRef: ComputedRef<(Date | undefined)[] | undefined>, +): RangeActiveValueContext { + const defaultOpenValue = computed(() => + convertArray(props.defaultOpenValue).map(value => convertToDate(dateConfig, value, formatRef.value)), + ) + const [fromActiveValue, setFromActiveValue] = useState(() => valuesRef.value?.[0] ?? defaultOpenValue.value[0]) + const [toActiveValue, setToActiveValue] = useState(() => valuesRef.value?.[1] ?? defaultOpenValue.value[1]) + + watch(valuesRef, values => { + setFromActiveValue(values?.[0] ?? defaultOpenValue.value[0]) + setToActiveValue(values?.[1] ?? defaultOpenValue.value[1]) + }) + + return { + fromActiveValue, + toActiveValue, + setFromActiveValue, + setToActiveValue, + } +} diff --git a/packages/components/time-picker/src/composables/usePanelProps.ts b/packages/components/time-picker/src/composables/usePanelProps.ts new file mode 100644 index 000000000..edc9e9600 --- /dev/null +++ b/packages/components/time-picker/src/composables/usePanelProps.ts @@ -0,0 +1,53 @@ +/** + * @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 { ɵBaseTimePanelProps } from '@idux/components/_private/time-panel' + +import { type ComputedRef, computed } from 'vue' + +import { TimePickerContext, TimeRangePickerContext } from '../tokens' + +export interface CommonPanelProps extends ɵBaseTimePanelProps { + hourEnabled: boolean + minuteEnabled: boolean + secondEnabled: boolean + use12Hours: boolean + amPmCapital: boolean +} + +export function usePanelProps(context: TimePickerContext | TimeRangePickerContext): ComputedRef { + return computed(() => { + const { props, config } = context + const { + disabledHours, + disabledMinutes, + disabledSeconds, + format, + hideDisabledOptions, + hourStep, + minuteStep, + secondStep, + } = props + + const _format = format ?? config.format + + return { + disabledHours, + disabledMinutes, + disabledSeconds, + hideDisabledOptions, + hourStep, + minuteStep, + secondStep, + hourEnabled: /[hH]/.test(_format), + minuteEnabled: /m/.test(_format), + secondEnabled: /s/.test(_format), + use12Hours: /[aA]/.test(_format), + amPmCapital: /A/.test(_format), + } + }) +} diff --git a/packages/components/time-picker/src/composables/usePickerControl.ts b/packages/components/time-picker/src/composables/usePickerControl.ts deleted file mode 100644 index c2ef27eb6..000000000 --- a/packages/components/time-picker/src/composables/usePickerControl.ts +++ /dev/null @@ -1,123 +0,0 @@ -/** - * @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 { TimePickerProps, TimeRangePickerProps } from '../types' -import type { DateConfig } from '@idux/components/config' -import type { ComputedRef, Ref } from 'vue' - -import { computed, watch } from 'vue' - -import { isArray, isString } from 'lodash-es' - -import { useState } from '@idux/cdk/utils' - -import { convertToDate } from '../utils' - -export type InputPreProcessor = (value: string) => string -export type OnPickerValueChange = (value: Date | undefined) => void -export type OnRangePickerValueChange = (value: [Date | undefined, Date | undefined]) => void -export type InputValidator = (value: string) => boolean - -type ValueType = TimePickerProps['value'] -type RangeValueType = TimeRangePickerProps['value'] - -export interface PickerControl { - inputValue: ComputedRef - panelValue: ComputedRef - setInputValue: (value: string | undefined) => void - setPanelValue: (value: Date | undefined) => void - - init: () => void - handleInputChange: (value: string) => void - handlePanelChange: (value: Date) => void -} - -export function usePickerControl( - valueProp: Ref, - dateConfig: DateConfig, - format: ComputedRef, - inputPreProcessors: InputPreProcessor[], - validateInput: InputValidator, - onChange: OnPickerValueChange, -): PickerControl { - const [inputValue, setInputValue] = useState(undefined) - const [panelValue, setPanelValue] = useState(undefined) - const { parse, format: formatDate } = dateConfig - - const dateValue = computed(() => convertToDate(dateConfig, valueProp.value, format.value)) - const formatedDateValue = computed(() => - isString(valueProp.value) ? valueProp.value : dateValue.value ? formatDate(dateValue.value, format.value) : '', - ) - - function init() { - if (!inputValue.value || parse(inputValue.value, format.value).valueOf() !== dateValue.value?.valueOf()) { - setInputValue(formatedDateValue.value) - } - - setPanelValue(dateValue.value) - } - - init() - watch(valueProp, () => { - init() - }) - - function handleInputChange(value: string) { - const processedValue = preProcessInputValue(value, inputPreProcessors) - setInputValue(value) - - if (validateInput(processedValue)) { - onChange(inputValue.value ? parse(inputValue.value, format.value) : undefined) - } - } - - function handlePanelChange(value: Date) { - onChange(value) - } - - return { - inputValue, - panelValue, - setInputValue, - setPanelValue, - - init, - handleInputChange, - handlePanelChange, - } -} - -function preProcessInputValue(value: string, inputPreProcessors: InputPreProcessor[]) { - return inputPreProcessors.reduce((result, processor) => processor(result), value) -} - -export function useRangePickerControl( - valueProp: Ref, - dateConfig: DateConfig, - format: ComputedRef, - inputPreProcessors: InputPreProcessor[], - validateInput: InputValidator, - onChange: OnRangePickerValueChange, -): [PickerControl, PickerControl] { - const rangeValueRef = computed(() => { - if (!isArray(valueProp.value)) { - return [undefined, undefined] - } - return valueProp.value.map(v => convertToDate(dateConfig, v, format.value)) - }) - const fromValue = computed(() => rangeValueRef.value[0]) - const toValue = computed(() => rangeValueRef.value[1]) - - const fromControl = usePickerControl(fromValue, dateConfig, format, inputPreProcessors, validateInput, value => { - onChange([value, toValue.value]) - }) - const toControl = usePickerControl(toValue, dateConfig, format, inputPreProcessors, validateInput, value => { - onChange([fromValue.value, value]) - }) - - return [fromControl, toControl] -} diff --git a/packages/components/time-picker/src/composables/usePickerState.ts b/packages/components/time-picker/src/composables/usePickerState.ts new file mode 100644 index 000000000..98c2195bc --- /dev/null +++ b/packages/components/time-picker/src/composables/usePickerState.ts @@ -0,0 +1,88 @@ +/** + * @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 { TimePickerProps, TimeRangePickerProps } 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/form' + +import { convertToDate, sortRangeValue } from '../utils' + +type StateValueType = T extends TimePickerProps + ? 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(newValue) + } + + 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/time-picker/src/composables/useProps.ts b/packages/components/time-picker/src/composables/useProps.ts deleted file mode 100644 index 61590eaca..000000000 --- a/packages/components/time-picker/src/composables/useProps.ts +++ /dev/null @@ -1,152 +0,0 @@ -/** - * @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 { - BaseTriggerProps, - TimePickerProps, - TimePickerTriggerProps, - TimeRangePickerProps, - TimeRangePickerTriggerProps, -} from '../types' -import type { PopperPlacement, PopperTrigger } from '@idux/cdk/popper' -import type { PortalTargetType } from '@idux/cdk/portal' -import type { ɵBaseTimePanelProps } from '@idux/components/_private/time-panel' -import type { TimePickerConfig, TimeRangePickerConfig } from '@idux/components/config' -import type { FormSize } from '@idux/components/form' -import type { ComputedRef } from 'vue' - -import { computed } from 'vue' - -import { isArray } from 'lodash-es' - -import { TimePickerContext, TimeRangePickerContext } from '../tokens' - -export interface CommonInputProps { - borderless: boolean - clearable: boolean - clearIcon: string - size: FormSize -} - -export function useCommonInputProps( - props: TimePickerProps | TimeRangePickerProps, - config: TimePickerConfig | TimeRangePickerConfig, -): ComputedRef { - return computed(() => { - return { - borderless: props.borderless ?? config.borderless, - clearable: props.clearable ?? config.clearable, - clearIcon: props.clearIcon ?? config.clearIcon, - size: 'sm', - } - }) -} - -export function useCommonTriggerProps< - T extends TimePickerTriggerProps | TimeRangePickerTriggerProps, - U extends T extends TimePickerTriggerProps ? TimePickerContext : TimeRangePickerContext, ->(props: T, timePickerContext: U): ComputedRef { - const { - props: pickerProps, - config, - overlayOpened, - formContext, - commonBindings: { isDisabled, isFocused, handleBlur, handleFocus }, - } = timePickerContext - return computed(() => { - const pickerClearable = pickerProps.clearable ?? config.clearable - const enableExternalInput = (pickerProps.allowInput ?? config.allowInput) === true - const valueNotEmpty = isArray(props.value) ? props.value.some(v => !!v) : !!props.value - - return { - disabled: isDisabled?.value ?? pickerProps.disabled, - focused: isFocused?.value || overlayOpened.value, - readonly: pickerProps.readonly, - borderless: pickerProps.borderless ?? config.borderless, - clearable: - (enableExternalInput || !overlayOpened?.value) && !pickerProps.readonly && pickerClearable && valueNotEmpty, - clearIcon: pickerProps.clearIcon ?? config.clearIcon, - size: pickerProps.size ?? formContext?.size.value ?? config.size, - suffix: pickerProps.suffix ?? config.suffix, - - onFocus: handleFocus, - onBlur: handleBlur, - } - }) -} - -export interface CommonPanelProps extends ɵBaseTimePanelProps { - hourEnabled: boolean - minuteEnabled: boolean - secondEnabled: boolean - use12Hours: boolean - amPmCapital: boolean -} - -export function useCommonPanelProps( - props: TimePickerProps | TimeRangePickerProps, - config: TimePickerConfig | TimeRangePickerConfig, -): ComputedRef { - return computed(() => { - const { - disabledHours, - disabledMinutes, - disabledSeconds, - format, - hideDisabledOptions, - hourStep, - minuteStep, - secondStep, - } = props - - const _format = format ?? config.format - - return { - disabledHours, - disabledMinutes, - disabledSeconds, - hideDisabledOptions, - hourStep, - minuteStep, - secondStep, - hourEnabled: /[hH]/.test(_format), - minuteEnabled: /m/.test(_format), - secondEnabled: /s/.test(_format), - use12Hours: /[aA]/.test(_format), - amPmCapital: /A/.test(_format), - } - }) -} - -export interface CommonOverlayProps { - class?: string - clickOutside: boolean - offset: [number, number] - placement: PopperPlacement - transitionName: string - trigger: PopperTrigger - target: PortalTargetType -} - -const defaultOffset: [number, number] = [0, 8] -export function useCommonOverlayProps( - props: TimePickerProps | TimeRangePickerProps, - config: TimePickerConfig | TimeRangePickerConfig, - mergedPrefixCls: ComputedRef, - setVisibility: (value: boolean) => void, -): ComputedRef { - return computed(() => ({ - class: props.overlayClassName, - clickOutside: true, - offset: defaultOffset, - placement: 'bottomStart', - transitionName: 'ix-fade', - target: props.target ?? config.target ?? `${mergedPrefixCls.value}-overlay-container`, - trigger: 'manual', - ['onUpdate:visible']: setVisibility, - })) -} diff --git a/packages/components/time-picker/src/composables/useRangeControl.ts b/packages/components/time-picker/src/composables/useRangeControl.ts new file mode 100644 index 000000000..b6762d7b9 --- /dev/null +++ b/packages/components/time-picker/src/composables/useRangeControl.ts @@ -0,0 +1,93 @@ +/** + * @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 { TimeRangePickerProps } from '../types' +import type { DateConfig } from '@idux/components/config' +import type { ComputedRef, Ref } from 'vue' + +import { computed, watch } from 'vue' + +import { convertArray, useState } from '@idux/cdk/utils' + +import { convertToDate } from '../utils' +import { type PickerControlContext, usePickerControl } from './useControl' + +type RangeValueType = TimeRangePickerProps['value'] + +export type OnRangePickerValueChange = (value: [Date | undefined, Date | undefined]) => void + +export interface PickerRangeControlContext { + buffer: ComputedRef<(Date | undefined)[] | undefined> + bufferUpdated: ComputedRef + + fromControl: PickerControlContext + toControl: PickerControlContext + + init: (force?: boolean) => void +} + +export function useRangePickerControl( + dateConfig: DateConfig, + formatRef: ComputedRef, + valueRef: Ref, +): PickerRangeControlContext { + 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) + } + watch(valueRef, handleBufferUpdate) + + const rangeValueRef = computed(() => convertArray(buffer.value)) + const fromValue = computed(() => rangeValueRef.value[0]) + const toValue = computed(() => rangeValueRef.value[1]) + + const fromControl = usePickerControl( + dateConfig, + formatRef, + value => { + handleBufferUpdate([value, toValue.value]) + }, + fromValue, + ) + const toControl = usePickerControl( + dateConfig, + formatRef, + value => { + handleBufferUpdate([fromValue.value, value]) + }, + toValue, + ) + + const init = (force = false) => { + handleBufferUpdate(valueRef.value) + setBufferUpdated(false) + fromControl.init(force) + toControl.init(force) + } + + return { + buffer, + bufferUpdated, + + fromControl, + toControl, + + init, + } +} + +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/time-picker/src/composables/useTimePickerCommonBindings.ts b/packages/components/time-picker/src/composables/useTimePickerCommonBindings.ts deleted file mode 100644 index 4e7ae9fb6..000000000 --- a/packages/components/time-picker/src/composables/useTimePickerCommonBindings.ts +++ /dev/null @@ -1,98 +0,0 @@ -/** - * @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 { TimePickerProps, TimeRangePickerProps } from '../types' -import type { ValueAccessor } from '@idux/cdk/forms' -import type { ComputedRef } from 'vue' - -import { computed } from 'vue' - -import { isArray } from 'lodash-es' - -import { callEmit, useState } from '@idux/cdk/utils' -import { useFormAccessor } from '@idux/components/form' - -type TimeValueProp = TimePickerProps['value'] -type TimeRangeValueProp = TimeRangePickerProps['value'] -type TimeValue = Date | undefined -type TimeRangeValue = [TimeValue, TimeValue] | undefined -type CommonBindingValueType = T extends TimePickerProps - ? TimeValue - : TimeRangeValue -type AccessorValueType = T extends TimePickerProps - ? TimeValueProp - : TimeRangeValueProp - -export interface TimePickerCommonBindings { - accessor: ValueAccessor> - isDisabled: ComputedRef - isFocused: ComputedRef - handleChange: (value: CommonBindingValueType) => void - handleClear: (evt: Event) => void - handleFocus: (evt: FocusEvent) => void - handleBlur: (evt: FocusEvent) => void -} - -function valueIsRange(value: TimeValue | TimeRangeValue): value is Exclude { - return isArray(value) -} -export function useTimePickerCommonBindings( - props: T, -): TimePickerCommonBindings { - const accessor = useFormAccessor>() - - const isDisabled = computed(() => accessor.disabled.value) - const [isFocused, setFocused] = useState(false) - - function handleChange(value: CommonBindingValueType) { - let newValue: CommonBindingValueType - if (valueIsRange(value)) { - newValue = getValidDateRangeValue(value, props.autoSwap) as CommonBindingValueType - } else { - newValue = value - } - - callEmit(props.onChange as (value: CommonBindingValueType) => void, newValue) - accessor.setValue(newValue) - } - - function handleClear(evt: Event) { - callEmit(props.onClear, evt as MouseEvent) - 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, - isDisabled, - isFocused, - handleChange, - handleClear, - handleFocus, - handleBlur, - } -} - -function getValidDateRangeValue(value: Exclude, autoSwap: boolean) { - const [fromDate, toDate] = value - - if (!fromDate || !toDate || fromDate.valueOf() <= toDate.valueOf()) { - return value - } - - return autoSwap ? (value.reverse() as Exclude) : value -} diff --git a/packages/components/time-picker/src/composables/useTriggerProps.ts b/packages/components/time-picker/src/composables/useTriggerProps.ts new file mode 100644 index 000000000..e59d87f78 --- /dev/null +++ b/packages/components/time-picker/src/composables/useTriggerProps.ts @@ -0,0 +1,55 @@ +/** + * @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 { ɵTriggerProps } from '@idux/components/_private/trigger' + +import { type ComputedRef, computed } from 'vue' + +import { TimePickerContext, TimeRangePickerContext } from '../tokens' + +export function useTriggerProps(context: TimePickerContext | TimeRangePickerContext): ComputedRef<ɵTriggerProps> { + const { + props, + config, + accessor, + formContext, + isFocused, + handleBlur, + handleFocus, + handleClear, + handleKeyDown, + overlayOpened, + setOverlayOpened, + inputEnableStatus, + } = context + + const handleClick = () => { + const currOpened = overlayOpened.value + if (currOpened || accessor.disabled.value) { + return + } + + setOverlayOpened(!currOpened) + } + + return computed(() => ({ + borderless: props.borderless, + clearable: + !props.readonly && !accessor.disabled.value && (props.clearable ?? config.clearable) && !!accessor.valueRef.value, + clearIcon: props.clearIcon ?? config.clearIcon, + disabled: accessor.disabled.value, + focused: isFocused.value, + readonly: props.readonly || !inputEnableStatus.value.enableExternalInput, + 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/time-picker/src/content/Content.tsx b/packages/components/time-picker/src/content/Content.tsx new file mode 100644 index 000000000..026035ea4 --- /dev/null +++ b/packages/components/time-picker/src/content/Content.tsx @@ -0,0 +1,105 @@ +/** + * @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 { defineComponent, inject, onMounted, onUpdated, ref } from 'vue' + +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 { useInputProps } from '../composables/useInputProps' +import { useActiveValue } from '../composables/usePanelActiveValue' +import { usePanelProps } from '../composables/usePanelProps' +import { timePickerContext } from '../tokens' + +export default defineComponent({ + setup() { + const context = inject(timePickerContext)! + const { + slots, + props, + locale, + formatRef, + dateConfig, + inputRef, + inputEnableStatus, + mergedPrefixCls, + overlayOpened, + handleClear, + handleKeyDown, + controlContext: { + inputValue, + panelValue, + inputFocused, + handleInput, + handleInputFocus, + handleInputBlur, + handlePanelChange, + }, + } = context + + const inputInstance = ref<ɵInputInstance>() + const setInputRef = () => { + if (inputEnableStatus.value.enableInternalInput) { + inputRef.value = inputInstance.value?.getInputElement() + } + } + onMounted(setInputRef) + onUpdated(setInputRef) + + const inputProps = useInputProps(context) + const panelProps = usePanelProps(context) + + const { activeValue, setActiveValue } = useActiveValue(props, dateConfig, formatRef, panelValue) + + const handleMouseDown = (e: MouseEvent) => { + if (!(e.target instanceof HTMLInputElement)) { + e.preventDefault() + } + } + + return () => { + const prefixCls = `${mergedPrefixCls.value}-overlay` + const boardPrefixCls = `${mergedPrefixCls.value}-board` + + const _panelProps = { + ...panelProps.value, + activeValue: activeValue.value, + visible: overlayOpened.value, + value: panelValue.value, + onChange: handlePanelChange, + 'onUpdate:activeValue': setActiveValue, + } + + const children = [ +
    + {inputEnableStatus.value.enableInternalInput && ( + <ɵInput + ref={inputInstance} + class={`${boardPrefixCls}-input`} + {...inputProps.value} + clearVisible={!!inputValue.value} + value={inputValue.value} + readonly={props.readonly} + focused={inputFocused.value} + placeholder={props.placeholder ?? locale.timePicker.placeholder} + onInput={handleInput} + onClear={handleClear} + onFocus={handleInputFocus} + onBlur={handleInputBlur} + onKeydown={handleKeyDown} + v-slots={slots} + /> + )} + <ɵTimePanel {..._panelProps} class={`${boardPrefixCls}-panel`} tabindex={-1} /> +
    , + <ɵFooter v-slots={slots} class={`${prefixCls}-footer`} footer={props.footer} />, + ] + return props.overlayRender ? props.overlayRender(children) :
    {children}
    + } + }, +}) diff --git a/packages/components/time-picker/src/content/RangeContent.tsx b/packages/components/time-picker/src/content/RangeContent.tsx new file mode 100644 index 000000000..969e6d7b1 --- /dev/null +++ b/packages/components/time-picker/src/content/RangeContent.tsx @@ -0,0 +1,141 @@ +/** + * @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 { ɵFooter } from '@idux/components/_private/footer' +import { ɵInput, type ɵInputInstance } from '@idux/components/_private/input' +import { ɵTimePanel } from '@idux/components/_private/time-panel' + +import { useInputProps } from '../composables/useInputProps' +import { useRangeActiveValue } from '../composables/usePanelActiveValue' +import { usePanelProps } from '../composables/usePanelProps' +import { timeRangePickerContext } from '../tokens' + +export default defineComponent({ + setup() { + const context = inject(timeRangePickerContext)! + const { + props, + slots, + dateConfig, + locale, + mergedPrefixCls, + formatRef, + inputRef, + inputEnableStatus, + overlayOpened, + handleChange, + handleKeyDown, + renderSeparator, + setOverlayOpened, + rangeControlContext: { buffer, fromControl, toControl }, + } = context + + const inputInstance = ref<ɵInputInstance>() + const setInputRef = () => { + if (inputEnableStatus.value.enableInternalInput) { + inputRef.value = inputInstance.value?.getInputElement() + } + } + onMounted(setInputRef) + onUpdated(setInputRef) + + const inputProps = useInputProps(context) + const panelProps = usePanelProps(context) + + const { fromActiveValue, toActiveValue, setFromActiveValue, setToActiveValue } = useRangeActiveValue( + props, + dateConfig, + formatRef, + computed(() => [fromControl.panelValue.value, toControl.panelValue.value]), + ) + + const handleConfirm = () => { + handleChange(buffer.value) + setOverlayOpened(false) + } + + const handleMouseDown = (e: MouseEvent) => { + if (!(e.target instanceof HTMLInputElement)) { + e.preventDefault() + } + } + + function renderBoard(isFrom: boolean) { + const { + inputValue, + inputFocused, + panelValue, + handleInput, + handleInputClear, + handleInputFocus, + handleInputBlur, + handlePanelChange, + } = isFrom ? fromControl : toControl + + const idx = isFrom ? 0 : 1 + const placeholder = props.placeholder?.[idx] ?? locale.timeRangePicker.placeholder[idx] + const prefixCls = `${mergedPrefixCls.value}-board` + + const _panelProps = { + ...panelProps.value, + activeValue: isFrom ? fromActiveValue.value : toActiveValue.value, + visible: overlayOpened.value, + value: panelValue.value, + onChange: handlePanelChange, + 'onUpdate:activeValue': isFrom ? setFromActiveValue : setToActiveValue, + } + + return ( +
    + {inputEnableStatus.value.enableInternalInput && ( + <ɵInput + ref={isFrom ? inputInstance : undefined} + class={`${prefixCls}-input`} + {...inputProps.value} + clearVisible={!!inputValue.value} + value={inputValue.value} + readonly={props.readonly} + focused={inputFocused.value} + placeholder={placeholder} + onChange={handleInput} + onClear={handleInputClear} + onFocus={handleInputFocus} + onBlur={handleInputBlur} + onKeydown={handleKeyDown} + v-slots={slots} + /> + )} + <ɵTimePanel {..._panelProps} class={`${prefixCls}-panel`} tabindex={-1} /> +
    + ) + } + + return () => { + const prefixCls = `${mergedPrefixCls.value}-overlay` + + const children = [ +
    + {renderBoard(true)} +
    {renderSeparator()}
    + {renderBoard(false)} +
    , + <ɵFooter + v-slots={slots} + class={`${prefixCls}-footer`} + footer={props.footer} + okText={locale.timeRangePicker.okText} + okButton={{ size: 'xs', mode: 'primary' }} + cancelVisible={false} + ok={handleConfirm} + />, + ] + return props.overlayRender ? props.overlayRender(children) :
    {children}
    + } + }, +}) diff --git a/packages/components/time-picker/src/overlay/Overlay.tsx b/packages/components/time-picker/src/overlay/Overlay.tsx deleted file mode 100644 index 54cd911ca..000000000 --- a/packages/components/time-picker/src/overlay/Overlay.tsx +++ /dev/null @@ -1,85 +0,0 @@ -/** - * @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 { InputInstance } from '@idux/components/input' - -import { defineComponent, inject, nextTick, ref, watch } from 'vue' - -import { ɵTimePanel } from '@idux/components/_private/time-panel' -import { IxInput } from '@idux/components/input' - -import { useCommonInputProps, useCommonPanelProps } from '../composables/useProps' -import { timePickerContext, timePickerControl } from '../tokens' -import { convertToDate } from '../utils' - -export default defineComponent({ - name: 'IxTimePickerOverlay', - setup() { - const { - slots, - props, - locale, - config, - format, - dateConfig, - inputEnableStatus, - mergedPrefixCls, - overlayOpened, - commonBindings: { isDisabled, handleClear }, - } = inject(timePickerContext)! - const { inputValue, panelValue, setInputValue, handleInputChange, handlePanelChange } = inject(timePickerControl)! - - const handleInputClear = (evt: Event) => { - evt.stopPropagation() - handleClear(evt) - setInputValue('') - } - - const inputProps = useCommonInputProps(props, config) - const panelProps = useCommonPanelProps(props, config) - - const inputSlots = { - clearIcon: slots.clearIcon, - } - - const inputRef = ref() - watch(overlayOpened, opened => { - if (opened) { - nextTick(() => inputRef.value?.focus()) - } - }) - - return () => { - const prefixCls = `${mergedPrefixCls.value}-overlay` - return ( -
    - {inputEnableStatus.value.enableInternalInput && ( - - )} - <ɵTimePanel - {...panelProps.value} - visible={overlayOpened.value} - defaultOpenValue={convertToDate(dateConfig, props.defaultOpenValue, format.value)} - value={panelValue.value} - onChange={handlePanelChange} - /> -
    - ) - } - }, -}) diff --git a/packages/components/time-picker/src/overlay/RangeOverlay.tsx b/packages/components/time-picker/src/overlay/RangeOverlay.tsx deleted file mode 100644 index 7ad679203..000000000 --- a/packages/components/time-picker/src/overlay/RangeOverlay.tsx +++ /dev/null @@ -1,119 +0,0 @@ -/** - * @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 { PickerControl } from '../composables/usePickerControl' - -import { defineComponent, inject } from 'vue' - -import { ɵTimePanel } from '@idux/components/_private/time-panel' -import { IxButton } from '@idux/components/button' -import { IxInput } from '@idux/components/input' - -import { useCommonInputProps, useCommonPanelProps } from '../composables/useProps' -import { timeRangePickerContext, timeRangePickerControl } from '../tokens' -import { convertToDate } from '../utils' - -export default defineComponent({ - name: 'IxTimeRangePickerOverlay', - setup() { - const { - props, - slots, - dateConfig, - locale, - config, - mergedPrefixCls, - format, - inputEnableStatus, - overlayOpened, - bufferValue, - commonBindings: { isDisabled, handleChange }, - renderSeparator, - setOverlayOpened, - } = inject(timeRangePickerContext)! - const [fromPickerControl, toPickerControl] = inject(timeRangePickerControl)! - - const inputProps = useCommonInputProps(props, config) - const panelProps = useCommonPanelProps(props, config) - - const handleConfirm = () => { - handleChange(bufferValue.value) - setOverlayOpened(false) - } - - function renderSide( - pickerControl: PickerControl, - placeholder: string, - defaultOpenValue: number | string | Date | undefined, - ) { - const { inputValue, panelValue, handleInputChange, handlePanelChange } = pickerControl - const prefixCls = `${mergedPrefixCls.value}-overlay-side` - const inputSlots = { - clearIcon: slots.clearIcon, - } - - const handleInputClear = (evt: Event) => { - evt.stopPropagation() - handleInputChange('') - } - - return ( -
    - {inputEnableStatus.value.enableInternalInput && ( - - )} - <ɵTimePanel - {...panelProps.value} - visible={overlayOpened.value} - defaultOpenValue={convertToDate(dateConfig, defaultOpenValue, format.value)} - value={panelValue.value} - onChange={handlePanelChange} - /> -
    - ) - } - - const renderFooter = () => - slots.footer?.({ onConfirm: handleConfirm }) ?? ( - - {locale.timeRangePicker.okText} - - ) - - return () => { - const prefixCls = `${mergedPrefixCls.value}-overlay` - return ( -
    -
    - {renderSide( - fromPickerControl, - props.placeholder?.[0] ?? locale.timeRangePicker.placeholder[0], - props.defaultOpenValue?.[0], - )} -
    {renderSeparator()}
    - {renderSide( - toPickerControl, - props.placeholder?.[1] ?? locale.timeRangePicker.placeholder[1], - props.defaultOpenValue?.[1], - )} -
    -
    {renderFooter()}
    -
    - ) - } - }, -}) diff --git a/packages/components/time-picker/src/tokens.ts b/packages/components/time-picker/src/tokens.ts index 66c68edf5..0941af466 100644 --- a/packages/components/time-picker/src/tokens.ts +++ b/packages/components/time-picker/src/tokens.ts @@ -5,37 +5,40 @@ * found in the LICENSE file at https://github.com/IDuxFE/idux/blob/main/LICENSE */ +import type { PickerControlContext } from './composables/useControl' import type { InputEnableStatus } from './composables/useInputEnableStatus' -import type { PickerControl } from './composables/usePickerControl' -import type { TimePickerCommonBindings } from './composables/useTimePickerCommonBindings' +import type { OverlayStateContext } from './composables/useOverlayState' +import type { PickerStateContext } from './composables/usePickerState' +import type { PickerRangeControlContext } from './composables/useRangeControl' import type { TimePickerProps, TimeRangePickerProps } from './types' import type { DateConfig, TimePickerConfig } from '@idux/components/config' import type { FormContext } from '@idux/components/form' import type { Locale } from '@idux/components/locales' -import type { ComputedRef, InjectionKey, Slots, VNodeTypes } from 'vue' +import type { ComputedRef, InjectionKey, Ref, Slots, VNodeTypes } from 'vue' -interface BasePickerContext { +interface BasePickerContext + extends PickerStateContext, + OverlayStateContext { props: T slots: Slots dateConfig: DateConfig locale: Locale config: TimePickerConfig + inputRef: Ref mergedPrefixCls: ComputedRef - format: ComputedRef + formatRef: ComputedRef formContext: FormContext | null - overlayOpened: ComputedRef inputEnableStatus: ComputedRef - commonBindings: TimePickerCommonBindings - setOverlayOpened: (open: boolean) => void + handleKeyDown: (evt: KeyboardEvent) => void } -export type TimePickerContext = BasePickerContext +export interface TimePickerContext extends BasePickerContext { + controlContext: PickerControlContext +} export interface TimeRangePickerContext extends BasePickerContext { - bufferValue: ComputedRef<[Date | undefined, Date | undefined]> + rangeControlContext: PickerRangeControlContext renderSeparator: () => VNodeTypes } export const timePickerContext: InjectionKey = Symbol('timePickerContext') export const timeRangePickerContext: InjectionKey = Symbol('timeRangePickerContext') -export const timePickerControl: InjectionKey = Symbol('timePickerControl') -export const timeRangePickerControl: InjectionKey<[PickerControl, PickerControl]> = Symbol('timeRangePickerControl') diff --git a/packages/components/time-picker/src/trigger/BaseTrigger.tsx b/packages/components/time-picker/src/trigger/BaseTrigger.tsx deleted file mode 100644 index 20804c960..000000000 --- a/packages/components/time-picker/src/trigger/BaseTrigger.tsx +++ /dev/null @@ -1,96 +0,0 @@ -/** - * @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, 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 { baseTriggerProps } from '../types' - -export default defineComponent({ - name: 'IxTimePickerBaseTrigger', - props: baseTriggerProps, - setup(props, { slots }) { - const common = useGlobalConfig('common') - const mergedPrefixCls = computed(() => `${common.prefixCls}-time-picker-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) - } - } - }) - }) - - const classes = computed(() => { - const prefixCls = mergedPrefixCls.value - return normalizeClass({ - [prefixCls]: true, - [`${prefixCls}-disabled`]: isDisabled.value, - [`${prefixCls}-borderless`]: props.borderless, - [`${prefixCls}-readonly`]: props.readonly, - [`${prefixCls}-focused`]: props.focused, - [`${prefixCls}-${props.size}`]: props.size, - }) - }) - - const handkeClick = (evt: Event) => { - if (isDisabled.value) { - return - } - - callEmit(props.onClick, evt) - } - - const onClear = (evt: MouseEvent) => { - evt.stopPropagation() - callEmit(props.onClear, evt) - } - const renderSuffix = () => { - if (!(props.clearable || slots.clearIcon || props.suffix)) { - return null - } - - return ( -
    - {slots.suffix?.() ?? (props.suffix && )} -
    - ) - } - const renderClearIcon = () => { - if (!props.clearable || isDisabled.value) { - return null - } - - return ( - - {slots.clearIcon ? slots.clearIcon() : props.clearIcon && } - - ) - } - - return () => ( -
    - {slots.default?.()} - {renderSuffix()} - {renderClearIcon()} -
    - ) - }, -}) diff --git a/packages/components/time-picker/src/trigger/RangeTrigger.tsx b/packages/components/time-picker/src/trigger/RangeTrigger.tsx index 06d5eaa1f..68c91edc5 100644 --- a/packages/components/time-picker/src/trigger/RangeTrigger.tsx +++ b/packages/components/time-picker/src/trigger/RangeTrigger.tsx @@ -7,126 +7,68 @@ import { type ComputedRef, computed, defineComponent, inject } from 'vue' -import { type DateConfig, useGlobalConfig } from '@idux/components/config' +import { ɵTrigger } from '@idux/components/_private/trigger' -import { type PickerControl } from '../composables/usePickerControl' -import { useCommonTriggerProps } from '../composables/useProps' -import { timeRangePickerContext, timeRangePickerControl } from '../tokens' -import { timeRangePickerTriggerProps } from '../types' -import BaseTrigger from './BaseTrigger' +import { useTriggerProps } from '../composables/useTriggerProps' +import { timeRangePickerContext } from '../tokens' export default defineComponent({ - name: 'IxTimeRangePickerTrigger', - props: timeRangePickerTriggerProps, - setup(props) { - const common = useGlobalConfig('common') - const commonPrefixCls = computed(() => common.prefixCls) - const mergedPrefixCls = computed(() => `${common.prefixCls}-time-range-picker-trigger`) - + inheritAttrs: false, + setup(_, { attrs }) { const context = inject(timeRangePickerContext)! const { props: pickerProps, slots, - dateConfig, locale, - format, + mergedPrefixCls, + inputRef, inputEnableStatus, - setOverlayOpened, renderSeparator, - commonBindings: { isDisabled, handleClear }, + rangeControlContext: { fromControl, toControl }, } = context - const [fromPickerControl, toPickerControl] = inject(timeRangePickerControl)! - const handleClick = () => { - if (pickerProps.readonly) { - return - } - - setOverlayOpened(true) - } - - const placeholder: ComputedRef<[string, string]> = computed(() => [ + const placeholders: ComputedRef<[string, string]> = computed(() => [ pickerProps.placeholder?.[0] ?? locale.timeRangePicker.placeholder[0], pickerProps.placeholder?.[1] ?? locale.timeRangePicker.placeholder[1], ]) - const triggerProps = useCommonTriggerProps(props, context) - const renderContent = () => { - const prefixCls = mergedPrefixCls.value - const enableInput = inputEnableStatus.value.enableExternalInput - const disabled = isDisabled.value || pickerProps.readonly + function renderSide(prefixCls: string, isFrom: boolean) { + const { inputValue, handleInput } = isFrom ? fromControl : toControl + const { disabled, readonly } = triggerProps.value + const placeholder = placeholders.value[isFrom ? 0 : 1] return ( - - - {renderSide( - props.value?.[0], - format.value, - dateConfig, - fromPickerControl, - enableInput, - disabled, - placeholder.value[0], - commonPrefixCls.value, - )} - - {renderSeparator()} - - {renderSide( - props.value?.[1], - format.value, - dateConfig, - toPickerControl, - enableInput, - disabled, - placeholder.value[1], - commonPrefixCls.value, - )} - - + ) } - return () => ( - - {renderContent()} - + const triggerProps = useTriggerProps(context) + const renderContent = (prefixCls: string) => ( + + {renderSide(prefixCls, true)} + {renderSeparator()} + {renderSide(prefixCls, false)} + ) - }, -}) -function renderSide( - value: Date | undefined, - format: string, - dateConfig: DateConfig, - pickerControl: PickerControl, - enableInput: boolean, - disabled: boolean, - placeholder: string, - prefixCls: string, -) { - if (!enableInput) { - return value ? ( - dateConfig.format(value, format) - ) : ( - {placeholder} - ) - } - - const { inputValue, handleInputChange } = pickerControl - const onInput = (evt: Event) => { - const { value } = evt.target as HTMLInputElement - handleInputChange(value) - } + return () => { + const prefixCls = mergedPrefixCls.value + const triggerSlots = { + default: () => renderContent(prefixCls), + suffix: slots.suffix, + clearIcon: slots.clearIcon, + } - return ( - - ) -} + return <ɵTrigger className={prefixCls} v-slots={triggerSlots} {...triggerProps.value} {...attrs} /> + } + }, +}) diff --git a/packages/components/time-picker/src/trigger/Trigger.tsx b/packages/components/time-picker/src/trigger/Trigger.tsx index e769e1f6b..9a260b9b2 100644 --- a/packages/components/time-picker/src/trigger/Trigger.tsx +++ b/packages/components/time-picker/src/trigger/Trigger.tsx @@ -5,80 +5,59 @@ * found in the LICENSE file at https://github.com/IDuxFE/idux/blob/main/LICENSE */ -import { computed, defineComponent, inject, withKeys } from 'vue' +import { computed, defineComponent, inject } from 'vue' -import { useGlobalConfig } from '@idux/components/config' +import { ɵTrigger } from '@idux/components/_private/trigger' -import { useCommonTriggerProps } from '../composables/useProps' -import { timePickerContext, timePickerControl } from '../tokens' -import { timePickerTriggerProps } from '../types' -import BaseTrigger from './BaseTrigger' +import { useTriggerProps } from '../composables/useTriggerProps' +import { timePickerContext } from '../tokens' export default defineComponent({ - name: 'IxTimePickerTrigger', - props: timePickerTriggerProps, - setup(props) { - const common = useGlobalConfig('common') - const mergedPrefixCls = computed(() => `${common.prefixCls}-time-picker-trigger`) - + inheritAttrs: false, + setup(_, { attrs }) { const context = inject(timePickerContext)! const { props: pickerProps, slots, - dateConfig, locale, - format, + mergedPrefixCls, + formatRef, + inputRef, inputEnableStatus, - setOverlayOpened, - commonBindings: { isDisabled, handleClear }, + controlContext: { inputValue, handleInput }, } = context - const { inputValue, handleInputChange } = inject(timePickerControl)! - const handleClick = () => { - if (pickerProps.readonly) { - return - } - - setOverlayOpened(true) - } - const placeholder = computed(() => pickerProps.placeholder ?? locale.timePicker.placeholder) + const inputSize = computed(() => Math.max(10, formatRef.value.length) + 2) + + const triggerProps = useTriggerProps(context) + const renderContent = (prefixCls: string) => { + const { readonly, disabled } = triggerProps.value + + return ( + + ) + } - const triggerProps = useCommonTriggerProps(props, context) - const renderContent = () => { + return () => { const prefixCls = mergedPrefixCls.value - if (inputEnableStatus.value.enableExternalInput) { - const onInput = (evt: Event) => { - const { value } = evt.target as HTMLInputElement - handleInputChange(value) - } - const onKeydown = withKeys(() => { - setOverlayOpened(false) - }, ['enter']) - return ( - - ) + const triggerSlots = { + default: () => renderContent(prefixCls), + suffix: slots.suffix, + clearIcon: slots.clearIcon, } - return props.value ? ( - dateConfig.format(props.value, format.value) - ) : ( - {placeholder.value} - ) + return <ɵTrigger className={prefixCls} v-slots={triggerSlots} {...triggerProps.value} {...attrs} /> } - - return () => ( - - {renderContent()} - - ) }, }) diff --git a/packages/components/time-picker/src/types.ts b/packages/components/time-picker/src/types.ts index a2f3166c7..c8fd989a6 100644 --- a/packages/components/time-picker/src/types.ts +++ b/packages/components/time-picker/src/types.ts @@ -5,80 +5,109 @@ * found in the LICENSE file at https://github.com/IDuxFE/idux/blob/main/LICENSE */ -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 { DefineComponent, HTMLAttributes } 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' import { ɵbaseTimePanelProps } from '@idux/components/_private/time-panel' -const basePickerProps = { +const timePickerCommonProps = { ...ɵbaseTimePanelProps, // v-model - open: IxPropTypes.bool, + open: { + type: Boolean, + default: undefined, + }, control: controlPropDef, - - allowInput: IxPropTypes.oneOfType([Boolean, IxPropTypes.oneOf(['overlay'])]), - autoSwap: IxPropTypes.bool.def(true), - - autofocus: IxPropTypes.bool, - borderless: IxPropTypes.bool, - clearable: IxPropTypes.bool, - clearIcon: IxPropTypes.string, - clearText: IxPropTypes.string, - format: IxPropTypes.string, - overlayClassName: IxPropTypes.string, - size: IxPropTypes.oneOf(['sm', 'md', 'lg']), - suffix: IxPropTypes.string, - target: ɵPortalTargetDef, + allowInput: { + type: [Boolean, String] as PropType, + default: undefined, + }, + autofocus: { + type: Boolean, + default: undefined, + }, + borderless: { + type: Boolean, + default: undefined, + }, + clearable: { + type: Boolean, + default: undefined, + }, + clearIcon: String, + clearText: String, + disabled: { + type: Boolean, + default: false, + }, + format: String, + overlayClassName: String, + overlayContainer: ɵPortalTargetDef, + overlayRender: Function as PropType<(children: VNode[]) => VNodeChild>, + readonly: { + type: Boolean as PropType, + default: false, + }, + size: String as PropType, + suffix: String, // 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 TimePickerCommonProps = ExtractInnerPropTypes +export interface TimePickerCommonBindings { + blur: () => void + focus: (options?: FocusOptions) => void } export const timePickerProps = { - ...basePickerProps, + ...timePickerCommonProps, // v-model - value: IxPropTypes.oneOfType([Number, String, Date]), + value: [String, Date, Number] as PropType, - defaultOpenValue: IxPropTypes.oneOfType([Number, String, Date]), - disabled: IxPropTypes.bool.def(false), - placeholder: IxPropTypes.string, - readonly: IxPropTypes.bool.def(false), + defaultOpenValue: [String, Date, Number] as PropType, + footer: { type: ɵFooterTypeDef, default: false }, + placeholder: String, // events - 'onUpdate:value': IxPropTypes.emit<(value: Date | undefined) => void>(), - onChange: IxPropTypes.emit<(value: Date | undefined) => void>(), + 'onUpdate:value': [Function, Array] as PropType void>>, + onChange: [Function, Array] as PropType void>>, + onInput: [Function, Array] as PropType void>>, } export type TimePickerProps = ExtractInnerPropTypes export type TimePickerPublicProps = ExtractPublicPropTypes export type TimePickerComponent = DefineComponent< - Omit & TimePickerPublicProps + Omit & TimePickerPublicProps, + TimePickerCommonBindings > -export type TimePickerInstance = InstanceType> +export type TimePickerInstance = InstanceType> export const timeRangePickerProps = { - ...basePickerProps, + ...timePickerCommonProps, // v-model - value: IxPropTypes.object<[number | string | Date | undefined, number | string | Date | undefined]>(), - - defaultOpenValue: IxPropTypes.object<[number | string | Date | undefined, number | string | Date | undefined]>(), - disabled: IxPropTypes.bool.def(false), - placeholder: IxPropTypes.object<[string, string]>(), - readonly: IxPropTypes.bool.def(false), - separator: IxPropTypes.oneOfType([String, IxPropTypes.vNode]), + 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, // events - 'onUpdate:value': IxPropTypes.emit<(value: [Date | undefined, Date | undefined] | undefined) => void>(), - onChange: IxPropTypes.emit<(value: [Date | undefined, Date | undefined] | undefined) => void>(), + '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 TimeRangePickerProps = ExtractInnerPropTypes @@ -87,29 +116,3 @@ export type TimeRangePickerComponent = DefineComponent< Omit & TimeRangePickerPublicProps > export type TimeRangePickerInstance = InstanceType> - -export const baseTriggerProps = { - borderless: IxPropTypes.bool, - clearable: IxPropTypes.bool, - clearIcon: IxPropTypes.string, - size: IxPropTypes.oneOf(['sm', 'md', 'lg']), - suffix: IxPropTypes.string, - disabled: IxPropTypes.bool.def(false), - readonly: IxPropTypes.bool.def(false), - focused: IxPropTypes.bool, - onFocus: IxPropTypes.emit<(evt: FocusEvent) => void>(), - onBlur: IxPropTypes.emit<(evt: FocusEvent) => void>(), - onClick: IxPropTypes.emit<(evt: Event) => void>(), - onClear: IxPropTypes.emit<(evt: MouseEvent) => void>(), -} -export type BaseTriggerProps = ExtractInnerPropTypes - -export const timePickerTriggerProps = { - value: IxPropTypes.object(), -} -export type TimePickerTriggerProps = ExtractInnerPropTypes - -export const timeRangePickerTriggerProps = { - value: IxPropTypes.object<[Date | undefined, Date | undefined]>(), -} -export type TimeRangePickerTriggerProps = ExtractInnerPropTypes diff --git a/packages/components/time-picker/src/utils.ts b/packages/components/time-picker/src/utils.ts index 32d08d42b..0d8d5de9c 100644 --- a/packages/components/time-picker/src/utils.ts +++ b/packages/components/time-picker/src/utils.ts @@ -19,3 +19,17 @@ export function convertToDate( return dateConfig.convert(value, format) } + +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/time-picker/style/index.less b/packages/components/time-picker/style/index.less index 47c40fb4c..d3ce7af1a 100644 --- a/packages/components/time-picker/style/index.less +++ b/packages/components/time-picker/style/index.less @@ -1,127 +1,77 @@ -@import '../../style/mixins/placeholder.less'; -@import '../../style/mixins/borderless.less'; -@import '../../style/mixins/reset.less'; +@import './mixin.less'; .@{time-picker-prefix} { - .reset-component(); - - width: 100%; - display: inline-block; + .time-picker-trigger(); + .time-picker-board(); } -.time-picker-trigger-size(@height, @vertical-padding, @horizontal-padding, @font-size) { - @icon-padding: @time-picker-icon-margin-left + @time-picker-icon-margin-right; - - height: @height; - padding: @vertical-padding (@icon-padding + @time-picker-font-size-sm) @vertical-padding @horizontal-padding; - font-size: @font-size; -} - -.@{time-picker-prefix}-trigger { - position: relative; - width: 100%; - line-height: @time-picker-line-height; - color: @time-picker-color; - border: @time-picker-border-width @time-picker-border-style @time-picker-border-color; - border-radius: @time-picker-border-radius; - background-color: @time-picker-background-color; - cursor: pointer; - transition: all @transition-duration-base @ease-in-out; - - @icon-padding: @time-picker-icon-margin-left + @time-picker-icon-margin-right; - - &-sm { - .time-picker-trigger-size(@time-picker-height-sm, @time-picker-padding-vertical-sm, @time-picker-padding-horizontal-sm, @time-picker-font-size-sm); - } - - &-md { - .time-picker-trigger-size(@time-picker-height-md, @time-picker-padding-vertical-md, @time-picker-padding-horizontal-md, @time-picker-font-size-md); - } - - &-lg { - .time-picker-trigger-size(@time-picker-height-lg, @time-picker-padding-vertical-lg, @time-picker-padding-horizontal-lg, @time-picker-font-size-lg); - } - - &:hover:not(&-disabled):not(&-borderless) { - border-color: @time-picker-hover-color; - } - - &-opened:not(&-disabled):not(&-borderless), - &-focused:not(&-disabled):not(&-borderless) { - border-color: @time-picker-active-color; - box-shadow: @time-picker-active-box-shadow; - } - - &-disabled { - color: @time-picker-disabled-color; - background-color: @time-picker-disabled-background-color; - cursor: not-allowed; - } - - &-disabled &-input { - cursor: not-allowed; - } - - &-borderless { - .borderless(); - } - - &-placeholder { - color: @time-picker-placeholder-color; - } +.@{time-picker-prefix}-overlay { + padding: @time-picker-overlay-padding; + width: @time-picker-overlay-width; - &-suffix, - &-clear-icon { - position: absolute; - top: 0; - right: @time-picker-icon-margin-right; - display: flex; - align-items: center; - height: 100%; - transition: opacity @transition-duration-base @ease-in-out; + &-body { + padding: @time-picker-overlay-body-padding; } - &-suffix { - color: @time-picker-icon-color; - } + &-footer { + text-align: end; + border-top: @time-picker-overlay-footer-border-width @time-picker-overlay-footer-border-style + @time-picker-overlay-footer-border-color; + padding: @time-picker-overlay-footer-padding; - &-clear-icon { - cursor: pointer; - color: @time-picker-clear-icon-color; - background-color: @time-picker-background-color; - opacity: 0; + .@{button-prefix} + .@{button-prefix} { + margin-left: @time-picker-overlay-footer-button-margin-left; + } } +} - &:not(&-disabled):hover &-clear-icon { - opacity: 1; - } +.@{time-range-picker-prefix} { + .time-picker-trigger(); + .time-picker-board(); &-input { - width: 100%; - margin: 0; - padding: 0; - background: transparent; - border: none; - outline: none; - appearance: none; - color: inherit; - font-size: inherit; - .placeholder(@time-picker-placeholder-color); + &-separator { + margin: 0 @time-range-picker-trigger-separator-margin; + } } -} -.@{time-picker-prefix}-overlay { - .reset-component(); + &-overlay { + padding: @time-range-picker-overlay-padding; + + &-body { + display: flex; + padding: @time-range-picker-overlay-body-padding; + } + + &-separator { + width: @time-range-picker-overlay-separator-width; + padding: @time-range-picker-overlay-separator-padding; + text-align: center; + color: @time-picker-color; + font-size: @time-range-picker-overlay-separator-font-size; + } + + &-footer { + text-align: end; + border-top: @time-range-picker-overlay-footer-border-width @time-range-picker-overlay-footer-border-style + @time-range-picker-overlay-footer-border-color; + padding: @time-range-picker-overlay-footer-padding; + + .@{button-prefix} + .@{button-prefix} { + margin-left: @time-range-picker-overlay-footer-button-margin-left; + } + } + } - width: @time-picker-overlay-width; - padding: @time-picker-overlay-padding; - background-color: @time-picker-overlay-background-color; - box-shadow: @time-picker-overlay-box-shadow; - font-size: @time-picker-overlay-font-size; + &-board { + width: @time-range-picker-board-width; - & &-input { - margin-bottom: @time-picker-input-margin; + &-panel { + border: @time-range-picker-board-panel-border-width @time-range-picker-board-panel-border-style + @time-range-picker-board-panel-border-color; + border-radius: @time-range-picker-board-panel-border-radius; + padding-top: 0; + padding-bottom: 0; + } } } - -@import './range.less'; diff --git a/packages/components/time-picker/style/mixin.less b/packages/components/time-picker/style/mixin.less new file mode 100644 index 000000000..70b8f1c02 --- /dev/null +++ b/packages/components/time-picker/style/mixin.less @@ -0,0 +1,41 @@ +.time-picker-trigger() { + display: inline-flex; + width: 100%; + line-height: @time-picker-line-height; + + &:not(.@{trigger-prefix}-disabled) { + color: @time-picker-color; + } + + &-input { + display: flex; + align-items: center; + &-inner { + display: inline-block; + min-width: 0; + } + } + + + &-overlay { + z-index: @time-picker-overlay-zindex; + background-color: @time-picker-overlay-background-color; + box-shadow: @time-picker-overlay-box-shadow; + } +} + +.time-picker-board() { + &-board { + .@{input-prefix}-focused { + box-shadow: none; + } + + &-input { + margin-bottom: @time-picker-overlay-input-margin-bottom; + color: @time-picker-color; + input { + color: @time-picker-color; + } + } + } +} \ No newline at end of file diff --git a/packages/components/time-picker/style/range.less b/packages/components/time-picker/style/range.less deleted file mode 100644 index 0b7a69990..000000000 --- a/packages/components/time-picker/style/range.less +++ /dev/null @@ -1,57 +0,0 @@ -.@{time-range-picker-prefix}-trigger { - .reset-component(); - - &-content { - display: flex; - - &-side { - flex: 1 1 0%; - } - - &-separator { - margin: 0 @time-range-picker-trigger-separator-margin; - } - } -} - -.@{time-range-picker-prefix}-overlay { - .reset-component(); - - background-color: @time-picker-overlay-background-color; - box-shadow: @time-picker-overlay-box-shadow; - font-size: @time-picker-overlay-font-size; - - &-content { - display: flex; - padding: @time-range-picker-overlay-padding; - } - - &-gap { - padding: @time-range-picker-overlay-gap-padding; - text-align: center; - color: @time-picker-color; - } - - &-footer { - text-align: end; - border-top: @time-picker-border-width @time-picker-border-style @time-picker-border-color; - padding: @time-picker-footer-padding; - margin: @time-picker-footer-margin; - } - - &-side { - width: @time-range-picker-overlay-side-width; - - & &-input { - margin-bottom: @time-picker-input-margin; - } - - .@{time-panel-prefix} { - border: @time-range-picker-panel-border-width @time-range-picker-panel-border-style - @time-range-picker-panel-border-color; - border-radius: @time-range-picker-panel-border-radius; - padding-top: 0; - padding-bottom: 0; - } - } -} diff --git a/packages/components/time-picker/style/themes/default.variable.less b/packages/components/time-picker/style/themes/default.variable.less index a907f3fb0..dbaa7566c 100644 --- a/packages/components/time-picker/style/themes/default.variable.less +++ b/packages/components/time-picker/style/themes/default.variable.less @@ -1,57 +1,38 @@ -@time-picker-font-size-sm: @form-font-size-sm; -@time-picker-font-size-md: @form-font-size-md; -@time-picker-font-size-lg: @form-font-size-lg; +@time-picker-color: @form-color; @time-picker-line-height: @form-line-height; -@time-picker-height-sm: @form-height-sm; -@time-picker-height-md: @form-height-md; -@time-picker-height-lg: @form-height-lg; -@time-picker-padding-horizontal-sm: @form-padding-horizontal-sm; -@time-picker-padding-horizontal-md: @form-padding-horizontal-md; -@time-picker-padding-horizontal-lg: @form-padding-horizontal-lg; -@time-picker-padding-vertical-sm: @form-padding-vertical-sm; -@time-picker-padding-vertical-md: @form-padding-vertical-md; -@time-picker-padding-vertical-lg: @form-padding-vertical-lg; - -@time-picker-border-width: @form-border-width; -@time-picker-border-style: @form-border-style; -@time-picker-border-color: @form-border-color; -@time-picker-border-radius: @border-radius-sm; -@time-picker-color: @form-color; -@time-picker-disabled-color: @form-disabled-color; +@time-range-picker-trigger-separator-margin: @spacing-xl; +@time-picker-overlay-zindex: @zindex-l4-3; @time-picker-overlay-width: 200px; -@time-picker-overlay-padding: @spacing-sm; +@time-picker-overlay-padding: @spacing-sm @spacing-sm 0 @spacing-sm; +@time-picker-overlay-body-padding: 0 0 @spacing-sm 0; @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; +@time-picker-overlay-input-margin-bottom: @spacing-sm; -@time-range-picker-overlay-padding: @spacing-lg; -@time-range-picker-trigger-separator-margin: @spacing-xl; -@time-range-picker-overlay-side-width: 184px; -@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: @time-picker-border-color; -@time-range-picker-panel-border-radius: @time-picker-border-radius; +@time-range-picker-overlay-padding: @spacing-lg @spacing-lg 0 @spacing-lg; +@time-range-picker-overlay-body-padding: 0 0 @spacing-lg 0; -@time-picker-input-margin: @spacing-sm; +@time-range-picker-overlay-board-width: 184px; +@time-range-picker-overlay-separator-width: @spacing-2xl; +@time-range-picker-overlay-separator-padding: 1px 0 0 0; +@time-range-picker-overlay-separator-font-size: @font-size-md; -@time-picker-color: @form-color; -@time-picker-color-secondary: @form-color-secondary; -@time-picker-background-color: @form-background-color; -@time-picker-placeholder-color: @form-placeholder-color; -@time-picker-hover-color: @form-hover-color; -@time-picker-active-color: @form-active-color; -@time-picker-active-box-shadow: @form-active-box-shadow; -@time-picker-disabled-color: @form-disabled-color; -@time-picker-disabled-background-color: @form-disabled-background-color; +@time-range-picker-board-width: 184px; +@time-range-picker-board-panel-border-width: @form-border-width; +@time-range-picker-board-panel-border-style: @form-border-style; +@time-range-picker-board-panel-border-color: @form-border-color; +@time-range-picker-board-panel-border-radius: @border-radius-sm; + +@time-picker-overlay-footer-border-width: @form-border-width; +@time-picker-overlay-footer-border-style: @form-border-style; +@time-picker-overlay-footer-border-color: @form-border-color; +@time-picker-overlay-footer-padding: @spacing-sm @spacing-lg; +@time-picker-overlay-footer-button-margin-left: @spacing-sm; -@time-picker-icon-font-size: @font-size-sm; -@time-picker-icon-margin-left: @spacing-xs; -@time-picker-icon-margin-right: @spacing-xs; -@time-picker-icon-color: @time-picker-placeholder-color; -@time-picker-clear-icon-color: @time-picker-color-secondary; -@time-picker-icon-background-color: @time-picker-background-color; +@time-range-picker-overlay-footer-border-width: @time-picker-overlay-footer-border-width; +@time-range-picker-overlay-footer-border-style: @time-picker-overlay-footer-border-style; +@time-range-picker-overlay-footer-border-color: @color-graphite-l30; +@time-range-picker-overlay-footer-padding: @spacing-sm 0; +@time-range-picker-overlay-footer-button-margin-left: @time-picker-overlay-footer-button-margin-left; \ No newline at end of file diff --git a/packages/components/time-picker/style/themes/seer.variable.less b/packages/components/time-picker/style/themes/seer.variable.less index 6caa4ed18..c7829c09c 100644 --- a/packages/components/time-picker/style/themes/seer.variable.less +++ b/packages/components/time-picker/style/themes/seer.variable.less @@ -1,6 +1,4 @@ @import './default.variable.less'; -@time-picker-border-color: @color-graphite-l30; -@time-picker-icon-font-size: @font-size-lg; -@time-range-picker-overlay-gap-padding: 2px 8px; -@time-picker-overlay-font-size: @font-size-sm; +@time-range-picker-overlay-separator-padding: 2px 8px; +@time-range-picker-overlay-separator-font-size: @font-size-sm; diff --git a/scripts/gen/style-variable/update.ts b/scripts/gen/style-variable/update.ts index 1a608f201..c899b0f67 100644 --- a/scripts/gen/style-variable/update.ts +++ b/scripts/gen/style-variable/update.ts @@ -1,7 +1,7 @@ import type { UpdateStyleVariableConfig } from './config' import type { Pattern } from 'fast-glob' -import { join } from 'path' +import { basename, join } from 'path' import fg from 'fast-glob' import { appendFile, lstatSync, readFile, readdir, writeFile } from 'fs-extra' @@ -46,7 +46,7 @@ class UpdateStyleVariable { } private getTheme(themeFile: string) { - return themeFile.match(/.*\/(.*)\.variable.less$/)?.[1] + return basename(themeFile, '.variable.less') } private isRequired(dir: string, component: string, curDirPath: string, config: UpdateStyleVariableConfig) {