From 54b6f2275d965dae5284b38a86367c94d686640a Mon Sep 17 00:00:00 2001 From: nico Date: Thu, 23 Jan 2025 13:12:39 +0100 Subject: [PATCH 01/12] fix(forms): add description for DS title --- src/components/forms/dataset/DatasetPropertiesTextFields.vue | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/components/forms/dataset/DatasetPropertiesTextFields.vue b/src/components/forms/dataset/DatasetPropertiesTextFields.vue index ba536f29b..3777f9d58 100644 --- a/src/components/forms/dataset/DatasetPropertiesTextFields.vue +++ b/src/components/forms/dataset/DatasetPropertiesTextFields.vue @@ -15,11 +15,16 @@ const { topicsName } = useTopicsConf() +

+ Décrivez l'indicateur ou l'objet géographique correspondant. Par + exemple : " Taux d'imperméabilisation des sols " +

From b0ce262e59ee62d9d6834a0ef4db8b70ef4b118f Mon Sep 17 00:00:00 2001 From: nico Date: Tue, 11 Feb 2025 13:02:51 +0100 Subject: [PATCH 02/12] fix(a11y forms): better form accessibility add search role or tag fix type on pages with search (not always form related) --- src/components/SearchComponent.vue | 28 +++++++++++++------------ src/store/SearchStore.js | 2 +- src/views/bouquets/BouquetsListView.vue | 1 - src/views/datasets/DatasetsListView.vue | 8 +++---- vite.config.mts | 8 ++++++- 5 files changed, 26 insertions(+), 21 deletions(-) diff --git a/src/components/SearchComponent.vue b/src/components/SearchComponent.vue index 508c6dded..27c454e5f 100644 --- a/src/components/SearchComponent.vue +++ b/src/components/SearchComponent.vue @@ -90,19 +90,21 @@ const clear = () => { diff --git a/src/components/forms/bouquet/BouquetForm.vue b/src/components/forms/bouquet/BouquetForm.vue index 1029cbd29..797d8317c 100644 --- a/src/components/forms/bouquet/BouquetForm.vue +++ b/src/components/forms/bouquet/BouquetForm.vue @@ -119,6 +119,7 @@ onMounted(() => {
{
+

+   +

From 86e4de4029c3890c00de67f608ff5c8336651e23 Mon Sep 17 00:00:00 2001 From: nico Date: Tue, 11 Feb 2025 17:25:10 +0100 Subject: [PATCH 04/12] fix(a11y forms): clean DatasetAddToBouquetModal --- .../datasets/DatasetAddToBouquetModal.vue | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/src/components/datasets/DatasetAddToBouquetModal.vue b/src/components/datasets/DatasetAddToBouquetModal.vue index 3b861c9ff..5add6d8a8 100644 --- a/src/components/datasets/DatasetAddToBouquetModal.vue +++ b/src/components/datasets/DatasetAddToBouquetModal.vue @@ -11,7 +11,6 @@ import { useTopicsConf } from '@/utils/config' import { useExtras } from '@/utils/bouquet' import { useGroups } from '@/utils/bouquetGroups' -import { templateRef } from '@vueuse/core' const props = defineProps({ show: { @@ -84,14 +83,7 @@ const sortedinputErrors = computed(() => const isValid = computed(() => { if (topicsDatasetEditorialization) { - return ( - !!datasetProperties.value.title.trim() && - !!datasetProperties.value.purpose.trim() && - !!selectedBouquetId.value && - (datasetProperties.value.group - ? datasetProperties.value.group.trim().length < 100 - : true) - ) + return !formErrors.value.length } else { return !!selectedBouquetId.value } @@ -111,7 +103,7 @@ const modalActions = computed(() => { ] }) -const errorStatus = templateRef('errorStatus') +const errorStatus = useTemplateRef('errorStatus') const isSubmitted: Ref = ref(false) const onSubmit = () => { @@ -122,7 +114,7 @@ const onSubmit = () => { validateFields() if (formErrors.value.length > 0) { - errorStatus.value.focus() + errorStatus.value?.focus() } else if (isValid.value) { isSubmitted.value = false submit() From 9252d755f515b5a161c7132c820df1226e64c031 Mon Sep 17 00:00:00 2001 From: nico Date: Thu, 13 Feb 2025 14:49:39 +0100 Subject: [PATCH 05/12] fix(a11y forms): fix types --- src/components/datasets/DatasetAddToBouquetModal.vue | 3 ++- src/components/forms/dataset/DatasetEditModal.vue | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/components/datasets/DatasetAddToBouquetModal.vue b/src/components/datasets/DatasetAddToBouquetModal.vue index 5add6d8a8..43370de16 100644 --- a/src/components/datasets/DatasetAddToBouquetModal.vue +++ b/src/components/datasets/DatasetAddToBouquetModal.vue @@ -11,6 +11,7 @@ import { useTopicsConf } from '@/utils/config' import { useExtras } from '@/utils/bouquet' import { useGroups } from '@/utils/bouquetGroups' +import type { DsfrButtonGroupProps } from '@gouvminint/vue-dsfr' const props = defineProps({ show: { @@ -89,7 +90,7 @@ const isValid = computed(() => { } }) -const modalActions = computed(() => { +const modalActions: Ref = computed(() => { return [ { label: 'Annuler', diff --git a/src/components/forms/dataset/DatasetEditModal.vue b/src/components/forms/dataset/DatasetEditModal.vue index 7fafec344..deeb66203 100644 --- a/src/components/forms/dataset/DatasetEditModal.vue +++ b/src/components/forms/dataset/DatasetEditModal.vue @@ -11,6 +11,7 @@ import { } from '@/model/topic' import { useDatasetStore } from '@/store/DatasetStore' +import type { DsfrButtonGroupProps } from '@gouvminint/vue-dsfr' import DatasetPropertiesFields from './DatasetPropertiesFields.vue' export interface DatasetEditModalType { @@ -37,7 +38,7 @@ const modalData: Ref = ref({ mode: 'edit' }) -const modalActions = computed(() => { +const modalActions: Ref = computed(() => { return [ { label: 'Annuler', From 13fbab15bf37523a076ae01fe1cc902e781accd5 Mon Sep 17 00:00:00 2001 From: nico Date: Thu, 13 Feb 2025 17:37:47 +0100 Subject: [PATCH 06/12] fix(a11y forms): refactor validation create a composable for error handling create a component for error summary --- .../datasets/DatasetAddToBouquetModal.vue | 85 +++++++++---------- src/components/forms/ErrorSummary.vue | 41 +++++++++ src/utils/form.ts | 33 +++++++ 3 files changed, 116 insertions(+), 43 deletions(-) create mode 100644 src/components/forms/ErrorSummary.vue create mode 100644 src/utils/form.ts diff --git a/src/components/datasets/DatasetAddToBouquetModal.vue b/src/components/datasets/DatasetAddToBouquetModal.vue index 43370de16..33dcf723e 100644 --- a/src/components/datasets/DatasetAddToBouquetModal.vue +++ b/src/components/datasets/DatasetAddToBouquetModal.vue @@ -13,6 +13,8 @@ import { useExtras } from '@/utils/bouquet' import { useGroups } from '@/utils/bouquetGroups' import type { DsfrButtonGroupProps } from '@gouvminint/vue-dsfr' +import { useForm, type FormErrorMessage } from '@/utils/form' + const props = defineProps({ show: { type: Boolean, @@ -50,14 +52,25 @@ const bouquetOptions = computed(() => { }) }) -const inputErrorMessages = new Map([ - ['bouquetId', `Veuillez sélectionner un ${topicsName}.`], - ['group', `Le groupe est limité à 100 caractères.`], - ['title', 'Veuillez renseigner un libellé.'], - ['purpose', "La raison d'utilisation ne doit pas être vide."] -]) +const errorMessages: FormErrorMessage[] = [ + { + inputName: 'bouquetId', + message: `Veuillez sélectionner un ${topicsName}.` + }, + { inputName: 'group', message: `Le groupe est limité à 100 caractères.` }, + { inputName: 'title', message: 'Veuillez renseigner un libellé.' }, + { + inputName: 'purpose', + message: "La raison d'utilisation ne doit pas être vide." + } +] const formErrors: Ref = ref([]) +const { formErrorMessagesMap, sortedErrors } = useForm( + errorMessages, + formErrors +) + const validateFields = () => { if (!datasetProperties.value.title.trim()) { formErrors.value.push('title') @@ -76,12 +89,6 @@ const validateFields = () => { } } -const sortedinputErrors = computed(() => - Array.from(inputErrorMessages.keys()).filter((key) => - formErrors.value.includes(key) - ) -) - const isValid = computed(() => { if (topicsDatasetEditorialization) { return !formErrors.value.length @@ -104,19 +111,28 @@ const modalActions: Ref = computed(() => { ] }) -const errorStatus = useTemplateRef('errorStatus') +const errorSummary = useTemplateRef('errorSummary') const isSubmitted: Ref = ref(false) -const onSubmit = () => { +const onSubmit = async () => { // reset error fields formErrors.value = [] isSubmitted.value = true + // check input fields validateFields() + // handle error summary if (formErrors.value.length > 0) { - errorStatus.value?.focus() - } else if (isValid.value) { + const errorSummaryTitle: HTMLHeadingElement | undefined | null = + errorSummary.value?.$el.querySelector('#error-summary-title') + if (errorSummaryTitle) { + await nextTick() + errorSummaryTitle.focus() + } + } + // submit if no error + else if (isValid.value) { isSubmitted.value = false submit() } @@ -188,29 +204,13 @@ onMounted(() => { class="form" @close="closeModal" > -
-

- Il y a {{ sortedinputErrors.length }} erreurs - de saisie dans le formulaire. -

-
    -
  1. - {{ inputErrorMessages.get(error) }} -
  2. -
-
+ ref="errorSummary" + :form-error-messages-map + :form-errors="sortedErrors" + heading-level="h3" + /> { class="error" > - {{ inputErrorMessages.get('bouquetId') }} + {{ formErrorMessagesMap.get('bouquetId') }}

{ />
@@ -256,10 +255,10 @@ onMounted(() => { v-model:dataset-properties-model="datasetProperties" > diff --git a/src/components/forms/ErrorSummary.vue b/src/components/forms/ErrorSummary.vue new file mode 100644 index 000000000..57f94cfec --- /dev/null +++ b/src/components/forms/ErrorSummary.vue @@ -0,0 +1,41 @@ + + + + + diff --git a/src/utils/form.ts b/src/utils/form.ts new file mode 100644 index 000000000..96d9525e1 --- /dev/null +++ b/src/utils/form.ts @@ -0,0 +1,33 @@ +import type { ComputedRef, Ref } from 'vue' + +export type FormErrorMessage = { inputName: string; message: string } + +export type FormErrorMessagesMap = Map< + FormErrorMessage['inputName'], + FormErrorMessage['message'] +> + +export function useForm( + formErrorMessages: FormErrorMessage[], + formErrors: Ref +): { + formErrorMessagesMap: ComputedRef + sortedErrors: ComputedRef +} { + const formErrorMessagesMap = computed(() => { + return formErrorMessages.reduce( + (acc, error) => acc.set(error.inputName, error.message), + new Map() + ) + }) + const sortedErrors = computed(() => { + return Array.from(formErrorMessagesMap.value.keys()).filter((key) => + formErrors.value.includes(key) + ) + }) + + return { + formErrorMessagesMap, + sortedErrors + } +} From c38365eb46ef582258e4ce910f763c1d2b72a1bc Mon Sep 17 00:00:00 2001 From: nico Date: Mon, 17 Feb 2025 17:31:57 +0100 Subject: [PATCH 07/12] fix(a11y forms): refactor validation Create error message component Handle error messages inside composable Handle form validation inside DatasetEditModal --- .../datasets/DatasetAddToBouquetModal.vue | 57 +++++---------- src/components/forms/ErrorMessage.vue | 37 ++++++++++ src/components/forms/SelectTopicGroup.vue | 17 +++-- .../forms/dataset/DatasetEditModal.vue | 71 +++++++++++++++++-- .../forms/dataset/DatasetPropertiesFields.vue | 10 +++ .../dataset/DatasetPropertiesTextFields.vue | 31 +++++--- src/utils/form.ts | 38 ++++++++-- 7 files changed, 194 insertions(+), 67 deletions(-) create mode 100644 src/components/forms/ErrorMessage.vue diff --git a/src/components/datasets/DatasetAddToBouquetModal.vue b/src/components/datasets/DatasetAddToBouquetModal.vue index 33dcf723e..60b508077 100644 --- a/src/components/datasets/DatasetAddToBouquetModal.vue +++ b/src/components/datasets/DatasetAddToBouquetModal.vue @@ -1,9 +1,10 @@ + + + diff --git a/src/components/forms/SelectTopicGroup.vue b/src/components/forms/SelectTopicGroup.vue index 8d974dfb1..0ab646c07 100644 --- a/src/components/forms/SelectTopicGroup.vue +++ b/src/components/forms/SelectTopicGroup.vue @@ -3,6 +3,7 @@ import type { DatasetProperties, DatasetsGroups } from '@/model/topic' import Multiselect from '@vueform/multiselect' import '@vueform/multiselect/themes/default.css' +import ErrorMessage from './ErrorMessage.vue' const datasetProperties = defineModel('properties-model', { type: Object as () => DatasetProperties, @@ -27,9 +28,9 @@ defineProps({ type: Boolean, default: false }, - isErrored: { - type: Boolean, - defautl: false + errorMessage: { + type: String, + default: '' } }) @@ -77,7 +78,7 @@ const trimGroupName = (groupName: string) => { 'aria-labelledby': null, 'aria-multiselectable': null, 'aria-placeholder': null, - 'aria-invalid': `${!!$slots.errorGroup}` + 'aria-invalid': `${!!errorMessage}` }" @select="trimGroupName" > @@ -101,7 +102,9 @@ const trimGroupName = (groupName: string) => { -

-   -

+ diff --git a/src/components/forms/dataset/DatasetEditModal.vue b/src/components/forms/dataset/DatasetEditModal.vue index deeb66203..134cfc2f9 100644 --- a/src/components/forms/dataset/DatasetEditModal.vue +++ b/src/components/forms/dataset/DatasetEditModal.vue @@ -11,6 +11,7 @@ import { } from '@/model/topic' import { useDatasetStore } from '@/store/DatasetStore' +import { useForm } from '@/utils/form' import type { DsfrButtonGroupProps } from '@gouvminint/vue-dsfr' import DatasetPropertiesFields from './DatasetPropertiesFields.vue' @@ -38,6 +39,25 @@ const modalData: Ref = ref({ mode: 'edit' }) +const formErrors: Ref = ref([]) + +const { formErrorMessagesMap, sortedErrors } = useForm(formErrors) + +const validateFields = () => { + if (!modalData.value.dataset?.title.trim()) { + formErrors.value.push('title') + } + if (!modalData.value.dataset?.purpose.trim()) { + formErrors.value.push('purpose') + } + if ( + modalData.value.dataset?.group && + modalData.value.dataset?.group.trim().length > 100 + ) { + formErrors.value.push('group') + } +} + const modalActions: Ref = computed(() => { return [ { @@ -45,22 +65,55 @@ const modalActions: Ref = computed(() => { type: 'button', secondary: true, onClick: () => { - closeModal() + onCancel() } }, { label: 'Enregistrer', type: 'button', - disabled: !modalData.value.isValid, onClick: ($event: MouseEvent) => { $event.preventDefault() - submitModal(modalData.value) - closeModal() + onSubmit() } } ] }) +const errorSummary = useTemplateRef('errorSummary') +const isSubmitted: Ref = ref(false) + +const onCancel = () => { + // reset error fields + formErrors.value = [] + isSubmitted.value = false + closeModal() +} + +const onSubmit = async () => { + // reset error fields + formErrors.value = [] + isSubmitted.value = true + + // check input fields + validateFields() + + // handle error summary + if (formErrors.value.length > 0) { + const errorSummaryTitle: HTMLHeadingElement | undefined | null = + errorSummary.value?.$el.querySelector('#error-summary-title') + if (errorSummaryTitle) { + await nextTick() + errorSummaryTitle.focus() + } + } + // submit if no error + else { + isSubmitted.value = false + submitModal(modalData.value) + closeModal() + } +} + const editDataset = (dataset: DatasetProperties, index: number) => { // clone the object to enable cancellation modalData.value = { @@ -144,7 +197,7 @@ defineExpose({ addDataset, editDataset }) +
diff --git a/src/components/forms/dataset/DatasetPropertiesFields.vue b/src/components/forms/dataset/DatasetPropertiesFields.vue index fed42e354..65063bdbd 100644 --- a/src/components/forms/dataset/DatasetPropertiesFields.vue +++ b/src/components/forms/dataset/DatasetPropertiesFields.vue @@ -10,6 +10,7 @@ import { } from '@/model/topic' import { useDatasetStore } from '@/store/DatasetStore' import { useTopicsConf } from '@/utils/config' +import { useForm } from '@/utils/form' import DatasetPropertiesTextFields from './DatasetPropertiesTextFields.vue' import SelectDataset from './SelectDataset.vue' @@ -26,6 +27,11 @@ const datasetsGroups = defineModel('groups-model', { default: [] }) +const formErrors = defineModel('errors-model', { + type: Array, + default: [] +}) + defineProps({ alreadySelectedDatasets: { type: Array, @@ -36,6 +42,7 @@ defineProps({ const router = useRouter() const datasetStore = useDatasetStore() const { topicsDatasetEditorialization } = useTopicsConf() +const { getErrorMessage } = useForm(formErrors) const selectedDataset: Ref = ref(undefined) @@ -126,6 +133,8 @@ onMounted(() => {
{ v-model:groups-model="datasetsGroups" label="Regroupement" description="Rechercher ou créer un regroupement (100 caractères maximum). Un regroupement contient un ou plusieurs jeux de données." + :error-message="getErrorMessage('group')" />
diff --git a/src/components/forms/dataset/DatasetPropertiesTextFields.vue b/src/components/forms/dataset/DatasetPropertiesTextFields.vue index 4cb6514e8..5fbfa5ce0 100644 --- a/src/components/forms/dataset/DatasetPropertiesTextFields.vue +++ b/src/components/forms/dataset/DatasetPropertiesTextFields.vue @@ -7,6 +7,17 @@ const datasetProperties = defineModel('datasetProperties-model', { default: {} }) +defineProps({ + errorTitle: { + type: String, + default: '' + }, + errorPurpose: { + type: String, + default: '' + } +}) + const { topicsName } = useTopicsConf() @@ -25,11 +36,13 @@ const { topicsName } = useTopicsConf() class="fr-input" type="text" aria-describedby="errors-title title-description" - :aria-invalid="!!$slots.errorTitle" + :aria-invalid="!!errorTitle" + /> + -

-   -

diff --git a/src/utils/form.ts b/src/utils/form.ts index 96d9525e1..ede260f30 100644 --- a/src/utils/form.ts +++ b/src/utils/form.ts @@ -1,21 +1,39 @@ import type { ComputedRef, Ref } from 'vue' +import { useTopicsConf } from './config' -export type FormErrorMessage = { inputName: string; message: string } +type FormErrorMessage = { inputName: string; message: string } + +const { topicsName } = useTopicsConf() export type FormErrorMessagesMap = Map< FormErrorMessage['inputName'], FormErrorMessage['message'] > -export function useForm( - formErrorMessages: FormErrorMessage[], - formErrors: Ref -): { +// define error messages for all inputs here +const errorMessages = [ + { + inputName: 'bouquetId', + message: `Veuillez sélectionner un ${topicsName}.` + }, + { inputName: 'group', message: `Le groupe est limité à 100 caractères.` }, + { inputName: 'title', message: 'Veuillez renseigner un libellé.' }, + { + inputName: 'purpose', + message: "La raison d'utilisation ne doit pas être vide." + } +] as const + +// create a union type of available input errors +export type AllowedInput = (typeof errorMessages)[number]['inputName'] + +export function useForm(formErrors: Ref): { formErrorMessagesMap: ComputedRef sortedErrors: ComputedRef + getErrorMessage: (inputName: AllowedInput) => string } { const formErrorMessagesMap = computed(() => { - return formErrorMessages.reduce( + return errorMessages.reduce( (acc, error) => acc.set(error.inputName, error.message), new Map() ) @@ -25,9 +43,15 @@ export function useForm( formErrors.value.includes(key) ) }) + const getErrorMessage = (inputName: AllowedInput): string => { + if (formErrors.value.includes(inputName)) + return formErrorMessagesMap.value.get(inputName) + return '' + } return { formErrorMessagesMap, - sortedErrors + sortedErrors, + getErrorMessage } } From 6b1d28f22b3d2844e0e07692efde22287bbf16a6 Mon Sep 17 00:00:00 2001 From: nico Date: Tue, 18 Feb 2025 11:08:34 +0100 Subject: [PATCH 08/12] fix(a11y forms): remove comments --- src/components/forms/ErrorMessage.vue | 32 ++++----------------------- 1 file changed, 4 insertions(+), 28 deletions(-) diff --git a/src/components/forms/ErrorMessage.vue b/src/components/forms/ErrorMessage.vue index f28daa27b..7398ce3b7 100644 --- a/src/components/forms/ErrorMessage.vue +++ b/src/components/forms/ErrorMessage.vue @@ -1,32 +1,8 @@ - - - diff --git a/src/components/forms/SelectTopicGroup.vue b/src/components/forms/SelectTopicGroup.vue index 0ab646c07..dc1337523 100644 --- a/src/components/forms/SelectTopicGroup.vue +++ b/src/components/forms/SelectTopicGroup.vue @@ -73,7 +73,8 @@ const trimGroupName = (groupName: string) => { :create-option="true" placeholder="" :aria="{ - 'aria-describedby': 'errors-group regroupement-description', + 'aria-describedby': 'regroupement-description', + 'aria-errormessage': 'errors-group', // useless or unsupported https://github.com/vueform/multiselect/issues/436 'aria-labelledby': null, 'aria-multiselectable': null, diff --git a/src/components/forms/bouquet/BouquetForm.vue b/src/components/forms/bouquet/BouquetForm.vue index 797d8317c..7cb4587a2 100644 --- a/src/components/forms/bouquet/BouquetForm.vue +++ b/src/components/forms/bouquet/BouquetForm.vue @@ -179,9 +179,7 @@ onMounted(() => { v-bind="themeAttrs" class="fr-select" :aria-invalid="errors.theme && isSubmitted ? true : undefined" - :aria-describedby=" - errors.theme && isSubmitted ? 'errors-theme' : undefined - " + aria-errormessage="errors-theme" @change="subtheme = ''" >
{ :value="Availability.NOT_AVAILABLE" label="Je n'ai pas cherché la donnée" /> +
@@ -201,6 +219,12 @@ onMounted(() => { .fr-fieldset { margin: 30px 0; } +.fr-fieldset:focus { + outline-style: solid; +} +fieldset legend { + inline-size: fit-content; +} textarea { height: 150px; } diff --git a/src/components/forms/dataset/DatasetPropertiesTextFields.vue b/src/components/forms/dataset/DatasetPropertiesTextFields.vue index 5fbfa5ce0..25d74de7b 100644 --- a/src/components/forms/dataset/DatasetPropertiesTextFields.vue +++ b/src/components/forms/dataset/DatasetPropertiesTextFields.vue @@ -28,14 +28,15 @@ const { topicsName } = useTopicsConf() >

Décrivez l'indicateur ou l'objet géographique correspondant. Par - exemple : " Taux d'imperméabilisation des sols " + exemple : « Taux d'imperméabilisation des sols »

Date: Thu, 20 Feb 2025 17:20:28 +0100 Subject: [PATCH 12/12] fix(a11y forms): dataset availability --- .../forms/dataset/DatasetPropertiesFields.vue | 133 +++++++++--------- .../forms/dataset/SelectDataset.vue | 10 ++ src/utils/form.ts | 2 +- 3 files changed, 80 insertions(+), 65 deletions(-) diff --git a/src/components/forms/dataset/DatasetPropertiesFields.vue b/src/components/forms/dataset/DatasetPropertiesFields.vue index 67e449464..821e5e361 100644 --- a/src/components/forms/dataset/DatasetPropertiesFields.vue +++ b/src/components/forms/dataset/DatasetPropertiesFields.vue @@ -136,73 +136,74 @@ onMounted(() => { :error-title="getErrorMessage('title')" :error-purpose="getErrorMessage('purpose')" /> -
- -
-
-
- - Vous ne trouvez pas le jeu de données dans data.gouv.fr ? - - +
+ -
+
+
- + Vous ne trouvez pas le jeu de données dans data.gouv.fr ? + + - + + +
+ -
- - - -
+ + +
+
{ .fr-fieldset { margin: 30px 0; } -.fr-fieldset:focus { +.fr-fieldset.availability[aria-invalid='true'] { + border: none; + outline: 2px solid var(--border-plain-error); +} +#input-availability:focus { outline-style: solid; } fieldset legend { diff --git a/src/components/forms/dataset/SelectDataset.vue b/src/components/forms/dataset/SelectDataset.vue index 34ec5d8fe..1fd7ec923 100644 --- a/src/components/forms/dataset/SelectDataset.vue +++ b/src/components/forms/dataset/SelectDataset.vue @@ -20,6 +20,14 @@ const props = defineProps({ alreadySelectedDatasets: { type: Array, default: [] + }, + isInvalid: { + type: Boolean, + default: false + }, + errorMessageId: { + type: String, + default: 'errors-availability' } }) @@ -85,6 +93,8 @@ const clear = () => { no-options-text="Aucun jeu de données trouvé, précisez ou élargissez votre recherche." :aria="{ 'aria-describedby': 'dataset-description', + 'aria-errormessage': `${errorMessageId}`, + 'aria-invalid': `${isInvalid ? true : undefined}`, // useless or unsupported https://github.com/vueform/multiselect/issues/436 'aria-labelledby': null, 'aria-multiselectable': null, diff --git a/src/utils/form.ts b/src/utils/form.ts index c46234a6a..ba127a8a5 100644 --- a/src/utils/form.ts +++ b/src/utils/form.ts @@ -27,7 +27,7 @@ const errorMessages = [ }, { inputName: 'availability', - message: 'La disponibilité doit être renseignée.' + message: 'Un jeu de données ou une disponibilité doit être sélectionné.' }, { inputName: 'availabilityUrl',