@zap-wunschlachen/wl-shared-components 1.0.35 → 1.0.38

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. package/App.vue +2 -7
  2. package/package.json +1 -1
  3. package/src/components/Audio/Waveform.vue +1 -0
  4. package/src/components/Button/Button.vue +39 -1
  5. package/src/components/CheckBox/CheckBox.css +29 -0
  6. package/src/components/CheckBox/Checkbox.vue +10 -2
  7. package/src/components/Dialog/Dialog.vue +14 -5
  8. package/src/components/Input/Input.vue +8 -2
  9. package/src/components/Loader/Loader.css +20 -0
  10. package/src/components/Loader/Loader.vue +1 -0
  11. package/src/components/OtpInput/OtpInput.vue +1 -1
  12. package/src/components/Select/Select.vue +1 -0
  13. package/src/components/StagingBanner/StagingBanner.css +19 -0
  14. package/src/components/StagingBanner/StagingBanner.vue +82 -0
  15. package/src/components/accessibility.css +218 -0
  16. package/src/components/index.ts +1 -0
  17. package/tests/unit/accessibility/component-a11y.spec.ts +469 -0
  18. package/tests/unit/components/Accordion/AccordionGroup.spec.ts +228 -0
  19. package/tests/unit/components/Accordion/AccordionItem.spec.ts +292 -0
  20. package/tests/unit/components/Appointment/AnamneseNotification.spec.ts +176 -0
  21. package/tests/unit/components/Background/Background.spec.ts +177 -0
  22. package/tests/unit/components/ErrorPage/ErrorPage.spec.ts +313 -0
  23. package/tests/unit/components/ErrorPage/ErrorPageLogo.spec.ts +153 -0
  24. package/tests/unit/components/Icons/AdvanceAppointments.spec.ts +61 -0
  25. package/tests/unit/components/Icons/Logo.spec.ts +228 -0
  26. package/tests/unit/components/Icons/MiniLogo.spec.ts +38 -0
  27. package/tests/unit/components/Icons/SolidArrowRight.spec.ts +49 -0
  28. package/tests/unit/components/Loader/Loader.spec.ts +197 -0
  29. package/tests/unit/components/MaintenanceBanner/MaintenanceBanner.spec.ts +302 -0
  30. package/tests/unit/utils/accessibility.spec.ts +318 -0
package/App.vue CHANGED
@@ -1,18 +1,13 @@
1
1
  <template>
2
2
  <div class="app">
3
3
  <div class="element-container">
4
- <Button
5
- :color="siteColors['slot-bg']"
6
- :label="'Some Text'"
7
- class="!font-bold mx-2" :text-color="siteColors['slot-text']"
8
- >
9
- </Button>
4
+ <StagingBanner show/>
10
5
  </div>
11
6
  </div>
12
7
  </template>
13
8
 
14
9
  <script setup lang="ts">
15
- import Button from '@/components/Button/Button.vue';
10
+ import StagingBanner from "@/components/StagingBanner/StagingBanner.vue";
16
11
  import { siteColors } from "@/utils/index";
17
12
  </script>
18
13
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zap-wunschlachen/wl-shared-components",
3
- "version": "1.0.35",
3
+ "version": "1.0.38",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "dev": "vite",
@@ -52,6 +52,7 @@ function createWaveform() {
52
52
  cursorWidth: 0,
53
53
  height: containerHeight,
54
54
  width: containerWidth,
55
+ backend: 'WebAudio',
55
56
  })
56
57
 
