mirror of
https://github.com/solidtime-io/solidtime.git
synced 2026-08-14 19:22:14 +01:00
fix focus state for dropdowns, fix taborder for timerange select in timetracker and timeentryrows
This commit is contained in:
@@ -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'),
|
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user