@madgex/design-system-ce 5.6.0 → 5.6.2

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,17 +1,19 @@
1
+ <!-- eslint-disable vue/multi-word-component-names -->
1
2
  <template>
2
3
  <div
4
+ ref="$el"
3
5
  class="mds-combobox"
4
6
  :class="{ 'mds-combobox--active': !listBoxHidden }"
5
- @keydown.down="hiddenGuard(onKeyDown)"
6
- @keydown.up="hiddenGuard(onKeyUp)"
7
- @keydown.home="hiddenGuard(onKeyHome)"
8
- @keydown.end="hiddenGuard(onKeyEnd)"
7
+ @keydown.down="onKeyDown"
8
+ @keydown.up="onKeyUp"
9
+ @keydown.home="onKeyHome"
10
+ @keydown.end="onKeyEnd"
9
11
  @keydown.esc="makeInactive"
10
12
  @keydown.enter="handleKeyDownEnter"
11
13
  >
12
14
  <input
13
15
  :id="comboboxid"
14
- ref="comboInput"
16
+ ref="$comboInput"
15
17
  :value="inputValue"
16
18
  class="mds-form-control"
17
19
  autocomplete="off"
@@ -43,271 +45,233 @@
43
45
  @mousedown="clickOption(option)"
44
46
  />
45
47
  </ListBox>
46
- <div aria-live="polite" role="status" class="mds-visually-hidden">{{ resultCountMessage }}</div>
48
+ <div aria-live="polite" role="status" class="mds-visually-hidden">
49
+ {{ resultCountMessage }}
50
+ </div>
47
51
  </div>
48
52
  </template>
49
53
 
50
- <script>
54
+ <script setup>
55
+ import { computed, onMounted, provide, ref, useTemplateRef } from 'vue';
51
56
  import ComboboxClear from './ComboboxClear.vue';
52
57
  import ListBox from './ListBox.vue';
53
58
  import ListBoxOption from './ListBoxOption.vue';
54
59
 
