Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions frontend/src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@
</div>
</template>
<template v-else>
<a
href="#main-content"
class="skip-to-content"
>
{{ $t('misc.skipToContent') }}
</a>
<template v-if="showAuthLayout">
<AppHeader />
<ContentAuth />
Expand Down
3 changes: 3 additions & 0 deletions frontend/src/components/base/BaseCheckbox.vue
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
class="is-sr-only"
:checked="modelValue"
:disabled="disabled || undefined"
:aria-label="ariaLabel"
@change="(event) => emit('update:modelValue', (event.target as HTMLInputElement).checked)"
>
<slot />
Expand All @@ -22,8 +23,10 @@
withDefaults(defineProps<{
modelValue?: boolean,
disabled: boolean,
ariaLabel?: string,
}>(), {
modelValue: false,
ariaLabel: undefined,
})

const emit = defineEmits<{
Expand Down
1 change: 1 addition & 0 deletions frontend/src/components/home/AddToHomeScreen.vue
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
{{ $t('home.addToHomeScreen') }}
</p>
<BaseButton
:aria-label="$t('misc.closeBanner')"
class="hide-button"
@click="() => hideMessage = true"
>
Expand Down
6 changes: 3 additions & 3 deletions frontend/src/components/home/AppHeader.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
<RouterLink
:to="{ name: 'home' }"
class="logo-link"
:aria-label="$t('navigation.overview')"
:aria-label="$t('navigation.home')"
>
<Logo
width="164"
Expand All @@ -21,9 +21,9 @@
v-if="currentProject?.id"
class="project-title-wrapper"
>
<h1 class="project-title">
<span class="project-title">
{{ currentProject.title === '' ? $t('misc.loading') : getProjectTitle(currentProject) }}
</h1>
</span>

<BaseButton
v-if="!isEditorContentEmpty(currentProject.description)"
Expand Down
4 changes: 4 additions & 0 deletions frontend/src/components/home/ContentAuth.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
<div class="content-auth">
<BaseButton
v-show="menuActive"
:aria-label="$t('navigation.closeSidebar')"
class="menu-hide-button d-print-none"
@click="baseStore.setMenuActive(false)"
>
Expand All @@ -22,6 +23,7 @@
/>
<Navigation class="d-print-none" />
<main
id="main-content"
class="app-content"
:class="[
{ 'is-menu-enabled': menuActive },
Expand All @@ -31,6 +33,7 @@
>
<BaseButton
v-show="menuActive"
:aria-label="$t('navigation.closeSidebar')"
class="mobile-overlay d-print-none"
@click="baseStore.setMenuActive(false)"
/>
Expand All @@ -50,6 +53,7 @@
:enabled="typeof currentModal !== 'undefined'"
variant="scrolling"
class="task-detail-view-modal"
:aria-label="$t('task.detail.title')"
@close="closeModal()"
>
<component
Expand Down
1 change: 1 addition & 0 deletions frontend/src/components/home/DemoMode.vue
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ const enabled = computed(() => configStore.demoModeEnabled && !hide.value)
<strong class="is-uppercase">{{ $t('demo.everythingWillBeDeleted') }}</strong>
</p>
<BaseButton
:aria-label="$t('misc.closeBanner')"
class="hide-button"
@click="() => hide = true"
>
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/home/Navigation.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
<RouterLink
:to="{name: 'home'}"
class="logo"
:aria-label="$t('navigation.overview')"
:aria-label="$t('navigation.home')"
>
<Logo
width="164"
Expand Down
1 change: 1 addition & 0 deletions frontend/src/components/input/ColorPicker.vue
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
type="color"
:list="colorListID"
:class="{'is-empty': isEmpty}"
:aria-label="$t('input.projectColor')"
>
<svg
v-show="isEmpty"
Expand Down
5 changes: 4 additions & 1 deletion frontend/src/components/input/FancyCheckbox.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
}"
:disabled="disabled"
:model-value="modelValue"
:aria-label="ariaLabel"
@update:modelValue="value => emit('update:modelValue', value)"
>
<CheckboxIcon class="fancy-checkbox__icon" />
Expand All @@ -26,10 +27,12 @@ import BaseCheckbox from '@/components/base/BaseCheckbox.vue'
withDefaults(defineProps<{
modelValue: boolean,
disabled?: boolean,
isBlock?: boolean
isBlock?: boolean,
ariaLabel?: string,
}>(), {
disabled: false,
isBlock: false,
ariaLabel: undefined,
})

