fix focus state for dropdowns, fix taborder for timerange select in timetracker and timeentryrows

This commit is contained in:
Gregor Vostrak
2025-01-29 13:16:54 +01:00
parent b783ea9ecd
commit 2dd80ba6cc
8 changed files with 68 additions and 50 deletions
+2 -5
View File
@@ -191,7 +191,7 @@ test('test that updating a the start of an existing time entry in the overview w
'time_entry_range_selector'
);
await timeEntryRangeElement.click();
await page.getByTestId('time_picker_input').first().fill('1');
await page.getByTestId('time_entry_range_start').first().fill('1');
await Promise.all([
page.waitForResponse(async (response) => {
return (
@@ -204,10 +204,7 @@ test('test that updating a the start of an existing time entry in the overview w
(await response.json()).data.end !== null
);
}),
page
.getByTestId('time_entry_range_end')
.getByTestId('time_picker_input')
.press('Enter'),
page.getByTestId('time_entry_range_end').press('Enter'),
]);
});
@@ -21,6 +21,7 @@ const activeClass = computed(() => {
<template>
<Badge
size="large"
tag="button"
:class="
twMerge(
'cursor-pointer hover:bg-card-background transition flex',
@@ -4,7 +4,6 @@ import {
flip,
limitShift,
type Placement,
type ReferenceElement,
shift,
useFloating,
} from '@floating-ui/vue';
@@ -45,6 +44,11 @@ watch(open, (value) => {
layers.value.push(id);
} else {
layers.value = layers.value.filter((layer) => layer !== id);
reference.value
?.querySelector<HTMLElement>(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
)
?.focus();
}
});
@@ -69,7 +73,7 @@ function onBackgroundClick() {
open.value = false;
}
const reference = ref<null | ReferenceElement>(null);
const reference = ref<null | HTMLElement>(null);
const floating = ref(null);
const { floatingStyles } = useFloating(reference, floating, {
placement: props.align,
@@ -61,6 +61,7 @@ watch(focused, (newValue, oldValue) => {
<TimePickerSimple
data-testid="time_entry_range_start"
tabindex="0"
@keydown.exact.tab.shift.stop.prevent="emit('close')"
:focus
@changed="updateTimeEntry"
v-model="tempStart"></TimePickerSimple>
@@ -84,6 +85,7 @@ watch(focused, (newValue, oldValue) => {
v-model="tempEnd"></DatePicker>
</div>
<div class="text-muted" v-else>-- : --</div>
<div tabindex="0" @focusin="emit('close')"></div>
</div>
</div>
</template>
@@ -152,7 +152,7 @@ function onSelectChange(event: Event) {
<div class="flex-1">
<button
@click="expanded = !expanded"
class="hidden lg:block text-muted w-[105px] px-1 py-1.5 bg-transparent text-center hover:bg-card-background rounded-lg border border-transparent hover:border-card-border text-sm font-medium focus-visible:outline-none focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:bg-tertiary">
class="hidden lg:block text-muted w-[110px] px-1 py-1.5 bg-transparent text-center hover:bg-card-background rounded-lg border border-transparent hover:border-card-border text-sm font-medium focus-visible:outline-none focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:bg-tertiary">
{{ formatStartEnd(timeEntry.start, timeEntry.end) }}
</button>
</div>
@@ -19,6 +19,11 @@ const emit = defineEmits<{
}>();
const open = ref(false);
const triggerElement = ref<HTMLButtonElement | null>(null);
function closeAndFocusButton() {
triggerElement.value?.focus();
open.value = false;
}
</script>
<template>
@@ -31,9 +36,10 @@ const open = ref(false);
<template #trigger>
<button
data-testid="time_entry_range_selector"
ref="triggerElement"
:class="
twMerge(
'text-muted w-[105px] px-2 bg-transparent text-center hover:bg-card-background rounded-lg border border-transparent hover:border-card-border focus-visible:outline-none focus:outline-none focus-visible:ring-2 focus-visible:text-text-primary focus-visible:ring-ring focus-visible:bg-tertiary',
'text-muted w-[110px] px-2 bg-transparent text-center hover:bg-card-background rounded-lg border border-transparent hover:border-card-border focus-visible:outline-none focus:outline-none focus-visible:ring-2 focus-visible:text-text-primary focus-visible:ring-ring focus-visible:bg-tertiary',
showDate
? 'text-xs py-1.5 font-semibold'
: 'text-sm py-1.5 font-medium',
@@ -53,6 +59,7 @@ const open = ref(false);
emit('changed', newStart, newEnd)
"
focus
@close="closeAndFocusButton"
:start="start"
:end="end">
</TimeRangeSelector>
@@ -6,8 +6,6 @@ import {
import { computed, defineProps, ref } from 'vue';
import parse from 'parse-duration';
import dayjs from 'dayjs';
import Dropdown from '@/packages/ui/src/Input/Dropdown.vue';
import TimeRangeSelector from '@/packages/ui/src/Input/TimeRangeSelector.vue';
const props = defineProps<{
start: string;
@@ -63,32 +61,14 @@ function selectInput(event: Event) {
</script>
<template>
<Dropdown
v-model="open"
@submit="open = false"
align="bottom"
:close-on-content-click="false">
<template #trigger>
<input
data-testid="time_entry_duration_input"
class="text-white w-[90px] px-2 py-1.5 bg-transparent text-center hover:bg-card-background rounded-lg border border-transparent hover:border-card-border text-sm font-semibold focus-visible:bg-tertiary focus-visible:border-transparent focus-visible:ring-2 focus-visible:ring-ring"
@focus="selectInput"
@keydown.tab="open = false"
@blur="updateTimerAndStartLiveTimerUpdate"
@keydown.enter="updateTimerAndStartLiveTimerUpdate"
v-model="currentTime" />
</template>
<template #content>
<TimeRangeSelector
@changed="
(newStart: string, newEnd: string) =>
emit('changed', newStart, newEnd)
"
:start="start"
:end="end">
</TimeRangeSelector>
</template>
</Dropdown>
<input
data-testid="time_entry_duration_input"
class="text-white w-[90px] px-2 py-1.5 bg-transparent text-center hover:bg-card-background rounded-lg border border-transparent hover:border-card-border text-sm font-semibold focus-visible:bg-tertiary focus-visible:border-transparent focus-visible:ring-2 focus-visible:ring-ring"
@focus="selectInput"
@keydown.tab="open = false"
@blur="updateTimerAndStartLiveTimerUpdate"
@keydown.enter="updateTimerAndStartLiveTimerUpdate"
v-model="currentTime" />
</template>
<style scoped></style>
@@ -1,6 +1,6 @@
<script setup lang="ts">
import Dropdown from '@/packages/ui/src/Input/Dropdown.vue';
import { computed, ref, watch } from 'vue';
import { computed, ref } from 'vue';
import TimeRangeSelector from '@/packages/ui/src/Input/TimeRangeSelector.vue';
import dayjs, { Dayjs } from 'dayjs';
import parse from 'parse-duration';
@@ -28,8 +28,8 @@ function pauseLiveTimerUpdate(event: FocusEvent) {
function onTimeEntryEnterPress() {
updateTimerAndStartLiveTimerUpdate();
const activeElement = document.activeElement as HTMLElement;
activeElement?.blur();
//const activeElement = document.activeElement as HTMLElement;
// activeElement?.blur();
}
const currentTime = computed({
@@ -111,6 +111,7 @@ function isHHMM(value: string): boolean {
function parseHHMM(value: string): string[] | null {
return value.match(HHMMtimeRegex);
}
const temporaryCustomTimerEntry = ref<string>('');
async function updateTimeRange(newStart: string) {
@@ -132,11 +133,31 @@ const startTime = computed(() => {
return dayjs().utc().format();
});
const inputField = ref<HTMLInputElement | null>(null);
watch(open, (isOpen) => {
if (!isOpen) {
inputField.value?.focus();
const timeRangeSelector = ref<HTMLElement | null>(null);
function openModalOnTab(e: FocusEvent) {
// check if the source is inside the dropdown
const source = e.relatedTarget as HTMLElement;
if (source && window.document.body.contains(source)) {
open.value = true;
}
});
}
function focusNextElement(e: KeyboardEvent) {
if (open.value) {
e.preventDefault();
const focusableElement = timeRangeSelector.value?.querySelector<HTMLElement>(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
focusableElement?.focus();
}
}
function closeAndFocusInput() {
inputField.value?.focus();
open.value = false;
}
</script>
<template>
@@ -150,20 +171,26 @@ watch(open, (isOpen) => {
<input
placeholder="00:00:00"
@focus="pauseLiveTimerUpdate"
@focusin="openModalOnTab"
@keydown.exact.tab="focusNextElement"
@keydown.exact.shift.tab="open = false"
ref="inputField"
data-testid="time_entry_time"
@blur="updateTimerAndStartLiveTimerUpdate"
@keydown.enter="onTimeEntryEnterPress"
v-model="currentTime"
class="w-[110px] lg:w-[130px] h-full text-white py-2.5 rounded-r-lg text-center px-4 text-base lg:text-lg font-bold bg-card-background border-none placeholder-muted focus:ring-0 transition"
class="w-[110px] lg:w-[130px] h-full text-white py-2.5 rounded-lg border-border-secondary border text-center px-4 text-base lg:text-lg font-bold bg-card-background border-none placeholder-muted focus:ring-0 transition"
type="text" />
</template>
<template #content>
<TimeRangeSelector
@changed="updateTimeRange"
:start="startTime"
:end="null">
</TimeRangeSelector>
<div ref="timeRangeSelector">
<TimeRangeSelector
@changed="updateTimeRange"
@close="closeAndFocusInput"
:start="startTime"
:end="null">
</TimeRangeSelector>
</div>
</template>
</Dropdown>
</div>