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

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' 'time_entry_range_selector'
); );
await timeEntryRangeElement.click(); 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([ await Promise.all([
page.waitForResponse(async (response) => { page.waitForResponse(async (response) => {
return ( 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 (await response.json()).data.end !== null
); );
}), }),
page page.getByTestId('time_entry_range_end').press('Enter'),
.getByTestId('time_entry_range_end')
.getByTestId('time_picker_input')
.press('Enter'),
]); ]);
}); });

View File

@@ -21,6 +21,7 @@ const activeClass = computed(() => {
<template> <template>
<Badge <Badge
size="large" size="large"
tag="button"
:class=" :class="
twMerge( twMerge(
'cursor-pointer hover:bg-card-background transition flex', 'cursor-pointer hover:bg-card-background transition flex',

View File

@@ -4,7 +4,6 @@ import {
flip, flip,
limitShift, limitShift,
type Placement, type Placement,
type ReferenceElement,
shift, shift,
useFloating, useFloating,
} from '@floating-ui/vue'; } from '@floating-ui/vue';
@@ -45,6 +44,11 @@ watch(open, (value) => {
layers.value.push(id); layers.value.push(id);
} else { } else {
layers.value = layers.value.filter((layer) => layer !== id); 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; open.value = false;
} }
const reference = ref<null | ReferenceElement>(null); const reference = ref<null | HTMLElement>(null);
const floating = ref(null); const floating = ref(null);
const { floatingStyles } = useFloating(reference, floating, { const { floatingStyles } = useFloating(reference, floating, {
placement: props.align, placement: props.align,

View File

@@ -61,6 +61,7 @@ watch(focused, (newValue, oldValue) => {
<TimePickerSimple <TimePickerSimple
data-testid="time_entry_range_start" data-testid="time_entry_range_start"
tabindex="0" tabindex="0"
@keydown.exact.tab.shift.stop.prevent="emit('close')"
:focus :focus
@changed="updateTimeEntry" @changed="updateTimeEntry"
v-model="tempStart"></TimePickerSimple> v-model="tempStart"></TimePickerSimple>
@@ -84,6 +85,7 @@ watch(focused, (newValue, oldValue) => {
v-model="tempEnd"></DatePicker> v-model="tempEnd"></DatePicker>
</div> </div>
<div class="text-muted" v-else>-- : --</div> <div class="text-muted" v-else>-- : --</div>
<div tabindex="0" @focusin="emit('close')"></div>
</div> </div>
</div> </div>
</template> </template>

View File

@@ -152,7 +152,7 @@ function onSelectChange(event: Event) {
<div class="flex-1"> <div class="flex-1">
<button <button
@click="expanded = !expanded" @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) }} {{ formatStartEnd(timeEntry.start, timeEntry.end) }}
</button> </button>
</div> </div>

View File

@@ -19,6 +19,11 @@ const emit = defineEmits<{
}>(); }>();
const open = ref(false); const open = ref(false);
const triggerElement = ref<HTMLButtonElement | null>(null);
function closeAndFocusButton() {
triggerElement.value?.focus();
open.value = false;
}
</script> </script>
<template> <template>
@@ -31,9 +36,10 @@ const open = ref(false);
<template #trigger> <template #trigger>
<button <button
data-testid="time_entry_range_selector" data-testid="time_entry_range_selector"
ref="triggerElement"
:class=" :class="
twMerge( 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 showDate
? 'text-xs py-1.5 font-semibold' ? 'text-xs py-1.5 font-semibold'
: 'text-sm py-1.5 font-medium', : 'text-sm py-1.5 font-medium',
@@ -53,6 +59,7 @@ const open = ref(false);
emit('changed', newStart, newEnd) emit('changed', newStart, newEnd)
" "
focus focus
@close="closeAndFocusButton"
:start="start" :start="start"
:end="end"> :end="end">
</TimeRangeSelector> </TimeRangeSelector>

View File

@@ -6,8 +6,6 @@ import {
import { computed, defineProps, ref } from 'vue'; import { computed, defineProps, ref } from 'vue';
import parse from 'parse-duration'; import parse from 'parse-duration';
import dayjs from 'dayjs'; 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<{ const props = defineProps<{
start: string; start: string;
@@ -63,32 +61,14 @@ function selectInput(event: Event) {
</script> </script>
<template> <template>
<Dropdown <input
v-model="open" data-testid="time_entry_duration_input"
@submit="open = false" 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"
align="bottom" @focus="selectInput"
:close-on-content-click="false"> @keydown.tab="open = false"
<template #trigger> @blur="updateTimerAndStartLiveTimerUpdate"
<input @keydown.enter="updateTimerAndStartLiveTimerUpdate"
data-testid="time_entry_duration_input" v-model="currentTime" />
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>
</template> </template>
<style scoped></style> <style scoped></style>

View File

@@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import Dropdown from '@/packages/ui/src/Input/Dropdown.vue'; 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 TimeRangeSelector from '@/packages/ui/src/Input/TimeRangeSelector.vue';
import dayjs, { Dayjs } from 'dayjs'; import dayjs, { Dayjs } from 'dayjs';
import parse from 'parse-duration'; import parse from 'parse-duration';
@@ -28,8 +28,8 @@ function pauseLiveTimerUpdate(event: FocusEvent) {
function onTimeEntryEnterPress() { function onTimeEntryEnterPress() {
updateTimerAndStartLiveTimerUpdate(); updateTimerAndStartLiveTimerUpdate();
const activeElement = document.activeElement as HTMLElement; //const activeElement = document.activeElement as HTMLElement;
activeElement?.blur(); // activeElement?.blur();
} }
const currentTime = computed({ const currentTime = computed({
@@ -111,6 +111,7 @@ function isHHMM(value: string): boolean {
function parseHHMM(value: string): string[] | null { function parseHHMM(value: string): string[] | null {
return value.match(HHMMtimeRegex); return value.match(HHMMtimeRegex);
} }
const temporaryCustomTimerEntry = ref<string>(''); const temporaryCustomTimerEntry = ref<string>('');
async function updateTimeRange(newStart: string) { async function updateTimeRange(newStart: string) {
@@ -132,11 +133,31 @@ const startTime = computed(() => {
return dayjs().utc().format(); return dayjs().utc().format();
}); });
const inputField = ref<HTMLInputElement | null>(null); const inputField = ref<HTMLInputElement | null>(null);
watch(open, (isOpen) => {
if (!isOpen) { const timeRangeSelector = ref<HTMLElement | null>(null);
inputField.value?.focus();
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> </script>
<template> <template>
@@ -150,20 +171,26 @@ watch(open, (isOpen) => {
<input <input
placeholder="00:00:00" placeholder="00:00:00"
@focus="pauseLiveTimerUpdate" @focus="pauseLiveTimerUpdate"
@focusin="openModalOnTab"
@keydown.exact.tab="focusNextElement"
@keydown.exact.shift.tab="open = false"
ref="inputField" ref="inputField"
data-testid="time_entry_time" data-testid="time_entry_time"
@blur="updateTimerAndStartLiveTimerUpdate" @blur="updateTimerAndStartLiveTimerUpdate"
@keydown.enter="onTimeEntryEnterPress" @keydown.enter="onTimeEntryEnterPress"
v-model="currentTime" 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" /> type="text" />
</template> </template>
<template #content> <template #content>
<TimeRangeSelector <div ref="timeRangeSelector">
@changed="updateTimeRange" <TimeRangeSelector
:start="startTime" @changed="updateTimeRange"
:end="null"> @close="closeAndFocusInput"
</TimeRangeSelector> :start="startTime"
:end="null">
</TimeRangeSelector>
</div>
</template> </template>
</Dropdown> </Dropdown>
</div> </div>