From 2c7739205f958d8b7fe88a2ad23f436d57987657 Mon Sep 17 00:00:00 2001 From: Nico <20305403+narduin@users.noreply.github.com> Date: Tue, 4 Feb 2025 15:17:46 +0100 Subject: [PATCH] feat: multiselect migration (#654) * feat(multiselect): migrate component All instances of vue-multiselect have been replaced by @vueform/multiselect Logic and styles have been ported. Accessibility and looks have been enhanced. * feat(multiselect): remove dep * feat(multiselect): global debounce wait time util --- configs/defis/config.yaml | 2 + configs/ecospheres/config.yaml | 2 + configs/hackathon/config.yaml | 2 + configs/logistique/config.yaml | 2 + configs/meteo-france/config.yaml | 2 + configs/simplification/config.yaml | 2 + package-lock.json | 19 +-- package.json | 3 +- src/assets/main.css | 1 + src/assets/multiselect.css | 139 ++++++------------ src/components/SearchComponent.vue | 109 +++++++------- .../forms/SelectSpatialCoverage.vue | 108 ++++++++------ src/components/forms/SelectTopicGroup.vue | 3 + .../forms/bouquet/BouquetOwnerForm.vue | 91 +++++++----- .../forms/dataset/DatasetEditModal.vue | 2 + .../forms/dataset/SelectDataset.vue | 18 +-- .../views/indicators/IndicatorsListView.vue | 3 +- src/utils/bouquetGroups.ts | 4 +- src/utils/config.ts | 3 + src/views/bouquets/BouquetFormView.vue | 5 +- src/views/bouquets/BouquetsListView.vue | 4 +- src/views/datasets/DatasetsListView.vue | 4 +- 22 files changed, 257 insertions(+), 271 deletions(-) diff --git a/configs/defis/config.yaml b/configs/defis/config.yaml index ab644bc88..407b1f08c 100644 --- a/configs/defis/config.yaml +++ b/configs/defis/config.yaml @@ -228,6 +228,8 @@ website: display: false content: Notice avec un [lien beta](/beta) et du _style_ closeable: false + # debounce high enough for accessibility (screen readers will announce results) + default_debounce_wait: 600 # display settings pagination_sizes: organizations_list: 9 diff --git a/configs/ecospheres/config.yaml b/configs/ecospheres/config.yaml index 1a4de50a4..f52b86825 100644 --- a/configs/ecospheres/config.yaml +++ b/configs/ecospheres/config.yaml @@ -129,6 +129,8 @@ website: list_search_topics: url_search_topics: oauth_option: true + # debounce high enough for accessibility (screen readers will announce results) + default_debounce_wait: 600 pagination_sizes: organizations_list: 9 topics_list: 100 diff --git a/configs/hackathon/config.yaml b/configs/hackathon/config.yaml index 2e10d5f2e..9ce62cb6e 100644 --- a/configs/hackathon/config.yaml +++ b/configs/hackathon/config.yaml @@ -111,6 +111,8 @@ website: display: false content: Notice avec un [lien beta](/beta) et du _style_ closeable: false + # debounce high enough for accessibility (screen readers will announce results) + default_debounce_wait: 600 # display settings pagination_sizes: organizations_list: 9 diff --git a/configs/logistique/config.yaml b/configs/logistique/config.yaml index 11943cad5..437f0a330 100644 --- a/configs/logistique/config.yaml +++ b/configs/logistique/config.yaml @@ -120,6 +120,8 @@ website: display: false content: Notice avec un [lien beta](/beta) et du _style_ closeable: false + # debounce high enough for accessibility (screen readers will announce results) + default_debounce_wait: 600 # display settings pagination_sizes: organizations_list: 9 diff --git a/configs/meteo-france/config.yaml b/configs/meteo-france/config.yaml index 5c7392a68..1cdc0d956 100644 --- a/configs/meteo-france/config.yaml +++ b/configs/meteo-france/config.yaml @@ -145,6 +145,8 @@ website: display: false content: Notice avec un [lien beta](/beta) et du _style_ closeable: false + # debounce high enough for accessibility (screen readers will announce results) + default_debounce_wait: 600 # display settings pagination_sizes: organizations_list: 9 diff --git a/configs/simplification/config.yaml b/configs/simplification/config.yaml index 6b607cf6a..c2d851ee6 100644 --- a/configs/simplification/config.yaml +++ b/configs/simplification/config.yaml @@ -233,6 +233,8 @@ website: display: false content: Notice avec un [lien beta](/beta) et du _style_ closeable: false + # debounce high enough for accessibility (screen readers will announce results) + default_debounce_wait: 600 # display settings pagination_sizes: organizations_list: 9 diff --git a/package-lock.json b/package-lock.json index f64e8ba24..b150b3019 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,7 +14,7 @@ "@json2csv/plainjs": "^7.0.6", "@unhead/vue": "^1.11.18", "@vee-validate/rules": "^4.15.0", - "@vueform/multiselect": "^2.6.10", + "@vueform/multiselect": "^2.6.11", "@vueuse/core": "^12.4.0", "@vueuse/integrations": "^12.4.0", "axios": "^1.7.9", @@ -27,7 +27,6 @@ "vue": "^3.5.13", "vue-loading-overlay": "^6.0.6", "vue-matomo": "^4.2.0", - "vue-multiselect": "^3.1.0", "vue-router": "^4.5.0", "vue3-text-clamp": "^0.1.2", "vue3-toastify": "^0.2.8" @@ -3430,9 +3429,9 @@ } }, "node_modules/@vueform/multiselect": { - "version": "2.6.10", - "resolved": "https://registry.npmjs.org/@vueform/multiselect/-/multiselect-2.6.10.tgz", - "integrity": "sha512-w1otA5P2F7Bg0Er9ohIsBg/Xkda/9ZzCO62PmXXmjS2mOEKWPdb/852FQbc8Gv99EYRotfxgyJ5lTC+c7YeCTA==", + "version": "2.6.11", + "resolved": "https://registry.npmjs.org/@vueform/multiselect/-/multiselect-2.6.11.tgz", + "integrity": "sha512-iG4TGfqE3baftbSGF0PhoS+xZOCnV0ChkDo9rwhJ/Qi2YlCdb6tyQCjvyug3jnzncga8+d85kx0WvG7rDYFqiA==", "license": "MIT" }, "node_modules/@vueuse/core": { @@ -14432,16 +14431,6 @@ "npm": ">= 3.0.0" } }, - "node_modules/vue-multiselect": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/vue-multiselect/-/vue-multiselect-3.1.0.tgz", - "integrity": "sha512-+i/fjTqFBpaay9NP+lU7obBeNaw2DdFDFs4mqhsM0aEtKRdvIf7CfREAx2o2B4XDmPrBt1r7x1YCM3BOMLaUgQ==", - "license": "MIT", - "engines": { - "node": ">= 14.18.1", - "npm": ">= 6.14.15" - } - }, "node_modules/vue-router": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.5.0.tgz", diff --git a/package.json b/package.json index 0b57fe47c..600b17fd5 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ "@json2csv/plainjs": "^7.0.6", "@unhead/vue": "^1.11.18", "@vee-validate/rules": "^4.15.0", - "@vueform/multiselect": "^2.6.10", + "@vueform/multiselect": "^2.6.11", "@vueuse/core": "^12.4.0", "@vueuse/integrations": "^12.4.0", "axios": "^1.7.9", @@ -39,7 +39,6 @@ "vue": "^3.5.13", "vue-loading-overlay": "^6.0.6", "vue-matomo": "^4.2.0", - "vue-multiselect": "^3.1.0", "vue-router": "^4.5.0", "vue3-text-clamp": "^0.1.2", "vue3-toastify": "^0.2.8" diff --git a/src/assets/main.css b/src/assets/main.css index a591a94b1..a4c8d2ede 100644 --- a/src/assets/main.css +++ b/src/assets/main.css @@ -5,6 +5,7 @@ --blue-france-950-100: var(--blue-cumulus-950-100); --blue-france-950-100-hover: var(--blue-cumulus-950-100-hover); --blue-france-950-100-active: var(--blue-cumulus-950-100-active); + --blue-france-sun-113: #000091; --blue-france-sun-113-625: var(--blue-cumulus-sun-368-moon-732); --blue-france-sun-113-625-hover: var(--blue-cumulus-sun-368-moon-732-hover); --blue-france-sun-113-625-active: var(--blue-cumulus-sun-368-moon-732-active); diff --git a/src/assets/multiselect.css b/src/assets/multiselect.css index 2a99dbd12..e083762ec 100644 --- a/src/assets/multiselect.css +++ b/src/assets/multiselect.css @@ -1,108 +1,67 @@ -.multiselect__option, -.multiselect__single { - white-space: normal; - h4 { - font-size: 1.2rem; +.multiselect { + --ms-max-height: 20rem; + --ms-bg: var(--background-contrast-grey); + --ms-spinner-color: var(--blue-cumulus-sun-368-moon-732); + --ms-option-bg-selected: var(--blue-cumulus-sun-368-moon-732); + --ms-option-bg-pointed: var(--blue-cumulus-sun-368-moon-732); + --ms-option-color-pointed: var(--background-contrast-grey); + --ms-option-bg-selected-pointed: var(--blue-cumulus-sun-368-moon-732-hover); + &&, + &&.is-active { + border: none; + box-shadow: none; } -} -.multiselect__option--highlight { - background-color: var(--background-alt-grey-active); - color: var(--text-default-grey); -} -.multiselect__option--highlight::after { - background-color: var(--background-alt-grey-active); - color: var(--text-default-grey); -} -.multiselect__spinner::before, -.multiselect__spinner::after { - border-color: var(--background-alt-grey-active) transparent transparent; - top: 60%; -} -.multiselect__select { - --data-uri-svg: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='%23161616' d='m12 13.1 5-4.9 1.4 1.4-6.4 6.3-6.4-6.4L7 8.1l5 5z'/%3E%3C/svg%3E"); - background-image: var(--data-uri-svg); - background-position: calc(100% - 1rem) 50%; - background-repeat: no-repeat; - background-size: 1rem 1rem; - .multiselect--active & { + .multiselect-caret { + --ms-px: 1rem; + --data-uri-svg: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='%23161616' d='m12 13.1 5-4.9 1.4 1.4-6.4 6.3-6.4-6.4L7 8.1l5 5z'/%3E%3C/svg%3E"); + -webkit-mask-image: var(--data-uri-svg); + mask-image: var(--data-uri-svg); + width: 1.1rem; + transition: none; + } + .multiselect-caret.is-open { transform: none; + transition: none; } -} -.multiselect__select::before { - content: unset; -} -.multiselect__clear { - position: absolute; - right: 15px; - height: 40px; - width: 40px; - display: block; - cursor: pointer; - z-index: 3; + .multiselect-option { + position: relative; - &:before, - &:after { - content: ''; - display: block; - position: absolute; - width: 3px; - height: 16px; - background-color: var(--border-plain-grey); - top: 30%; - right: 4px; - } + &:has(button) { + padding: 0; + } - &:before { - transform: rotate(45deg); - } + button { + --hover-tint: inherit; + --active-tint: var(--background-action-high-blue-france-active); + padding: 0.75rem; + block-size: 100%; + inline-size: 100%; + text-align: left; - &:after { - transform: rotate(-45deg); + &:focus { + outline-offset: -4px; + } + } + + &.is-pointed { + button:focus { + outline-color: var(--background-default-grey); + } + } + } + .multiselect-dropdown { + overflow-y: auto; } -} -.multiselect { - --ms-bg: var(--background-contrast-grey); } .multiselect:has(.multiselect-single-label .card) { --ms-bg: var(--background-default-grey); + border: 1px solid var(--background-contrast-grey); } .multiselect-wrapper { --ms-radius: 0.25rem 0.25rem 0 0; --ms-caret-color: var(--border-plain-grey); } +/* dsfr-input style */ .multiselect-search { box-shadow: inset 0 -2px 0 0 var(--border-plain-grey); } -.multiselect__tags { - --idle: transparent; - --hover: var(--background-contrast-grey-hover); - --active: var(--background-contrast-grey-active); - appearance: none; - -moz-appearance: none; - -webkit-appearance: none; - background-color: var(--background-contrast-grey); - border: 0; - box-shadow: inset 0 -2px 0 0 var(--border-plain-grey); - color: var(--text-title-grey); - display: block; - font-size: 1rem; - line-height: 1.5rem; - padding: 0.5rem 2.5rem 0.5rem 1rem; - width: 100%; - border-radius: 0.25rem 0.25rem 0 0; - & > .multiselect__single { - padding: 0; - background-color: var(--background-contrast-grey); - } -} -.multiselect__tags:focus-within { - outline: 2px solid #0a76f6; - outline-offset: -2px; -} -.multiselect__input { - padding: 0; - background-color: transparent; -} -.multiselect__placeholder { - color: var(--text-default-grey); -} diff --git a/src/components/SearchComponent.vue b/src/components/SearchComponent.vue index 14cee1a09..508c6dded 100644 --- a/src/components/SearchComponent.vue +++ b/src/components/SearchComponent.vue @@ -1,8 +1,10 @@ @@ -103,43 +113,52 @@ const dropdownLabel = (text: string) => { /> - - Rechercher. Saisissez un mot clé puis choisissez une des options situés - après le champ pour lancer la recherche dans la rubrique souhaitée + Rechercher + + Saisissez un mot clé puis choisissez une des options situés après le champ + pour lancer la recherche dans la rubrique souhaitée + - + + + {{ query }} + + + + - + + - - {{ placeholder }} - @@ -155,27 +174,24 @@ const dropdownLabel = (text: string) => { :deep(.fr-label.invisible + .filter-input) { margin: 0; } -:deep(.filter-input) { +:deep(.filter-input), +.select-search :deep(input), +:deep(.multiselect-placeholder) { padding-inline-start: calc(var(--icon-width) + 1rem); } .select-search { position: relative; display: flex; } +.select-search :deep(input) { + padding-inline-start: calc(var(--icon-width) + 1rem); + box-shadow: inset 0 -2px 0 0 var(--blue-france-sun-113); +} .visible-label { margin-inline-start: var(--icon-width); } -:deep(.multiselect__placeholder) { - margin: 0; - font-style: italic; - color: var(--text-mention-grey); - white-space: nowrap; - text-overflow: ellipsis; - inline-size: 100%; - overflow: hidden; -} -::placeholder { +:deep(.multiselect-placeholder) { font-style: italic; color: var(--text-mention-grey); } @@ -187,27 +203,4 @@ const dropdownLabel = (text: string) => { translate: 0 -10px; max-inline-size: var(--icon-width); } -:deep(.multiselect__tags) { - border: 0; - box-shadow: inset 0 -2px 0 0 var(--border-action-high-blue-france); - margin: 0; - max-block-size: none; - --hover: var(--background-contrast-grey-hover); - --active: var(--background-contrast-grey-active); - background-color: var(--background-contrast-grey); - padding: 6px 40px 0 15px; - min-inline-size: 42px; - inline-size: 100%; -} -:deep(.multiselect__input), -:deep(.multiselect__single) { - background: var(--background-contrast-grey); -} -.select-search :deep(input) { - margin-inline-start: var(--icon-width); - inline-size: 100%; -} -:deep(.multiselect__content-wrapper) { - margin-block-start: 40px; -} diff --git a/src/components/forms/SelectSpatialCoverage.vue b/src/components/forms/SelectSpatialCoverage.vue index a1edb2bcb..e43fe6765 100644 --- a/src/components/forms/SelectSpatialCoverage.vue +++ b/src/components/forms/SelectSpatialCoverage.vue @@ -1,14 +1,16 @@ diff --git a/src/utils/bouquetGroups.ts b/src/utils/bouquetGroups.ts index a56c068c4..e85d2ca21 100644 --- a/src/utils/bouquetGroups.ts +++ b/src/utils/bouquetGroups.ts @@ -3,6 +3,8 @@ import { type ComputedRef, type Ref, ref } from 'vue' import type { DatasetProperties, DatasetsGroups } from '@/model/topic' +import { debounceWait } from '@/utils/config' + export const NO_GROUP = 'Sans regroupement' export const isOnlyNoGroup = (groups: DatasetsGroups) => { @@ -147,7 +149,7 @@ export function useDatasetFilter(datasetsProperties: Ref) { // apply search query const filterDatasetsProperties = useDebounceFn((value: string) => { searchQuery.value = value - }, 600) + }, debounceWait) return { isFiltering, diff --git a/src/utils/config.ts b/src/utils/config.ts index 512686ee2..0e139a0f5 100644 --- a/src/utils/config.ts +++ b/src/utils/config.ts @@ -34,3 +34,6 @@ export const useTopicsConf = (): TopicsConfNormalized => { topicsActivateReadMore: topicsConf.activate_read_more } } + +// Get debounce value or set default. +export const debounceWait: number = config.website.default_debounce_wait ?? 600 diff --git a/src/views/bouquets/BouquetFormView.vue b/src/views/bouquets/BouquetFormView.vue index a0ba9c330..82f3bc9ae 100644 --- a/src/views/bouquets/BouquetFormView.vue +++ b/src/views/bouquets/BouquetFormView.vue @@ -214,7 +214,7 @@ const onSubmit = async () => { {{ isCreate ? `Nouveau ${topicsName}` : topic.name }} - + { { @click.prevent="destroy" /> { .then(() => { setLiveResults() }) -}, 600) +}, debounceWait) watch( props, diff --git a/src/views/datasets/DatasetsListView.vue b/src/views/datasets/DatasetsListView.vue index 2893223ca..e594decc2 100644 --- a/src/views/datasets/DatasetsListView.vue +++ b/src/views/datasets/DatasetsListView.vue @@ -24,7 +24,7 @@ import { useOrganizationStore } from '@/store/OrganizationStore' import { useSearchStore } from '@/store/SearchStore' import { useTopicStore } from '@/store/TopicStore' import { fromMarkdown } from '@/utils' -import { useTopicsConf } from '@/utils/config' +import { debounceWait, useTopicsConf } from '@/utils/config' defineEmits(['search']) const props = defineProps({ @@ -203,7 +203,7 @@ const delayedSearch = useDebounceFn( loadingInstance.hide() }) }, - 600 + debounceWait ) watch(
+ Saisissez un mot clé puis choisissez une des options situés après le champ + pour lancer la recherche dans la rubrique souhaitée +