Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(VTimeInput): add new component #19709

Draft
wants to merge 3 commits into
base: dev
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
6 changes: 3 additions & 3 deletions packages/vuetify/src/composables/date/adapters/vuetify.ts
Expand Up @@ -333,13 +333,13 @@ function format (
options = { second: 'numeric' }
break
case 'fullTime':
options = { hour: 'numeric', minute: 'numeric', second: 'numeric', hour12: true }
options = { hour: 'numeric', minute: 'numeric', hour12: true }
break
case 'fullTime12h':
options = { hour: 'numeric', minute: 'numeric', second: 'numeric', hour12: true }
options = { hour: 'numeric', minute: 'numeric', hour12: true }
break
case 'fullTime24h':
options = { hour: 'numeric', minute: 'numeric', second: 'numeric', hour12: false }
options = { hour: 'numeric', minute: 'numeric', hour12: false }
break
case 'fullDateTime':
options = { year: 'numeric', month: 'long', day: 'numeric', hour: 'numeric', minute: 'numeric', second: 'numeric', hour12: true }
Expand Down
1 change: 1 addition & 0 deletions packages/vuetify/src/iconsets/mdi.ts
Expand Up @@ -23,6 +23,7 @@ const aliases: IconAliases = {
checkboxOn: 'mdi-checkbox-marked',
checkboxOff: 'mdi-checkbox-blank-outline',
checkboxIndeterminate: 'mdi-minus-box',
clock: 'mdi-clock',
delimiter: 'mdi-circle', // for carousel
sortAsc: 'mdi-arrow-up',
sortDesc: 'mdi-arrow-down',
Expand Down
139 changes: 139 additions & 0 deletions packages/vuetify/src/labs/VTimeInput/VTimeInput.tsx
@@ -0,0 +1,139 @@
// Components
import { VMenu } from '@/components/VMenu/VMenu'
import { makeVTextFieldProps, VTextField } from '@/components/VTextField/VTextField'
import { makeVConfirmEditProps, VConfirmEdit } from '@/labs/VConfirmEdit/VConfirmEdit'
import { makeVTimePickerProps, VTimePicker } from '@/labs/VTimePicker/VTimePicker'

// Composables
import { useDate } from '@/composables/date'
import { makeFocusProps, useFocus } from '@/composables/focus'
import { useProxiedModel } from '@/composables/proxiedModel'

// Utilities
import { computed, shallowRef } from 'vue'
import { genericComponent, omit, propsFactory, useRender } from '@/util'

// Types
export interface VTimeInputSlots {
default: never
}

export const makeVTimeInputProps = propsFactory({
hideActions: Boolean,

...makeFocusProps(),
...makeVConfirmEditProps(),
...makeVTextFieldProps({
placeholder: 'hh:mm',
prependIcon: '$clock',
}),
...omit(makeVTimePickerProps({
hideHeader: true,
}), ['active']),
}, 'VTimeInput')

export const VTimeInput = genericComponent()({
name: 'VTimeInput',

props: makeVTimeInputProps(),

emits: {
'update:modelValue': (val: string) => true,
},

setup (props, { slots }) {
const adapter = useDate()
const { isFocused, focus, blur } = useFocus(props)
const model = useProxiedModel(props, 'modelValue', null)
const menu = shallowRef(false)

const display = computed(() => adapter.isValid(model.value)
? adapter.format(model.value, props.format === '24hr' ? 'fullTime24h' : 'fullTime12h')
: '')

function onKeydown (e: KeyboardEvent) {
if (e.key !== 'Enter') return

if (!menu.value || !isFocused.value) {
menu.value = true

return
}

const target = e.target as HTMLInputElement

model.value = adapter.date(target.value)
}

function onClick (e: MouseEvent) {
e.preventDefault()
e.stopPropagation()

menu.value = true
}

function onSave () {
menu.value = false
}

useRender(() => {
const confirmEditProps = VConfirmEdit.filterProps(props)
const timePickerProps = VTimePicker.filterProps(omit(props, ['active']))
const textFieldProps = VTextField.filterProps(props)

return (
<VTextField
{ ...textFieldProps }
modelValue={ display.value }
onKeydown={ onKeydown }
focused={ menu.value || isFocused.value }
onFocus={ focus }
onBlur={ blur }
onClick:control={ onClick }
onClick:prepend={ onClick }
>
<VMenu
v-model={ menu.value }
activator="parent"
min-width="0"
closeOnContentClick={ false }
openOnClick={ false }
>
<VConfirmEdit
{ ...confirmEditProps }
v-model={ model.value }
onSave={ onSave }
>
{{
default: ({ actions, model: proxyModel }) => {
return (
<VTimePicker
{ ...timePickerProps }
modelValue={ props.hideActions ? model.value : proxyModel.value }
onUpdate:modelValue={ val => {
if (!props.hideActions) {
proxyModel.value = val
} else {
model.value = val
}
}}
onMousedown={ (e: MouseEvent) => e.preventDefault() }
>
{{
actions: !props.hideActions ? () => actions : undefined,
}}
</VTimePicker>
)
},
}}
</VConfirmEdit>
</VMenu>

{ slots.default?.() }
</VTextField>
)
})
},
})

export type VTimeInput = InstanceType<typeof VTimeInput>
1 change: 1 addition & 0 deletions packages/vuetify/src/labs/VTimeInput/index.ts
@@ -0,0 +1 @@
export { VTimeInput } from './VTimeInput'
3 changes: 3 additions & 0 deletions packages/vuetify/src/labs/VTimePicker/VTimePicker.sass
Expand Up @@ -9,3 +9,6 @@
.v-picker-title
padding: 0
margin-bottom: 20px

.v-picker__body
padding: 0
5 changes: 3 additions & 2 deletions packages/vuetify/src/labs/VTimePicker/VTimePicker.tsx
Expand Up @@ -295,18 +295,19 @@ export const VTimePicker = genericComponent<VTimePickerSlots>()({
return (
<VPicker
{ ...pickerProps }
hide-header={ undefined }
color={ undefined }
class={[
'v-time-picker',
props.class,
]}
style={ props.style }
v-slots={{
title: () => slots.title?.() ?? (
title: () => slots.title?.() ?? (!props.hideHeader && (
<div class="v-time-picker__title">
{ t(props.title) }
</div>
),
)) ?? undefined,
header: () => (
<VTimePickerControls
{ ...timePickerControlsProps }
Expand Down
Expand Up @@ -15,7 +15,7 @@
color: rgb(var(--v-theme-on-surface-variant))

.v-time-picker-clock
margin: 0 auto
margin: 24px auto
background: rgb(var(--v-theme-surface-light))
border-radius: 50%
position: relative
Expand Down
Expand Up @@ -11,7 +11,7 @@
padding-bottom: 4px
// padding-inline-start: 6px
// padding-inline-end: 12px
margin-bottom: 36px
margin-bottom: 12px

&__text
padding-bottom: 12px
Expand All @@ -22,6 +22,11 @@
direction: ltr
justify-content: center

&.v-time-picker-hide-controls
width: 100%
justify-content: end
padding-right: 24px

&__btn.v-btn--density-default.v-btn
width: $time-picker-contols-btn-width
height: $time-picker-contols-btn-height
Expand Down
123 changes: 64 additions & 59 deletions packages/vuetify/src/labs/VTimePicker/VTimePickerControls.tsx
Expand Up @@ -53,71 +53,76 @@ export const VTimePickerControls = genericComponent()({
<div
class={{
'v-time-picker-controls__time': true,
'v-time-picker-hide-controls': props.hideHeader,
'v-time-picker-controls__time--with-seconds': props.useSeconds,
}}
>
<VBtn
active={ props.selecting === 1 }
color={ props.selecting === 1 ? props.color : undefined }
variant="tonal"
class={{
'v-time-picker-controls__time__btn': true,
'v-time-picker-controls__time--with-ampm__btn': props.ampm,
'v-time-picker-controls__time--with-seconds__btn': props.useSeconds,
}}
text={ props.hour == null ? '--' : pad(`${hour}`) }
onClick={ () => emit('update:selecting', SelectingTimes.Hour) }
/>

<span
class={[
'v-time-picker-controls__time__separator',
{ 'v-time-picker-controls--with-seconds__time__separator': props.useSeconds },
]}
>:</span>

<VBtn
active={ props.selecting === 2 }
color={ props.selecting === 2 ? props.color : undefined }
class={{
'v-time-picker-controls__time__btn': true,
'v-time-picker-controls__time__btn__active': props.selecting === 2,
'v-time-picker-controls__time--with-ampm__btn': props.ampm,
'v-time-picker-controls__time--with-seconds__btn': props.useSeconds,
}}
variant="tonal"
text={ props.minute == null ? '--' : pad(props.minute) }
onClick={ () => emit('update:selecting', SelectingTimes.Minute) }
/>

{
props.useSeconds && (
<span
class={[
'v-time-picker-controls__time__separator',
{ 'v-time-picker-controls--with-seconds__time__separator': props.useSeconds },
]}
key="secondsDivider"
>:</span>
)
}
!props.hideHeader && (
<div key="timePickerTimeBtns">
<VBtn
active={ props.selecting === 1 }
color={ props.selecting === 1 ? props.color : undefined }
variant="tonal"
class={{
'v-time-picker-controls__time__btn': true,
'v-time-picker-controls__time--with-ampm__btn': props.ampm,
'v-time-picker-controls__time--with-seconds__btn': props.useSeconds,
}}
text={ props.hour == null ? '--' : pad(`${hour}`) }
onClick={ () => emit('update:selecting', SelectingTimes.Hour) }
/>

{
props.useSeconds && (
<VBtn
key="secondsVal"
variant="tonal"
onClick={ () => emit('update:selecting', SelectingTimes.Second) }
class={{
'v-time-picker-controls__time__btn': true,
'v-time-picker-controls__time__btn__active': props.selecting === 3,
'v-time-picker-controls__time--with-seconds__btn': props.useSeconds,
}}
text={ props.second == null ? '--' : pad(props.second) }
/>
)
}
<span
class={[
'v-time-picker-controls__time__separator',
{ 'v-time-picker-controls--with-seconds__time__separator': props.useSeconds },
]}
>:</span>

<VBtn
active={ props.selecting === 2 }
color={ props.selecting === 2 ? props.color : undefined }
class={{
'v-time-picker-controls__time__btn': true,
'v-time-picker-controls__time__btn__active': props.selecting === 2,
'v-time-picker-controls__time--with-ampm__btn': props.ampm,
'v-time-picker-controls__time--with-seconds__btn': props.useSeconds,
}}
variant="tonal"
text={ props.minute == null ? '--' : pad(props.minute) }
onClick={ () => emit('update:selecting', SelectingTimes.Minute) }
/>

{
props.useSeconds && (
<span
class={[
'v-time-picker-controls__time__separator',
{ 'v-time-picker-controls--with-seconds__time__separator': props.useSeconds },
]}
key="secondsDivider"
>:</span>
)
}

{
props.useSeconds && (
<VBtn
key="secondsVal"
variant="tonal"
onClick={ () => emit('update:selecting', SelectingTimes.Second) }
class={{
'v-time-picker-controls__time__btn': true,
'v-time-picker-controls__time__btn__active': props.selecting === 3,
'v-time-picker-controls__time--with-seconds__btn': props.useSeconds,
}}
text={ props.second == null ? '--' : pad(props.second) }
/>
)
}
</div>
)}
{
props.ampm && (
<div
Expand Down
2 changes: 1 addition & 1 deletion packages/vuetify/src/labs/VTimePicker/_variables.scss
@@ -1,6 +1,6 @@
@forward '../VPicker/variables';

$time-picker-padding: 24px !default;
$time-picker-padding: 0px !default;
$time-picker-contols-btn-font: 56px !default;
$time-picker-contols-btn-height: 80px !default;
$time-picker-contols-btn-width: 96px !default;
Expand Down