mirror of
https://github.com/element-plus/element-plus.git
synced 2025-01-18 10:59:10 +08:00
refactor(components): [time-select] switch to script-setup syntax (#7833)
* refactor(components): [time-select] switch to script-setup syntax * fix(components): [time-select] fix typing * fix(components): [time-select] fix props reviews * fix(components): [time-select] fix props and build Co-authored-by: metanas <matanas@pre-history.com>
This commit is contained in:
parent
66a6d0dc70
commit
1069e9ff34
@ -35,7 +35,6 @@
|
||||
"packages/components/table-column/",
|
||||
"packages/components/table-v2/",
|
||||
"packages/components/time-picker/",
|
||||
"packages/components/time-select/",
|
||||
"packages/components/timeline/",
|
||||
"packages/components/timeline-item/",
|
||||
"packages/components/tooltip/",
|
||||
|
@ -52,7 +52,7 @@ describe('TimeSelect', () => {
|
||||
input.trigger('focus')
|
||||
await nextTick()
|
||||
expect(document.querySelector('.selected')).toBeDefined()
|
||||
expect(document.querySelector('.selected').textContent).toBe('14:30')
|
||||
expect(document.querySelector('.selected')?.textContent).toBe('14:30')
|
||||
})
|
||||
|
||||
it('set minTime', async () => {
|
||||
@ -73,7 +73,7 @@ describe('TimeSelect', () => {
|
||||
input.trigger('focus')
|
||||
await nextTick()
|
||||
const elm = document.querySelector('.is-disabled')
|
||||
expect(elm.textContent).toBe('14:30')
|
||||
expect(elm?.textContent).toBe('14:30')
|
||||
})
|
||||
|
||||
it('set value update', async () => {
|
||||
@ -86,12 +86,10 @@ describe('TimeSelect', () => {
|
||||
|
||||
expect(input.exists()).toBe(true)
|
||||
expect(input.element.value).toBe('10:00')
|
||||
// wrapper.setData is not supported until version 2.0.0-beta.8
|
||||
// change value directly on `wrapper.vm`
|
||||
const vm = wrapper.vm as any
|
||||
vm.value = '10:30'
|
||||
|
||||
wrapper.setData({ value: '10:30' })
|
||||
await nextTick()
|
||||
expect(vm.value).toBe('10:30')
|
||||
expect(wrapper.vm.value).toBe('10:30')
|
||||
expect(input.element.value).toBe('10:30')
|
||||
})
|
||||
|
||||
@ -109,8 +107,8 @@ describe('TimeSelect', () => {
|
||||
.findAllComponents(Option)
|
||||
.find((w) => w.text().trim() === '11:00')
|
||||
|
||||
expect(option.exists()).toBe(true)
|
||||
option.trigger('click')
|
||||
expect(option?.exists()).toBe(true)
|
||||
option?.trigger('click')
|
||||
await nextTick()
|
||||
expect(vm.value).toBe('11:00')
|
||||
expect(input.element.value).toBe('11:00')
|
||||
@ -160,7 +158,7 @@ describe('TimeSelect', () => {
|
||||
await nextTick()
|
||||
|
||||
const popperEl = document.querySelector('.el-select__popper')
|
||||
const attr = popperEl.getAttribute('aria-hidden')
|
||||
const attr = popperEl?.getAttribute('aria-hidden')
|
||||
expect(attr).toEqual('false')
|
||||
})
|
||||
|
||||
@ -176,7 +174,7 @@ describe('TimeSelect', () => {
|
||||
await nextTick()
|
||||
|
||||
const popperEl = document.querySelector('.el-select__popper')
|
||||
const attr = popperEl.getAttribute('aria-hidden')
|
||||
const attr = popperEl?.getAttribute('aria-hidden')
|
||||
expect(attr).toEqual('true')
|
||||
})
|
||||
|
||||
@ -196,7 +194,7 @@ describe('TimeSelect', () => {
|
||||
await input.trigger('click')
|
||||
await nextTick()
|
||||
const option = document.querySelector('.el-select-dropdown__item')
|
||||
expect(option.textContent).toBe('01:00 PM')
|
||||
expect(option?.textContent).toBe('01:00 PM')
|
||||
})
|
||||
|
||||
describe('form item accessibility integration', () => {
|
||||
|
55
packages/components/time-select/src/time-select.ts
Normal file
55
packages/components/time-select/src/time-select.ts
Normal file
@ -0,0 +1,55 @@
|
||||
import { buildProps, definePropType } from '@element-plus/utils'
|
||||
import { CircleClose, Clock } from '@element-plus/icons-vue'
|
||||
import { useSizeProp } from '@element-plus/hooks'
|
||||
import type TimeSelect from './time-select.vue'
|
||||
import type { Component, ExtractPropTypes, PropType } from 'vue'
|
||||
|
||||
export const timeSelectProps = buildProps({
|
||||
format: {
|
||||
type: String,
|
||||
default: 'HH:mm',
|
||||
},
|
||||
modelValue: String,
|
||||
disabled: Boolean,
|
||||
editable: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
effect: {
|
||||
type: String as PropType<'light' | 'dark' | string>,
|
||||
default: 'light',
|
||||
},
|
||||
clearable: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
size: useSizeProp,
|
||||
placeholder: String,
|
||||
start: {
|
||||
type: String,
|
||||
default: '09:00',
|
||||
},
|
||||
end: {
|
||||
type: String,
|
||||
default: '18:00',
|
||||
},
|
||||
step: {
|
||||
type: String,
|
||||
default: '00:30',
|
||||
},
|
||||
minTime: String,
|
||||
maxTime: String,
|
||||
name: String,
|
||||
prefixIcon: {
|
||||
type: definePropType<string | Component>([String, Object]),
|
||||
default: () => Clock,
|
||||
},
|
||||
clearIcon: {
|
||||
type: definePropType<string | Component>([String, Object]),
|
||||
default: () => CircleClose,
|
||||
},
|
||||
} as const)
|
||||
|
||||
export type TimeSelectProps = ExtractPropTypes<typeof timeSelectProps>
|
||||
|
||||
export type TimeSelectInstance = InstanceType<typeof TimeSelect>
|
@ -30,204 +30,90 @@
|
||||
</el-select>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent, ref } from 'vue'
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref } from 'vue'
|
||||
import dayjs from 'dayjs'
|
||||
import customParseFormat from 'dayjs/plugin/customParseFormat.js'
|
||||
import customParseFormat from 'dayjs/plugin/customParseFormat'
|
||||
import ElSelect from '@element-plus/components/select'
|
||||
import ElIcon from '@element-plus/components/icon'
|
||||
import { CircleClose, Clock } from '@element-plus/icons-vue'
|
||||
import { useNamespace } from '@element-plus/hooks'
|
||||
import { timeSelectProps } from './time-select'
|
||||
import { compareTime, formatTime, nextTime, parseTime } from './utils'
|
||||
|
||||
import { componentSizes } from '@element-plus/constants'
|
||||
import type { Component, PropType } from 'vue'
|
||||
dayjs.extend(customParseFormat)
|
||||
|
||||
const { Option: ElOption } = ElSelect
|
||||
|
||||
interface Time {
|
||||
hours: number
|
||||
minutes: number
|
||||
}
|
||||
|
||||
const parseTime = (time: string): null | Time => {
|
||||
const values = (time || '').split(':')
|
||||
if (values.length >= 2) {
|
||||
let hours = Number.parseInt(values[0], 10)
|
||||
const minutes = Number.parseInt(values[1], 10)
|
||||
const timeUpper = time.toUpperCase()
|
||||
if (timeUpper.includes('AM') && hours === 12) {
|
||||
hours = 0
|
||||
} else if (timeUpper.includes('PM') && hours !== 12) {
|
||||
hours += 12
|
||||
}
|
||||
return {
|
||||
hours,
|
||||
minutes,
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
const compareTime = (time1: string, time2: string): number => {
|
||||
const value1 = parseTime(time1)
|
||||
const value2 = parseTime(time2)
|
||||
const minutes1 = value1.minutes + value1.hours * 60
|
||||
const minutes2 = value2.minutes + value2.hours * 60
|
||||
if (minutes1 === minutes2) {
|
||||
return 0
|
||||
}
|
||||
return minutes1 > minutes2 ? 1 : -1
|
||||
}
|
||||
const padTime = (time: number | string) => {
|
||||
return `${time}`.padStart(2, '0')
|
||||
}
|
||||
const formatTime = (time: Time): string => {
|
||||
return `${padTime(time.hours)}:${padTime(time.minutes)}`
|
||||
}
|
||||
const nextTime = (time: string, step: string): string => {
|
||||
const timeValue = parseTime(time)
|
||||
const stepValue = parseTime(step)
|
||||
const next = {
|
||||
hours: timeValue.hours,
|
||||
minutes: timeValue.minutes,
|
||||
}
|
||||
next.minutes += stepValue.minutes
|
||||
next.hours += stepValue.hours
|
||||
next.hours += Math.floor(next.minutes / 60)
|
||||
next.minutes = next.minutes % 60
|
||||
return formatTime(next)
|
||||
}
|
||||
|
||||
export default defineComponent({
|
||||
defineOptions({
|
||||
name: 'ElTimeSelect',
|
||||
components: { ElSelect, ElOption, ElIcon },
|
||||
model: {
|
||||
prop: 'value',
|
||||
event: 'change',
|
||||
},
|
||||
props: {
|
||||
format: {
|
||||
type: String,
|
||||
default: 'HH:mm',
|
||||
},
|
||||
modelValue: String,
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
editable: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
effect: {
|
||||
type: String as PropType<'light' | 'dark' | string>,
|
||||
default: 'light',
|
||||
},
|
||||
clearable: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
size: {
|
||||
type: String as PropType<ComponentSize>,
|
||||
values: componentSizes,
|
||||
default: '',
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
start: {
|
||||
type: String,
|
||||
default: '09:00',
|
||||
},
|
||||
end: {
|
||||
type: String,
|
||||
default: '18:00',
|
||||
},
|
||||
step: {
|
||||
type: String,
|
||||
default: '00:30',
|
||||
},
|
||||
minTime: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
maxTime: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
prefixIcon: {
|
||||
type: [String, Object] as PropType<string | Component>,
|
||||
default: Clock,
|
||||
},
|
||||
clearIcon: {
|
||||
type: [String, Object] as PropType<string | Component>,
|
||||
default: CircleClose,
|
||||
},
|
||||
},
|
||||
emits: ['change', 'blur', 'focus', 'update:modelValue'],
|
||||
setup(props) {
|
||||
const nsInput = useNamespace('input')
|
||||
const select = ref(null)
|
||||
})
|
||||
|
||||
const value = computed(() => props.modelValue)
|
||||
const start = computed(() => {
|
||||
const time = parseTime(props.start)
|
||||
return formatTime(time)
|
||||
})
|
||||
const end = computed(() => {
|
||||
const time = parseTime(props.end)
|
||||
return formatTime(time)
|
||||
})
|
||||
const step = computed(() => {
|
||||
const time = parseTime(props.step)
|
||||
return formatTime(time)
|
||||
})
|
||||
const minTime = computed(() => {
|
||||
const time = parseTime(props.minTime)
|
||||
return time ? formatTime(time) : null
|
||||
})
|
||||
const maxTime = computed(() => {
|
||||
const time = parseTime(props.maxTime)
|
||||
return time ? formatTime(time) : null
|
||||
})
|
||||
const items = computed(() => {
|
||||
const result = []
|
||||
if (props.start && props.end && props.step) {
|
||||
let current = start.value
|
||||
let currentTime
|
||||
while (compareTime(current, end.value) <= 0) {
|
||||
currentTime = dayjs(current, 'HH:mm').format(props.format)
|
||||
result.push({
|
||||
value: currentTime,
|
||||
disabled:
|
||||
compareTime(current, minTime.value || '-1:-1') <= 0 ||
|
||||
compareTime(current, maxTime.value || '100:100') >= 0,
|
||||
})
|
||||
current = nextTime(current, step.value)
|
||||
}
|
||||
}
|
||||
return result
|
||||
})
|
||||
const blur = () => {
|
||||
select.value?.blur?.()
|
||||
}
|
||||
const focus = () => {
|
||||
select.value?.focus?.()
|
||||
}
|
||||
defineEmits(['change', 'blur', 'focus', 'update:modelValue'])
|
||||
|
||||
return {
|
||||
nsInput,
|
||||
select,
|
||||
value,
|
||||
items,
|
||||
blur,
|
||||
focus,
|
||||
const props = defineProps(timeSelectProps)
|
||||
|
||||
const nsInput = useNamespace('input')
|
||||
const select = ref<typeof ElSelect>()
|
||||
|
||||
const value = computed(() => props.modelValue)
|
||||
const start = computed(() => {
|
||||
const time = parseTime(props.start)
|
||||
return time ? formatTime(time) : null
|
||||
})
|
||||
|
||||
const end = computed(() => {
|
||||
const time = parseTime(props.end)
|
||||
return time ? formatTime(time) : null
|
||||
})
|
||||
|
||||
const step = computed(() => {
|
||||
const time = parseTime(props.step)
|
||||
return time ? formatTime(time) : null
|
||||
})
|
||||
|
||||
const minTime = computed(() => {
|
||||
const time = parseTime(props.minTime || '')
|
||||
return time ? formatTime(time) : null
|
||||
})
|
||||
|
||||
const maxTime = computed(() => {
|
||||
const time = parseTime(props.maxTime || '')
|
||||
return time ? formatTime(time) : null
|
||||
})
|
||||
|
||||
const items = computed(() => {
|
||||
const result: { value: string; disabled: boolean }[] = []
|
||||
if (props.start && props.end && props.step) {
|
||||
let current = start.value
|
||||
let currentTime: string
|
||||
while (current && end.value && compareTime(current, end.value) <= 0) {
|
||||
currentTime = dayjs(current, 'HH:mm').format(props.format)
|
||||
result.push({
|
||||
value: currentTime,
|
||||
disabled:
|
||||
compareTime(current, minTime.value || '-1:-1') <= 0 ||
|
||||
compareTime(current, maxTime.value || '100:100') >= 0,
|
||||
})
|
||||
current = nextTime(current, step.value!)
|
||||
}
|
||||
},
|
||||
}
|
||||
return result
|
||||
})
|
||||
|
||||
const blur = () => {
|
||||
select.value?.blur?.()
|
||||
}
|
||||
|
||||
const focus = () => {
|
||||
select.value?.focus?.()
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
blur,
|
||||
focus,
|
||||
})
|
||||
</script>
|
||||
|
62
packages/components/time-select/src/utils.ts
Normal file
62
packages/components/time-select/src/utils.ts
Normal file
@ -0,0 +1,62 @@
|
||||
interface Time {
|
||||
hours: number
|
||||
minutes: number
|
||||
}
|
||||
|
||||
export const parseTime = (time: string): null | Time => {
|
||||
const values = (time || '').split(':')
|
||||
if (values.length >= 2) {
|
||||
let hours = Number.parseInt(values[0], 10)
|
||||
const minutes = Number.parseInt(values[1], 10)
|
||||
const timeUpper = time.toUpperCase()
|
||||
if (timeUpper.includes('AM') && hours === 12) {
|
||||
hours = 0
|
||||
} else if (timeUpper.includes('PM') && hours !== 12) {
|
||||
hours += 12
|
||||
}
|
||||
return {
|
||||
hours,
|
||||
minutes,
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
export const compareTime = (time1: string, time2: string): number => {
|
||||
const value1 = parseTime(time1)
|
||||
if (!value1) return -1
|
||||
const value2 = parseTime(time2)
|
||||
if (!value2) return -1
|
||||
const minutes1 = value1.minutes + value1.hours * 60
|
||||
const minutes2 = value2.minutes + value2.hours * 60
|
||||
if (minutes1 === minutes2) {
|
||||
return 0
|
||||
}
|
||||
return minutes1 > minutes2 ? 1 : -1
|
||||
}
|
||||
|
||||
export const padTime = (time: number | string) => {
|
||||
return `${time}`.padStart(2, '0')
|
||||
}
|
||||
export const formatTime = (time: Time): string => {
|
||||
return `${padTime(time.hours)}:${padTime(time.minutes)}`
|
||||
}
|
||||
|
||||
export const nextTime = (time: string, step: string): string => {
|
||||
const timeValue = parseTime(time)
|
||||
if (!timeValue) return ''
|
||||
|
||||
const stepValue = parseTime(step)
|
||||
if (!stepValue) return ''
|
||||
|
||||
const next = {
|
||||
hours: timeValue.hours,
|
||||
minutes: timeValue.minutes,
|
||||
}
|
||||
next.minutes += stepValue.minutes
|
||||
next.hours += stepValue.hours
|
||||
next.hours += Math.floor(next.minutes / 60)
|
||||
next.minutes = next.minutes % 60
|
||||
return formatTime(next)
|
||||
}
|
Loading…
Reference in New Issue
Block a user