55
- export default {
56
- name: 'Combobox',
57
- components: {
58
- ComboboxClear,
59
- ListBox,
60
- ListBoxOption,
60
+ const props = defineProps({
61
+ comboboxid: { type: String, required: true },
62
+ placeholder: { type: String, default: '' },
63
+ name: { type: [String, Boolean], default: false },
64
+ value: { type: String, default: '' },
65
+ options: { type: Array, default: () => [] },
66
+ filterOptions: { type: Boolean, default: true },
67
+ iconpath: { type: String, default: '/assets/icons.svg' },
68
+ dataAriaInvalid: { type: String, default: '' },
69
+ i18n: { type: String, default: '' },
70
+ describedbyId: { type: String, default: '' },
71
+ minSearchCharacters: { type: Number, default: 2 },
72
+ });
73
+ const emit = defineEmits(['search', 'select-option', 'clear-all']);
74
+
75
+ const $el = useTemplateRef('$el');
76
+ const $comboInput = useTemplateRef('$comboInput');
77
+
78
+ const expanded = ref(false);
79
+ const selected = ref(null);
80
+ const chosen = ref(null);
81
+ const searchValue = ref(props.value);
82
+ const resultCountMessage = ref(null);
83
+
84
+ const i18nText = computed(() => {
85
+ return props.i18n
86
+ ? JSON.parse(props.i18n)
87
+ : {
88
+ loadingText: 'Loading',
89
+ resultsMessage: '{count} result available',
90
+ resultsMessage_plural: '{count} results available',
91
+ clearInput: 'clear input',
92
+ };
93
+ });
94
+ const inputValue = computed(() => {
95
+ if (chosenOption.value) {
96
+ return chosenOption.value.label;
97
+ }
98
+
99
+ return searchValue.value;
100
+ });
101
+ const visibleOptions = computed(() => {
102
+ if (props.filterOptions) {
103
+ return props.options.filter((opt) => opt.label.toLowerCase().includes(searchValue.value.toLowerCase()));
104
+ }
105
+
106
+ return props.options;
107
+ });
108
+ const listBoxId = computed(() => {
109
+ return `${props.comboboxid}-listbox`;
110
+ });
111
+ const optionId = computed(() => {
112
+ return `${props.comboboxid}-option`;
113
+ });
114
+ const isLoading = computed(() => {
115
+ return props.options.length === 0 && expanded.value;
116
+ });
117
+ const selectedOptionId = computed(() => {
118
+ // make sure comparison is treated as strings (in case option value Number etc)
119
+ const index = visibleOptions.value.findIndex((obj) => String(obj.value) === String(selectedOption.value?.value));
120
+
121
+ if (index > -1) {
122
+ return `${optionId.value}-${index}`;
123
+ }
124
+
125
+ return undefined;
126
+ });
127
+ const listBoxHidden = computed(() => {
128
+ return !expanded.value;
129
+ });
130
+ const lastOptionIndex = computed(() => {
131
+ return visibleOptions.value.length - 1;
132
+ });
133
+ const ariaExpanded = computed(() => {
134
+ // These must be strings to apply as an aria attribute of the same name
135
+ return expanded.value ? 'true' : 'false';
136
+ });
137
+
138
+ const ariaInvalid = computed(() => {
139
+ return props.dataAriaInvalid ? 'true' : 'false';
140
+ });
141
+
142
+ const selectedOption = computed({
143
+ get() {
144
+ return selected.value;
61
145
  },
62
- provide() {
63
- return {
64
- iconPath: this.iconpath,
65
- loadingText: this.i18nText.loadingText,
66
- clearInput: this.i18nText.clearInput,
67
- };
146
+ set(newOption) {
147
+ selected.value = newOption;
68
148
  },
69
- props: {
70
- comboboxid: {
71
- type: String,
72
- required: true,
73
- },
74
- placeholder: {
75
- type: String,
76
- default: '',
77
- },
78
- name: {
79
- type: [String, Boolean],
80
- default: false,
81
- },
82
- value: {
83
- type: String,
84
- default: '',
85
- },
86
- options: {
87
- type: Array,
88
- default: () => [],
89
- },
90
- filterOptions: {
91
- type: Boolean,
92
- default: true,
93
- },
94
- iconpath: {
95
- type: String,
96
- default: '/assets/icons.svg',
97
- },
98
- dataAriaInvalid: {
99
- type: String,
100
- default: '',
101
- },
102
- i18n: {
103
- type: String,
104
- default: '',
105
- },
106
- describedbyId: {
107
- type: String,
108
- default: ''
109
- },
110
- minSearchCharacters: {
111
- type: Number,
112
- default: 2
113
- }
149
+ });
150
+ const chosenOption = computed({
151
+ get() {
152
+ return chosen.value;
114
153
  },
115
- emits: ['search', 'select-option', 'clear-all'],
116
- data() {
117
- return {
118
- expanded: false,
119
- selected: null,
120
- chosen: null,
121
- searchValue: this.$props.value,
122
- resultCountMessage: null,
123
- };
154
+ set(newOption) {
155
+ chosen.value = newOption;
156
+ selectedOption.value = newOption;
157
+ emit('select-option', chosen.value);
124
158
  },
125
- computed: {
126
- inputValue() {
127
- if (this.chosenOption) {
128
- return this.chosenOption.label;
129
- }
159
+ });
130
160
 
131
- return this.searchValue;
132
- },
133
- selectedOption: {
134
- get() {
135
- return this.selected;
136
- },
137
- set(newOption) {
138
- this.selected = newOption;
139
- },
140
- },
141
- chosenOption: {
142
- get() {
143
- return this.chosen;
144
- },
145
- set(newOption) {
146
- this.chosen = newOption;
147
- this.selectedOption = newOption;
148
- this.$emit('select-option', this.chosen);
149
- },
150
- },
151
- visibleOptions() {
152
- if (this.filterOptions) {
153
- return this.options.filter((opt) => opt.label.toLowerCase().includes(this.searchValue.toLowerCase()));
154
- }
161
+ onMounted(() => {
162
+ // TODO: Get rid of code which couples to the nunjucks MdsCombobox template!
163
+ const fallbackInput = $el.value?.parentElement?.parentElement?.querySelector('.mds-form-element__fallback input');
164
+ const fallbackSelect = $el.value?.parentElement?.parentElement?.querySelector('.mds-form-element__fallback select');
155
165
 
156
- return this.options;
157
- },
158
- listBoxId() {
159
- return `${this.comboboxid}-listbox`;
160
- },
161
- optionId() {
162
- return `${this.comboboxid}-option`;
163
- },
164
- isLoading() {
165
- return this.options.length === 0 && this.expanded;
166
- },
167
- selectedOptionId() {
168
- const index = this.visibleOptions.findIndex((obj) => obj.value == this.selectedOption?.value);
166
+ if (fallbackInput) fallbackInput.remove();
167
+ if (fallbackSelect) fallbackSelect.removeAttribute('id');
168
+ });
169
169
 
170
- if (index > -1) {
171
- return `${this.optionId}-${index}`;
172
- }
170
+ function makeActive() {
171
+ expanded.value = true;
172
+ }
173
+ function makeInactive() {
174
+ expanded.value = false;
175
+ }
176
+ function handleInput(event) {
177
+ // Reset any chosen option if user is typing again
178
+ chosenOption.value = null;
179
+ searchValue.value = event.target ? event.target.value : '';
180
+ handleChange();
181
+ emit('search', searchValue.value);
182
+ if (visibleOptions.value.length > 0) {
183
+ updateCount();
184
+ }
185
+ }
186
+ function handleChange() {
187
+ if (searchValue.value.length === 0) clearField();
188
+ if (searchValue.value.length >= props.minSearchCharacters) {
189
+ makeActive();
190
+ updateCount();
191
+ } else {
192
+ makeInactive();
193
+ }
194
+ }
195
+ function handleFocus() {
196
+ handleChange();
197
+ if (visibleOptions.value.length > 1) {
198
+ updateCount();
199
+ }
200
+ }
201
+ function handleClear() {
202
+ clearField();
203
+ $comboInput.value?.focus();
204
+ }
205
+ function clearField() {
206
+ searchValue.value = '';
207
+ chosenOption.value = null;
208
+ emit('clear-all');
209
+ }
210
+ function clickOption(option = selectedOption.value) {
211
+ chosenOption.value = option;
212
+ makeInactive();
213
+ }
214
+ /* When expanded then enter key down selects an option, otherwise enter key will be native behaviour, eg submit form */
215
+ function handleKeyDownEnter(event) {
216
+ if (expanded.value) {
217
+ event.preventDefault();
218
+ chooseOption();
219
+ }
220
+ }
221
+ function chooseOption() {
222
+ chosenOption.value = selectedOption.value;
223
+ makeInactive();
224
+ clearCount();
225
+ }
173
226
 
174
- return undefined;
175
- },
176
- listBoxHidden() {
177
- return !this.expanded;
178
- },
179
- lastOptionIndex() {
180
- return this.visibleOptions.length - 1;
181
- },
182
- ariaExpanded() {
183
- // These must be strings to apply as an aria attribute of the same name
184
- return this.expanded ? 'true' : 'false';
185
- },
186
- ariaInvalid() {
187
- return this.dataAriaInvalid ? 'true' : 'false';
188
- },
189
- i18nText() {
190
- return this.i18n
191
- ? JSON.parse(this.i18n)
192
- : {
193
- loadingText: 'Loading',
194
- resultsMessage: '{count} result available',
195
- resultsMessage_plural: '{count} results available',
196
- clearInput: 'clear input',
197
- };
198
- },
199
- },
200
- mounted() {
201
- // TODO: Get rid of this code which couples this to the nunjucks MdsCombobox template!
202
- const fallbackInput = this.$el.parentElement?.parentElement?.querySelector('.mds-form-element__fallback input');
203
- const fallbackSelect = this.$el.parentElement?.parentElement?.querySelector('.mds-form-element__fallback select');
227
+ function onInputBlur() {
228
+ makeInactive();
229
+ clearCount();
230
+ }
231
+ function onKeyDown() {
232
+ if (listBoxHidden.value) return;
233
+ if (selectedOption.value) {
234
+ const currentIndex = visibleOptions.value.findIndex((item) => item.value === selectedOption.value.value);
235
+ const nextIndex = currentIndex === lastOptionIndex.value ? currentIndex : currentIndex + 1;
204
236
 
205
- if (fallbackInput) fallbackInput.remove();
206
- if (fallbackSelect) fallbackSelect.removeAttribute('id');
207
- },
208
- methods: {
209
- makeActive() {
210
- this.expanded = true;
211
- },
212
- makeInactive() {
213
- this.expanded = false;
214
- },
215
- handleInput(event) {
216
- // Reset any chosen option if user is typing again
217
- this.chosenOption = null;
218
- this.searchValue = event.target ? event.target.value : '';
219
- this.handleChange();
220
- this.$emit('search', this.searchValue);
221
- if (this.visibleOptions.length > 0) {
222
- this.updateCount();
223
- }
224
- },
225
- handleChange() {
226
- if (this.searchValue.length === 0) this.clearField();
227
- if (this.searchValue.length >= this.minSearchCharacters) {
228
- this.makeActive();
229
- this.updateCount();
230
- } else {
231
- this.makeInactive();
232
- }
233
- },
234
- handleFocus() {
235
- this.handleChange();
236
- if (this.visibleOptions.length > 1) {
237
- this.updateCount();
238
- }
239
- },
240
- handleClear() {
241
- this.clearField();
242
- this.$refs.comboInput.focus();
243
- },
244
- clearField() {
245
- this.searchValue = '';
246
- this.chosenOption = null;
247
- this.$emit('clear-all');
248
- },
249
- clickOption(option = this.selectedOption) {
250
- this.chosenOption = option;
251
- this.makeInactive();
252
- },
253
- /* When expanded then enter key down selects an option, otherwise enter key will be native behaviour, eg submit form */
254
- handleKeyDownEnter(event) {
255
- if (this.expanded) {
256
- event.preventDefault();
257
- this.chooseOption();
258
- }
259
- },
260
- chooseOption() {
261
- this.chosenOption = this.selectedOption;
262
- this.makeInactive();
263
- this.clearCount();
264
- },
265
- hiddenGuard(fn) {
266
- if (this.listBoxHidden) return;
267
- fn.call(this);
268
- },
269
- onInputBlur() {
270
- this.makeInactive();
271
- this.clearCount();
272
- },
273
- onKeyDown() {
274
- if (this.selectedOption) {
275
- const currentIndex = this.visibleOptions.findIndex((item) => item.value === this.selectedOption.value);
276
- const nextIndex = currentIndex === this.lastOptionIndex ? currentIndex : currentIndex + 1;
237
+ selectedOption.value = visibleOptions.value[nextIndex];
238
+ } else {
239
+ [selectedOption.value] = visibleOptions.value;
240
+ }
241
+ }
242
+ function onKeyUp() {
243
+ if (listBoxHidden.value) return;
244
+ if (selectedOption.value) {
245
+ const currentIndex = visibleOptions.value.findIndex((item) => item.value === selectedOption.value.value);
246
+ const nextIndex = currentIndex === 0 ? currentIndex : currentIndex - 1;
277
247
 
278
- this.selectedOption = this.visibleOptions[nextIndex];
279
- } else {
280
- [this.selectedOption] = this.visibleOptions;
281
- }
282
- },
283
- onKeyUp() {
284
- if (this.selectedOption) {
285
- const currentIndex = this.visibleOptions.findIndex((item) => item.value === this.selectedOption.value);
286
- const nextIndex = currentIndex === 0 ? currentIndex : currentIndex - 1;
248
+ selectedOption.value = visibleOptions.value[nextIndex];
249
+ } else {
250
+ selectedOption.value = visibleOptions.value[lastOptionIndex.value];
251
+ }
252
+ }
253
+ function onKeyHome() {
254
+ if (listBoxHidden.value) return;
255
+ [selectedOption.value] = visibleOptions.value;
256
+ }
257
+ function onKeyEnd() {
258
+ if (listBoxHidden.value) return;
259
+ selectedOption.value = visibleOptions.value[lastOptionIndex.value];
260
+ }
261
+ function updateCount() {
262
+ clearCount();
263
+ setTimeout(() => {
264
+ const message =
265
+ visibleOptions.value.length === 1 ? i18nText.value.resultsMessage : i18nText.value.resultsMessage_plural;
287
266
 
288
- this.selectedOption = this.visibleOptions[nextIndex];
289
- } else {
290
- this.selectedOption = this.visibleOptions[this.lastOptionIndex];
291
- }
292
- },
293
- onKeyHome() {
294
- [this.selectedOption] = this.visibleOptions;
295
- },
296
- onKeyEnd() {
297
- this.selectedOption = this.visibleOptions[this.lastOptionIndex];
298
- },
299
- updateCount() {
300
- this.clearCount();
301
- setTimeout(() => {
302
- const message =
303
- this.visibleOptions.length === 1 ? this.i18nText.resultsMessage : this.i18nText.resultsMessage_plural;
267
+ resultCountMessage.value = message.replace('{count}', visibleOptions.value.length);
268
+ }, 1400);
269
+ }
270
+ function clearCount() {
271
+ resultCountMessage.value = null;
272
+ }
304
273
 
305
- this.resultCountMessage = message.replace('{count}', this.visibleOptions.length);
306
- }, 1400);
307
- },
308
- clearCount() {
309
- this.resultCountMessage = null;
310
- },
311
- },
312
- };
274
+ provide('iconPath', props.iconpath);
275
+ provide('loadingText', i18nText.value.loadingText);
276
+ provide('clearInput', i18nText.value.clearInput);
313
277
  </script>
@@ -13,9 +13,9 @@
13
13
  </button>
14
14
  </template>
15
15
 
16
- <script>
17
- export default {
18
- name: 'ComboboxClear',
19
- inject: ['iconPath', 'clearInput'],
20
- };
16
+ <script setup>
17
+ import { inject } from 'vue';
18
+
19
+ const iconPath = inject('iconPath');
20
+ const clearInput = inject('clearInput');
21
21
  </script>
@@ -12,19 +12,14 @@
12
12
  </ul>
13
13
  </template>
14
14
 
15
- <script>
16
- export default {
17
- name: 'ListBox',
18
- props: {
19
- hidden: {
20
- type: Boolean,
21
- default: true,
22
- },
23
- isLoading: {
24
- type: Boolean,
25
- default: true,
26
- },
27
- },
28
- inject: ['iconPath', 'loadingText'],
29
- };
15
+ <script setup>
16
+ import { inject } from 'vue';
17
+
18
+ defineProps({
19
+ hidden: { type: Boolean, default: true },
20
+ isLoading: { type: Boolean, default: true },
21
+ });
22
+
23
+ const iconPath = inject('iconPath');
24
+ const loadingText = inject('loadingText');
30
25
  </script>
@@ -1,6 +1,6 @@
1
1
  <template>
2
2
  <li
3
- ref="listItem"
3
+ ref="$listItem"
4
4
  class="mds-combobox__option"
5
5
  role="option"
6
6
  :class="{ 'mds-combobox__option--focused': focused }"
@@ -10,41 +10,30 @@
10
10
  />
11
11
  </template>
12
12
 
13
- <script>
14
- export default {
15
- name: 'ListBoxOption',
16
- props: {
17
- option: {
18
- type: Object,
19
- required: true,
20
- },
21
- focused: {
22
- type: Boolean,
23
- default: false,
24
- },
25
- searchValue: {
26
- type: String,
27
- default: '',
28
- },
29
- },
30
- watch: {
31
- searchValue(newSearchValue) {
32
- return newSearchValue;
33
- },
34
- focused(value) {
35
- if (value) {
36
- this.$refs.listItem.scrollIntoView({block: 'nearest', inline: 'nearest'});
37
- }
38
- },
39
- },
40
- methods: {
41
- highlightOption() {
42
- const optionLabelHtml = this.option.label.replace(
43
- new RegExp(this.searchValue, 'gi'),
44
- (match) => `<span class="mds-combobox__option--marked">${match}</span>`
45
- );
46
- return optionLabelHtml;
47
- },
13
+ <script setup>
14
+ import { useTemplateRef, watch } from 'vue';
15
+
16
+ const props = defineProps({
17
+ option: { type: Object, required: true },
18
+ focused: { type: Boolean, default: false },
19
+ searchValue: { type: String, default: '' },
20
+ });
21
+
22
+ const $listItem = useTemplateRef('$listItem');
23
+
24
+ watch(
25
+ () => props.focused,
26
+ (value) => {
27
+ if (value) {
28
+ $listItem.value?.scrollIntoView({ block: 'nearest', inline: 'nearest' });
29
+ }
48
30
  },
49
- };
31
+ );
32
+ function highlightOption() {
33
+ const optionLabelHtml = props.option.label.replace(
34
+ new RegExp(props.searchValue, 'gi'),
35
+ (match) => `<span class="mds-combobox__option--marked">${match}</span>`,
36
+ );
37
+ return optionLabelHtml;
38
+ }
50
39
  </script>
@@ -1,10 +1,5 @@
1
1
  import '@ungap/custom-elements';
2
- /**
3
- * We're using a modified `defineCustomElement` where we can choose not to use shadowDom
4
- * shadowDom prevents access to external CSS, which is how design-system css works!
5
- * https://github.com/vuejs/core/issues/4314#issuecomment-1021393430
6
- */
7
- import { defineCustomElement } from '../temp-define-custom-element';
2
+ import { defineCustomElement } from 'vue';
8
3
  // --- Components ---
9
4
  import Combobox from '../components/combobox/Combobox.ce.vue';
10
5
 
@@ -1,10 +1,5 @@
1
1
  import '@ungap/custom-elements';
2
- /**
3
- * We're using a modified `defineCustomElement` where we can choose not to use shadowDom
4
- * shadowDom prevents access to external CSS, which is how design-system css works!
5
- * https://github.com/vuejs/core/issues/4314#issuecomment-1021393430
6
- */
7
- import { defineCustomElement } from '../temp-define-custom-element';
2
+ import { defineCustomElement } from 'vue';
8
3
  import TextEditor from '../components/text-editor/TextEditor.ce.vue';
9
4
 
10
5
  const MdsTextEditor = defineCustomElement(TextEditor, { shadowRoot: false });