const emit = defineEmits<{
Expand Down
17 changes: 15 additions & 2 deletions frontend/src/components/input/FormField.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@
import {computed, useSlots, useId, ref} from 'vue'

interface Props {
modelValue?: string | number

Check warning on line 5 in frontend/src/components/input/FormField.vue

View workflow job for this annotation

GitHub Actions / Test / frontend-lint

Prop 'modelValue' requires default value to be set
label?: string

Check warning on line 6 in frontend/src/components/input/FormField.vue

View workflow job for this annotation

GitHub Actions / Test / frontend-lint

Prop 'label' requires default value to be set
error?: string | null

Check warning on line 7 in frontend/src/components/input/FormField.vue

View workflow job for this annotation

GitHub Actions / Test / frontend-lint

Prop 'error' requires default value to be set
id?: string

Check warning on line 8 in frontend/src/components/input/FormField.vue

View workflow job for this annotation

GitHub Actions / Test / frontend-lint

Prop 'id' requires default value to be set
disabled?: boolean
loading?: boolean
layout?: 'stacked' | 'two-col'
Expand Down Expand Up @@ -35,6 +35,7 @@
const generatedId = useId()

const inputId = computed(() => props.id ?? generatedId)
const errorId = computed(() => props.error ? `${inputId.value}-error` : undefined)
const hasAddon = computed(() => !!slots.addon)

const fieldClasses = computed(() => [
Expand Down Expand Up @@ -82,13 +83,18 @@
class="two-col"
>
<span>{{ label }}</span>
<slot :id="inputId">
<slot
:id="inputId"
:error-id="errorId"
>
<input
:id="inputId"
ref="inputRef"
v-bind="{ ...$attrs, ...inputBindings }"
:class="inputClasses"
:disabled="disabled || undefined"
:aria-invalid="error ? true : undefined"
:aria-describedby="errorId"
@input="handleInput"
>
</slot>
Expand All @@ -109,13 +115,18 @@
{{ label }}
</label>
<div :class="controlClasses">
<slot :id="inputId">
<slot
:id="inputId"
:error-id="errorId"
>
<input
:id="inputId"
ref="inputRef"
v-bind="{ ...$attrs, ...inputBindings }"
:class="inputClasses"
:disabled="disabled || undefined"
:aria-invalid="error ? true : undefined"
:aria-describedby="errorId"
@input="handleInput"
>
</slot>
Expand All @@ -129,7 +140,9 @@
</template>
<p
v-if="error"
:id="errorId"
class="help is-danger"
role="alert"
>
{{ error }}
</p>
Expand Down
5 changes: 5 additions & 0 deletions frontend/src/components/input/FormInput.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@
import {computed, ref, useId} from 'vue'

interface Props {
modelValue?: string | number | Date | null

Check warning on line 5 in frontend/src/components/input/FormInput.vue

View workflow job for this annotation

GitHub Actions / Test / frontend-lint

Prop 'modelValue' requires default value to be set
modelModifiers?: {number?: boolean}
id?: string

Check warning on line 7 in frontend/src/components/input/FormInput.vue

View workflow job for this annotation

GitHub Actions / Test / frontend-lint

Prop 'id' requires default value to be set
disabled?: boolean
loading?: boolean
error?: string | null

Check warning on line 10 in frontend/src/components/input/FormInput.vue

View workflow job for this annotation

GitHub Actions / Test / frontend-lint

Prop 'error' requires default value to be set
}

const props = withDefaults(defineProps<Props>(), {
Expand All @@ -22,6 +22,7 @@

const fallbackId = useId()
const inputId = computed(() => props.id ?? fallbackId)
const errorId = computed(() => props.error ? `${inputId.value}-error` : undefined)

const inputClasses = computed(() => [
'input',
Expand Down Expand Up @@ -67,11 +68,15 @@
v-bind="{ ...$attrs, ...inputBindings }"
:class="inputClasses"
:disabled="disabled || undefined"
:aria-invalid="error ? true : undefined"
:aria-describedby="errorId"
@input="handleInput"
>
<p
v-if="error"
:id="errorId"
class="help is-danger"
role="alert"
>
{{ error }}
</p>
Expand Down
5 changes: 5 additions & 0 deletions frontend/src/components/input/FormSelect.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,12 @@
| {value: string | number, label: string, disabled?: boolean}

interface Props {
modelValue?: string | number | null

Check warning on line 10 in frontend/src/components/input/FormSelect.vue

View workflow job for this annotation

GitHub Actions / Test / frontend-lint

Prop 'modelValue' requires default value to be set
modelModifiers?: {number?: boolean}
id?: string

Check warning on line 12 in frontend/src/components/input/FormSelect.vue

View workflow job for this annotation

GitHub Actions / Test / frontend-lint

Prop 'id' requires default value to be set
disabled?: boolean
loading?: boolean
error?: string | null

Check warning on line 15 in frontend/src/components/input/FormSelect.vue

View workflow job for this annotation

GitHub Actions / Test / frontend-lint

Prop 'error' requires default value to be set
options?: SelectOption[]
}

Expand All @@ -27,6 +27,7 @@

const fallbackId = useId()
const selectId = computed(() => props.id ?? fallbackId)
const errorId = computed(() => props.error ? `${selectId.value}-error` : undefined)

const wrapperClasses = computed(() => [
'select',
Expand Down Expand Up @@ -70,6 +71,8 @@
:id="selectId"
v-bind="{ ...$attrs, ...selectBindings }"
:disabled="disabled || undefined"
:aria-invalid="error ? true : undefined"
:aria-describedby="errorId"
@change="handleChange"
>
<template v-if="normalizedOptions">
Expand All @@ -87,7 +90,9 @@
</div>
<p
v-if="error"
:id="errorId"
class="help is-danger"
role="alert"
>
{{ error }}
</p>
Expand Down
11 changes: 9 additions & 2 deletions frontend/src/components/input/Password.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@
:placeholder="$t('user.auth.passwordPlaceholder')"
required
:type="passwordFieldType"
autocomplete="current-password"
:autocomplete="autocomplete"
:tabindex="tabindex"
:aria-invalid="isValid !== true ? true : undefined"
:aria-describedby="errorId"
@keyup.enter="e => $emit('submit', e)"
@focusout="() => {validate(); validateAfterFirst = true}"
@keyup="() => {validateAfterFirst ? validate() : null}"
Expand All @@ -25,14 +27,16 @@
</div>
<p
v-if="isValid !== true"
:id="errorId"
class="help is-danger"
role="alert"
>
{{ isValid }}
</p>
</template>

<script lang="ts" setup>
import {ref, watchEffect} from 'vue'
import {computed, ref, watchEffect} from 'vue'
import {useDebounceFn} from '@vueuse/core'
import {useI18n} from 'vue-i18n'
import BaseButton from '@/components/base/BaseButton.vue'
Expand All @@ -44,9 +48,11 @@ const props = withDefaults(defineProps<{
// This prop is a workaround to trigger validation from the outside when the user never had focus in the input.
validateInitially?: boolean,
validateMinLength?: boolean,
autocomplete?: string,
}>(), {
tabindex: undefined,
validateMinLength: true,
autocomplete: 'current-password',
})

const emit = defineEmits<{
Expand All @@ -59,6 +65,7 @@ const password = ref('')
// eslint-disable-next-line vue/no-setup-props-reactivity-loss
const isValid = ref<true | string>(props.validateInitially === true ? true : '')
const validateAfterFirst = ref(false)
const errorId = computed(() => isValid.value !== true ? 'password-error' : undefined)

const validate = useDebounceFn(() => {
const valid = validatePassword(password.value, props.validateMinLength)
Expand Down
1 change: 1 addition & 0 deletions frontend/src/components/misc/CreateEdit.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
<Modal
:overflow="true"
:wide="wide"
:aria-label="title"
@close="$router.back()"
>
<Card
Expand Down
3 changes: 3 additions & 0 deletions frontend/src/components/misc/Dropdown.vue
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
>
<BaseButton
class="dropdown-trigger is-flex"
:aria-label="triggerLabel"
@click="toggleOpen"
>
<Icon
Expand Down Expand Up @@ -49,8 +50,10 @@ import BaseButton from '@/components/base/BaseButton.vue'

withDefaults(defineProps<{
triggerIcon?: IconProp
triggerLabel?: string
}>(), {
triggerIcon: 'ellipsis-h',
triggerLabel: undefined,
})

const emit = defineEmits<{
Expand Down
14 changes: 14 additions & 0 deletions frontend/src/components/misc/Modal.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,14 @@ import {mount, flushPromises} from '@vue/test-utils'
import {nextTick} from 'vue'
import Modal from './Modal.vue'

const globalMocks = {
global: {
mocks: {
$t: (key: string) => key,
},
},
}

// jsdom does not implement HTMLDialogElement.showModal/close.
// Provide stubs so that the [open] attribute — which CSS and our tests
// check — is flipped the same way the real browser would.
Expand Down Expand Up @@ -50,6 +58,7 @@ afterEach(() => {
describe('Modal.vue — open race condition (#2590)', () => {
it('opens the dialog when enabled flips false → true', async () => {
const wrapper = mount(Modal, {
...globalMocks,
attachTo: document.body,
props: {enabled: false},
slots: {default: '<p class="test-body">hi</p>'},
Expand Down Expand Up @@ -84,6 +93,7 @@ describe('Modal.vue — open race condition (#2590)', () => {
// resolves after the first state change, the dialog must already have
// [open] set — no additional flushPromises or extra ticks required.
const wrapper = mount(Modal, {
...globalMocks,
attachTo: document.body,
props: {enabled: false},
slots: {default: '<p class="test-body">hi</p>'},
Expand Down Expand Up @@ -111,6 +121,7 @@ describe('Modal.vue — open race condition (#2590)', () => {
// nextTick callback whose timing could fire before the dialog mounted,
// skipping the showModal() call entirely and leaving .open === false.
const wrapper = mount(Modal, {
...globalMocks,
attachTo: document.body,
props: {enabled: true},
slots: {default: '<p class="test-body">hi</p>'},
Expand All @@ -132,6 +143,7 @@ describe('Modal.vue — open race condition (#2590)', () => {

it('closes the dialog when enabled flips true → false', async () => {
const wrapper = mount(Modal, {
...globalMocks,
attachTo: document.body,
props: {enabled: true},
slots: {default: '<p class="test-body">hi</p>'},
Expand Down Expand Up @@ -159,6 +171,7 @@ describe('Modal.vue — open race condition (#2590)', () => {
// element mounts. If props.enabled has flipped back to false by the
// time the mount happens, the watcher must not call showModal().
const wrapper = mount(Modal, {
...globalMocks,
attachTo: document.body,
props: {enabled: false},
slots: {default: '<p class="test-body">hi</p>'},
Expand Down Expand Up @@ -189,6 +202,7 @@ describe('Modal.vue — open race condition (#2590)', () => {
// sure openDialog() clears the leftover data-closing flag itself;
// otherwise the dialog stays stuck at opacity 0.
const wrapper = mount(Modal, {
...globalMocks,
attachTo: document.body,
props: {enabled: true},
slots: {default: '<p class="test-body">hi</p>'},
Expand Down
1 change: 1 addition & 0 deletions frontend/src/components/misc/Modal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
@mousedown.self.prevent.stop="$emit('close')"
>
<BaseButton
:aria-label="$t('misc.closeDialog')"
class="close"
@click="$emit('close')"
>
Expand Down
Loading
Loading