@ouestfrance/sipa-bms-ui 7.14.2 → 8.0.0

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.
Files changed (30) hide show
  1. package/README.md +1 -1
  2. package/dist/components/form/BmsAutocomplete.vue.d.ts +8 -8
  3. package/dist/components/form/BmsInputText.vue.d.ts +13 -13
  4. package/dist/components/form/BmsSearch.vue.d.ts +13 -13
  5. package/dist/components/form/BmsSelect.vue.d.ts +9 -9
  6. package/dist/components/form/RawAutocomplete.vue.d.ts +30 -21
  7. package/dist/components/form/RawInputText.vue.d.ts +6 -6
  8. package/dist/components/navigation/UiTenantSwitcher.vue.d.ts +13 -13
  9. package/dist/components/table/BmsTableFilters.vue.d.ts +13 -13
  10. package/dist/models/form.model.d.ts +6 -0
  11. package/dist/plugins/field/FieldDatalist.spec.d.ts +1 -0
  12. package/dist/plugins/field/FieldDatalist.vue.d.ts +13 -12
  13. package/dist/sipa-bms-ui.css +37 -32
  14. package/dist/sipa-bms-ui.es.js +3476 -3409
  15. package/dist/sipa-bms-ui.es.js.map +1 -1
  16. package/dist/sipa-bms-ui.umd.js +3475 -3408
  17. package/dist/sipa-bms-ui.umd.js.map +1 -1
  18. package/package.json +11 -11
  19. package/src/components/form/BmsAutocomplete.vue +14 -8
  20. package/src/components/form/BmsSelect.spec.ts +5 -14
  21. package/src/components/form/BmsSelect.vue +8 -52
  22. package/src/components/form/RawAutocomplete.spec.ts +20 -17
  23. package/src/components/form/RawAutocomplete.vue +56 -53
  24. package/src/components/table/BmsTableFilters.vue +2 -2
  25. package/src/models/form.model.ts +7 -0
  26. package/src/plugins/field/FieldDatalist.spec.ts +35 -0
  27. package/src/plugins/field/FieldDatalist.stories.js +10 -0
  28. package/src/plugins/field/FieldDatalist.vue +85 -7
  29. package/src/showroom/pages/autocomplete.vue +7 -0
  30. package/src/showroom/pages/forms.vue +1 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ouestfrance/sipa-bms-ui",
3
- "version": "7.14.2",
3
+ "version": "8.0.0",
4
4
  "author": "Ouest-France BMS",
5
5
  "license": "ISC",
