@madgex/design-system 1.34.1 → 1.35.1

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 (70) hide show
  1. package/.eslintignore +0 -1
  2. package/.eslintrc.js +1 -1
  3. package/.prettierrc +4 -1
  4. package/README.md +2 -2
  5. package/__tests__/.eslintrc.js +8 -0
  6. package/__tests__/unit/src/components/combobox.spec.js +107 -0
  7. package/coverage/cobertura-coverage.xml +334 -5
  8. package/coverage/components/accordion/accordion.js.html +61 -51
  9. package/coverage/components/accordion/index.html +62 -49
  10. package/coverage/components/combobox/combobox.js.html +139 -0
  11. package/coverage/components/combobox/index.html +110 -0
  12. package/coverage/components/combobox/vue-components/Combobox.vue.html +709 -0
  13. package/coverage/components/combobox/vue-components/index.html +110 -0
  14. package/coverage/components/notification/index.html +62 -49
  15. package/coverage/components/notification/notification.js.html +61 -51
  16. package/coverage/components/popover/index.html +62 -49
  17. package/coverage/components/popover/popover.js.html +61 -51
  18. package/coverage/components/switch-state/index.html +62 -49
  19. package/coverage/components/switch-state/switch-state.js.html +61 -51
  20. package/coverage/components/tabs/index.html +62 -49
  21. package/coverage/components/tabs/tabs.js.html +61 -51
  22. package/coverage/index.html +126 -67
  23. package/coverage/js/common.js.html +61 -51
  24. package/coverage/js/fractal-scripts/combobox.js.html +229 -0
  25. package/coverage/js/fractal-scripts/index.html +80 -50
  26. package/coverage/js/fractal-scripts/notification.js.html +61 -51
  27. package/coverage/js/fractal-scripts/switch-state.js.html +61 -51
  28. package/coverage/js/index-fractal.js.html +68 -52
  29. package/coverage/js/index-polyfills.js.html +65 -52
  30. package/coverage/js/index-vue.js.html +82 -0
  31. package/coverage/js/index.html +88 -54
  32. package/coverage/js/index.js.html +62 -55
  33. package/coverage/js/polyfills/closest.js.html +61 -51
  34. package/coverage/js/polyfills/index.html +77 -49
  35. package/coverage/js/polyfills/remove.js.html +100 -0
  36. package/coverage/tokens/_config.js.html +61 -51
  37. package/coverage/tokens/index.html +62 -49
  38. package/cypress/integration/components/combobox.spec.js +87 -0
  39. package/cypress/integration/components/switch-state.spec.js +2 -8
  40. package/cypress/support/index.js +1 -0
  41. package/dist/_tokens/css/_tokens.css +169 -168
  42. package/dist/_tokens/js/_tokens-module.js +22 -1
  43. package/dist/_tokens/scss/_tokens.scss +5 -1
  44. package/dist/assets/icons.json +1 -1
  45. package/dist/css/index.css +1 -1
  46. package/dist/js/index.js +18 -4
  47. package/gulpfile.js +1 -1
  48. package/jest.config.js +5 -1
  49. package/package.json +25 -4
  50. package/src/components/button/button.scss +1 -0
  51. package/src/components/combobox/README.md +27 -0
  52. package/src/components/combobox/_macro.njk +3 -0
  53. package/src/components/combobox/_template.njk +45 -0
  54. package/src/components/combobox/combobox.config.js +30 -0
  55. package/src/components/combobox/combobox.js +20 -0
  56. package/src/components/combobox/combobox.njk +14 -0
  57. package/src/components/combobox/combobox.scss +52 -0
  58. package/src/components/combobox/vue-components/Combobox.vue +210 -0
  59. package/src/components/combobox/vue-components/ComboboxInput.vue +39 -0
  60. package/src/components/combobox/vue-components/ListBox.vue +17 -0
  61. package/src/components/combobox/vue-components/ListBoxOption.vue +27 -0
  62. package/src/js/fractal-scripts/combobox.js +50 -0
  63. package/src/js/index-fractal.js +2 -0
  64. package/src/js/index-polyfills.js +1 -0
  65. package/src/js/index-vue.js +1 -0
  66. package/src/js/index.js +0 -1
  67. package/src/js/polyfills/remove.js +7 -0
  68. package/src/scss/components/__index.scss +1 -0
  69. package/src/tokens/font.json +5 -0
  70. package/tasks/js-bundle.js +7 -0
