@mirweb/mir-web-components 1.15.3 → 2.0.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 (184) hide show
  1. package/README.md +2 -2
  2. package/dist/assets/index-Cl4fzBs2.js +17 -0
  3. package/dist/assets/scss/globals.scss +231 -0
  4. package/dist/assets/scss/index.scss +4 -0
  5. package/dist/assets/scss/normalize.scss +393 -0
  6. package/dist/assets/scss/reset.scss +102 -0
  7. package/dist/assets/scss/variables.scss +95 -0
  8. package/dist/components/atoms/button/button.vue +81 -0
  9. package/dist/components/atoms/checkbox/checkbox.vue +125 -0
  10. package/dist/components/atoms/chip/chip.vue +55 -0
  11. package/dist/components/atoms/dropdown/dropdown.vue +490 -0
  12. package/dist/components/atoms/image/image.vue +42 -0
  13. package/dist/components/atoms/label/label.vue +52 -0
  14. package/dist/components/atoms/link/link.vue +166 -0
  15. package/dist/components/atoms/radio-button/radio-button.vue +110 -0
  16. package/dist/components/atoms/select/select.vue +116 -0
  17. package/dist/components/atoms/select-multiple/select-multiple.vue +210 -0
  18. package/dist/components/atoms/slider/slider.vue +322 -0
  19. package/dist/components/atoms/text-field/text-field.vue +273 -0
  20. package/dist/components/atoms/textarea/textarea.vue +179 -0
  21. package/dist/components/atoms/video/video.vue +98 -0
  22. package/dist/components/blocks/accordion/accordion.vue +222 -0
  23. package/dist/components/blocks/card-display/card-display.vue +125 -0
  24. package/dist/components/blocks/column-grid/column-grid.vue +201 -0
  25. package/dist/components/blocks/facts/facts.vue +156 -0
  26. package/dist/components/blocks/features/features.vue +176 -0
  27. package/dist/components/blocks/flashcards/flashcards.vue +212 -0
  28. package/dist/components/blocks/form-script/form-script.vue +172 -0
  29. package/dist/components/blocks/frontpage-hero/frontpage-hero.vue +214 -0
  30. package/dist/components/blocks/headline/headline.vue +93 -0
  31. package/dist/components/blocks/hero/hero.vue +173 -0
  32. package/dist/components/blocks/image/image.vue +93 -0
  33. package/dist/components/blocks/image-gallery/image-gallery.vue +289 -0
  34. package/dist/components/blocks/logo-wall/logo-wall.vue +125 -0
  35. package/dist/components/blocks/micro-stories/micro-stories.vue +316 -0
  36. package/dist/components/blocks/pallet-jack/pallet-jack.vue +440 -0
  37. package/dist/components/blocks/policy/policy.vue +106 -0
  38. package/dist/components/blocks/product-hero/product-hero.vue +140 -0
  39. package/dist/components/blocks/promo/promo.vue +403 -0
  40. package/dist/components/blocks/quote/quote.vue +127 -0
  41. package/dist/components/blocks/rich-text/rich-text-columns.vue +159 -0
  42. package/dist/components/blocks/rich-text/rich-text.vue +296 -0
  43. package/dist/components/blocks/timeline/timeline.vue +232 -0
  44. package/dist/components/blocks/vimeo/vimeo.vue +52 -0
  45. package/dist/components/index.ts +51 -0
  46. package/dist/components/molecules/address/address.vue +123 -0
  47. package/dist/components/molecules/bullet-list/bullet-list.vue +99 -0
  48. package/dist/components/molecules/card/card.vue +302 -0
  49. package/dist/components/molecules/column-card/column-card.vue +178 -0
  50. package/dist/components/molecules/event-card/event-card.vue +111 -0
  51. package/dist/components/molecules/flashcard/flashcard.vue +293 -0
  52. package/dist/components/molecules/modal/modal.vue +113 -0
  53. package/dist/components/molecules/text-card/text-card.vue +74 -0
  54. package/dist/components/organisms/404/404.vue +79 -0
  55. package/dist/components/organisms/filter/filter.vue +89 -0
  56. package/dist/components/organisms/footer/footer.vue +356 -0
  57. package/dist/components/organisms/header/header.vue +754 -0
  58. package/dist/components/organisms/language-switcher/language-switcher.vue +68 -0
  59. package/dist/components/organisms/pagination/pagination.vue +85 -0
  60. package/dist/components/organisms/search/search.vue +153 -0
  61. package/dist/components/templates/404-error-page.vue +0 -0
  62. package/dist/directives/clickOutside.ts +15 -0
  63. package/dist/fonts/OpenSans-Light.woff2 +0 -0
  64. package/dist/fonts/OpenSans-Medium.woff2 +0 -0
  65. package/dist/fonts/OpenSans-Regular.woff2 +0 -0
  66. package/dist/fonts/OpenSans-SemiBold.woff2 +0 -0
  67. package/dist/fonts/Oscine_Bd.woff2 +0 -0
  68. package/dist/fonts/Oscine_Lt.woff2 +0 -0
  69. package/dist/fonts/Oscine_Rg.woff2 +0 -0
  70. package/dist/index.html +12 -0
  71. package/dist/main.css +1 -0
  72. package/package.json +8 -10
  73. package/dist/components/atoms/button/button.vue.d.ts +0 -5
  74. package/dist/components/atoms/button/button.vue.d.ts.map +0 -1
  75. package/dist/components/atoms/checkbox/checkbox.vue.d.ts +0 -5
  76. package/dist/components/atoms/checkbox/checkbox.vue.d.ts.map +0 -1
  77. package/dist/components/atoms/chip/chip.vue.d.ts +0 -5
  78. package/dist/components/atoms/chip/chip.vue.d.ts.map +0 -1
  79. package/dist/components/atoms/dropdown/dropdown.vue.d.ts +0 -5
  80. package/dist/components/atoms/dropdown/dropdown.vue.d.ts.map +0 -1
  81. package/dist/components/atoms/image/image.vue.d.ts +0 -5
  82. package/dist/components/atoms/image/image.vue.d.ts.map +0 -1
  83. package/dist/components/atoms/label/label.vue.d.ts +0 -5
  84. package/dist/components/atoms/label/label.vue.d.ts.map +0 -1
  85. package/dist/components/atoms/link/link.vue.d.ts +0 -5
  86. package/dist/components/atoms/link/link.vue.d.ts.map +0 -1
  87. package/dist/components/atoms/radio-button/radio-button.vue.d.ts +0 -5
  88. package/dist/components/atoms/radio-button/radio-button.vue.d.ts.map +0 -1
  89. package/dist/components/atoms/select/select.vue.d.ts +0 -5
  90. package/dist/components/atoms/select/select.vue.d.ts.map +0 -1
  91. package/dist/components/atoms/select-multiple/select-multiple.vue.d.ts +0 -5
  92. package/dist/components/atoms/select-multiple/select-multiple.vue.d.ts.map +0 -1
  93. package/dist/components/atoms/slider/slider.vue.d.ts +0 -5
  94. package/dist/components/atoms/slider/slider.vue.d.ts.map +0 -1
  95. package/dist/components/atoms/text-field/text-field.vue.d.ts +0 -5
  96. package/dist/components/atoms/text-field/text-field.vue.d.ts.map +0 -1
  97. package/dist/components/atoms/textarea/textarea.vue.d.ts +0 -5
  98. package/dist/components/atoms/textarea/textarea.vue.d.ts.map +0 -1
  99. package/dist/components/atoms/video/video.vue.d.ts +0 -5
  100. package/dist/components/atoms/video/video.vue.d.ts.map +0 -1
  101. package/dist/components/blocks/accordion/accordion.vue.d.ts +0 -5
  102. package/dist/components/blocks/accordion/accordion.vue.d.ts.map +0 -1
  103. package/dist/components/blocks/card-display/card-display.vue.d.ts +0 -6
  104. package/dist/components/blocks/card-display/card-display.vue.d.ts.map +0 -1
  105. package/dist/components/blocks/column-grid/column-grid.vue.d.ts +0 -5
  106. package/dist/components/blocks/column-grid/column-grid.vue.d.ts.map +0 -1
  107. package/dist/components/blocks/facts/facts.vue.d.ts +0 -5
  108. package/dist/components/blocks/facts/facts.vue.d.ts.map +0 -1
  109. package/dist/components/blocks/features/features.vue.d.ts +0 -5
  110. package/dist/components/blocks/features/features.vue.d.ts.map +0 -1
  111. package/dist/components/blocks/flashcards/flashcards.vue.d.ts +0 -5
  112. package/dist/components/blocks/flashcards/flashcards.vue.d.ts.map +0 -1
  113. package/dist/components/blocks/form-script/form-script.vue.d.ts +0 -5
  114. package/dist/components/blocks/form-script/form-script.vue.d.ts.map +0 -1
  115. package/dist/components/blocks/frontpage-hero/frontpage-hero.vue.d.ts +0 -5
  116. package/dist/components/blocks/frontpage-hero/frontpage-hero.vue.d.ts.map +0 -1
  117. package/dist/components/blocks/headline/headline.vue.d.ts +0 -5
  118. package/dist/components/blocks/headline/headline.vue.d.ts.map +0 -1
  119. package/dist/components/blocks/hero/hero.vue.d.ts +0 -5
  120. package/dist/components/blocks/hero/hero.vue.d.ts.map +0 -1
  121. package/dist/components/blocks/image/image.vue.d.ts +0 -5
  122. package/dist/components/blocks/image/image.vue.d.ts.map +0 -1
  123. package/dist/components/blocks/image-gallery/image-gallery.vue.d.ts +0 -5
  124. package/dist/components/blocks/image-gallery/image-gallery.vue.d.ts.map +0 -1
  125. package/dist/components/blocks/logo-wall/logo-wall.vue.d.ts +0 -5
  126. package/dist/components/blocks/logo-wall/logo-wall.vue.d.ts.map +0 -1
  127. package/dist/components/blocks/micro-stories/micro-stories.vue.d.ts +0 -5
  128. package/dist/components/blocks/micro-stories/micro-stories.vue.d.ts.map +0 -1
  129. package/dist/components/blocks/pallet-jack/pallet-jack.vue.d.ts +0 -5
  130. package/dist/components/blocks/pallet-jack/pallet-jack.vue.d.ts.map +0 -1
  131. package/dist/components/blocks/policy/policy.vue.d.ts +0 -4
  132. package/dist/components/blocks/policy/policy.vue.d.ts.map +0 -1
  133. package/dist/components/blocks/product-hero/product-hero.vue.d.ts +0 -5
  134. package/dist/components/blocks/product-hero/product-hero.vue.d.ts.map +0 -1
  135. package/dist/components/blocks/promo/promo.vue.d.ts +0 -5
  136. package/dist/components/blocks/promo/promo.vue.d.ts.map +0 -1
  137. package/dist/components/blocks/quote/quote.vue.d.ts +0 -5
  138. package/dist/components/blocks/quote/quote.vue.d.ts.map +0 -1
  139. package/dist/components/blocks/rich-text/rich-text-columns.vue.d.ts +0 -4
  140. package/dist/components/blocks/rich-text/rich-text-columns.vue.d.ts.map +0 -1
  141. package/dist/components/blocks/rich-text/rich-text.vue.d.ts +0 -5
  142. package/dist/components/blocks/rich-text/rich-text.vue.d.ts.map +0 -1
  143. package/dist/components/blocks/timeline/timeline.vue.d.ts +0 -5
  144. package/dist/components/blocks/timeline/timeline.vue.d.ts.map +0 -1
  145. package/dist/components/blocks/vimeo/vimeo.vue.d.ts +0 -5
  146. package/dist/components/blocks/vimeo/vimeo.vue.d.ts.map +0 -1
  147. package/dist/components/index.d.ts +0 -51
  148. package/dist/components/main.d.ts +0 -59
  149. package/dist/components/molecules/address/address.vue.d.ts +0 -5
  150. package/dist/components/molecules/address/address.vue.d.ts.map +0 -1
  151. package/dist/components/molecules/bullet-list/bullet-list.vue.d.ts +0 -5
  152. package/dist/components/molecules/bullet-list/bullet-list.vue.d.ts.map +0 -1
  153. package/dist/components/molecules/card/card.vue.d.ts +0 -5
  154. package/dist/components/molecules/card/card.vue.d.ts.map +0 -1
  155. package/dist/components/molecules/column-card/column-card.vue.d.ts +0 -5
  156. package/dist/components/molecules/column-card/column-card.vue.d.ts.map +0 -1
  157. package/dist/components/molecules/event-card/event-card.vue.d.ts +0 -5
  158. package/dist/components/molecules/event-card/event-card.vue.d.ts.map +0 -1
  159. package/dist/components/molecules/flashcard/flashcard.vue.d.ts +0 -5
  160. package/dist/components/molecules/flashcard/flashcard.vue.d.ts.map +0 -1
  161. package/dist/components/molecules/modal/modal.vue.d.ts +0 -5
  162. package/dist/components/molecules/modal/modal.vue.d.ts.map +0 -1
  163. package/dist/components/molecules/text-card/text-card.vue.d.ts +0 -5
  164. package/dist/components/molecules/text-card/text-card.vue.d.ts.map +0 -1
  165. package/dist/components/organisms/404/404.vue.d.ts +0 -5
  166. package/dist/components/organisms/404/404.vue.d.ts.map +0 -1
  167. package/dist/components/organisms/filter/filter.vue.d.ts +0 -5
  168. package/dist/components/organisms/filter/filter.vue.d.ts.map +0 -1
  169. package/dist/components/organisms/footer/footer.vue.d.ts +0 -5
  170. package/dist/components/organisms/footer/footer.vue.d.ts.map +0 -1
  171. package/dist/components/organisms/header/header.vue.d.ts +0 -6
  172. package/dist/components/organisms/header/header.vue.d.ts.map +0 -1
  173. package/dist/components/organisms/language-switcher/language-switcher.vue.d.ts +0 -5
  174. package/dist/components/organisms/language-switcher/language-switcher.vue.d.ts.map +0 -1
  175. package/dist/components/organisms/pagination/pagination.vue.d.ts +0 -5
  176. package/dist/components/organisms/pagination/pagination.vue.d.ts.map +0 -1
  177. package/dist/components/organisms/search/search.vue.d.ts +0 -4
  178. package/dist/components/organisms/search/search.vue.d.ts.map +0 -1
  179. package/dist/directives/clickOutside.d.ts +0 -3
  180. package/dist/main.d.ts +0 -1
  181. package/dist/mir-web-components.cjs.js +0 -1
  182. package/dist/mir-web-components.css +0 -1
  183. package/dist/mir-web-components.es.js +0 -3187
  184. package/dist/mir-web-components.umd.js +0 -2
