@fmidev/smartmet-alert-client 4.4.19 → 4.7.0-beta.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 (123) hide show
  1. package/.eslintignore +2 -14
  2. package/.github/workflows/test.yaml +26 -0
  3. package/.nvmrc +1 -0
  4. package/AGENTS.md +26 -0
  5. package/index.html +1 -1
  6. package/package.json +80 -22
  7. package/src/AlertClientVue.vue +160 -0
  8. package/src/App.vue +154 -296
  9. package/src/assets/img/ui/arrow-down.svg +4 -11
  10. package/src/assets/img/ui/arrow-up.svg +4 -11
  11. package/src/assets/img/ui/clear.svg +7 -21
  12. package/src/assets/img/ui/close.svg +4 -15
  13. package/src/assets/img/ui/toggle-selected.svg +5 -6
  14. package/src/assets/img/ui/toggle-unselected.svg +5 -6
  15. package/src/assets/img/warning/cold-weather.svg +3 -6
  16. package/src/assets/img/warning/flood-level-3.svg +4 -7
  17. package/src/assets/img/warning/forest-fire-weather.svg +2 -6
  18. package/src/assets/img/warning/grass-fire-weather.svg +2 -6
  19. package/src/assets/img/warning/hot-weather.svg +3 -6
  20. package/src/assets/img/warning/pedestrian-safety.svg +3 -7
  21. package/src/assets/img/warning/rain.svg +2 -7
  22. package/src/assets/img/warning/sea-icing.svg +2 -6
  23. package/src/assets/img/warning/sea-thunder-storm.svg +2 -5
  24. package/src/assets/img/warning/sea-water-height-high-water.svg +3 -8
  25. package/src/assets/img/warning/sea-water-height-shallow-water.svg +3 -7
  26. package/src/assets/img/warning/sea-wave-height.svg +4 -7
  27. package/src/assets/img/warning/sea-wind-legend.svg +2 -5
  28. package/src/assets/img/warning/sea-wind.svg +2 -5
  29. package/src/assets/img/warning/several.svg +2 -5
  30. package/src/assets/img/warning/thunder-storm.svg +2 -5
  31. package/src/assets/img/warning/traffic-weather.svg +2 -6
  32. package/src/assets/img/warning/uv-note.svg +2 -6
  33. package/src/assets/img/warning/wind.svg +2 -5
  34. package/src/components/AlertClient.vue +330 -251
  35. package/src/components/CollapsiblePanel.vue +281 -0
  36. package/src/components/DayLarge.vue +146 -110
  37. package/src/components/DaySmall.vue +97 -81
  38. package/src/components/Days.vue +229 -159
  39. package/src/components/DescriptionWarning.vue +63 -38
  40. package/src/components/GrayScaleToggle.vue +58 -54
  41. package/src/components/Legend.vue +102 -325
  42. package/src/components/MapLarge.vue +574 -351
  43. package/src/components/MapSmall.vue +137 -122
  44. package/src/components/PopupRow.vue +24 -12
  45. package/src/components/Region.vue +168 -118
  46. package/src/components/RegionWarning.vue +40 -33
  47. package/src/components/Regions.vue +189 -105
  48. package/src/components/Warning.vue +70 -45
  49. package/src/components/Warnings.vue +136 -72
  50. package/src/composables/useAlertClient.ts +360 -0
  51. package/src/composables/useConfig.ts +573 -0
  52. package/src/composables/useFields.ts +66 -0
  53. package/src/composables/useI18n.ts +62 -0
  54. package/src/composables/useKeyCodes.ts +16 -0
  55. package/src/composables/useMapPaths.ts +477 -0
  56. package/src/composables/useUtils.ts +683 -0
  57. package/src/composables/useWarningsProcessor.ts +1007 -0
  58. package/src/data/geometries.json +993 -0
  59. package/src/{main.js → main.ts} +1 -0
  60. package/src/mixins/geojsonsvg.d.ts +57 -0
  61. package/src/mixins/geojsonsvg.js +5 -3
  62. package/src/plugins/index.ts +5 -0
  63. package/src/scss/_utilities.scss +193 -0
  64. package/src/scss/constants.scss +2 -1
  65. package/src/scss/warningImages.scss +8 -3
  66. package/src/types/index.ts +509 -0
  67. package/src/vite-env.d.ts +23 -0
  68. package/src/vue.ts +41 -0
  69. package/svgo.config.js +45 -0
  70. package/tests/README.md +430 -0
  71. package/tests/fixtures/mockWarningData.ts +152 -0
  72. package/tests/integration/warning-flow.spec.ts +445 -0
  73. package/tests/setup.ts +41 -0
  74. package/tests/unit/components/AlertClient.spec.ts +701 -0
  75. package/tests/unit/components/DayLarge.spec.ts +348 -0
  76. package/tests/unit/components/DaySmall.spec.ts +352 -0
  77. package/tests/unit/components/Days.spec.ts +548 -0
  78. package/tests/unit/components/DescriptionWarning.spec.ts +385 -0
  79. package/tests/unit/components/GrayScaleToggle.spec.ts +318 -0
  80. package/tests/unit/components/Legend.spec.ts +295 -0
  81. package/tests/unit/components/MapLarge.spec.ts +448 -0
  82. package/tests/unit/components/MapSmall.spec.ts +367 -0
  83. package/tests/unit/components/PopupRow.spec.ts +270 -0
  84. package/tests/unit/components/Region.spec.ts +373 -0
  85. package/tests/unit/components/RegionWarning.snapshot.spec.ts +361 -0
  86. package/tests/unit/components/RegionWarning.spec.ts +381 -0
  87. package/tests/unit/components/Regions.spec.ts +503 -0
  88. package/tests/unit/components/Warning.snapshot.spec.ts +483 -0
  89. package/tests/unit/components/Warning.spec.ts +489 -0
  90. package/tests/unit/components/Warnings.spec.ts +343 -0
  91. package/tests/unit/components/__snapshots__/RegionWarning.snapshot.spec.ts.snap +41 -0
  92. package/tests/unit/components/__snapshots__/Warning.snapshot.spec.ts.snap +433 -0
  93. package/tests/unit/composables/useConfig.spec.ts +279 -0
  94. package/tests/unit/composables/useI18n.spec.ts +116 -0
  95. package/tests/unit/composables/useKeyCodes.spec.ts +27 -0
  96. package/tests/unit/composables/useUtils.spec.ts +213 -0
  97. package/tsconfig.json +43 -0
  98. package/tsconfig.node.json +11 -0
  99. package/vite.config.js +96 -26
  100. package/vitest.config.js +40 -0
  101. package/dist/favicon.ico +0 -0
  102. package/dist/index.dark.html +0 -20
  103. package/dist/index.en.html +0 -15
  104. package/dist/index.fi.html +0 -15
  105. package/dist/index.html +0 -15
  106. package/dist/index.js +0 -281
  107. package/dist/index.mjs +0 -281
  108. package/dist/index.mjs.map +0 -1
  109. package/dist/index.relative.html +0 -19
  110. package/dist/index.start.html +0 -20
  111. package/dist/index.sv.html +0 -15
  112. package/playwright.config.ts +0 -18
  113. package/public/index.relative.html +0 -19
  114. package/public/index.start.html +0 -20
  115. package/src/mixins/config.js +0 -1378
  116. package/src/mixins/fields.js +0 -26
  117. package/src/mixins/i18n.js +0 -25
  118. package/src/mixins/keycodes.js +0 -10
  119. package/src/mixins/panzoom.js +0 -900
  120. package/src/mixins/utils.js +0 -900
  121. package/src/plugins/index.js +0 -3
  122. package/test/snapshot.test.ts +0 -126
  123. package/vitest.config.ts +0 -6