package/gulpfile.js CHANGED
@@ -17,7 +17,7 @@ function watchFiles() {
17
17
  gulp.watch(['src/scss/core/**/*.scss', 'src/components/**/*.scss'], { awaitWriteFinish: true }, gulp.series(css));
18
18
  gulp.watch('src/tokens/**/*.json', gulp.series(tokens, css));
19
19
  gulp.watch('src/icons/**/*.svg', gulp.series(svgsprite, fractalBuild));
20
- gulp.watch(['src/js/**/*', 'src/components/**/*.js'], gulp.series(jsbundle));
20
+ gulp.watch(['src/js/**/*', 'src/components/**/*.js', 'src/components/**/*.vue'], gulp.series(jsbundle));
21
21
  }
22
22
 
23
23
  const watch = gulp.parallel(watchFiles);
package/jest.config.js CHANGED
@@ -1,6 +1,10 @@
1
1
  module.exports = {
2
2
  collectCoverage: true,
3
- collectCoverageFrom: ['src/**/*.js', '!src/**/*.config.js'],
3
+ collectCoverageFrom: ['src/**/*.js', 'src/**/*.vue', '!src/**/*.config.js'],
4
4
  coverageReporters: ['cobertura', 'html'],
5
5
  testMatch: ['**/__tests__/unit/**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)'],
6
+ transform: {
7
+ '^.+\\.js$': 'babel-jest',
8
+ '^.+\\.vue$': 'vue-jest',
9
+ },
6
10
  };
