Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Usability improvements around the picker components #3172

Merged
merged 37 commits into from
Dec 19, 2022
Merged
Show file tree
Hide file tree
Changes from 32 commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
a6f2499
Replace deprecated slot syntax
carlobeltrame Nov 15, 2022
46657bf
Fix unusual translation
carlobeltrame Nov 15, 2022
a8c22fb
Refactor and simplify pickers to be more user friendly
carlobeltrame Nov 15, 2022
e11a7a3
Allow closing pickers using the escape key
carlobeltrame Nov 16, 2022
39a9710
Disallow selecting invalid date and time combinations
carlobeltrame Nov 16, 2022
54a2a06
Take the locale into account when validating time inputs
carlobeltrame Nov 16, 2022
131fe4a
Adjust tests to removed buttons
carlobeltrame Nov 16, 2022
2e251cf
Validate value when changed, not when clicking anywhere on the page
carlobeltrame Nov 16, 2022
1ade19f
Also allow shorter versions of manually entered dates in picker
carlobeltrame Nov 16, 2022
6e034ba
Clean up handling of the many values in BasePicker
carlobeltrame Nov 17, 2022
b9d4c24
Correct documentation comments
carlobeltrame Nov 17, 2022
3192c9e
Add aria label to picker open buttons
carlobeltrame Nov 17, 2022
aee1ca8
Translate format validation error
carlobeltrame Nov 17, 2022
7bb0974
Rewrite date picker specs using testing library
carlobeltrame Nov 17, 2022
e03afb5
Reuse code, follow recommendations of VTL
carlobeltrame Nov 17, 2022
f4e3036
Improve aria text wording
carlobeltrame Nov 17, 2022
272ec2c
Rewrite time picker specs using testing library
carlobeltrame Nov 17, 2022
72e32e0
Rewrite color picker tests using testing library
carlobeltrame Nov 17, 2022
ea7826b
Improve accessibility and testability of button components
carlobeltrame Nov 18, 2022
acbf28d
Avoid double submit when clicking save button
carlobeltrame Nov 18, 2022
0073b26
The reload button should not submit the form field
carlobeltrame Nov 18, 2022
303e663
Rewrite api date picker tests using testing library
carlobeltrame Nov 18, 2022
885dd8e
Rewrite api time picker tests using testing library
carlobeltrame Nov 18, 2022
20c96b9
Rewrite api color picker tests using testing library
carlobeltrame Nov 18, 2022
d829bd0
Rework accessibility of generic button components
carlobeltrame Nov 23, 2022
f9aa961
Add tests for opening and closing the pickers
carlobeltrame Nov 23, 2022
55bb7b1
Add a test for supporting short date notation
carlobeltrame Nov 23, 2022
f3720de
Extract and unit test our date/time validations
carlobeltrame Nov 23, 2022
7f8e1f9
Display days of adjacent months in date picker, for easier selection …
carlobeltrame Nov 24, 2022
b079395
Initialize veeValidate after dayjs, to make sure dayjs is available f…
carlobeltrame Nov 24, 2022
1ae28c0
Update tests after newly displaying adjacent months' days
carlobeltrame Dec 1, 2022
b0199cc
Try to display a useful month when opening the date picker
carlobeltrame Dec 1, 2022
2ebe629
Reactivate the capability to move schedule entries to different perio…
carlobeltrame Dec 6, 2022
7dcc984
Merge remote-tracking branch 'origin/devel' into picker-fixes
carlobeltrame Dec 6, 2022
a407ffd
Simplify picker closing behaviour
carlobeltrame Dec 7, 2022
bb452e1
Use 12 hour AM/PM format in time picker depending on locale
carlobeltrame Dec 7, 2022
4eb65ff
Fix time greater_than validation for different locales
carlobeltrame Dec 7, 2022
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
2 changes: 2 additions & 0 deletions common/helpers/dayjs.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,12 @@ import utc from 'dayjs/plugin/utc'
import customParseFormat from 'dayjs/plugin/customParseFormat'
import localizedFormat from 'dayjs/plugin/localizedFormat'
import isBetween from 'dayjs/plugin/isBetween'
import duration from 'dayjs/plugin/duration'

dayjs.extend(utc)
dayjs.extend(customParseFormat)
dayjs.extend(localizedFormat)
dayjs.extend(isBetween)
dayjs.extend(duration)

