Skip to content

Commit

Permalink
Refactor date chips
Browse files Browse the repository at this point in the history
RISDEV-3075
  • Loading branch information
FabioTacke committed Jan 18, 2024
1 parent a847bbc commit cc77a1e
Show file tree
Hide file tree
Showing 4 changed files with 111 additions and 458 deletions.
2 changes: 1 addition & 1 deletion frontend/src/fields/caselaw/coreDataFields.ts
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,7 @@ export const coreDataFields: InputField[] = [
"Entscheidungsdatum",
"Entscheidungsdatum",
),
child: defineChipsField(
child: defineChipsDateField(
"deviatingDecisionDates",
"Abweichendes Entscheidungsdatum",
"Abweichendes Entscheidungsdatum",
Expand Down
335 changes: 37 additions & 298 deletions frontend/src/shared/components/input/ChipsDateInput.vue
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
<script lang="ts" setup>
import dayjs from "dayjs"
import customParseFormat from "dayjs/plugin/customParseFormat"
import { vMaska, MaskaDetail } from "maska"
import { ref, watch, computed } from "vue"
import { computed } from "vue"
import ChipsInput from "@/shared/components/input/ChipsInput.vue"
import { ValidationError } from "@/shared/components/input/types"
import IconClear from "~icons/ic/baseline-clear"
const props = defineProps<Props>()
const emit = defineEmits<{
Expand All @@ -15,323 +14,63 @@ const emit = defineEmits<{
interface Props {
id: string
value?: string[]
modelValue?: string[]
ariaLabel: string
placeholder?: string
isFutureDate?: boolean
validationError?: ValidationError
}
const chips = ref<string[]>(props.modelValue ?? [])
const currentInput = ref<string | undefined>()
const currentInputField = ref<HTMLInputElement>()
const focusedItemIndex = ref<number>()
const containerRef = ref<HTMLElement>()
watch(
props,
() =>
(chips.value = props.modelValue
? props.modelValue.map((value) => dayjs(value).format("DD.MM.YYYY"))
: []),
{
immediate: true,
},
)
watch(
() => props.modelValue,
(is) => {
chips.value = is
? is.map((value) => dayjs(value, "YYYY-MM-DD", true).format("DD.MM.YYYY"))
const chips = computed<string[]>({
get: () => {
return props.modelValue
? props.modelValue.map((value) =>
dayjs(value, "YYYY-MM-DD", true).format("DD.MM.YYYY"),
)
: []
},
)
function updateModelValue() {
emit(
"update:modelValue",
chips.value.length === 0
? undefined
: chips.value.map((value) =>
dayjs(value, "DD.MM.YYYY", true).format("YYYY-MM-DD"),
),
)
}
function saveChip() {
if (!hasError.value && currentInput.value && currentInput.value.length > 0) {
chips.value.push(currentInput.value)
updateModelValue()
currentInput.value = undefined
resetFocus()
}
}
function deleteChip(index: number) {
chips.value.splice(index, 1)
updateModelValue()
resetFocus()
}
function resetFocus() {
currentInputField.value?.blur()
focusedItemIndex.value = undefined
currentInputField.value?.focus()
}
function backspaceDelete() {
if (currentInput.value === undefined) {
chips.value.splice(chips.value.length - 1)
updateModelValue()
resetFocus()
} else currentInput.value = undefined
}
function enterDelete() {
if (focusedItemIndex.value !== undefined) {
currentInput.value = undefined
chips.value.splice(focusedItemIndex.value, 1)
// bring focus on second last item if last item was deleted
if (focusedItemIndex.value === chips.value.length) {
focusPrevious()
}
if (focusedItemIndex.value === 0) {
resetFocus()
}
}
updateModelValue()
}
const focusPrevious = () => {
if (
(currentInput.value && currentInput.value.length > 0) ||
focusedItemIndex.value === 0
) {
return
}
focusedItemIndex.value =
focusedItemIndex.value === undefined
? chips.value.length - 1
: focusedItemIndex.value - 1
const prev = containerRef.value?.children[
focusedItemIndex.value
] as HTMLElement
if (prev) prev.focus()
}
const focusNext = () => {
if (
(currentInput.value && currentInput.value.length > 0) ||
focusedItemIndex.value === undefined
) {
return
}
if (focusedItemIndex.value == chips.value.length - 1) {
resetFocus()
return
}
focusedItemIndex.value =
focusedItemIndex.value === undefined ? 0 : focusedItemIndex.value + 1
const next = containerRef.value?.children[
focusedItemIndex.value
] as HTMLElement
if (next) next.focus()
}
const setFocusedItemIndex = (index: number) => {
focusedItemIndex.value = index
}
const inputCompleted = ref<boolean>(false)
dayjs.extend(customParseFormat)
const isValidDate = computed(() => {
return dayjs(currentInput.value, "DD.MM.YYYY", true).isValid()
})
const isInPast = computed(() => {
if (props.isFutureDate) return true
return dayjs(currentInput.value, "DD.MM.YYYY", true).isBefore(dayjs())
})
const onMaska = (event: CustomEvent<MaskaDetail>) => {
inputCompleted.value = event.detail.completed
}
const hasError = computed(
() =>
props.validationError ||
(inputCompleted.value && !isInPast.value && !props.isFutureDate) ||
(inputCompleted.value && !isValidDate.value),
)
set: (newValue: string[]) => {
const lastValue = newValue[newValue.length - 1]
const conditionalClasses = computed(() => ({
input__error: props.validationError ?? hasError.value,
}))
const isValidDate = dayjs(lastValue, "DD.MM.YYYY", true).isValid()
function validateInput() {
if (inputCompleted.value) {
if (isValidDate.value) {
if (isValidDate) {
// if valid date, check for future dates
if (!isInPast.value && !props.isFutureDate && isValidDate.value)
const isInFuture = dayjs(lastValue, "DD.MM.YYYY", true).isAfter(dayjs())
if (isInFuture) {
emit("update:validationError", {
message:
"Das " + props.ariaLabel + " darf nicht in der Zukunft liegen",
message: props.ariaLabel + " darf nicht in der Zukunft liegen",
instance: props.id,
})
else emit("update:validationError", undefined)
return
} else {
emit("update:validationError", undefined)
}
} else {
emit("update:validationError", {
message: "Kein valides Datum",
instance: props.id,
})
return
}
} else if (currentInput.value) {
emit("update:validationError", {
message: "Unvollständiges Datum",
instance: props.id,
})
} else {
emit("update:validationError", undefined)
}
}
function inputDelete() {
emit("update:validationError", undefined)
}
function onBlur() {
validateInput()
}
watch(inputCompleted, () => {
validateInput()
emit(
"update:modelValue",
newValue.length === 0
? undefined
: newValue.map((value) =>
dayjs(value, "DD.MM.YYYY", true).format("YYYY-MM-DD"),
),
)
},
})
dayjs.extend(customParseFormat)
</script>

<template>
<div class="input bg-white" :class="conditionalClasses">
<div ref="containerRef" class="flex flex-row flex-wrap" tabindex="-1">
<!-- eslint-disable-next-line vuejs-accessibility/no-static-element-interactions -->
<div
v-for="(chip, i) in chips"
:key="i"
aria-label="chip"
class="chip ds-body-01-reg bg-blue-500"
tabindex="0"
@click="setFocusedItemIndex(i)"
@keydown.delete="backspaceDelete"
@keypress.enter="enterDelete"
@keyup.left="focusPrevious"
@keyup.right="focusNext"
>
<div class="label-wrapper">{{ chip }}</div>

<button
aria-Label="Löschen"
class="icon-wrapper"
@click="deleteChip(i)"
@keydown.enter="deleteChip(i)"
>
<IconClear />
</button>
</div>
</div>

<input
:id="id"
ref="currentInputField"
v-model="currentInput"
v-maska
:aria-label="ariaLabel"
:class="conditionalClasses"
data-maska="##.##.####"
placeholder="TT.MM.JJJJ"
@blur="onBlur"
@keydown.delete="inputDelete"
@keypress.enter="saveChip"
@keyup.left="focusPrevious"
@keyup.right="focusNext"
@maska="onMaska"
/>
</div>
<ChipsInput
:id="id"
v-model="chips"
:aria-label="ariaLabel"
maska="##.##.####"
/>
</template>

<style lang="scss" scoped>
.input {
display: flex;
width: 100%;
min-height: 3.75rem;
flex-wrap: wrap;
align-content: space-between;
padding: 12px 16px 4px;
@apply border-2 border-solid border-blue-800 uppercase;
&:focus {
outline: none;
}
&:autofill {
@apply text-inherit shadow-white;
}
&:autofill:focus {
@apply text-inherit shadow-white;
}
&__error {
width: 100%;
@apply border-red-800 bg-red-200;
&:autofill {
@apply text-inherit shadow-error;
}
&:autofill:focus {
@apply text-inherit shadow-error;
}
}
.chip {
display: flex;
align-items: center;
border-radius: 10px;
margin: 0 8px 8px 0;
.icon-wrapper {
display: flex;
padding: 4px 3px;
border-radius: 0 10px 10px 0;
}
.label-wrapper {
display: flex;
padding: 3px 0 3px 8px;
margin-right: 8px;
}
&:focus {
outline: none;
.icon-wrapper {
@apply bg-blue-900;
color: white;
}
}
}
input {
min-width: 8.74rem;
flex: 1 1 auto;
border: none;
margin-bottom: 8px;
outline: none;
text-transform: uppercase;
}
}
</style>
Loading

0 comments on commit cc77a1e

Please sign in to comment.