package/package.json CHANGED
@@ -1,12 +1,14 @@
1
1
  {
2
2
  "name": "@madgex/design-system",
3
- "version": "1.34.1",
3
+ "author": "Madgex",
4
+ "license": "UNLICENSED",
5
+ "version": "1.35.1",
4
6
  "scripts": {
5
7
  "clean": "rimraf dist public tokens/build",
6
8
  "commit": "commit",
7
9
  "semantic-release": "semantic-release --prepare && semantic-release --publish",
8
10
  "tokens": "style-dictionary --config ./src/tokens/_config.js build",
9
- "start": "gulp dev",
11
+ "start": "cross-env NODE_ENV=development gulp dev",
10
12
  "build": "gulp build",
11
13
  "build:icons": "svgo -f src/icons ./dist/assets/icons",
12
14
  "build:webpack": "NODE_ENV=production webpack",
@@ -25,6 +27,7 @@
25
27
  "dependencies": {
26
28
  "@ctrl/tinycolor": "^2.5.4",
27
29
  "css-loader": "^3.2.0",
30
+ "document-register-element": "^1.14.3",
28
31
  "mini-css-extract-plugin": "^0.8.0",
29
32
  "node-sass": "^4.12.0",
30
33
  "popper.js": "^1.16.0",
@@ -34,7 +37,9 @@
34
37
  "sass-loader": "^7.3.1",
35
38
  "style-dictionary": "^2.8.2",
36
39
  "style-loader": "^0.23.1",
37
- "svgxuse": "^1.2.6"
40
+ "svgxuse": "^1.2.6",
41
+ "vue": "^2.6.11",
42
+ "vue-custom-element": "^3.2.12"
38
43
  },
39
44
  "devDependencies": {
40
45
  "@babel/core": "^7.6.4",
@@ -47,21 +52,33 @@
47
52
  "@frctl/fractal": "^1.2.0",
48
53
  "@frctl/mandelbrot": "^1.2.1",
49
54
  "@frctl/nunjucks": "^2.0.1",
50
- "@madgex/eslint-config-madgex": "^1.2.0",
55
+ "@madgex/eslint-config-madgex": "^1.2.3",
56
+ "@vue/eslint-config-prettier": "^6.0.0",
57
+ "@vue/test-utils": "^1.0.0-beta.31",
51
58
  "autoprefixer": "^9.6.5",
52
59
  "axe-core": "^3.4.0",
60
+ "babel-core": "^7.0.0-bridge.0",
53
61
  "babel-jest": "^24.9.0",
54
62
  "babel-loader": "^8.0.6",
55
63
  "bubleify": "^1.2.1",
56
64
  "commitizen": "^3.1.2",
57
65
  "concurrently": "^4.1.2",
58
66
  "core-js": "^3.3.2",
67
+ "cross-env": "^7.0.0",
59
68
  "cssnano": "^4.1.10",
60
69
  "cypress": "^3.6.1",
61
70
  "cypress-axe": "^0.5.1",
71
+ "cypress-commands": "^1.0.0",
62
72
  "cz-conventional-changelog": "^2.1.0",
63
73
  "del": "^5.1.0",
74
+ "eslint": "^6.8.0",
75
+ "eslint-config-airbnb-base": "^14.0.0",
76
+ "eslint-config-prettier": "^6.10.0",
64
77
  "eslint-plugin-cypress": "^2.7.0",
78
+ "eslint-plugin-import": "^2.20.0",
79
+ "eslint-plugin-prettier": "^3.1.2",
80
+ "eslint-plugin-promise": "^4.2.1",
81
+ "eslint-plugin-vue": "^6.1.2",
65
82
  "file-loader": "^4.2.0",
66
83
  "flat": "^4.1.0",
67
84
  "gulp": "^4.0.2",
@@ -72,12 +89,16 @@
72
89
  "lint-staged": "^9.4.2",
73
90
  "mini-css-extract-plugin": "^0.8.0",
74
91
  "node-sass": "^4.12.0",
92
+ "prettier": "^1.19.1",
75
93
  "rimraf": "^2.7.1",
76
94
  "semantic-release": "^15.13.24",
77
95
  "svg-sprite-loader": "^4.1.6",
78
96
  "svgo": "^1.3.0",
79
97
  "svgo-loader": "^2.2.1",
80
98
  "svgstore": "^3.0.0-2",
99
+ "vue-jest": "^3.0.5",
100
+ "vue-loader": "^15.8.3",
101
+ "vue-template-compiler": "^2.6.11",
81
102
  "webpack": "^4.41.2",
82
103
  "webpack-cli": "^3.3.9",
83
104
  "webpack-dev-server": "^3.8.2",
@@ -12,6 +12,7 @@
12
12
  text-align: center;
13
13
  color: $mds-color-button-text-base;
14
14
  @extend .mds-font-body-copy;
15
+ font-family: $mds-font-family-button-base;
15
16
 
16
17
  @include mq($from: md) {
17
18
  padding: ($mds-size-baseline * 2) ($mds-size-baseline * 5);
@@ -0,0 +1,27 @@
1
+ ## Parameters - Nunjucks
2
+
3
+ - `id`: the id of your combobox
4
+ - `name`: the name of the input for form submission
5
+ - `resultMessage`: to be used as a prop for the Vue component below, will not be used if Javascript is not available
6
+ - `labelText`: the text used in the label
7
+ - `options`: a json object of key, value pairs e.g { 45: 'Orange' }. To be used when falling back to a native select if Javascript is not available
8
+ - `fallbackTo`: the form element to use as a fallback. Should be either 'select' or 'input'
9
+ - `placeholder`: the placeholder for your input (defaults to 'Please select')
10
+
11
+ ## Props - Vue component
12
+
13
+ - `comboboxid`: the id of your combobox. Populated automatically from Nunjucks parameters
14
+ - `labeltext`: the text used in the label. Populated automatically from Nunjucks parameters
15
+ - `placeholder`: the placeholder for your input. Populated automatically from Nunjucks parameters
16
+ - `resultmessage`: the visually hidden message to inform screenreader users when the options in the listbox have changed. Can either be a string which will be prefixed with the number of options, or a function which takes the number of options as its parameter and which should return a string. Defaults to 'options available', populated automatically from Nunjucks parameters if passed in
17
+ - `name`: the name of the input for form submission. Will only be populated automatically if fallbackTo is 'input'
18
+ - `options`: an array of options, which should be objects with a `label` and a `value`. The array should be provided externally by selecting the custom `<mds-combobox>` element and setting it as an attribute
19
+ - `filterOptions`: whether or not the Vue component should internally filter the options array according to the search input (defaults to true)
20
+
21
+ ## Accessibility
22
+
23
+ The Vue component is a [WAI-ARIA 1.0 combobox](https://www.w3.org/TR/wai-aria-practices/#combobox).
24
+
25
+ NB. The AJAX demo above currently only works in browsers, not <= IE11. Fetch will be polyfilled or replaced in due course.
26
+
27
+ When Javascript is not available, either a native combobox or native input will be available as a fallback.
@@ -0,0 +1,3 @@
1
+ {% macro MdsCombobox(params) %}
2
+ {%- include "./_template.njk" -%}
3
+ {% endmacro %}
@@ -0,0 +1,45 @@
1
+ {% from "../icons/_macro.njk" import MdsIcon %}
2
+
3
+ {%- set comboboxId %}
4
+ {%- if params.id -%}
5
+ {{ params.id }}
6
+ {%- else -%}
7
+ {#
8
+ if the id is missing it will be constructed using:
9
+ [component name]-[item labelText]-[index]
10
+ e.g. period-next-week-2
11
+ #}
12
+ {%- if params.name -%}{{ params.name | lower | trim | replace(' ', '-')}}{%- else -%}
13
+ {{ params.labelText | lower | trim | replace(' ', '-') }}{%- endif -%}
14
+ {%- endif -%}
15
+ {% endset -%}
16
+
17
+ {%- set placeholder %}
18
+ {%- if params.placeholder -%}
19
+ {{ params.placeholder }}
20
+ {%- else -%}
21
+ Please select
22
+ {%- endif -%}
23
+ {% endset -%}
24
+
25
+ {%- if comboboxId -%}
26
+ <div class="mds-combobox js-mds-combobox" data-combobox-id="{{ comboboxId }}" data-test="combobox">
27
+ <label class="mds-combobox__label mds-font-pica mds-combobox--fallback" id="{{ comboboxId }}-label" for="{{ comboboxId }}">{{ params.labelText }}</label>
28
+ {% if params.fallbackTo === 'select' and params.options %}
29
+ <select class="mds-combobox__select mds-combobox--fallback" id="select-{{ comboboxId }}" name="{{ params.name }}" aria-labelledby="{{ comboboxId }}-label" value="{{ params.defaultValue|default('') }}">
30
+ <option>{{ placeholder }}</option>
31
+ {%- if params.options -%}
32
+ {%- for value, option in params.options -%}
33
+ <option value="{{ value }}">{{ option }}</option>
34
+ {%- endfor -%}
35
+ {%- endif -%}
36
+ </select>
37
+ {% elseif params.fallbackTo === 'input' %}
38
+ <input class="mds-combobox__input mds-border mds-border-radius mds-combobox--fallback" autocomplete="off" name="{{ params.name }}" type="text"
39
+ id="{{ comboboxId }}" aria-labelledby="{{ comboboxId }}-label" value="{{ params.defaultValue|default('') }}" placeholder="{{ placeholder }}">
40
+ {% endif %}
41
+ {# Leave the custom element at the bottom so it has access to the above elements on render #}
42
+ <mds-combobox comboboxid="{{ comboboxId }}" labeltext="{{ params.labelText }}" placeholder="{{ placeholder }}"
43
+ {% if params.fallbackTo === 'input' %}name="{{ params.name }}"{% endif %} {% if params.resultMessage %}resultmessage="{{ params.resultMessage }}"{% endif %}></mds-combobox>
44
+ </div>
45
+ {%- endif -%}
@@ -0,0 +1,30 @@
1
+ module.exports = {
2
+ title: 'Combobox',
3
+ label: 'Combobox',
4
+ status: 'wip',
5
+ variants: [
6
+ {
7
+ name: 'default',
8
+ context: {
9
+ variantTitle: 'Hidden select',
10
+ name: 'distance',
11
+ id: 'distance-selection',
12
+ labelText: 'How far are you willing to travel?',
13
+ options: { 5: 'Within 5 miles', 10: 'Within 10 miles', 15: 'Within 15 miles', 20: 'Within 20 miles' },
14
+ fallbackTo: 'select',
15
+ },
16
+ },
17
+ {
18
+ name: 'AJAX autocomplete',
19
+ context: {
20
+ variantTitle: 'AJAX autocomplete',
21
+ name: 'keywords',
22
+ id: 'keywords-lookup',
23
+ resultMessage: 'suggestions',
24
+ labelText: 'Keywords:',
25
+ placeholder: 'eg. Web developer',
26
+ fallbackTo: 'input',
27
+ },
28
+ },
29
+ ],
30
+ };
@@ -0,0 +1,20 @@
1
+ import 'document-register-element/build/document-register-element';
2
+ import Vue from 'vue';
3
+ import vueCustomElement from 'vue-custom-element';
4
+ import Combobox from './vue-components/Combobox.vue';
5
+
6
+ Vue.use(vueCustomElement);
7
+ Vue.config.devtools = process.env.NODE_ENV === 'development';
8
+
9
+ Vue.config.keyCodes.end = 35;
10
+ Vue.config.keyCodes.home = 36;
11
+
12
+ Vue.customElement('mds-combobox', Combobox, {
13
+ connectedCallback() {
14
+ // If the custom element has loaded, clear up any fallback elements that could cause ID clashes or weirdness in form submission
15
+ const fallbackInput = this.parentElement.querySelector('input');
16
+ const fallbackLabel = this.parentElement.querySelector('label');
17
+ if (fallbackInput) fallbackInput.remove();
18
+ if (fallbackLabel) fallbackLabel.remove();
19
+ },
20
+ });
@@ -0,0 +1,14 @@
1
+ {% from "./combobox/_macro.njk" import MdsCombobox %}
2
+
3
+ <h3>{{ variantTitle }}</h3>
4
+ {{ MdsCombobox({
5
+ id: id,
6
+ name: name,
7
+ resultMessage: resultMessage,
8
+ labelText: labelText,
9
+ options: options,
10
+ fallbackTo: fallbackTo,
11
+ placeholder: placeholder
12
+ }) }}
13
+
14
+ <br><br>
@@ -0,0 +1,52 @@
1
+ .mds-combobox {
2
+ display: block;
3
+ position: relative;
4
+ & .mds-combobox__input, .mds-combobox__select {
5
+ display: block;
6
+ width: 100%;
7
+ padding: $mds-size-baseline;
8
+ padding-right: $mds-size-baseline * 5;
9
+ overflow: hidden;
10
+ text-overflow: ellipsis;
11
+ white-space: nowrap;
12
+ line-height: inherit;
13
+ }
14
+ & .mds-combobox__input-wrapper {
15
+ position: relative;
16
+ }
17
+ & .mds-icon {
18
+ position: absolute;
19
+ right: $mds-size-baseline * 5;
20
+ top: 50%;
21
+ pointer-events: none;
22
+ transform: translateY(-50%);
23
+ transition: transform 0.1s ease-in-out;
24
+ }
25
+ &.mds-combobox--active .mds-icon {
26
+ transform: translateY(-50%) rotate(90deg);
27
+ }
28
+ & .mds-combobox__listbox {
29
+ position: absolute;
30
+ left: 0;
31
+ right: 0;
32
+ max-height: 250px;
33
+ overflow-y: scroll;
34
+ z-index: 1;
35
+ .mds-combobox__option {
36
+ padding: $mds-size-baseline;
37
+ background-color: $mds-color-neutral-white;
38
+ &:last-child {
39
+ border-bottom: 0;
40
+ }
41
+ &.mds-combobox__option--focused, &:hover {
42
+ cursor: pointer;
43
+ background-color: $mds-color-neutral-lighter;
44
+ }
45
+ }
46
+ }
47
+ }
48
+
49
+ .js .mds-combobox .mds-combobox--fallback {
50
+ display: none;
51
+ }
52
+
@@ -0,0 +1,210 @@
1
+ <template>
2
+ <div
3
+ class="mds-combobox js-mds-combobox"
4
+ :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)"
9
+ @keydown.esc="makeInactive"
10
+ @keydown.enter="chooseOption"
11
+ >
12
+ <label class="mds-combobox__label mds-font-pica" :id="labelId" :for="comboboxid">{{ labeltext }}</label>
13
+ <ComboboxInput
14
+ @focus="makeActive"
15
+ @blur="onInputBlur"
16
+ :id="comboboxid"
17
+ :name="name"
18
+ :placeholder="placeholder"
19
+ :aria-labelledby="labelId"
20
+ :aria-owns="listBoxId"
21
+ :aria-expanded="expanded"
22
+ :activeDescendent="selectedOptionId"
23
+ v-model="inputValue"
24
+ />
25
+ <ListBox :id="listBoxId" :aria-labelledby="labelId" :hidden="listBoxHidden">
26
+ <template>
27
+ <ListBoxOption
28
+ v-for="(option, index) in visibleOptions"
29
+ :key="index"
30
+ :option="option"
31
+ :id="`${optionId}-${index}`"
32
+ :focused="selectedOption === option"
33
+ @mousedown="clickOption(option)"
34
+ />
35
+ </template>
36
+ </ListBox>
37
+ <div aria-live="polite" role="status" class="mds-visually-hidden">
38
+ {{ resultCount }}
39
+ </div>
40
+ </div>
41
+ </template>
42
+
43
+ <script>
44
+ import ComboboxInput from './ComboboxInput.vue';
45
+ import ListBox from './ListBox.vue';
46
+ import ListBoxOption from './ListBoxOption.vue';
47
+
48
+ export default {
49
+ name: 'Combobox',
50
+ components: {
51
+ ComboboxInput,
52
+ ListBox,
53
+ ListBoxOption,
54
+ },
55
+ props: {
56
+ comboboxid: {
57
+ type: String,
58
+ required: true,
59
+ },
60
+ labeltext: {
61
+ type: String,
62
+ required: true,
63
+ },
64
+ placeholder: {
65
+ type: String,
66
+ default: 'Please combobox',
67
+ },
68
+ resultmessage: {
69
+ type: [String, Function],
70
+ default: 'options available',
71
+ },
72
+ name: {
73
+ type: [String, Boolean],
74
+ default: false,
75
+ },
76
+ options: {
77
+ type: Array,
78
+ default: () => [],
79
+ },
80
+ filterOptions: {
81
+ type: Boolean,
82
+ default: true,
83
+ },
84
+ },
85
+ data() {
86
+ return {
87
+ expanded: 'false',
88
+ selected: null,
89
+ chosen: null,
90
+ searchValue: '',
91
+ };
92
+ },
93
+ computed: {
94
+ inputValue: {
95
+ get() {
96
+ if (this.chosenOption) {
97
+ return this.chosenOption.label;
98
+ }
99
+ return this.searchValue;
100
+ },
101
+ set(event) {
102
+ // Reset any chosen option if user is typing again
103
+ this.chosenOption = null;
104
+ this.searchValue = event.target ? event.target.value : '';
105
+ this.makeActive();
106
+ this.$emit('search', this.searchValue);
107
+ },
108
+ },
109
+ selectedOption: {
110
+ get() {
111
+ return this.selected;
112
+ },
113
+ set(newOption) {
114
+ this.selected = newOption;
115
+ },
116
+ },
117
+ chosenOption: {
118
+ get() {
119
+ return this.chosen;
120
+ },
121
+ set(newOption) {
122
+ this.chosen = newOption;
123
+ this.selectedOption = newOption;
124
+ this.$emit('select-option', this.chosen);
125
+ },
126
+ },
127
+ visibleOptions() {
128
+ if (this.filterOptions) {
129
+ return this.options.filter((opt) => opt.label.toLowerCase().includes(this.searchValue.toLowerCase()));
130
+ }
131
+ return this.options;
132
+ },
133
+ labelId() {
134
+ return `${this.comboboxid}-label`;
135
+ },
136
+ listBoxId() {
137
+ return `${this.comboboxid}-listbox`;
138
+ },
139
+ optionId() {
140
+ return `${this.comboboxid}-option`;
141
+ },
142
+ selectedOptionId() {
143
+ const index = this.visibleOptions.indexOf(this.selectedOption);
144
+ if (index > -1) {
145
+ return `${this.optionId}-${index}`;
146
+ }
147
+ return false;
148
+ },
149
+ listBoxHidden() {
150
+ return this.expanded === 'false';
151
+ },
152
+ lastOptionIndex() {
153
+ return this.visibleOptions.length - 1;
154
+ },
155
+ resultCount() {
156
+ if (typeof this.resultmessage === 'function') {
157
+ return this.resultmessage(this.visibleOptions.length);
158
+ }
159
+ return `${this.visibleOptions.length} ${this.resultmessage}`;
160
+ },
161
+ },
162
+ methods: {
163
+ makeActive() {
164
+ this.expanded = 'true';
165
+ },
166
+ makeInactive() {
167
+ this.expanded = 'false';
168
+ },
169
+ clickOption(option = this.selectedOption) {
170
+ this.chosenOption = option;
171
+ this.makeInactive();
172
+ },
173
+ chooseOption() {
174
+ this.chosenOption = this.selectedOption;
175
+ this.makeInactive();
176
+ },
177
+ hiddenGuard(fn) {
178
+ if (this.listBoxHidden) return;
179
+ fn.call(this);
180
+ },
181
+ onInputBlur() {
182
+ this.makeInactive();
183
+ },
184
+ onKeyDown() {
185
+ if (this.selectedOption) {
186
+ const currentIndex = this.visibleOptions.indexOf(this.selectedOption);
187
+ const nextIndex = currentIndex === this.lastOptionIndex ? 0 : currentIndex + 1;
188
+ this.selectedOption = this.visibleOptions[nextIndex];
189
+ } else {
190
+ [this.selectedOption] = this.visibleOptions;
191
+ }
192
+ },
193
+ onKeyUp() {
194
+ if (this.selectedOption) {
195
+ const currentIndex = this.visibleOptions.indexOf(this.selectedOption);
196
+ const nextIndex = currentIndex === 0 ? this.lastOptionIndex : currentIndex - 1;
197
+ this.selectedOption = this.visibleOptions[nextIndex];
198
+ } else {
199
+ this.selectedOption = this.visibleOptions[this.lastOptionIndex];
200
+ }
201
+ },
202
+ onKeyHome() {
203
+ [this.selectedOption] = this.visibleOptions;
204
+ },
205
+ onKeyEnd() {
206
+ this.selectedOption = this.visibleOptions[this.lastOptionIndex];
207
+ },
208
+ },
209
+ };
210
+ </script>
@@ -0,0 +1,39 @@
1
+ <template>
2
+ <div role="presentation" class="mds-combobox__input-wrapper">
3
+ <input
4
+ v-bind="$attrs"
5
+ class="mds-combobox__input mds-border mds-border-radius"
6
+ autocomplete="off"
7
+ type="text"
8
+ :value="value"
9
+ @input="$emit('input', $event)"
10
+ @focus="$emit('focus', $event)"
11
+ @blur="$emit('blur', $event)"
12
+ role="combobox"
13
+ aria-autocomplete="list"
14
+ :aria-activedescendant="activeDescendent"
15
+ />
16
+ <!-- Ideally we would pass this in as a slot in the custom element with MdsIcon, but something is borked
17
+ with passing slots in Chrome https://github.com/karol-f/vue-custom-element/issues/162 -->
18
+ <svg aria-hidden="true" focusable="false" class="mds-icon mds-icon--chevron-right mds-icon--after mds-icon--sm">
19
+ <use href="/assets/icons.svg#chevron-right" />
20
+ </svg>
21
+ </div>
22
+ </template>
23
+
24
+ <script>
25
+ export default {
26
+ name: 'ComboboxInput',
27
+ props: {
28
+ value: {
29
+ type: String,
30
+ default: '',
31
+ },
32
+ activeDescendent: {
33
+ type: [String, Boolean],
34
+ default: false,
35
+ },
36
+ },
37
+ inheritAttrs: false,
38
+ };
39
+ </script>
@@ -0,0 +1,17 @@
1
+ <template>
2
+ <ul class="mds-combobox__listbox mds-border mds-border-top-none" role="listbox" :hidden="hidden">
3
+ <slot></slot>
4
+ </ul>
5
+ </template>
6
+
7
+ <script>
8
+ export default {
9
+ name: 'ListBox',
10
+ props: {
11
+ hidden: {
12
+ type: Boolean,
13
+ default: true,
14
+ },
15
+ },
16
+ };
17
+ </script>
@@ -0,0 +1,27 @@
1
+ <template>
2
+ <li
3
+ class="mds-combobox__option mds-border-bottom mds-font-pica"
4
+ role="option"
5
+ :class="{ 'mds-combobox__option--focused': focused }"
6
+ :aria-selected="focused.toString()"
7
+ @mousedown="$emit('mousedown', $event)"
8
+ >
9
+ {{ option.label }}
10
+ </li>
11
+ </template>
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
+ },
26
+ };
27
+ </script>
@@ -0,0 +1,50 @@
1
+ const containerClass = 'js-mds-combobox';
2
+ const elementName = 'mds-combobox';
3
+
4
+ function bindToSelect() {
5
+ const el = document.querySelector(`.${containerClass}[data-combobox-id="distance-selection"]`);
6
+ if (el) {
7
+ const selectInput = document.getElementById('select-distance-selection');
8
+ const options = Array.from(selectInput.querySelectorAll('option'));
9
+ const vueSelect = el.querySelector(elementName);
10
+ vueSelect.options = options.slice(1).map((opt) => ({ value: opt.value, label: opt.textContent }));
11
+ vueSelect.addEventListener('select-option', (event) => {
12
+ const [option] = event.detail;
13
+ if (!option) return;
14
+ selectInput.value = option.value;
15
+ selectInput.querySelector(`option[value="${option.value}"]`).setAttribute('selected', true);
16
+ });
17
+ }
18
+ }
19
+
20
+ function bindToApi() {
21
+ const el = document.querySelector(`.${containerClass}[data-combobox-id="keywords-lookup"]`);
22
+ if (el) {
23
+ const vueSelect = el.querySelector(elementName);
24
+ vueSelect.filterOptions = false;
25
+ vueSelect.addEventListener('search', (event) => {
26
+ const [searchValue] = event.detail;
27
+ if (searchValue) {
28
+ fetch(`https://api.datamuse.com/sug?s=${searchValue}`)
29
+ .then((res) => res.json())
30
+ .then((data) => {
31
+ const options = data.map(({ word }) => ({ value: word, label: word }));
32
+ vueSelect.options = options;
33
+ return options;
34
+ })
35
+ .catch(console.log);
36
+ } else {
37
+ vueSelect.options = [];
38
+ }
39
+ });
40
+ }
41
+ }
42
+
43
+ const combobox = {
44
+ init: () => {
45
+ bindToSelect();
46
+ bindToApi();
47
+ },
48
+ };
49
+
50
+ export default combobox;
@@ -1,7 +1,9 @@
1
1
  import switchStateScript from './fractal-scripts/switch-state';
2
2
  import notificationScript from './fractal-scripts/notification';
3
+ import comboboxScript from './fractal-scripts/combobox';
3
4
 
4
5
  document.addEventListener('DOMContentLoaded', () => {
5
6
  switchStateScript.init();
6
7
  notificationScript.init();
8
+ comboboxScript.init();
7
9
  });
@@ -1,2 +1,3 @@
1
1
  import 'svgxuse';
2
2
  import './polyfills/closest';
3
+ import './polyfills/remove';