@datagouv/components-next 1.0.2-dev.170 → 1.0.2-dev.172

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,4 @@
1
- import { g as Ke } from "./main-DNoYWfit.js";
1
+ import { g as Ke } from "./main-D7EaQcIR.js";
2
2
  import We from "vue";
3
3
  function Fe($, K) {
4
4
  for (var V = 0; V < K.length; V++) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@datagouv/components-next",
3
- "version": "1.0.2-dev.170",
3
+ "version": "1.0.2-dev.172",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "engines": {
@@ -10,7 +10,9 @@
10
10
  >
11
11
  <div>
12
12
  <div class="text-gray-900">
13
- {{ title }}
13
+ <slot name="title">
14
+ {{ title }}
15
+ </slot>
14
16
  </div>
15
17
  <div
16
18
  v-if="$slots.default"
@@ -34,7 +36,9 @@ export const bannerActionTypeKey = Symbol() as InjectionKey<'primary' | 'danger'
34
36
  <script setup lang="ts">
35
37
  const props = defineProps<{
36
38
  type: 'primary' | 'danger' | 'warning'
37
- title: string
39
+ // Optional so the title can be a slot, for instance to make it the toggle of a
40
+ // disclosure whose panel lives in the banner
41
+ title?: string
38
42
  }>()
39
43
 
40
44
  provide(bannerActionTypeKey, props.type)
@@ -8,6 +8,7 @@
8
8
  :placeholder="t('Tous les schémas')"
9
9
  :get-option-id="(s: RegisteredSchema) => s.name"
10
10
  :display-value="(s: RegisteredSchema) => s.name"
11
+ :filter="schemaMatchesQuery"
11
12
  :multiple="false"
12
13
  />
13
14
  </template>
@@ -16,6 +17,7 @@
16
17
  import { useTranslation } from '../../composables/useTranslation'
17
18
  import { useSelectModelSync } from '../../composables/useSelectModelSync'
18
19
  import { useFetch } from '../../functions/api'
20
+ import { schemaMatchesQuery } from '../../functions/schemas'
19
21
  import type { RegisteredSchema } from '../../types/schemas'
20
22
  import SearchableSelect from './SearchableSelect.vue'
21
23
 
@@ -1,8 +1,14 @@
1
1
  <template>
2
2
  <div
3
+ :id
3
4
  class="p-4 relative border bg-white hover:bg-gray-some"
4
- :class="{ 'border-gray-default': !selected, 'border-datagouv': selected }"
5
+ :class="{
6
+ 'border-gray-default': !selected && !active,
7
+ 'border-datagouv': selected && !active,
8
+ 'border-blue-outline': active,
9
+ }"
5
10
  :role="selectable ? 'option' : undefined"
11
+ :tabindex="selectable ? -1 : undefined"
6
12
  :aria-selected="selectable && selected"
7
13
  >
8
14
  <h3 class="text-base font-bold mb-0 flex items-center gap-1">
@@ -73,7 +79,10 @@ import { useComponentsConfig } from '../config'
73
79
 
74
80
  defineProps<{
75
81
  schema: RegisteredSchema
82
+ id?: string
76
83
  selectable?: boolean
84
+ // Highlighted by the keyboard inside the listbox, which is not the same as chosen
85
+ active?: boolean
77
86
  selected?: boolean
78
87
  showLinks?: boolean
79
88
  }>()
@@ -682,8 +682,9 @@ const props = defineProps<{
682
682
  // Note: combined via AND with any existing column-specific `contains` filters,
683
683
  // so it acts as an additional narrowing constraint, not a replacement.
684
684
  globalSearch?: string
685
- // Initial filters applied on mount, e.g. { 'Administration': { contains: 'Ministère' } }.
686
- // Used when navigating from the detail page badge with query params.
685
+ // Filters seeded on mount, e.g. { 'Administration': { contains: 'Ministère' } }.
686
+ // The explorer owns them afterwards: later changes to this prop are ignored,
687
+ // so pass a fresh instance (or remount) to reset them.
687
688
  initialFilters?: Record<string, ColumnFilters>
688
689
  // When set, renders <a> tags inside the specified column cells for native
689
690
  // browser UX (hover URL, ctrl+click, middle-click).
@@ -60,23 +60,28 @@ export function useActiveDescendant<T extends Option>(options: MaybeRefOrGetter<
60
60
 
61
61
  function handleKeyPressForActiveDescendant(key: KeyboardEvent, alreadyMovedDown = false) {
62
62
  switch (key.key as CommonKeyboardKeys) {
63
+ // An arrow that moves the active option must not also scroll the page behind it
63
64
  case 'ArrowDown':
64
65
  if (direction === 'vertical' && !alreadyMovedDown && !key.altKey) {
66
+ key.preventDefault()
65
67
  activateNextOption()
66
68
  }
67
69
  break
68
70
  case 'ArrowUp':
69
71
  if (direction === 'vertical') {
72
+ key.preventDefault()
70
73
  activatePreviousOption()
71
74
  }
72
75
  break
73
76
  case 'ArrowLeft':
74
77
  if (direction === 'horizontal') {
78
+ key.preventDefault()
75
79
  activatePreviousOption()
76
80
  }
77
81
  break
78
82
  case 'ArrowRight':
79
83
  if (direction === 'horizontal' && !alreadyMovedDown && !key.altKey) {
84
+ key.preventDefault()
80
85
  activateNextOption()
81
86
  }
82
87
  break
@@ -31,6 +31,33 @@ export function getSchemaVersion(schema: RegisteredSchema | null) {
31
31
  }
32
32
  }
33
33
 
34
+ // Accents and separators must not stand between a query and a schema: people type
35
+ // "lave linge" for « Lave-linge », or « reparabilite » without its accents
36
+ function normalizeForSearch(value: string): string {
37
+ return value
38
+ .toLowerCase()
39
+ .normalize('NFD')
40
+ .replace(/\p{Diacritic}/gu, '')
41
+ .replace(/[^a-z0-9]+/g, ' ')
42
+ .trim()
43
+ }
44
+
45
+ // The name is searched along with the title and the description: it is the identifier
46
+ // people know and paste, such as `etalab/indice-durabilite-lave-linge`
47
+ const searchableText = (schema: RegisteredSchema) => normalizeForSearch(`${schema.name} ${schema.title ?? ''} ${schema.description ?? ''}`)
48
+
49
+ const queryTokens = (query: string) => normalizeForSearch(query).split(' ').filter(Boolean)
50
+
51
+ export function schemaMatchesQuery(schema: RegisteredSchema, query: string): boolean {
52
+ // A query made only of punctuation holds no word to match. Callers short-circuit the
53
+ // empty query, so matching everything here would only ever answer that case.
54
+ const tokens = queryTokens(query)
55
+ if (!tokens.length) return false
56
+
57
+ const haystack = searchableText(schema)
58
+ return tokens.every(token => haystack.includes(token))
59
+ }
60
+
34
61
  export function findSchemaInCatalog(catalog: Array<RegisteredSchema>, schema: Schema | null): RegisteredSchema | null {
35
62
  if (!schema) return null
36
63
  return catalog.find(registeredSchema => schema.name === registeredSchema.name) || null