57
58
  ws.on('audioprocess', (currentTime) => {
@@ -11,7 +11,7 @@
11
11
  :variant="variant"
12
12
  :size="size"
13
13
  :readonly="readonly"
14
- :style="buttonStyle"
14
+ :style="rootStyle"
15
15
  data-testid="root"
16
16
  >
17
17
  <slot>{{ label }}</slot>
@@ -127,10 +127,48 @@ const buttonStyle = computed(() => {
127
127
  }
128
128
  return {};
129
129
  });
130
+
131
+ // Compute a CSS variable for focus color to avoid hardcoded values.
132
+ // Use provided color prop, fall back to siteColors.btn_bg, then to a sensible default.
133
+ const focusColor = computed(() => {
134
+ const col = props.color || siteColors['btn_bg'] || '#172774';
135
+ return col;
136
+ });
137
+
138
+ // Expose a style object that includes the focus color custom property.
139
+ const rootStyle = computed(() => ({
140
+ ...buttonStyle.value,
141
+ '--wl-button-focus-color': focusColor.value
142
+ }));
130
143
  </script>
131
144
 
132
145
  <style>
133
146
  .wl-button.v-btn--variant-outlined {
134
147
  border-width: 1.5px;
135
148
  }
149
+
150
+ /* Accessibility: Focus Indicators (WCAG 2.4.7) */
151
+ .wl-button:focus-visible {
152
+ outline: 2px solid currentColor;
153
+ outline-offset: 2px;
154
+ }
155
+
156
+ .wl-button.v-btn--variant-outlined:focus-visible {
157
+ /* Use component-provided focus color with fallback */
158
+ outline-color: var(--wl-button-focus-color, var(--Dental-Blue-0, #172774));
159
+ outline-width: 3px;
160
+ box-shadow: 0 0 0 4px rgba(0,0,0,0.08), 0 0 0 6px rgba(255,255,255,0.06) inset;
161
+ }
162
+
163
+ .wl-button.v-btn--variant-flat:focus-visible {
164
+ outline-color: white;
165
+ box-shadow: 0 0 0 4px rgba(255, 255, 255, 0.3);
166
+ }
167
+
168
+ /* High contrast mode support */
169
+ @media (forced-colors: active) {
170
+ .wl-button {
171
+ border: 2px solid currentColor;
172
+ }
173
+ }
136
174
  </style>
@@ -183,3 +183,32 @@
183
183
  .label-text {
184
184
  font-weight: 300;
185
185
  }
186
+
187
+ /* =====================================================
188
+ ACCESSIBILITY: Focus Indicators
189
+ WCAG 2.4.7: Focus Visible (Level AA)
190
+ ===================================================== */
191
+
192
+ .input-checkbox:focus-visible {
193
+ outline: 2px solid var(--Dental-Blue-0);
194
+ outline-offset: 2px;
195
+ }
196
+
197
+ .input-success:focus-visible {
198
+ outline-color: var(--Success-Green-0);
199
+ }
200
+
201
+ .input-error:focus-visible {
202
+ outline-color: var(--Error-Red-0);
203
+ }
204
+
205
+ /* High contrast mode support */
206
+ @media (forced-colors: active) {
207
+ .input-checkbox {
208
+ border: 2px solid currentColor;
209
+ }
210
+
211
+ .input-checkbox:checked::before {
212
+ background-color: currentColor;
213
+ }
214
+ }
@@ -1,16 +1,21 @@
1
1
  <template>
2
- <label :class="labelClasses" data-testid="root">
2
+ <label :class="labelClasses" data-testid="root" :for="checkboxId">
3
3
  <span class="checkbox-container">
4
4
  <input
5
5
  type="checkbox"
6
+ :id="checkboxId"
6
7
  :name="value"
7
8
  :checked="isChecked"
8
9
  @change="updateValue"
9
10
  :class="inputClasses"
10
11
  :disabled="disabled"
12
+ :aria-checked="isChecked"
13
+ :aria-disabled="disabled"
14
+ :aria-invalid="error"
15
+ :aria-describedby="error ? `${checkboxId}-error` : undefined"
11
16
  />
12
17
  </span>
13
- {{ label }}
18
+ <span class="label-text">{{ label }}</span>
14
19
  </label>
15
20
  </template>
16
21
 
@@ -82,6 +87,9 @@ const props = defineProps({
82
87
  // Define events that can be emitted from the component
83
88
  const emit = defineEmits(['update:modelValue', 'input-error']);
84
89
 
90
+ // Generate stable unique ID for accessibility
91
+ const checkboxId = `checkbox-${props.value}-${Math.random().toString(36).substr(2, 9)}`;
92
+
85
93
  // Computed property to check if the checkbox is selected
86
94
  const isChecked = computed(() => {
87
95
  if (Array.isArray(props.modelValue)) {
@@ -3,9 +3,16 @@
3
3
  <!-- Scoped slot for the activator with click handler to open dialog -->
4
4
  <slot name="activator" :openDialog="openDialog" />
5
5
 
6
- <!-- Dialog component with direct binding to dialog.value -->
7
- <v-dialog v-model="dialog" width="auto">
8
- <slot :closeDialog="closeDialog" name="content"></slot>
6
+ <!-- Dialog component with accessibility attributes -->
7
+ <v-dialog
8
+ v-model="dialog"
9
+ width="auto"
10
+ role="dialog"
11
+ aria-modal="true"
12
+ :aria-labelledby="dialogTitleId"
13
+ @keydown.escape="closeDialog"
14
+ >
15
+ <slot :closeDialog="closeDialog" :titleId="dialogTitleId" name="content"></slot>
9
16
  </v-dialog>
10
17
  </div>
11
18
  </template>
@@ -17,13 +24,15 @@ import './Dialog.css';
17
24
  // Reactive dialog state
18
25
  const dialog = ref(false);
19
26
 
27
+ // Generate unique ID for accessibility (stable across re-renders)
28
+ const dialogTitleId = `dialog-title-${Math.random().toString(36).slice(2, 9)}`;
29
+
20
30
  // Methods to open and close the dialog
21
31
  const openDialog = () => {
22
- console.log('entereed 1');
23
32
  dialog.value = true;
24
33
  };
34
+
25
35
  const closeDialog = () => {
26
- console.log('entereed 2');
27
36
  dialog.value = false;
28
37
  };
29
38
  </script>
@@ -25,6 +25,9 @@
25
25
  @click:prepend-inner="onPrependInnerClick"
26
26
  @click:append="onAppendClick"
27
27
  @click:append-inner="onAppendInnerClick"
28
+ :error="error"
29
+ :aria-invalid="error || undefined"
30
+ :aria-describedby="hint ? inputHintId : undefined"
28
31
  data-testid="root"
29
32
  >
30
33
  <!-- Prepend Slot (before v-field) -->
@@ -49,8 +52,8 @@
49
52
 
50
53
  <!-- Details Slot (for messages, hints, etc.) -->
51
54
  <template #details>
52
- <div class="d-flex flex-row ga-1 align-center">
53
- <v-icon icon="heroicons:exclamation-circle"></v-icon>
55
+ <div v-if="hint" class="d-flex flex-row ga-1 align-center" :id="inputHintId" role="alert" aria-hidden="true">
56
+ <v-icon icon="heroicons:exclamation-circle" aria-hidden="true"></v-icon>
54
57
  <span>{{ hint }}</span>
55
58
  </div>
56
59
  </template>
@@ -76,6 +79,9 @@ const emit = defineEmits([
76
79
  return props.placeholder || t('wl.input.placeholder');
77
80
  });
78
81
 
82
+ // Generate unique ID for accessibility - connects input to hint/error message
83
+ const inputHintId = `input-hint-${Math.random().toString(36).slice(2, 11)}`;
84
+
79
85
  // Define the component props
80
86
  const props = defineProps({
81
87
  /**
@@ -49,3 +49,23 @@ div:focus-visible {
49
49
  outline: 2px solid;
50
50
  outline-offset: 2px;
51
51
  }
52
+
53
+ /* Screen reader only text - visually hidden but accessible */
54
+ .sr-only {
55
+ position: absolute;
56
+ width: 1px;
57
+ height: 1px;
58
+ padding: 0;
59
+ margin: -1px;
60
+ overflow: hidden;
61
+ clip: rect(0, 0, 0, 0);
62
+ white-space: nowrap;
63
+ border: 0;
64
+ }
65
+
66
+ /* Reduced motion support */
67
+ @media (prefers-reduced-motion: reduce) {
68
+ .loader-spinner-arc {
69
+ animation: none;
70
+ }
71
+ }
@@ -5,6 +5,7 @@
5
5
  <!-- Spinner and loading text -->
6
6
  <div class="loader-content">
7
7
  <div role="status">
8
+ <span class="sr-only">Loading, please wait...</span>
8
9
  <svg
9
10
  aria-hidden="true"
10
11
  class="loader-svg"
@@ -21,7 +21,7 @@
21
21
  type="number"
22
22
  >
23
23
  <template #loader>
24
- <Loader :size="40" />
24
+ <Loader :size="40"/>
25
25
  </template>
26
26
  </v-otp-input>
27
27
  </div>
@@ -28,6 +28,7 @@
28
28
  :return-object="returnObject"
29
29
  :placeholder="placeholder"
30
30
  :persistent-placeholder="persistentPlaceholder"
31
+ :aria-invalid="error || undefined"
31
32
  v-model="internalValue"
32
33
  @click:append="onClickAppend"
33
34
  @click:append-inner="onClickAppendInner"
@@ -0,0 +1,19 @@
1
+ .staging-banner {
2
+ position: fixed;
3
+ top: 0;
4
+ left: 0;
5
+ right: 0;
6
+ background: #ff6b35;
7
+ color: white;
8
+ text-align: center;
9
+ padding: 10px;
10
+ z-index: 99999;
11
+ font-weight: bold;
12
+ font-size: 14px;
13
+ font-family: inherit;
14
+ box-sizing: border-box;
15
+ }
16
+
17
+ .staging-banner--hidden {
18
+ display: none;
19
+ }
@@ -0,0 +1,82 @@
1
+ <template>
2
+ <div v-if="isVisible" class="staging-banner">
3
+ {{ displayText }}
4
+ </div>
5
+ </template>
6
+
7
+ <script setup lang="ts">
8
+ import { computed, onMounted, ref } from 'vue';
9
+ import './StagingBanner.css';
10
+
11
+ const props = defineProps({
12
+ /**
13
+ * Custom text to display in the banner.
14
+ * Default: "⚠️ STAGING - Testumgebung (keine echten Daten)"
15
+ */
16
+ text: {
17
+ type: String,
18
+ default: ''
19
+ },
20
+ /**
21
+ * Force the banner to show regardless of hostname.
22
+ * Useful for testing or manual control.
23
+ */
24
+ show: {
25
+ type: Boolean,
26
+ default: undefined
27
+ },
28
+ /**
29
+ * Whether to auto-detect staging based on hostname.
30
+ * Looks for "staging." or "staging-" in the hostname.
31
+ * Default: true
32
+ */
33
+ autoDetect: {
34
+ type: Boolean,
35
+ default: true
36
+ },
37
+ /**
38
+ * Whether to add padding to the body to account for the banner.
39
+ * Default: true
40
+ */
41
+ addBodyPadding: {
42
+ type: Boolean,
43
+ default: true
44
+ },
45
+ /**
46
+ * Custom padding value for the body (in pixels).
47
+ * Default: 40
48
+ */
49
+ bodyPaddingPx: {
50
+ type: Number,
51
+ default: 40
52
+ }
53
+ });
54
+
55
+ const isStaging = ref(false);
56
+
57
+ const displayText = computed(() => {
58
+ return props.text || '⚠️ STAGING - Testumgebung (keine echten Daten)';
59
+ });
60
+
61
+ const isVisible = computed(() => {
62
+ // If show prop is explicitly set, use that
63
+ if (props.show !== undefined) {
64
+ return props.show;
65
+ }
66
+ // Otherwise use auto-detection result
67
+ return props.autoDetect && isStaging.value;
68
+ });
69
+
70
+ onMounted(() => {
71
+ // Check if running in browser
72
+ if (typeof window !== 'undefined') {
73
+ const hostname = window.location.hostname;
74
+ isStaging.value = hostname.includes('staging.') || hostname.includes('staging-');
75
+
76
+ // Add body padding if visible and option is enabled
77
+ if (isVisible.value && props.addBodyPadding) {
78
+ document.body.style.paddingTop = `${props.bodyPaddingPx}px`;
79
+ }
80
+ }
81
+ });
82
+ </script>
@@ -0,0 +1,218 @@
1
+ /**
2
+ * Global Accessibility Styles
3
+ *
4
+ * These styles ensure WCAG 2.1 AA compliance for all components.
5
+ * Required for German accessibility requirements (BITV 2.0).
6
+ */
7
+
8
+ /* =====================================================
9
+ FOCUS INDICATORS
10
+ WCAG 2.4.7: Focus Visible (Level AA)
11
+ ===================================================== */
12
+
13
+ /* Universal focus-visible style for all interactive elements */
14
+ .wl-button:focus-visible,
15
+ button:focus-visible,
16
+ a:focus-visible,
17
+ input:focus-visible,
18
+ select:focus-visible,
19
+ textarea:focus-visible,
20
+ [tabindex="0"]:focus-visible {
21
+ outline: 2px solid var(--Dental-Blue-0, #172774);
22
+ outline-offset: 2px;
23
+ }
24
+
25
+ /* High contrast focus for dark backgrounds */
26
+ .wl-button.v-btn--variant-flat:focus-visible {
27
+ outline-color: white;
28
+ box-shadow: 0 0 0 4px rgba(255, 255, 255, 0.5);
29
+ }
30
+
31
+ /* Checkbox focus indicator */
32
+ .input-checkbox:focus-visible {
33
+ outline: 2px solid var(--Dental-Blue-0, #172774);
34
+ outline-offset: 2px;
35
+ }
36
+
37
+ /* Button focus styles */
38
+ .wl-button:focus-visible {
39
+ outline: 2px solid currentColor;
40
+ outline-offset: 2px;
41
+ }
42
+
43
+ .wl-button.v-btn--variant-outlined:focus-visible {
44
+ outline: 2px solid var(--Dental-Blue-0, #172774);
45
+ outline-offset: 2px;
46
+ }
47
+
48
+ /* Input focus styles */
49
+ .v-text-field input:focus-visible,
50
+ .v-textarea textarea:focus-visible {
51
+ outline: none; /* Vuetify handles this via border */
52
+ }
53
+
54
+ .v-text-field:focus-within {
55
+ border-color: var(--Dental-Blue-0, #172774) !important;
56
+ }
57
+
58
+ /* Select/Combobox focus */
59
+ .wl-select .v-field:focus-within {
60
+ border-color: var(--Dental-Blue-0, #172774) !important;
61
+ }
62
+
63
+ /* =====================================================
64
+ REDUCED MOTION
65
+ WCAG 2.3.3: Animation from Interactions (Level AAA)
66
+ ===================================================== */
67
+
68
+ @media (prefers-reduced-motion: reduce) {
69
+ *,
70
+ *::before,
71
+ *::after {
72
+ animation-duration: 0.01ms !important;
73
+ animation-iteration-count: 1 !important;
74
+ transition-duration: 0.01ms !important;
75
+ scroll-behavior: auto !important;
76
+ }
77
+ }
78
+
79
+ /* =====================================================
80
+ HIGH CONTRAST MODE
81
+ Windows High Contrast Mode Support
82
+ ===================================================== */
83
+
84
+ @media (forced-colors: active) {
85
+ .wl-button {
86
+ border: 2px solid currentColor;
87
+ }
88
+
89
+ .input-checkbox {
90
+ border: 2px solid currentColor;
91
+ }
92
+
93
+ .input-checkbox:checked::before {
94
+ background-color: currentColor;
95
+ }
96
+ }
97
+
98
+ /* =====================================================
99
+ DISABLED STATES
100
+ Ensure disabled elements maintain contrast
101
+ WCAG 1.4.3: Contrast (Minimum) - Note: Disabled elements are exempt
102
+ but we still aim for visibility
103
+ ===================================================== */
104
+
105
+ .wl-button:disabled,
106
+ .wl-button[aria-disabled="true"] {
107
+ opacity: 0.6;
108
+ cursor: not-allowed;
109
+ }
110
+
111
+ .input-checkbox:disabled {
112
+ opacity: 0.6;
113
+ cursor: not-allowed;
114
+ }
115
+
116
+ /* =====================================================
117
+ SKIP LINKS
118
+ WCAG 2.4.1: Bypass Blocks (Level A)
119
+ ===================================================== */
120
+
121
+ .skip-link {
122
+ position: absolute;
123
+ top: -40px;
124
+ left: 0;
125
+ background: var(--Dental-Blue-0, #172774);
126
+ color: white;
127
+ padding: 8px 16px;
128
+ z-index: 10000;
129
+ text-decoration: none;
130
+ font-weight: 600;
131
+ }
132
+
133
+ .skip-link:focus {
134
+ top: 0;
135
+ }
136
+
137
+ /* =====================================================
138
+ SCREEN READER ONLY
139
+ Visually hidden but accessible to screen readers
140
+ ===================================================== */
141
+
142
+ .sr-only {
143
+ position: absolute;
144
+ width: 1px;
145
+ height: 1px;
146
+ padding: 0;
147
+ margin: -1px;
148
+ overflow: hidden;
149
+ clip: rect(0, 0, 0, 0);
150
+ white-space: nowrap;
151
+ border: 0;
152
+ }
153
+
154
+ /* =====================================================
155
+ ICON BUTTONS
156
+ Ensure icon-only buttons are accessible
157
+ ===================================================== */
158
+
159
+ .icon-button {
160
+ position: relative;
161
+ }
162
+
163
+ /* Show in development environments */
164
+ body.dev-mode .icon-button:not([aria-label]):not([aria-labelledby])::after {
165
+ content: "Warning: Missing accessible name";
166
+ position: absolute;
167
+ background: red;
168
+ color: white;
169
+ font-size: 10px;
170
+ padding: 2px;
171
+ display: block;
172
+ }
173
+
174
+ /* =====================================================
175
+ LOADING STATES
176
+ WCAG 4.1.3: Status Messages (Level AA)
177
+ ===================================================== */
178
+
179
+ .loading-indicator {
180
+ /* Announce loading state to screen readers */
181
+ }
182
+
183
+ [aria-busy="true"] {
184
+ cursor: wait;
185
+ }
186
+
187
+ /* =====================================================
188
+ ERROR STATES
189
+ Ensure error messages are visible and announced
190
+ ===================================================== */
191
+
192
+ .error-message {
193
+ color: var(--Error-Red-0, #d32f2f);
194
+ font-weight: 500;
195
+ }
196
+
197
+ [aria-invalid="true"] {
198
+ border-color: var(--Error-Red-0, #d32f2f) !important;
199
+ }
200
+
201
+ /* =====================================================
202
+ MINIMUM TOUCH TARGET SIZE
203
+ WCAG 2.5.5: Target Size (Level AAA) - 44x44px minimum
204
+ ===================================================== */
205
+
206
+ .touch-target-min {
207
+ min-width: 44px;
208
+ min-height: 44px;
209
+ }
210
+
211
+ /* =====================================================
212
+ TEXT SPACING
213
+ WCAG 1.4.12: Text Spacing (Level AA)
214
+ Content should support user adjustments
215
+ ===================================================== */
216
+
217
+ /* These rules ensure content adapts when users adjust text spacing */
218
+
@@ -24,5 +24,6 @@ export { default as Loader } from './Loader/Loader.vue';
24
24
  export { default as AnamneseNotification } from './Appointment/Card/AnamneseNotification.vue';
25
25
  export { default as ErrorPage } from './ErrorPage/ErrorPage.vue';
26
26
  export { default as MaintenanceBanner } from './MaintenanceBanner/MaintenanceBanner.vue';
27
+ export { default as StagingBanner } from './StagingBanner/StagingBanner.vue';
27
28
  export { default as Logo } from './Icons/Logo.vue';
28
29
  export { default as MiniLogo } from './Icons/MiniLogo.vue';