@@ -21,8 +21,8 @@
21
21
  :opacity="path.opacity" />
22
22
  <path
23
23
  v-for="path in seaBorders"
24
- class="border-path"
25
24
  :key="path.key"
25
+ class="border-path"
26
26
  :stroke="strokeColor"
27
27
  :stroke-width="path.strokeWidth"
28
28
  :d="path.d"
@@ -95,10 +95,10 @@
95
95
  fill-opacity="0" />
96
96
  <path
97
97
  v-for="path in landBorders"
98
- class="border-path"
99
98
  :key="path.key"
99
+ class="border-path"
100
100
  :stroke="strokeColor"
101
- :stroke-width="1.5*path.strokeWidth"
101
+ :stroke-width="1.5 * Number(path.strokeWidth)"
102
102
  :d="path.d"
103
103
  fill-opacity="0" />
104
104
  <path
@@ -115,126 +115,141 @@
115
115
  </div>
116
116
  </template>
117
117
 
118
- <script>
119
- import { onMounted, onUnmounted, ref } from 'vue'
120
-
121
- import config from '../mixins/config'
122
- import utils from '../mixins/utils'
123
-
124
- export default {
125
- name: 'MapSmall',
126
- mixins: [config, utils],
127
- props: {
128
- index: {
129
- type: Number,
130
- },
131
- input: {
132
- type: Object,
133
- default: () => ({}),
134
- },
135
- visibleWarnings: {
136
- type: Array,
137
- default: () => [],
138
- },
139
- warnings: {
140
- type: Object,
141
- default: null,
142
- },
143
- geometryId: {
144
- type: Number,
145
- },
146
- loading: {
147
- type: Boolean,
148
- default: true,
149
- },
150
- theme: {
151
- type: String,
152
- default: 'light-theme',
153
- },
154
- },
155
- setup() {
156
- const windowWidth = ref(window.innerWidth)
157
- const updateWidth = () => {
158
- windowWidth.value = window.innerWidth
159
- }
160
- onMounted(() => {
161
- window.addEventListener('resize', updateWidth)
162
- })
163
- onUnmounted(() => {
164
- window.removeEventListener('resize', updateWidth)
165
- })
166
- return { windowWidth }
167
- },
168
- data() {
169
- return {
170
- coverageRegions: {},
171
- coverageWarnings: [],
172
- pathsNeeded: false,
173
- }
174
- },
175
- computed: {
176
- size() {
177
- return 'Small'
178
- },
179
- strokeColor() {
180
- return 'DarkSlateGray'
181
- },
182
- strokeWidth() {
183
- return 0.6
184
- },
185
- },
186
- watch: {
187
- windowWidth() {
188
- this.pathsNeeded = this.isFullMode()
189
- },
190
- input() {
191
- this.coverageRegions = {}
192
- this.coverageWarnings = []
193
- },
194
- },
195
- mounted() {
196
- this.pathsNeeded = this.isFullMode()
197
- },
198
- methods: {
199
- paths(options) {
200
- return this.pathsNeeded
201
- ? this.regionIds.reduce((regions, regionId) => {
202
- if (
203
- this.geometries[this.geometryId][regionId].pathSmall &&
204
- (this.geometries[this.geometryId][regionId].type ===
205
- options.type) ===
206
- (this.geometries[this.geometryId][regionId].subType == null)
207
- ) {
208
- const visualization = this.regionVisualization(regionId)
209
- if (
210
- options.severity == null ||
211
- visualization.severity === options.severity
212
- ) {
213
- regions.push({
214
- key: `${regionId}${this.size}${this.index}Path`,
215
- fill: this.loading
216
- ? this.colors[this.theme].missing
217
- : visualization.color,
218
- d: visualization.geom.pathSmall,
219
- opacity: visualization.visible ? '1' : '0',
220
- strokeWidth:
221
- this.geometries[this.geometryId][regionId].type === 'sea' &&
222
- this.geometries[this.geometryId][regionId].subType !==
223
- 'lake'
224
- ? this.strokeWidth
225
- : 0,
226
- })
227
- }
228
- }
229
- return regions
230
- }, [])
231
- : []
232
- },
233
- isFullMode() {
234
- return true;
235
- },
236
- },
118
+ <script setup lang="ts">
119
+ import { ref, computed, watch, onMounted, onUnmounted, toRef } from 'vue'
120
+ import type { DayRegions, WarningsMap, Theme } from '@/types'
121
+ import { useMapPaths } from '@/composables/useMapPaths'
122
+
123
+ // ============================================================================
124
+ // Props
125
+ // ============================================================================
126
+
127
+ const props = withDefaults(
128
+ defineProps<{
129
+ index?: number
130
+ input?: DayRegions
131
+ visibleWarnings?: string[]
132
+ warnings?: WarningsMap | null
133
+ geometryId?: number
134
+ loading?: boolean
135
+ theme?: Theme | string
136
+ }>(),
137
+ {
138
+ index: 0,
139
+ input: () => ({}) as DayRegions,
140
+ visibleWarnings: () => [],
141
+ warnings: null,
142
+ geometryId: 2021,
143
+ loading: true,
144
+ theme: 'light-theme',
145
+ }
146
+ )
147
+
148
+ // ============================================================================
149
+ // Local State
150
+ // ============================================================================
151
+
152
+ const windowWidth = ref<number>(
153
+ typeof window !== 'undefined' ? window.innerWidth : 0
154
+ )
155
+ const pathsNeeded = ref<boolean>(false)
156
+ const strokeWidthValue = ref<number>(0.6)
157
+
158
+ // ============================================================================
159
+ // Computed refs for composable
160
+ // ============================================================================
161
+
162
+ const size = computed<'Large' | 'Small'>(() => 'Small')
163
+ const indexRef = toRef(props, 'index')
164
+ const inputRef = toRef(props, 'input')
165
+ const warningsRef = toRef(props, 'warnings')
166
+ const visibleWarningsRef = toRef(props, 'visibleWarnings')
167
+ const geometryIdRef = toRef(props, 'geometryId')
168
+ const themeRef = toRef(props, 'theme')
169
+ const loadingRef = toRef(props, 'loading')
170
+
171
+ // ============================================================================
172
+ // Composables
173
+ // ============================================================================
174
+
175
+ const {
176
+ strokeColor,
177
+ bluePaths,
178
+ greenPaths,
179
+ yellowPaths,
180
+ orangePaths,
181
+ redPaths,
182
+ overlayPaths,
183
+ landBorders,
184
+ seaBorders,
185
+ yellowCoverages,
186
+ orangeCoverages,
187
+ redCoverages,
188
+ overlayCoverages,
189
+ coverageRegions,
190
+ coverageWarnings,
191
+ } = useMapPaths({
192
+ size,
193
+ index: indexRef,
194
+ input: inputRef,
195
+ warnings: warningsRef,
196
+ visibleWarnings: visibleWarningsRef,
197
+ geometryId: geometryIdRef,
198
+ theme: themeRef,
199
+ loading: loadingRef,
200
+ strokeWidth: strokeWidthValue,
201
+ })
202
+
203
+ // ============================================================================
204
+ // Methods
205
+ // ============================================================================
206
+
207
+ function updateWidth(): void {
208
+ windowWidth.value = window.innerWidth
237
209
  }
210
+
211
+ function isFullMode(): boolean {
212
+ return true
213
+ }
214
+
215
+ // ============================================================================
216
+ // Watchers
217
+ // ============================================================================
218
+
219
+ watch(windowWidth, () => {
220
+ pathsNeeded.value = isFullMode()
221
+ })
222
+
223
+ watch(
224
+ () => props.input,
225
+ () => {
226
+ coverageRegions.value = {}
227
+ coverageWarnings.value = []
228
+ }
229
+ )
230
+
231
+ // ============================================================================
232
+ // Lifecycle
233
+ // ============================================================================
234
+
235
+ onMounted(() => {
236
+ window.addEventListener('resize', updateWidth)
237
+ pathsNeeded.value = isFullMode()
238
+ })
239
+
240
+ onUnmounted(() => {
241
+ window.removeEventListener('resize', updateWidth)
242
+ })
243
+
244
+ // ============================================================================
245
+ // Expose for testing
246
+ // ============================================================================
247
+
248
+ defineExpose({
249
+ size,
250
+ strokeWidth: strokeWidthValue,
251
+ pathsNeeded,
252
+ })
238
253
  </script>
239
254
 
240
255
  <style scoped lang="scss">
@@ -26,19 +26,31 @@
26
26
  </div>
27
27
  </template>
28
28
 
29
- <script>
30
- import fields from '../mixins/fields'
29
+ <script setup lang="ts">
30
+ import { toRef } from 'vue'
31
+ import { useFields } from '@/composables/useFields'
32
+ import type { PopupRowInput } from '@/types'
31
33
 
32
- export default {
33
- name: 'PopupRow',
34
- mixins: [fields],
35
- props: {
36
- input: {
37
- type: Object,
38
- default: () => ({}),
39
- },
40
- },
41
- }
34
+ // Props
35
+ const props = withDefaults(
36
+ defineProps<{
37
+ input?: PopupRowInput
38
+ }>(),
39
+ {
40
+ input: () => ({
41
+ type: '',
42
+ severity: 0,
43
+ direction: 0,
44
+ text: '',
45
+ interval: '',
46
+ }),
47
+ }
48
+ )
49
+
50
+ // Composables
51
+ const { typeClass, rotation, invertedRotation, severity } = useFields(
52
+ toRef(props, 'input')
53
+ )
42
54
  </script>
43
55
 
44
56
  <style scoped lang="scss">
@@ -1,11 +1,11 @@
1
1
  <template>
2
2
  <h3>
3
3
  <button
4
+ :id="`accordion-${code}`"
4
5
  type="button"
5
6
  :aria-expanded="open"
6
7
  :class="['accordion-trigger', 'focus-ring', open ? '' : 'collapsed']"
7
8
  :aria-controls="`accordion-section-${code}`"
8
- :id="`accordion-${code}`"
9
9
  :aria-label="ariaButton"
10
10
  @click="onRegionToggle">
11
11
  <div class="region-header">
@@ -15,15 +15,13 @@
15
15
  <div>
16
16
  <RegionWarning
17
17
  v-for="warning in warningsSummary"
18
- :key="warning.key"
18
+ :key="warning.id"
19
19
  :input="warning"
20
20
  :language="language">
21
21
  </RegionWarning>
22
22
  </div>
23
23
  </div>
24
- <div
25
- block
26
- :class="['current-warning-toggle', open ? '' : 'collapsed']" />
24
+ <div block :class="['current-warning-toggle', open ? '' : 'collapsed']" />
27
25
  </button>
28
26
  </h3>
29
27
  <div
@@ -32,128 +30,161 @@
32
30
  :aria-labelledby="`accordion-${code}`"
33
31
  :aria-expanded="open"
34
32
  class="accordion-panel"
35
- :hidden="open ? null : ''"
36
- >
33
+ :hidden="open ? undefined : ''">
37
34
  <div class="current-description">
38
35
  <div class="current-description-table">
39
36
  <DescriptionWarning
40
37
  v-for="warning in reducedWarnings"
41
- :key="warning.identification"
42
- :input="warning"
43
- :theme="theme"
44
- :language="language"
45
- />
38
+ :key="warning.id"
39
+ :input="warning"
40
+ :theme="theme"
41
+ :language="language" />
46
42
  </div>
47
43
  </div>
48
44
  </div>
49
45
  </template>
50
46
 
51
- <script>
52
- import config from '../mixins/config'
53
- import i18n from '../mixins/i18n'
47
+ <script setup lang="ts">
48
+ import { ref, computed, toRef } from 'vue'
49
+ import { useI18n } from '@/composables/useI18n'
50
+ import { useConfig } from '@/composables/useConfig'
54
51
  import DescriptionWarning from './DescriptionWarning.vue'
55
52
  import RegionWarning from './RegionWarning.vue'
56
-
57
- export default {
58
- name: 'Region',
59
- components: { RegionWarning, DescriptionWarning },
60
- mixins: [i18n, config],
61
- props: {
62
- type: {
63
- type: String,
64
- },
65
- code: {
66
- type: String,
67
- },
68
- name: {
69
- type: String,
70
- },
71
- input: {
72
- type: Array,
73
- default: () => [],
74
- },
75
- warnings: {
76
- type: Object,
77
- default: null,
78
- },
79
- theme: {
80
- type: String,
81
- default: 'light-theme',
82
- },
83
- language: {
84
- type: String,
85
- },
86
- },
87
- data() {
88
- return {
89
- open: false,
53
+ import type {
54
+ Warning,
55
+ WarningsMap,
56
+ RegionWarningItem,
57
+ Theme,
58
+ Language,
59
+ } from '@/types'
60
+
61
+ // ============================================================================
62
+ // Props
63
+ // ============================================================================
64
+
65
+ const props = withDefaults(
66
+ defineProps<{
67
+ type?: string
68
+ code?: string
69
+ name?: string
70
+ input?: RegionWarningItem[]
71
+ warnings?: WarningsMap | null
72
+ theme?: Theme | string
73
+ language?: Language
74
+ }>(),
75
+ {
76
+ type: undefined,
77
+ code: undefined,
78
+ name: undefined,
79
+ input: () => [],
80
+ warnings: null,
81
+ theme: 'light-theme',
82
+ language: undefined,
83
+ }
84
+ )
85
+
86
+ // ============================================================================
87
+ // Composables
88
+ // ============================================================================
89
+
90
+ const { t } = useI18n(toRef(() => props.language))
91
+ const { coverageCriterion } = useConfig()
92
+
93
+ // ============================================================================
94
+ // State
95
+ // ============================================================================
96
+
97
+ const open = ref<boolean>(false)
98
+
99
+ // ============================================================================
100
+ // Computed Properties
101
+ // ============================================================================
102
+
103
+ const identifier = computed<string>(() => {
104
+ return `accordion-item-${props.code}`
105
+ })
106
+
107
+ const regionName = computed<string>(() => {
108
+ return t(props.name)
109
+ })
110
+
111
+ const warningsSummary = computed<Warning[]>(() => {
112
+ return props.input.reduce((summaryWarnings: Warning[], warningInfo) => {
113
+ const firstIdentifier = warningInfo?.identifiers?.[0]
114
+ if (
115
+ warningInfo != null &&
116
+ firstIdentifier != null &&
117
+ warningInfo.coverage >= coverageCriterion
118
+ ) {
119
+ const warning = props.warnings?.[firstIdentifier]
120
+ if (warning != null) {
121
+ summaryWarnings.push(warning)
122
+ }
90
123
  }
91
- },
92
- computed: {
93
- identifier() {
94
- return `accordion-item-${this.code}`
95
- },
96
- regionName() {
97
- return this.t(this.name)
98
- },
99
- warningsSummary() {
100
- return this.input.reduce((summaryWarnings, warningInfo) => {
101
- if (
102
- warningInfo != null &&
103
- warningInfo.identifiers != null &&
104
- warningInfo.identifiers.length > 0 &&
105
- warningInfo.coverage >= this.coverageCriterion
106
- ) {
107
- const warning = this.warnings[warningInfo.identifiers[0]]
108
- if (warning != null) {
109
- summaryWarnings.push(warning)
124
+ return summaryWarnings
125
+ }, [])
126
+ })
127
+
128
+ const reducedWarnings = computed<Warning[]>(() => {
129
+ return props.input.reduce(
130
+ (allWarnings: Warning[], warningInfo) =>
131
+ allWarnings.concat(
132
+ warningInfo.identifiers.reduce((identifiers: Warning[], identifier) => {
133
+ const warning = props.warnings?.[identifier]
134
+ if (
135
+ warning != null &&
136
+ warningsSummary.value.some(
137
+ (summaryWarning) => summaryWarning.type === warning.type
138
+ )
139
+ ) {
140
+ identifiers.push(warning)
110
141
  }
111
- }
112
- return summaryWarnings
113
- }, [])
114
- },
115
- reducedWarnings() {
116
- return this.input.reduce(
117
- (allWarnings, warningInfo) =>
118
- allWarnings.concat(
119
- warningInfo.identifiers.reduce((identifiers, identifier) => {
120
- const warning = this.warnings[identifier]
121
- if (
122
- warning != null &&
123
- this.warningsSummary.some(
124
- (summaryWarning) => summaryWarning.type === warning.type
125
- )
126
- ) {
127
- identifiers.push(warning)
128
- }
129
- return identifiers
130
- }, [])
131
- ),
132
- []
133
- )
134
- },
135
- ariaButton() {
136
- return `${
137
- this.open
138
- ? this.t('infoButtonAriaLabelCloseRegion')
139
- : this.t('infoButtonAriaLabelShowRegion')
140
- } ${this.regionName} ${this.t('infoButtonAriaLabelValidWarnings')}`
141
- },
142
- ariaInfo() {
143
- return this.reducedWarnings.map(
144
- (warning, index) =>
145
- `${index > 0 ? ' ' : ''}${this.t(warning.type)}: ${this.t(
146
- `warningLevel${warning.severity}`
147
- )}.`
148
- )
149
- },
150
- },
151
- methods: {
152
- onRegionToggle() {
153
- this.open = !this.open
154
- },
155
- },
156
- }
142
+ return identifiers
143
+ }, [])
144
+ ),
145
+ []
146
+ )
147
+ })
148
+
149
+ const ariaButton = computed<string>(() => {
150
+ return `${
151
+ open.value
152
+ ? t('infoButtonAriaLabelCloseRegion')
153
+ : t('infoButtonAriaLabelShowRegion')
154
+ } ${regionName.value} ${t('infoButtonAriaLabelValidWarnings')}`
155
+ })
156
+
157
+ const ariaInfo = computed<string[]>(() => {
158
+ return reducedWarnings.value.map(
159
+ (warning, index) =>
160
+ `${index > 0 ? ' ' : ''}${t(warning.type)}: ${t(
161
+ `warningLevel${warning.severity}`
162
+ )}.`
163
+ )
164
+ })
165
+
166
+ // ============================================================================
167
+ // Methods
168
+ // ============================================================================
169
+
170
+ const onRegionToggle = (): void => {
171
+ open.value = !open.value
172
+ }
173
+
174
+ // ============================================================================
175
+ // Expose for tests
176
+ // ============================================================================
177
+
178
+ defineExpose({
179
+ open,
180
+ identifier,
181
+ regionName,
182
+ warningsSummary,
183
+ reducedWarnings,
184
+ ariaButton,
185
+ ariaInfo,
186
+ onRegionToggle,
187
+ })
157
188
  </script>
158
189
 
159
190
  <style scoped lang="scss">
@@ -192,6 +223,7 @@ export default {
192
223
 
193
224
  button {
194
225
  border: none;
226
+ cursor: pointer;
195
227
  &:focus:not(:focus-visible) {
196
228
  box-shadow: none;
197
229
  }
@@ -366,7 +398,10 @@ h3 {
366
398
  border-radius: 0;
367
399
  }
368
400
 
369
- .accordion > div:first-child:last-child .accordion-trigger.collapsed > .region-header {
401
+ .accordion
402
+ > div:first-child:last-child
403
+ .accordion-trigger.collapsed
404
+ > .region-header {
370
405
  border-radius: 0;
371
406
  }
372
407
 
@@ -398,15 +433,30 @@ h3 {
398
433
  border-radius: 0;
399
434
  }
400
435
 
401
- .accordion > div:last-child > div > h3 > button > div.current-warning-toggle.collapsed {
436
+ .accordion
437
+ > div:last-child
438
+ > div
439
+ > h3
440
+ > button
441
+ > div.current-warning-toggle.collapsed {
402
442
  border-radius: 0;
403
443
  }
404
444
 
405
- .accordion > div:first-child:last-child > div > h3 > button > div.current-warning-toggle {
445
+ .accordion
446
+ > div:first-child:last-child
447
+ > div
448
+ > h3
449
+ > button
450
+ > div.current-warning-toggle {
406
451
  border-radius: 0;
407
452
  }
408
453
 
409
- .accordion > div:first-child:last-child > div > h3 > button > div.current-warning-toggle.collapsed {
454
+ .accordion
455
+ > div:first-child:last-child
456
+ > div
457
+ > h3
458
+ > button
459
+ > div.current-warning-toggle.collapsed {
410
460
  border-radius: 0;
411
461
  }
412
462