mirror of
https://github.com/solidtime-io/solidtime.git
synced 2026-05-07 20:32:26 +00:00
fix focus state for dropdowns, fix taborder for timerange select in timetracker and timeentryrows
This commit is contained in:
+2
-5
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user