6
6
  "scripts": {
@@ -38,29 +38,29 @@
38
38
  "@mdx-js/react": "3.1.0",
39
39
  "@storybook/addon-actions": "^8.6.14",
40
40
  "@storybook/addon-docs": "^9.0.4",
41
- "@storybook/addon-links": "9.0.8",
42
- "@storybook/vue3-vite": "9.0.8",
43
- "@types/lodash": "4.17.17",
41
+ "@storybook/addon-links": "9.0.11",
42
+ "@storybook/vue3-vite": "9.0.11",
43
+ "@types/lodash": "4.17.18",
44
44
  "@types/uuid": "10.0.0",
45
45
  "@vitejs/plugin-vue": "5.2.4",
46
46
  "@vue/test-utils": "2.4.6",
47
47
  "@vueuse/core": "13.3.0",
48
48
  "@vueuse/motion": "^3.0.0",
49
- "axios": "1.9.0",
49
+ "axios": "1.10.0",
50
50
  "blob-util": "^2.0.2",
51
51
  "chromatic": "12.2.0",
52
52
  "codemirror": "6.0.1",
53
53
  "cors": "^2.8.5",
54
54
  "cross-env": "^7.0.3",
55
55
  "cy2": "4.0.9",
56
- "cypress": "14.4.1",
56
+ "cypress": "14.5.0",
57
57
  "express": "^5.0.0",
58
58
  "husky": "9.1.7",
59
59
  "jsdom": "26.1.0",
60
60
  "keycloak-js": "26.1.2",
61
- "lint-staged": "16.1.0",
61
+ "lint-staged": "16.1.2",
62
62
  "lodash": "4.17.21",
63
- "lucide-vue-next": "0.514.0",
63
+ "lucide-vue-next": "0.516.0",
64
64
  "msw-storybook-addon": "^2.0.3",
65
65
  "normalize.css": "8.0.1",
66
66
  "path": "0.12.7",
@@ -68,8 +68,8 @@
68
68
  "sass": "1.89.2",
69
69
  "semantic-release": "24.2.5",
70
70
  "start-server-and-test": "2.0.12",
71
- "storybook": "9.0.8",
72
- "storybook-addon-pseudo-states": "9.0.8",
71
+ "storybook": "9.0.11",
72
+ "storybook-addon-pseudo-states": "9.0.11",
73
73
  "storybook-vue3-router": "^5.0.0",
74
74
  "typescript": "5.2.2",
75
75
  "uuid": "11.1.0",
@@ -78,7 +78,7 @@
78
78
  "vite-plugin-mkcert": "1.17.8",
79
79
  "vite-plugin-pages": "0.33.0",
80
80
  "vite-svg-loader": "5.1.0",
81
- "vitest": "3.2.3",
81
+ "vitest": "3.2.4",
82
82
  "vue": "3.5.16",
83
83
  "vue-codemirror": "6.1.1",
84
84
  "vue-loader": "17.4.2",
@@ -11,6 +11,9 @@
11
11
  :required="required"
12
12
  :helperText="helperText"
13
13
  :placeholder="placeholder"
14
+ :can-add-new-option="canAddNewOption"
15
+ @select="(option: any) => emits('select', option)"
16
+ @add-new-option="(newOption: string) => emits('addNewOption', newOption)"
14
17
  >
15
18
  <template #icon-start v-if="currentOptionIcon">
16
19
  <span
@@ -36,14 +39,12 @@
36
39
  </template>
37
40
 
38
41
  <script setup lang="ts">
39
- import { type Component, computed } from 'vue';
42
+ import { computed } from 'vue';
40
43
  import RawAutocomplete from './RawAutocomplete.vue';
41
- import { Caption } from '@/models';
44
+ import { Caption, InputOption } from '@/models';
42
45
 
43
46
  export interface Props {
44
- options:
45
- | string[]
46
- | { label: string; value: string; icon?: Component | string }[];
47
+ options: string[] | InputOption[];
47
48
  modelValue?: string;
48
49
  label?: string;
49
50
  required?: boolean;
@@ -54,13 +55,18 @@ export interface Props {
54
55
  captions?: string[] | Caption[];
55
56
  errors?: string[] | Caption[];
56
57
  open?: boolean;
58
+ canAddNewOption?: boolean;
57
59
  }
58
60
 
59
61
  const props = withDefaults(defineProps<Props>(), {
60
62
  open: false,
61
63
  });
62
64
 
63
- const modelValue = defineModel<string>('modelValue');
65
+ const modelValue = defineModel<string>('modelValue', { required: true });
66
+ const emits = defineEmits<{
67
+ addNewOption: [newOption: string];
68
+ select: [option: InputOption];
69
+ }>();
64
70
 
65
71
  const currentOptionIcon = computed(() => {
66
72
  const option = props.options.find(
@@ -74,8 +80,8 @@ const optionsLabelValue = computed(() =>
74
80
  Array.isArray(props.options) &&
75
81
  !!props.options.length &&
76
82
  typeof props.options[0] === 'string'
77
- ? props.options.map((o) => ({ label: o, value: o }))
78
- : props.options,
83
+ ? props.options.map((o) => ({ label: o, value: o }) as InputOption)
84
+ : (props.options as InputOption[]),
79
85
  );
80
86
  </script>
81
87
 
@@ -3,7 +3,11 @@ import BmsSelect, { Props } from './BmsSelect.vue';
3
3
  import { field } from '@/plugins/field';
4
4
  import { nextTick } from 'vue';
5
5
 
6
- const factory = (props: Props) => {
6
+ interface PropsAndModel extends Props {
7
+ modelValue: any;
8
+ }
9
+
10
+ const factory = (props: PropsAndModel) => {
7
11
  const wrapper = mount(BmsSelect, { props, global: { plugins: [field] } });
8
12
  const inputElement = wrapper.find('input');
9
13
  const optionsElement = wrapper.find('ul');
@@ -33,19 +37,6 @@ describe('BmsSelect', () => {
33
37
  expect(inputElement.element.value).toStrictEqual('toto-label');
34
38
  });
35
39
 
36
- it('should update when select with keys', async () => {
37
- const { inputElement, optionsElement } = factory({
38
- modelValue: '',
39
- options: [{ label: 'toto-label', value: 'toto' }],
40
- });
41
-
42
- expect(inputElement.element.value).toStrictEqual('');
43
- await inputElement.trigger('keydown.down');
44
- await inputElement.trigger('keydown.enter');
45
- await nextTick();
46
- expect(inputElement.element.value).toStrictEqual('toto-label');
47
- });
48
-
49
40
  it('should not display value if not in options', () => {
50
41
  const { inputElement } = factory({
51
42
  modelValue: 'toto',
@@ -13,13 +13,10 @@
13
13
  ref="selectInput"
14
14
  type="text"
15
15
  role="input"
16
- :value="getDisplayValue"
16
+ :value="displayValue"
17
17
  :placeholder="placeholder"
18
18
  onkeypress="return false;"
19
19
  :required="required"
20
- @keydown.up="keyUp"
21
- @keydown.down="keyDown"
22
- @keydown.enter="keyEnter"
23
20
  />
24
21
  <span class="icon-down-container">
25
22
  <ChevronDown class="icon-down-button" />
@@ -29,7 +26,7 @@
29
26
  <FieldDatalist
30
27
  v-show="isInputFocused"
31
28
  :options="options"
32
- :currentSelectedItemIndex="currentSelectedItemIndex"
29
+ :is-input-focused="isInputFocused"
33
30
  @select="(option: any) => selectItem(option)"
34
31
  />
35
32
  </template>
@@ -42,10 +39,10 @@ import { Ref, computed, onMounted, ref, watch } from 'vue';
42
39
  import _ from 'lodash';
43
40
  import FieldDatalist from '@/plugins/field/FieldDatalist.vue';
44
41
  import { Caption } from '@/models/caption.model';
42
+ import { InputOption } from '@/models';
45
43
 
46
44
  export interface Props {
47
- modelValue: any;
48
- options: { label: string; value: any }[];
45
+ options: InputOption[];
49
46
  label?: string;
50
47
  errors?: string[] | Caption[];
51
48
  captions?: string[] | Caption[];
@@ -58,17 +55,14 @@ export interface Props {
58
55
  }
59
56
 
60
57
  const props = withDefaults(defineProps<Props>(), {
61
- modelValue: '',
62
58
  label: '',
63
59
  disabled: false,
64
60
  required: false,
65
61
  open: false,
66
62
  });
67
- const $emits = defineEmits<{ (e: 'update:modelValue', value: any): void }>();
63
+ const modelValue = defineModel<any>('modelValue', { required: true });
68
64
 
69
65
  const isInputFocused = ref<boolean>(props.open);
70
- const inputValue = ref<any>(props.modelValue);
71
- const currentSelectedItemIndex = ref<number>(-1);
72
66
  const selectInput: Ref<HTMLElement | null> = ref(null);
73
67
  const selectWrapper: Ref<HTMLElement | null> = ref(null);
74
68
 
@@ -87,57 +81,19 @@ onMounted(() => {
87
81
  });
88
82
  });
89
83
 
90
- const getDisplayValue = computed(() => {
84
+ const displayValue = computed(() => {
91
85
  const option = props.options.find((o) =>
92
- _.isEqual(o.value, inputValue.value),
86
+ _.isEqual(o.value, modelValue.value),
93
87
  );
94
88
  if (option) return option.label;
95
89
  return '';
96
90
  });
97
91
 
98
- watch(inputValue, function emitUpdate(newValue) {
99
- $emits('update:modelValue', newValue);
100
- });
101
-
102
- watch(
103
- () => props.modelValue,
104
- function updateFromProps(newValue) {
105
- inputValue.value = newValue;
106
- },
107
- );
108
-
109
92
  const selectItem = (option: any) => {
110
- inputValue.value = option;
93
+ modelValue.value = option.value;
111
94
  isInputFocused.value = false;
112
95
  };
113
96
 
114
- const keyDown = () => {
115
- if (!isInputFocused.value) isInputFocused.value = true;
116
- if (currentSelectedItemIndex.value < props.options.length - 1) {
117
- currentSelectedItemIndex.value++;
118
- } else {
119
- currentSelectedItemIndex.value = 0;
120
- }
121
- };
122
-
123
- const keyUp = () => {
124
- if (!isInputFocused.value) isInputFocused.value = true;
125
- if (currentSelectedItemIndex.value > 0) {
126
- currentSelectedItemIndex.value--;
127
- } else {
128
- currentSelectedItemIndex.value = props.options.length - 1;
129
- }
130
- };
131
-
132
- const keyEnter = () => {
133
- if (currentSelectedItemIndex.value >= 0) {
134
- selectItem(props.options[currentSelectedItemIndex.value].value);
135
- currentSelectedItemIndex.value = -1;
136
- } else {
137
- isInputFocused.value = false;
138
- }
139
- };
140
-
141
97
  const setFocus = () => {
142
98
  isInputFocused.value = true;
143
99
  if (selectInput.value) {
@@ -5,7 +5,11 @@ import { mount } from '@vue/test-utils';
5
5
  import { field } from '@/plugins/field';
6
6
  import { nextTick } from 'vue';
7
7
 
8
- const factory = (props?: Props) => {
8
+ interface PropsAndModel extends Props {
9
+ modelValue: any;
10
+ }
11
+
12
+ const factory = (props?: PropsAndModel) => {
9
13
  const wrapper = mount(RawAutocomplete, {
10
14
  global: {
11
15
  plugins: [field],
@@ -45,12 +49,14 @@ describe('BmsAutocomplete', () => {
45
49
  });
46
50
 
47
51
  it('should load with default value', async () => {
48
- const { inputElement } = factory({ modelValue: 'i' } as Props);
52
+ const { inputElement } = factory({ modelValue: 'i' } as PropsAndModel);
49
53
  expect(inputElement.element.value).toStrictEqual('titi');
50
54
  });
51
55
 
52
56
  it('should change input value if parent modelvalue change', async () => {
53
- const { wrapper, inputElement } = factory({ modelValue: 'i' } as Props);
57
+ const { wrapper, inputElement } = factory({
58
+ modelValue: 'i',
59
+ } as PropsAndModel);
54
60
  expect(inputElement.element.value).toStrictEqual('titi');
55
61
  wrapper.setProps({ modelValue: 'u' });
56
62
  await nextTick();
@@ -58,7 +64,9 @@ describe('BmsAutocomplete', () => {
58
64
  });
59
65
 
60
66
  it('should reset input value if parent modelvalue change for unknown value', async () => {
61
- const { wrapper, inputElement } = factory({ modelValue: 'i' } as Props);
67
+ const { wrapper, inputElement } = factory({
68
+ modelValue: 'i',
69
+ } as PropsAndModel);
62
70
  expect(inputElement.element.value).toStrictEqual('titi');
63
71
  wrapper.setProps({ modelValue: 'not_found' });
64
72
  await nextTick();
@@ -66,29 +74,24 @@ describe('BmsAutocomplete', () => {
66
74
  });
67
75
 
68
76
  it('should not reset input value if parent modelvalue change for nullable value', async () => {
69
- const { wrapper, inputElement } = factory({ modelValue: 'i' } as Props);
77
+ const { wrapper, inputElement } = factory({
78
+ modelValue: 'i',
79
+ } as PropsAndModel);
70
80
  expect(inputElement.element.value).toStrictEqual('titi');
71
81
  wrapper.setProps({ modelValue: undefined });
72
82
  await nextTick();
73
83
  expect(inputElement.element.value).toStrictEqual('titi');
74
84
  });
75
85
 
76
- it('should populate input on option selection', async () => {
86
+ it('should emit event on option selection', async () => {
77
87
  const { wrapper, inputElement } = factory();
78
88
  const titi = wrapper.get('[data-testid="i"]');
79
89
  expect(inputElement.element.value).toStrictEqual('');
80
90
  await titi.trigger('click');
81
- expect(inputElement.element.value).toStrictEqual('titi');
82
- });
83
-
84
- it('should be able to select with keyboard', async () => {
85
- const { wrapper, inputElement } = factory();
86
- const titi = wrapper.get('[data-testid="i"]');
87
-
88
- expect(titi.classes()).toStrictEqual([]);
89
- await inputElement.trigger('focus');
90
- await inputElement.trigger('keydown.down');
91
- expect(titi.classes()).toStrictEqual(['selected']);
91
+ expect(wrapper.emitted()['select']).toHaveLength(1);
92
+ expect(wrapper.emitted()['select'][0]).toStrictEqual([
93
+ { value: 'i', label: 'titi' },
94
+ ]);
92
95
  });
93
96
 
94
97
  it('should filter options', async () => {
@@ -13,9 +13,6 @@
13
13
  v-model="inputText"
14
14
  :type="InputType.TEXT"
15
15
  :disabled="disabled"
16
- @keyDown="keyDown"
17
- @keyUp="keyUp"
18
- @keyEnter="keyEnter"
19
16
  :class="classes"
20
17
  :placeholder="placeholder"
21
18
  :required="required"
@@ -26,18 +23,31 @@
26
23
  <slot name="icon-start"></slot>
27
24
  </template>
28
25
  <template #icon-end>
29
- <slot name="icon-end"></slot>
26
+ <slot name="icon-end">
27
+ <span v-if="inputText.length" class="icon" @click="clearInput">
28
+ <X />
29
+ </span>
30
+ <span
31
+ v-else
32
+ class="icon"
33
+ @click.stop="isInputFocused = !isInputFocused"
34
+ >
35
+ <ChevronDown v-if="!isInputFocused" />
36
+ <ChevronUp v-else />
37
+ </span>
38
+ </slot>
30
39
  </template>
31
40
  </RawInputText>
32
41
  <template #datalist>
33
42
  <FieldDatalist
34
- data-testid="autocomplete-menu"
35
43
  v-show="isInputFocused"
44
+ data-testid="autocomplete-menu"
45
+ :is-input-focused="isInputFocused"
46
+ :can-add-new-option="canAddNewOption"
47
+ :new-option="inputText"
36
48
  :options="filteredMenuItems"
37
- :currentSelectedItemIndex="currentSelectedItemIndex"
38
- @select="
39
- (optionValue: string) => selectItem({ value: optionValue, label: '' })
40
- "
49
+ @select="selectItem"
50
+ @add-new-option="(option: string) => emits('addNewOption', option)"
41
51
  >
42
52
  <template #option="{ option }: { option: any }">
43
53
  <slot name="option" :option="option">{{ option }}</slot>
@@ -53,11 +63,11 @@ import FieldDatalist from '@/plugins/field/FieldDatalist.vue';
53
63
  import { computed, ref, Ref, watch } from 'vue';
54
64
  import { Caption } from '@/models/caption.model';
55
65
  import RawInputText from '@/components/form/RawInputText.vue';
56
- import { InputType } from '@/models';
66
+ import { InputOption, InputType } from '@/models';
67
+ import { ChevronDown, ChevronUp, X } from 'lucide-vue-next';
57
68
 
58
69
  export interface Props {
59
- options: any[];
60
- modelValue?: string | null;
70
+ options: InputOption[];
61
71
  label?: string;
62
72
  required?: boolean;
63
73
  optional?: boolean;
@@ -67,6 +77,9 @@ export interface Props {
67
77
  captions?: string[] | Caption[];
68
78
  errors?: string[] | Caption[];
69
79
  open?: boolean;
80
+
81
+ canAddNewOption?: boolean;
82
+ newOption?: string;
70
83
  }
71
84
 
72
85
  const props = withDefaults(defineProps<Props>(), {
@@ -74,8 +87,11 @@ const props = withDefaults(defineProps<Props>(), {
74
87
  open: false,
75
88
  });
76
89
 
90
+ const modelValue = defineModel<string | null>('modelValue', { required: true });
91
+
77
92
  const emits = defineEmits<{
78
- (e: 'update:modelValue', payload: string | null): void;
93
+ addNewOption: [newOption: string];
94
+ select: [option: InputOption];
79
95
  }>();
80
96
 
81
97
  const getValidOptionByLabel = (label: string) =>
@@ -85,10 +101,9 @@ const getValidOptionByValue = (value: string | null) =>
85
101
  props.options.find((o) => o.value === value);
86
102
 
87
103
  const inputText = ref<string>(
88
- getValidOptionByValue(props.modelValue)?.label ?? '',
104
+ getValidOptionByValue(modelValue.value)?.label ?? '',
89
105
  );
90
106
  const isInputFocused = ref<boolean>(props.open);
91
- const currentSelectedItemIndex = ref<number>(-1);
92
107
  const autocompleteInput: Ref<HTMLElement | null> = ref(null);
93
108
 
94
109
  const classes = computed(() => {
@@ -99,64 +114,42 @@ const filteredMenuItems = computed(() =>
99
114
  props.options.filter((o) => searchString(o.label, inputText.value)),
100
115
  );
101
116
 
102
- const selectItem = (option: { label: string; value: string | null }) => {
117
+ const selectItem = (option: InputOption) => {
118
+ emits('select', option);
103
119
  const existingOption =
104
120
  getValidOptionByLabel(option.label) || getValidOptionByValue(option.value);
121
+ modelValue.value = existingOption?.value ?? null;
122
+ isInputFocused.value = false;
123
+ };
124
+
125
+ const displayItem = (option: InputOption) => {
126
+ const existingOption =
127
+ getValidOptionByLabel(option.label) || getValidOptionByValue(option.value);
128
+
105
129
  if (existingOption) {
106
130
  inputText.value = existingOption.label;
131
+ modelValue.value = existingOption.value;
107
132
  } else {
133
+ modelValue.value = null;
108
134
  inputText.value = option.label;
109
135
  }
110
- isInputFocused.value = false;
111
136
  };
112
137
 
113
- watch(inputText, function emitUpdate(newLabel) {
114
- const option = getValidOptionByLabel(newLabel);
115
- if (option) selectItem(option);
116
- emits('update:modelValue', option?.value ?? null);
117
- });
118
-
119
138
  watch(
120
- () => props.modelValue,
139
+ () => modelValue.value,
121
140
  function updateFromProps(newValue) {
122
141
  const option = getValidOptionByValue(newValue);
123
142
  if (option) {
124
- selectItem(option);
143
+ displayItem(option);
144
+ if (modelValue.value !== option.value) selectItem(option);
125
145
  // drop input value only if parent set nullable value
126
- } else if (props.modelValue !== undefined && props.modelValue !== null) {
146
+ } else if (modelValue.value !== undefined && modelValue.value !== null) {
127
147
  inputText.value = '';
128
148
  }
129
149
  },
130
150
  );
131
-
132
151
  const onInput = () => {
133
152
  isInputFocused.value = true;
134
- currentSelectedItemIndex.value = -1;
135
- };
136
-
137
- const keyDown = () => {
138
- if (currentSelectedItemIndex.value < filteredMenuItems.value.length - 1) {
139
- currentSelectedItemIndex.value++;
140
- } else {
141
- currentSelectedItemIndex.value = 0;
142
- }
143
- };
144
-
145
- const keyUp = () => {
146
- if (currentSelectedItemIndex.value > 0) {
147
- currentSelectedItemIndex.value--;
148
- } else {
149
- currentSelectedItemIndex.value = props.options.length - 1;
150
- }
151
- };
152
-
153
- const keyEnter = () => {
154
- if (currentSelectedItemIndex.value >= 0) {
155
- selectItem(filteredMenuItems.value[currentSelectedItemIndex.value]);
156
- currentSelectedItemIndex.value = -1;
157
- } else {
158
- isInputFocused.value = false;
159
- }
160
153
  };
161
154
 
162
155
  const setFocus = () => {
@@ -164,6 +157,10 @@ const setFocus = () => {
164
157
  autocompleteInput.value.focus();
165
158
  }
166
159
  };
160
+ const clearInput = () => {
161
+ inputText.value = '';
162
+ modelValue.value = null;
163
+ };
167
164
 
168
165
  defineExpose({
169
166
  setFocus,
@@ -215,4 +212,10 @@ defineExpose({
215
212
  }
216
213
  }
217
214
  }
215
+ .icon {
216
+ height: 1em;
217
+ width: 1em;
218
+
219
+ cursor: pointer;
220
+ }
218
221
  </style>
@@ -76,7 +76,7 @@
76
76
 
77
77
  <script setup lang="ts">
78
78
  import { Filter as FilterIcon, RefreshCcw, Save } from 'lucide-vue-next';
79
- import { Filter, FilterType, SavedFilter } from '@/models';
79
+ import { Filter, FilterType, InputOption, SavedFilter } from '@/models';
80
80
  import BmsSelect from '../form/BmsSelect.vue';
81
81
  import BmsAutocomplete from '../form/BmsAutocomplete.vue';
82
82
  import { useRoute } from 'vue-router';
@@ -155,7 +155,7 @@ const getFilterComponent = (type: FilterType) => {
155
155
 
156
156
  const getFilterOptions = (
157
157
  filter: Filter,
158
- ): undefined | { label: string; value: string }[] | string[] => {
158
+ ): undefined | InputOption[] | string[] => {
159
159
  switch (filter.type) {
160
160
  case 'boolean':
161
161
  return [
@@ -1,4 +1,5 @@
1
1
  import { Caption } from '@/models/caption.model';
2
+ import { Component } from 'vue';
2
3
 
3
4
  export interface BmsInputRadioGroupOption {
4
5
  value: any;
@@ -28,3 +29,9 @@ export enum InputType {
28
29
  DATE = 'date',
29
30
  DATETIME = 'datetime-local',
30
31
  }
32
+
33
+ export interface InputOption {
34
+ label: string;
35
+ value: any;
36
+ icon?: Component;
37
+ }
@@ -0,0 +1,35 @@
1
+ import FieldDatalist, { type Props } from '@/plugins/field/FieldDatalist.vue';
2
+ import { mount } from '@vue/test-utils';
3
+ import { field } from '@/plugins/field';
4
+
5
+ const factory = (props?: Props) => {
6
+ const wrapper = mount(FieldDatalist, {
7
+ global: {
8
+ plugins: [field],
9
+ },
10
+ props: {
11
+ isInputFocused: false,
12
+ options: [
13
+ { label: 'titi', value: 'i' },
14
+ { label: 'toto', value: 'o' },
15
+ { label: 'tutu', value: 'u' },
16
+ ],
17
+ modelValue: '',
18
+ ...props,
19
+ },
20
+ attachTo: document.body,
21
+ });
22
+
23
+ return { wrapper };
24
+ };
25
+
26
+ describe('FieldDatalist', () => {
27
+ it('should be able to select with keyboard', async () => {
28
+ const { wrapper } = factory();
29
+ const titi = wrapper.get('[data-testid="i"]');
30
+
31
+ expect(titi.classes()).toStrictEqual([]);
32
+ await wrapper.trigger('keydown.down');
33
+ expect(titi.classes()).toStrictEqual(['selected']);
34
+ });
35
+ });
@@ -33,3 +33,13 @@ WithSelectedItem.args = {
33
33
  value: i,
34
34
  })),
35
35
  };
36
+
37
+ export const CanAddNewOption = Template.bind({});
38
+ CanAddNewOption.args = {
39
+ options: ['toto', 'titi', 'tutu', 'tirlitititututatoooo'].map((i) => ({
40
+ label: i,
41
+ value: i,
42
+ })),
43
+ canAddNewOption: true,
44
+ newOption: 'manual input',
45
+ };