export default dayjs
14 changes: 14 additions & 0 deletions frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@
"devDependencies": {
"@babel/eslint-parser": "7.19.1",
"@testing-library/jest-dom": "5.16.5",
"@testing-library/user-event": "14.4.3",
"@testing-library/vue": "5.8.3",
"@vitejs/plugin-vue2": "2.1.0",
"@vue/cli-plugin-babel": "5.0.8",
Expand Down
7 changes: 6 additions & 1 deletion frontend/src/components/buttons/ButtonBack.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
<template>
<v-btn aria-label="Zurück" icon v-bind="$attrs" @click="$router.go(-1)">
<v-btn
icon
:aria-label="$tc('global.button.back')"
v-bind="$attrs"
@click="$router.go(-1)"
>
<v-icon>mdi-arrow-left</v-icon>
</v-btn>
</template>
Expand Down
3 changes: 3 additions & 0 deletions frontend/src/components/buttons/ButtonDelete.vue
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@
<span v-if="!iconOnly" class="d-none d-sm-block">
<slot>{{ $tc('global.button.delete') }}</slot>
</span>
<span class="d-sr-only" :class="{ 'd-sm-none': !iconOnly }">
<slot>{{ $tc('global.button.delete') }}</slot>
</span>
</v-btn>
</template>

Expand Down
3 changes: 3 additions & 0 deletions frontend/src/components/buttons/ButtonEdit.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
<span class="d-none d-sm-block">
<slot>{{ $tc('global.button.edit') }}</slot>
</span>
<span class="d-sr-only d-sm-none">
<slot>{{ $tc('global.button.edit') }}</slot>
</span>
</v-btn>
</template>

Expand Down
2 changes: 2 additions & 0 deletions frontend/src/components/campAdmin/CreateCampPeriods.vue
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
:name="$tc('entity.period.fields.start')"
vee-id="start"
vee-rules="required"
:max="period.end"
:my="2"
:filled="false"
required
Expand All @@ -59,6 +60,7 @@
input-class="ml-2"
:name="$tc('entity.period.fields.end')"
vee-rules="required|greaterThanOrEqual_date:@start"
:min="period.start"
:my="2"
:filled="false"
required
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/components/campAdmin/DialogPeriodForm.vue
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,14 @@
:name="$tc('entity.period.fields.start')"
vee-id="start"
vee-rules="required"
:max="localPeriod.end"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This one depends a bit on how we progress in #3127
Depending on the edit mode this can/cannot make sense

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Absolutely. I just wanted to leave the code in a consistent state, no matter what the decision in #3127 comes out to be. I didn't want to introduce client-side validation on period create, but not on update.

/>

<e-date-picker
v-model="localPeriod.end"
:name="$tc('entity.period.fields.end')"
vee-rules="required|greaterThanOrEqual_date:@start"
:min="localPeriod.start"
/>
</div>
</template>
Expand Down
24 changes: 5 additions & 19 deletions frontend/src/components/form/api/ApiWrapperAppend.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,6 @@
<v-icon color="green" class="checkIcon" :class="checkIconAddon">
mdi-content-save
</v-icon>
<!--
<v-btn
fab
dark
depressed
x-small
color="success"
class="checkIcon"
:class="checkIconAddon">
<v-icon>mdi-content-save</v-icon>
</v-btn>
-->
</div>

<!-- Retry/Cancel button if saving failed -->
Expand All @@ -31,6 +19,7 @@
color="error"
type="submit"
class="mr-1"
:aria-label="$tc('global.button.tryagain')"
v-on="on"
@click="wrapper.on.save"
>
Expand All @@ -47,6 +36,7 @@
depressed
x-small
color="grey"
:aria-label="$tc('global.button.cancel')"
v-on="on"
@click="wrapper.on.reset"
>
Expand All @@ -68,8 +58,8 @@
color="success"
type="submit"
class="mr-1"
:aria-label="$tc('global.button.save')"
v-on="on"
@click="wrapper.on.save"
>
<v-icon>mdi-check</v-icon>
</v-btn>
Expand All @@ -84,6 +74,7 @@
depressed
x-small
color="grey"
:aria-label="$tc('global.button.cancel')"
v-on="on"
@click="wrapper.on.reset"
>
Expand All @@ -95,12 +86,7 @@
</template>

<!-- Retry button if loading failed -->
<button-retry
v-if="wrapper.hasLoadingError"
text
type="submit"
@click="wrapper.on.reload"
/>
<button-retry v-if="wrapper.hasLoadingError" text @click="wrapper.on.reload" />
</div>
</template>

Expand Down
125 changes: 57 additions & 68 deletions frontend/src/components/form/api/__tests__/ApiColorPicker.spec.js
Original file line number Diff line number Diff line change
@@ -1,101 +1,90 @@
import ApiColorPicker from '../ApiColorPicker'
import ApiWrapper from '@/components/form/api/ApiWrapper'
import Vue from 'vue'
import Vuetify from 'vuetify'
import flushPromises from 'flush-promises'
import formBaseComponents from '@/plugins/formBaseComponents'
import merge from 'lodash/merge'
import { screen, waitFor } from '@testing-library/vue'
import { render } from '@/test/renderWithVuetify.js'
import user from '@testing-library/user-event'
import { ApiMock } from '@/components/form/api/__tests__/ApiMock'
import { extend } from 'vee-validate'
import { i18n } from '@/plugins'
import { mount as mountComponent } from '@vue/test-utils'
import { regex } from 'vee-validate/dist/rules'
import { waitForDebounce } from '@/test/util'

Vue.use(Vuetify)
Vue.use(formBaseComponents)

extend('regex', regex)

describe('An ApiColorPicker', () => {
let vuetify
let wrapper
let apiMock

const fieldName = 'test-field/123'
const COLOR_1 = '#ff0000'
const COLOR_2 = '#ff00ff'
const FIELD_NAME = 'test-field/123'
const FIELD_LABEL = 'Test field'
const COLOR_1 = '#FF0000'
const COLOR_2 = '#FAFFAF'
const PICKER_BUTTON_LABEL_TEXT = 'Dialog öffnen um eine Farbe für Test field zu wählen'

beforeEach(() => {
vuetify = new Vuetify()
apiMock = ApiMock.create()
})

afterEach(() => {
jest.restoreAllMocks()
wrapper.destroy()
})

const mount = (options) => {
const app = Vue.component('App', {
components: { ApiColorPicker },
test('triggers api.patch and status update if input changes', async () => {
// given
apiMock.get().thenReturn(ApiMock.success(COLOR_1).forFieldName(FIELD_NAME))
apiMock.patch().thenReturn(ApiMock.success(COLOR_2))
const { container } = render(ApiColorPicker, {
props: {
fieldName: { type: String, default: fieldName },
autoSave: false,
fieldname: FIELD_NAME,
uri: 'test-field/123',
label: FIELD_LABEL,
required: true,
},
template: `
<div data-app>
<api-color-picker
:auto-save="false"
:fieldname="fieldName"
uri="test-field/123"
label="Test field"
required="true"
/>
</div>
`,
})
apiMock.get().thenReturn(ApiMock.success(COLOR_1).forFieldName(fieldName))
const defaultOptions = {
mocks: {
$tc: () => {},
api: apiMock.getMocks(),
},
}
return mountComponent(app, {
vuetify,
i18n,
attachTo: document.body,
...merge(defaultOptions, options),
})
}

test('triggers api.patch and status update if input changes', async () => {
apiMock.patch().thenReturn(ApiMock.success(COLOR_2))
wrapper = mount()

await flushPromises()

const input = wrapper.find('input')
await input.setValue(COLOR_2)
await input.trigger('submit')

await waitForDebounce()
await flushPromises()

expect(apiMock.getMocks().patch).toBeCalledTimes(1)
expect(wrapper.findComponent(ApiWrapper).vm.localValue).toBe(COLOR_2)
// when
// click the button to open the picker
await user.click(screen.getByLabelText(PICKER_BUTTON_LABEL_TEXT))
// click inside the color picker canvas to select a different color
const canvas = container.querySelector('canvas')
await user.click(canvas, { clientX: 10, clientY: 10 })
// click the save button
await user.click(screen.getByLabelText('Speichern'))

// then
await waitFor(async () => {
const inputField = await screen.findByLabelText(FIELD_LABEL)
expect(inputField.value).toBe(COLOR_2)
expect(apiMock.getMocks().patch).toBeCalledTimes(1)
})
})

test('updates state if value in store is refreshed and has new value', async () => {
wrapper = mount()
apiMock.get().thenReturn(ApiMock.success(COLOR_2).forFieldName(fieldName))

wrapper.findComponent(ApiWrapper).vm.reload()
// given
apiMock.get().thenReturn(ApiMock.networkError().forFieldName(FIELD_NAME))
render(ApiColorPicker, {
props: {
autoSave: false,
fieldname: FIELD_NAME,
uri: 'test-field/123',
label: FIELD_LABEL,
required: true,
},
mocks: {
api: apiMock.getMocks(),
},
})
await screen.findByText('A network error occurred.')
expect((await screen.findByLabelText(FIELD_LABEL)).value).not.toBe(COLOR_1)
const retryButton = await screen.findByText('Erneut versuchen')
apiMock.get().thenReturn(ApiMock.success(COLOR_1).forFieldName(FIELD_NAME))

await waitForDebounce()
await flushPromises()
// when
await user.click(retryButton)

expect(wrapper.findComponent(ApiWrapper).vm.localValue).toBe(COLOR_2)
expect(wrapper.find('input[type=text]').element.value).toBe(COLOR_2)
// then
await waitFor(async () => {
expect((await screen.findByLabelText(FIELD_LABEL)).value).toBe(COLOR_1)
})
})
})
Loading