Skip to content

Commit

Permalink
Remove v-calender replace with native browser input date field for ex…
Browse files Browse the repository at this point in the history
…piryDates (#11377)
  • Loading branch information
AlexAndBear authored Aug 22, 2024
1 parent 193d77a commit 6cb9068
Show file tree
Hide file tree
Showing 31 changed files with 469 additions and 1,018 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
Enhancement: Replace custom datepicker with native html element

We've replaced the custom datepicker with a native html date input element.
This change will improve the user experience and accessibility of the datepicker.

https://github.com/owncloud/web/pull/11377
https://github.com/owncloud/web/issues/11374
5 changes: 2 additions & 3 deletions packages/design-system/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,6 @@
"typescript": "5.5.3",
"url": "^0.11.3",
"url-loader": "^4.1.1",
"v-calendar": "github:dschmidt/v-calendar#3ce6e3b8afd5491cb53ee811281d5fa8a45b044d",
"vue": "3.4.21",
"vue-inline-svg": "3.1.2",
"vue-loader": "^17.4.2",
Expand All @@ -129,11 +128,11 @@
"focus-trap-vue": "^4.0.1",
"postcss-import": "^16.0.0",
"tippy.js": "^6.3.7",
"v-calendar": "github:dschmidt/v-calendar#3ce6e3b8afd5491cb53ee811281d5fa8a45b044d",
"vue": "3.4.21",
"vue-inline-svg": "3.1.2",
"vue-select": "^3.12.0",
"webfontloader": "^1.6.28"
"webfontloader": "^1.6.28",
"luxon": "3.2.1"
},
"engines": {
"node": ">= 14.0.0",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,23 +1,41 @@
import { defineComponent } from 'vue'
import Datepicker from './OcDatepicker.vue'
import { mount } from 'web-test-helpers'
import { ComponentProps, defaultPlugins, shallowMount } from 'web-test-helpers'
import { DateTime } from 'luxon'
import { nextTick } from 'vue'

const DatePickerComponent = defineComponent({
template: '<div id="foo"><slot></slot></div>'
describe('OcDatePicker', () => {
it('renders', () => {
const wrapper = getWrapper({ label: 'Datepicker label' })
expect(wrapper.html()).toMatchSnapshot()
})
it('sets the initial date correctly', async () => {
const wrapper = getWrapper({ label: 'Datepicker label', currentDate: DateTime.now() })
await nextTick()
const inputEl = wrapper.find('.oc-text-input').element as HTMLInputElement
expect(inputEl.value).toEqual(DateTime.now().toISODate())
})
it('sets the minimum date correctly', async () => {
const wrapper = getWrapper({ label: 'Datepicker label', minDate: DateTime.now() })
await nextTick()
const inputEl = wrapper.find('.oc-text-input')
expect(inputEl.attributes('min')).toEqual(DateTime.now().toISODate())
})
it('emits event on date change', async () => {
const wrapper = getWrapper({ label: 'Datepicker label' })
const inputEl = wrapper.find('.oc-text-input')
await inputEl.setValue(DateTime.now().toISODate())
expect(wrapper.emitted('dateChanged')).toBeTruthy()
})
})

describe('OcDatePicker', () => {
it('renders default scoped slot', () => {
const slotDefault = "<button id='default-slot'>Open datepicker</button>"
const wrapper = mount(Datepicker, {
slots: { default: slotDefault },
props: { value: null },
global: {
renderStubDefaultSlot: true,
stubs: { DatePicker: DatePickerComponent }
function getWrapper(props: ComponentProps<typeof Datepicker>) {
return shallowMount(Datepicker, {
props,
global: {
plugins: [...defaultPlugins()],
stubs: {
OcTextInput: false
}
})

expect(wrapper.find('#default-slot').exists()).toBeTruthy()
}
})
})
}
186 changes: 77 additions & 109 deletions packages/design-system/src/components/OcDatepicker/OcDatepicker.vue
Original file line number Diff line number Diff line change
@@ -1,145 +1,113 @@
<template>
<date-picker ref="datePicker" class="oc-datepicker" v-bind="$attrs" :popover="popperOpts">
<template #default="args">
<!-- @slot Default slot to use as the popover anchor for datepicker -->
<!-- args is undefined during initial render, hence we check it here -->
<slot
v-if="args"
:input-value="args.inputValue"
:toggle-popover="args.togglePopover"
:hide-popover="args.hidePopover"
/>
</template>
</date-picker>
<oc-text-input
v-model="dateInputString"
v-bind="$attrs"
:label="label"
type="date"
:min="minDate?.toISODate()"
:fix-message-line="true"
:error-message="errorMessage"
:clear-button-enabled="isClearable"
:clear-button-accessible-label="$gettext('Clear date')"
class="oc-date-picker"
/>
</template>

<script lang="ts">
import {
computed,
ComponentPublicInstance,
defineComponent,
defineAsyncComponent,
ref,
unref,
watch
} from 'vue'
import { Modifier } from '@popperjs/core'
import 'v-calendar/dist/style.css'
import OcSpinner from '../OcSpinner/OcSpinner.vue'
/**
* Datepicker component based on [v-calendar](https://vcalendar.io/). For detailed documentation, please visit https://vcalendar.io/vue-3.html
*/
import { computed, defineComponent, onMounted, PropType, ref, unref, watch } from 'vue'
import { useGettext } from 'vue3-gettext'
import { DateTime } from 'luxon'
export default defineComponent({
name: 'OcDatepicker',
status: 'ready',
release: '1.0.0',
props: {
label: { type: String, required: true },
isClearable: { type: Boolean, default: true },
currentDate: { type: Object as PropType<DateTime>, required: false, default: null },
minDate: { type: Object as PropType<DateTime>, required: false, default: null }
},
emits: ['dateChanged'],
setup(props, { emit }) {
const { $gettext, current } = useGettext()
const dateInputString = ref<string>('')
components: {
DatePicker: defineAsyncComponent({
loader: async () => {
const { DatePicker } = await import('v-calendar')
return DatePicker
},
loadingComponent: OcSpinner
const date = computed(() => {
const date = DateTime.fromISO(unref(dateInputString)).endOf('day')
return date.isValid ? date : null
})
},
inheritAttrs: true,
setup() {
const datePicker = ref<ComponentPublicInstance>()
const isMinDateUndercut = computed(() => {
if (!props.minDate || !unref(date)) {
return false
}
return unref(date) < props.minDate
})
const popperOpts = computed(() => {
return {
modifiers: [
{
name: 'fixVerticalPosition',
enabled: true,
phase: 'beforeWrite',
requiresIfExists: ['offset', 'flip'],
fn({ state }) {
const dropHeight =
state.modifiersData.fullHeight || state.elements.popper.offsetHeight
const rect = state.elements.popper.getBoundingClientRect()
const neededScreenSpace =
(state.elements.reference as HTMLElement).offsetHeight + rect.top + dropHeight
const errorMessage = computed(() => {
if (unref(isMinDateUndercut)) {
return $gettext('The date must be after %{date}', {
date: props.minDate
.minus({ day: 1 })
.setLocale(current)
.toLocaleString(DateTime.DATE_SHORT)
})
}
return ''
})
if (state.placement !== 'top-start' && neededScreenSpace > window.innerHeight) {
state.styles.popper.top = `-${150}px`
}
}
} as Modifier<'fixVerticalPosition', unknown>
]
onMounted(() => {
if (props.currentDate) {
dateInputString.value = props.currentDate.toISODate()
}
})
watch(
datePicker,
date,
() => {
if (unref(datePicker)) {
// for e2e tests
unref(datePicker).$el.__datePicker = unref(datePicker)
}
emit('dateChanged', { date: unref(date), error: unref(isMinDateUndercut) })
},
{ immediate: true }
{
deep: true
}
)
return { popperOpts, datePicker }
return {
dateInputString,
errorMessage
}
}
})
</script>

<style lang="scss">
.vc-pane-layout {
color: var(--oc-color-text-default) !important;
background-color: var(--oc-color-background-default) !important;
}
.vc-arrow svg path {
fill: var(--oc-color-text-default) !important;
}
.vc-title {
color: var(--oc-color-text-default) !important;
}
.vc-weekday {
color: var(--oc-color-text-muted) !important;
}
.vc-day {
color: var(--oc-color-text-default) !important;
font-weight: var(--oc-font-weight-bold);
}
.vc-highlights {
.vc-highlight {
background-color: var(--oc-color-swatch-primary-default) !important;
}
+ span {
color: var(--oc-color-text-inverse) !important;
.oc-date-picker {
input::-webkit-calendar-picker-indicator {
cursor: pointer;
}
}
.vc-day-content.is-disabled {
color: var(--oc-color-text-muted) !important;
background: none;
cursor: not-allowed;
font-weight: var(--oc-font-weight-extralight);
}
</style>
<docs>
```js
<template>
<div>
<oc-datepicker v-model="date">
<template #default="{ togglePopover }">
<oc-button @click="togglePopover">Open datepicker</oc-button>
</template>
</oc-datepicker>
<p v-if="date" v-text="date" />
</div>
<div>
<oc-datepicker :current-date="currentDate" :min-date="minDate" label="Enter or pick a date"
@date-changed="onDateChanged"/>
<p v-if="selectedDate" v-text="selectedDate"/>
</div>
</template>
<script>
export default {
data: () => ({ date: null })
}
import {DateTime} from "luxon";

export default {
data: () => ({
minDate: DateTime.now(), currentDate: DateTime.now(), selectedDate: ''
}),
methods: {
onDateChanged({date}) {
this.selectedDate = date ? date.toLocaleString(DateTime.DATE_FULL) : ''
}
}
}
</script>
```
</docs>
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html

exports[`OcDatePicker > renders 1`] = `
"<div class="oc-date-picker"><label class="oc-label" for="oc-textinput-1">Datepicker label</label>
<div class="oc-position-relative">
<!--v-if--> <input id="oc-textinput-1" aria-invalid="false" class="oc-text-input oc-input oc-rounded" type="date">
<!--v-if-->
</div>
<div class="oc-text-input-message">
<!--v-if--> <span id="oc-textinput-1-message" class=""></span>
</div>
<!--v-if-->
</div>"
`;
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
v-if="showClearButton"
:aria-label="clearButtonAccessibleLabelValue"
class="oc-pr-s oc-position-center-right oc-text-input-btn-clear"
:class="{ 'oc-mr-l': type === 'date' }"
appearance="raw"
@click="onClear"
>
Expand Down
2 changes: 1 addition & 1 deletion packages/design-system/src/styles/theme/helper.scss
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ hr {
}

// outline for visible focus (via tab)
*:focus-visible:not(input[type=text]):not(input[type=textarea]):not(input[type=search]):not(input[type=password]):not(input[type=email]) {
*:focus-visible:not(input[type=text]):not(input[type=textarea]):not(input[type=search]):not(input[type=password]):not(input[type=email]):not(input[type=date]) {
outline: 2px var(--oc-color-swatch-passive-contrast) solid;
outline-offset: 0;
box-shadow: 0 0 0 4px var(--oc-color-swatch-passive-default);
Expand Down
Loading

0 comments on commit 6cb9068

Please sign in to comment.