@@ -0,0 +1,125 @@
1
+ <template>
2
+ <div class="checkbox__wrapper" :class="disabled ? 'disabled' : ''">
3
+ <input
4
+ :id="id"
5
+ type="checkbox"
6
+ :name="name"
7
+ :value="value"
8
+ :disabled="disabled"
9
+ :required="required"
10
+ :checked="checked"
11
+ class="checkbox__checkbox"
12
+ @change="onChange"
13
+ />
14
+ <label
15
+ :for="id"
16
+ class="checkbox__label"
17
+ :class="required ? 'required' : ''"
18
+ >
19
+ <span class="checkmark"></span>
20
+ <slot></slot>
21
+ </label>
22
+ </div>
23
+ </template>
24
+
25
+ <script lang="ts" setup>
26
+ const emit = defineEmits(["input"]);
27
+ const onChange = (event: Event) => {
28
+ const target = event.target as HTMLInputElement;
29
+ emit("input", target.checked);
30
+ };
31
+
32
+ export type Props = {
33
+ name: string;
34
+ value: string | number | boolean;
35
+ disabled: boolean;
36
+ id: string;
37
+ required: boolean;
38
+ checked?: boolean;
39
+ };
40
+
41
+ withDefaults(defineProps<Props>(), {
42
+ name: "checkbox",
43
+ value: "value",
44
+ disabled: false,
45
+ id: "checkbox",
46
+ required: false,
47
+ checked: false,
48
+ });
49
+ </script>
50
+
51
+ <style lang="scss" scoped>
52
+ @use "../../../assets/scss/variables.scss" as *;
53
+ .checkbox {
54
+ &__checkbox {
55
+ position: absolute;
56
+ opacity: 0;
57
+ cursor: pointer;
58
+ height: 0;
59
+ width: 0;
60
+
61
+ &:checked ~ .checkbox__label .checkmark {
62
+ background-color: $blue-800;
63
+ border-color: $grey-400;
64
+ }
65
+ &:focus ~ .checkbox__label .checkmark {
66
+ box-shadow: $box-shadow;
67
+ }
68
+ }
69
+
70
+ &__label {
71
+ position: relative;
72
+ padding-left: 30px;
73
+ cursor: pointer;
74
+ font-size: $font-size-xsm;
75
+ font-family: $font-opensans;
76
+ font-weight: 300;
77
+ line-height: $line-height-xsm;
78
+ user-select: none;
79
+ display: block;
80
+
81
+ .checkmark {
82
+ position: absolute;
83
+ top: 0;
84
+ left: 0;
85
+ height: 20px;
86
+ width: 20px;
87
+ background-color: $white;
88
+ border-radius: 5px;
89
+ border: 1px solid $grey-400;
90
+
91
+ &:after {
92
+ content: "";
93
+ position: absolute;
94
+ display: none;
95
+ left: 6px;
96
+ top: 2px;
97
+ width: 6px;
98
+ height: 12px;
99
+ border: solid $white;
100
+ border-width: 0 2px 2px 0;
101
+ transform: rotate(45deg);
102
+ }
103
+ &:hover {
104
+ box-shadow: $box-shadow;
105
+ }
106
+ }
107
+ }
108
+ }
109
+
110
+ .checkbox__checkbox:checked ~ .checkbox__label .checkmark:after {
111
+ display: block;
112
+ }
113
+
114
+ .disabled {
115
+ opacity: 0.5;
116
+ pointer-events: none;
117
+ }
118
+
119
+ .required {
120
+ &:after {
121
+ content: "\00a0*";
122
+ display: inline-block;
123
+ }
124
+ }
125
+ </style>
@@ -0,0 +1,55 @@
1
+ <template>
2
+ <div class="chip__wrapper" @click="$emit('remove-chip')">
3
+ <span class="chip" :aria-label="ariaLabel">
4
+ {{ text }}
5
+ <img
6
+ src="https://a.storyblok.com/f/230581/9x9/e4fb715dc9/close.svg?cv=1695125714598"
7
+ alt="close"
8
+ class="close-icon"
9
+ />
10
+ </span>
11
+ </div>
12
+ </template>
13
+
14
+ <script setup lang="ts">
15
+ import { computed } from "vue";
16
+
17
+ export type Props = {
18
+ text: string;
19
+ };
20
+
21
+ const props = withDefaults(defineProps<Props>(), {
22
+ text: "",
23
+ });
24
+
25
+ defineEmits(["remove-chip"]);
26
+ const ariaLabel = computed(() => `Chip: ${props.text}`);
27
+ </script>
28
+
29
+ <style lang="scss" scoped>
30
+ @use "../../../assets/scss/variables.scss" as *;
31
+ .chip {
32
+ display: inline-flex;
33
+ align-items: center;
34
+ height: 27px;
35
+ padding: 0px 10px;
36
+ background-color: $blue-300;
37
+ font-size: $font-size-xxsm;
38
+ line-height: $line-height-md;
39
+ color: $blue-900;
40
+ font-family: $font-opensans;
41
+ border-radius: 3px;
42
+ cursor: pointer;
43
+
44
+ .close-icon {
45
+ margin-left: 10px;
46
+
47
+ color: $blue-900;
48
+ }
49
+ &:hover,
50
+ &:focus {
51
+ box-shadow: $box-shadow;
52
+ transition: $transition-box-shadow;
53
+ }
54
+ }
55
+ </style>
@@ -0,0 +1,490 @@
1
+ <template>
2
+ <div ref="listbox" class="listbox__wrapper" :value="modelValue">
3
+ <label
4
+ :id="`${name}-label`"
5
+ :class="{
6
+ 'listbox__label--visually-hidden': !showLabel,
7
+ 'is-disabled': disabled,
8
+ 'is-required': required,
9
+ }"
10
+ class="listbox__label"
11
+ >
12
+ {{ label }}
13
+ </label>
14
+ <div v-click-outside="hideListbox" class="listbox__dropdown">
15
+ <button
16
+ :id="`${name}-button-label`"
17
+ ref="listboxButton"
18
+ :aria-labelledby="`${name}-label ${name}-button-label`"
19
+ type="button"
20
+ aria-haspopup="listbox"
21
+ :aria-disabled="disabled"
22
+ class="listbox__button"
23
+ :class="selectedVariant"
24
+ @click="toggleListbox"
25
+ @keydown="checkShow"
26
+ >
27
+ {{ modelValue ? selectedOptionLabel : placeholder }}
28
+ </button>
29
+ <ul
30
+ v-show="!listboxHidden"
31
+ ref="listboxNode"
32
+ :aria-labelledby="`${name}-label`"
33
+ :aria-activedescendant="modelValue"
34
+ tabindex="0"
35
+ role="listbox"
36
+ class="listbox__list"
37
+ @keydown="checkKeyDown"
38
+ @click="checkClickItem"
39
+ >
40
+ <li
41
+ v-for="(option, index) in options"
42
+ :key="`${name}-option-${index}`"
43
+ ref="listboxOptions"
44
+ :aria-selected="option.value === modelValue"
45
+ :data-value="option.value"
46
+ class="listbox__option"
47
+ role="option"
48
+ >
49
+ {{ option.label }}
50
+ </li>
51
+ </ul>
52
+ </div>
53
+ </div>
54
+ </template>
55
+
56
+ <script lang="ts" setup>
57
+ import { ref, computed } from "vue";
58
+ import { vClickOutside } from "../../../directives/clickOutside";
59
+
60
+ interface OptionType {
61
+ value: string;
62
+ label: string;
63
+ }
64
+
65
+ const VARIANTS = {
66
+ primary: "dropdown-dark-bg-primary",
67
+ dark: "dropdown-dark dropdown-dark-bg-dark",
68
+ } as const;
69
+
70
+ type Variant = keyof typeof VARIANTS;
71
+
72
+ export type Props = {
73
+ modelValue: string;
74
+ label: string;
75
+ options: Array<OptionType>;
76
+ name: string;
77
+ placeholder?: string;
78
+ showLabel?: boolean;
79
+ required?: boolean;
80
+ disabled?: boolean;
81
+ variant?: Variant;
82
+ };
83
+
84
+ const props = withDefaults(defineProps<Props>(), {
85
+ placeholder: "Choose a value",
86
+ showLabel: false,
87
+ required: false,
88
+ disabled: false,
89
+ variant: "primary",
90
+ });
91
+
92
+ const selectedVariant = computed(() => VARIANTS[props.variant]);
93
+
94
+ const listboxButton = ref<HTMLButtonElement | null>(null);
95
+ const listboxNode = ref<HTMLUListElement | null>(null);
96
+ const listboxOptions = ref<HTMLOptionElement[]>([]);
97
+ const keyClear = ref(0);
98
+ const keysSoFar = ref("");
99
+ const listboxHidden = ref(true);
100
+ const searchIndex = ref(0);
101
+
102
+ const emit = defineEmits(["update:modelValue"]);
103
+
104
+ // Computed property for label of the selected option
105
+ const selectedOptionLabel = computed(() => {
106
+ const selectedOption = props.options.find(
107
+ (option: OptionType) => option.value === props.modelValue,
108
+ );
109
+ return selectedOption && selectedOption.label;
110
+ });
111
+
112
+ // Method to update the selected value
113
+ function updateValue(value: string) {
114
+ emit("update:modelValue", value);
115
+ }
116
+
117
+ // Method to check if clicked item is an option and handle it accordingly
118
+ function checkClickItem(event: Event) {
119
+ const target = event.target as HTMLOptionElement;
120
+ if (target.getAttribute("role") === "option") {
121
+ focusItem(target);
122
+ hideListbox();
123
+ listboxButton.value?.focus();
124
+ }
125
+ }
126
+
127
+ // Method to handle keydown events when listbox is shown
128
+ function checkKeyDown(event: KeyboardEvent) {
129
+ const key = event.key;
130
+
131
+ switch (key) {
132
+ case "ArrowUp":
133
+ case "ArrowDown": {
134
+ event.preventDefault();
135
+ const selectedItemIndex = props.options.findIndex(
136
+ (option) => option.value === props.modelValue,
137
+ );
138
+ let nextItem = selectedItemIndex
139
+ ? listboxOptions.value[selectedItemIndex]
140
+ : listboxOptions.value[0];
141
+
142
+ if (key === "ArrowUp") {
143
+ if (selectedItemIndex - 1 >= 0) {
144
+ nextItem = listboxOptions.value[selectedItemIndex - 1];
145
+ }
146
+ } else {
147
+ if (selectedItemIndex + 1 <= props.options.length) {
148
+ nextItem = listboxOptions.value[selectedItemIndex + 1];
149
+ }
150
+ }
151
+
152
+ if (nextItem) {
153
+ focusItem(nextItem);
154
+ }
155
+
156
+ break;
157
+ }
158
+ case "Home":
159
+ case "PageUp":
160
+ event.preventDefault();
161
+ focusFirstItem();
162
+ break;
163
+ case "End":
164
+ case "PageDown":
165
+ event.preventDefault();
166
+ focusLastItem();
167
+ break;
168
+ case "Enter":
169
+ case "Escape":
170
+ event.preventDefault();
171
+ hideListbox();
172
+ listboxButton.value?.focus();
173
+ break;
174
+ default: {
175
+ const itemToFocus = findItemToFocus(key);
176
+ if (itemToFocus) {
177
+ focusItem(itemToFocus);
178
+ }
179
+ break;
180
+ }
181
+ }
182
+ }
183
+
184
+ // Method to handle keydown events to show listbox
185
+ function checkShow(event: KeyboardEvent) {
186
+ if (!props.disabled) {
187
+ const key = event.key;
188
+
189
+ switch (key) {
190
+ case "ArrowUp":
191
+ case "ArrowDown":
192
+ event.preventDefault();
193
+ showListbox();
194
+ checkKeyDown(event);
195
+ break;
196
+ default:
197
+ break;
198
+ }
199
+ }
200
+ }
201
+
202
+ // Method to remove focus from item
203
+ function defocusItem(element: HTMLOptionElement) {
204
+ if (!element) {
205
+ return;
206
+ }
207
+ element.removeAttribute("aria-selected");
208
+ }
209
+
210
+ // Method to clear keysSoFar after 500ms
211
+ function clearKeysSoFarAfterDelay() {
212
+ if (keyClear.value) {
213
+ clearTimeout(keyClear.value);
214
+ keyClear.value = 0;
215
+ }
216
+ keyClear.value = setTimeout(() => {
217
+ keysSoFar.value = "";
218
+ keyClear.value = 0;
219
+ }, 500);
220
+ }
221
+
222
+ // Method to search for item to focus based on keys pressed
223
+ function findItemToFocus(key: string) {
224
+ let lastKeyPressed = "";
225
+
226
+ // Reset search index if first key press or different key
227
+ if (keysSoFar.value === "" || lastKeyPressed !== key) {
228
+ searchIndex.value = props.options.findIndex(
229
+ (option) => option.value === props.modelValue,
230
+ );
231
+ }
232
+
233
+ // Keep or append key to keysSoFar based on last key press
234
+ keysSoFar.value = lastKeyPressed === key ? key : keysSoFar.value + key;
235
+
236
+ lastKeyPressed = key;
237
+
238
+ clearKeysSoFarAfterDelay();
239
+
240
+ // Find next match from search index onwards
241
+ let nextMatch = findMatchInOptions(
242
+ searchIndex.value + 1,
243
+ props.options.length,
244
+ );
245
+
246
+ // If no match, search from start to current search index
247
+ if (!nextMatch && keysSoFar.value.length === 1) {
248
+ nextMatch = findMatchInOptions(0, searchIndex.value);
249
+ }
250
+
251
+ // Cycle searchIndex with each key press
252
+ searchIndex.value = (searchIndex.value + 1) % props.options.length;
253
+
254
+ return nextMatch;
255
+ }
256
+
257
+ // Method to find match in options between start and end index
258
+ function findMatchInOptions(startIndex: number, endIndex: number) {
259
+ for (let i = startIndex; i < endIndex; i++) {
260
+ if (
261
+ props.options[i].label &&
262
+ props.options[i].label
263
+ .toUpperCase()
264
+ .indexOf(keysSoFar.value.toUpperCase()) === 0
265
+ ) {
266
+ return listboxOptions.value[i];
267
+ }
268
+ }
269
+ return null;
270
+ }
271
+
272
+ // Method to focus on the first item in listbox
273
+ function focusFirstItem() {
274
+ focusItem(listboxOptions.value[0]);
275
+ }
276
+
277
+ // Method to set focus on given item
278
+ function focusItem(element: HTMLOptionElement) {
279
+ const dataValue = element.getAttribute("data-value");
280
+
281
+ // Defocus active element
282
+ if (props.modelValue) {
283
+ const index = props.options.findIndex(
284
+ (option) => option.value === props.modelValue,
285
+ );
286
+ const listboxOption = listboxOptions.value[index];
287
+ defocusItem(listboxOption);
288
+ }
289
+ element.setAttribute("aria-selected", "true");
290
+ listboxNode.value?.setAttribute(
291
+ "aria-activedescendant",
292
+ dataValue ? dataValue : "",
293
+ );
294
+ // Trigger the v-model "input" event to update the modelValue
295
+ updateValue(dataValue ? dataValue : "");
296
+
297
+ // Scroll up/down to show the listbox within the viewport
298
+ if (listboxNode.value) {
299
+ if (listboxNode.value.scrollHeight > listboxNode.value.clientHeight) {
300
+ const scrollBottom =
301
+ listboxNode.value.clientHeight + listboxNode.value.scrollTop;
302
+ const elementBottom = element.offsetTop + element.offsetHeight;
303
+
304
+ if (elementBottom > scrollBottom) {
305
+ listboxNode.value.scrollTop =
306
+ elementBottom - listboxNode.value?.clientHeight;
307
+ } else if (element.offsetTop < listboxNode.value.scrollTop) {
308
+ listboxNode.value.scrollTop = element.offsetTop;
309
+ }
310
+ }
311
+ }
312
+ }
313
+
314
+ // Method to focus on the last item in listbox
315
+ function focusLastItem() {
316
+ const lastListboxOption = listboxOptions.value?.[props.options.length - 1];
317
+ focusItem(lastListboxOption);
318
+ }
319
+
320
+ // Hide listbox
321
+ function hideListbox() {
322
+ listboxHidden.value = true;
323
+ listboxButton.value?.removeAttribute("aria-expanded");
324
+ }
325
+
326
+ // Show listbox and focus on it
327
+ function showListbox() {
328
+ listboxHidden.value = false;
329
+ listboxButton.value?.setAttribute("aria-expanded", "true");
330
+ listboxNode.value?.focus();
331
+ }
332
+
333
+ // Toggle visibility of listbox
334
+ function toggleListbox() {
335
+ if (!props.disabled) {
336
+ listboxHidden.value ? showListbox() : hideListbox();
337
+ }
338
+ }
339
+ </script>
340
+
341
+ <style lang="scss" scoped>
342
+ @use "../../../assets/scss/variables.scss" as *;
343
+ .listbox {
344
+ &__wrapper {
345
+ font-family: $font-opensans;
346
+ font-size: $font-size-xsm;
347
+ color: $blue-900;
348
+ }
349
+
350
+ &__dropdown {
351
+ position: relative;
352
+ display: inline-block;
353
+ width: 100%;
354
+ }
355
+
356
+ &__button {
357
+ padding: 10px 15px;
358
+ width: 100%;
359
+ min-width: 120px;
360
+ position: relative;
361
+ border: 1px solid $grey-400;
362
+ border-radius: $border-radius;
363
+ height: 42px;
364
+ font-weight: 300;
365
+
366
+ &:hover,
367
+ &:focus,
368
+ &:active,
369
+ &:target {
370
+ cursor: pointer;
371
+ box-shadow: $box-shadow;
372
+ border: 1px solid $blue-500;
373
+ transition: $transition-box-shadow;
374
+ transition: $transition-border;
375
+ }
376
+
377
+ &::after {
378
+ position: absolute;
379
+ content: "";
380
+ top: 15px;
381
+ right: 15px;
382
+ border: 7px solid transparent;
383
+
384
+ background-repeat: no-repeat;
385
+ background-position: center;
386
+ }
387
+
388
+ &[aria-expanded="true"] {
389
+ border: 1px solid $blue-500;
390
+ box-shadow: $box-shadow;
391
+ border-radius: $border-radius $border-radius 0 0;
392
+ border-bottom: 0;
393
+
394
+ &::after {
395
+ transform: rotate(180deg);
396
+ }
397
+ }
398
+ }
399
+
400
+ &__list {
401
+ color: $blue-900;
402
+ font-weight: 300;
403
+ border-radius: 0 0 0.4rem 0.4rem;
404
+ overflow-y: auto;
405
+ max-height: 200px;
406
+ margin: 0;
407
+ border-right: 1px solid $blue-500;
408
+ border-left: 1px solid $blue-500;
409
+ border-bottom: 1px solid $blue-500;
410
+ position: absolute;
411
+ background-color: $white;
412
+ width: 100%;
413
+ z-index: 1;
414
+ cursor: pointer;
415
+ box-shadow: 0px 6px 7.5px $blue-300;
416
+ }
417
+
418
+ &__label {
419
+ margin-bottom: 7px;
420
+ display: inline-block;
421
+ font-weight: 300;
422
+
423
+ &--visually-hidden {
424
+ position: absolute;
425
+ height: 1px;
426
+ width: 1px;
427
+ overflow: hidden;
428
+ clip: rect(1px, 1px, 1px, 1px);
429
+ white-space: nowrap;
430
+ }
431
+
432
+ &.is-disabled {
433
+ color: $grey-600;
434
+ }
435
+
436
+ &.is-required::after {
437
+ content: "\00a0*";
438
+ display: inline-block;
439
+ }
440
+ }
441
+ }
442
+
443
+ [role="listbox"] {
444
+ background: white;
445
+ border-radius: 0 0 $border-radius $border-radius;
446
+ }
447
+
448
+ [role="option"] {
449
+ display: block;
450
+ padding: 10px 15px;
451
+ position: relative;
452
+
453
+ &:hover {
454
+ background-color: $blue-200;
455
+ transition: $transition-background-color;
456
+ }
457
+ }
458
+
459
+ [role="option"][aria-selected="true"] {
460
+ background-color: $blue-100;
461
+ }
462
+
463
+ button {
464
+ &[aria-disabled="true"] {
465
+ background-color: $grey-100;
466
+ border: 1px solid $grey-400;
467
+ pointer-events: none;
468
+
469
+ &:hover,
470
+ &:active,
471
+ &:focus,
472
+ &:target {
473
+ box-shadow: none;
474
+ }
475
+
476
+ &::after {
477
+ opacity: 0.5;
478
+ }
479
+ }
480
+ }
481
+ .dropdown-dark {
482
+ color: #fff;
483
+ }
484
+ .dropdown-dark-bg-primary::after {
485
+ background-image: url("https://a.storyblok.com/f/230581/9x6/8cecdca15f/arrow-down.svg?cv=1695125714195");
486
+ }
487
+ .dropdown-dark-bg-dark::after {
488
+ background-image: url("https://a.storyblok.com/f/230581/9x6/7f613e9b8f/arrow-down-white.svg?cv=1695125714435");
489
+ }
490
+ </style>
@@ -0,0 +1,42 @@
1
+ <template>
2
+ <div class="image__wrapper">
3
+ <slot v-bind="$attrs"></slot>
4
+ </div>
5
+ </template>
6
+
7
+ <script lang="ts" setup>
8
+ export type Props = {
9
+ src?: string;
10
+ srcset?: string;
11
+ sizes?: string;
12
+ alt?: string;
13
+ width?: string | number;
14
+ height?: string | number;
15
+ loading?: "lazy" | "eager" | "auto";
16
+ };
17
+
18
+ withDefaults(defineProps<Props>(), {
19
+ src: undefined,
20
+ srcset: undefined,
21
+ sizes: undefined,
22
+ alt: undefined,
23
+ width: undefined,
24
+ height: "auto",
25
+ loading: "auto",
26
+ });
27
+ </script>
28
+
29
+ <style lang="scss" scoped>
30
+ .image__wrapper {
31
+ width: 100%;
32
+ height: 100%;
33
+
34
+ :slotted(img) {
35
+ width: 100%;
36
+ height: auto;
37
+ display: block;
38
+ border: 0;
39
+ max-inline-size: 100%;
40
+ }
41
+ }
42
+ </style>