@carbon/vue 3.0.26 → 3.0.29

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "framework": "vue",
3
3
  "name": "@carbon/vue",
4
- "version": "3.0.25",
4
+ "version": "3.0.28",
5
5
  "contributions": {
6
6
  "html": {
7
7
  "description-markup": "markdown",
@@ -770,6 +770,14 @@
770
770
  "type": "string"
771
771
  },
772
772
  "default": "'main'"
773
+ },
774
+ {
775
+ "name": "id",
776
+ "value": {
777
+ "kind": "expression",
778
+ "type": "string"
779
+ },
780
+ "default": "'main-content'"
773
781
  }
774
782
  ],
775
783
  "slots": [
@@ -2280,7 +2288,7 @@
2280
2288
  "kind": "expression",
2281
2289
  "type": "string"
2282
2290
  },
2283
- "default": "undefined"
2291
+ "default": "'menuitem'"
2284
2292
  }
2285
2293
  ],
2286
2294
  "slots": [
@@ -2320,6 +2328,14 @@
2320
2328
  "name": "CvHeaderNav",
2321
2329
  "description": "",
2322
2330
  "attributes": [
2331
+ {
2332
+ "name": "ariaLabel",
2333
+ "value": {
2334
+ "kind": "expression",
2335
+ "type": "string"
2336
+ },
2337
+ "default": "undefined"
2338
+ },
2323
2339
  {
2324
2340
  "name": "ariaLabelledBy",
2325
2341
  "value": {
@@ -5595,6 +5611,14 @@
5595
5611
  "module": "./src/components/CvTooltip/CvTooltip.vue",
5596
5612
  "symbol": "default"
5597
5613
  }
5614
+ },
5615
+ {
5616
+ "name": "PanelFocusTestComponent",
5617
+ "description": "",
5618
+ "source": {
5619
+ "module": "./src/components/CvUIShell/__tests__/PanelFocusTestComponent.vue",
5620
+ "symbol": "default"
5621
+ }
5598
5622
  }
5599
5623
  ]
5600
5624
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@carbon/vue",
3
3
  "license": "Apache-2.0",
4
- "version": "3.0.26",
4
+ "version": "3.0.29",
5
5
  "description": "A collection of components for the Carbon Design System built using Vue.js",
6
6
  "packageManager": "yarn@3.2.0",
7
7
  "main": "dist/carbon-vue-3.umd.min.js",
@@ -40,7 +40,8 @@
40
40
  ref="date"
41
41
  type="text"
42
42
  data-date-picker-input
43
- role="datepicker"
43
+ data-testid="datepicker"
44
+ :aria-description="placeholder"
44
45
  :data-invalid="isInvalid || null"
45
46
  :disabled="disabled || null"
46
47
  :data-date-picker-input-from="`${getKind === 'range'}`"
@@ -85,7 +86,8 @@
85
86
  ref="todate"
86
87
  type="text"
87
88
  data-date-picker-input
88
- role="todatepicker"
89
+ data-testid="todatepicker"
90
+ :aria-description="placeholder"
89
91
  :data-date-picker-input-to="`${kind === 'range'}`"
90
92
  :data-invalid="isInvalid || null"
91
93
  :disabled="disabled || null"
@@ -323,6 +323,13 @@ const wrapperStyleOverride = computed(() => {
323
323
  });
324
324
  const el = ref(null);
325
325
  function onClickOut() {
326
+ // Move focus to button if focus is inside dropdown list
327
+ const dropListEl = dropList.value;
328
+ if (dropListEl && dropListEl.contains(document.activeElement)) {
329
+ if (button.value && typeof button.value.focus === 'function') {
330
+ button.value.focus();
331
+ }
332
+ }
326
333
  open.value = false;
327
334
  }
328
335
  onClickOutside(el, onClickOut);
@@ -372,8 +379,14 @@ function onUp() {
372
379
  }
373
380
  }
374
381
  function onEsc() {
382
+ // Move focus to button if focus is inside dropdown list
383
+ const dropListEl = dropList.value;
384
+ if (dropListEl && dropListEl.contains(document.activeElement)) {
385
+ if (button.value && typeof button.value.focus === 'function') {
386
+ button.value.focus();
387
+ }
388
+ }
375
389
  open.value = false;
376
- doFocus();
377
390
  }
378
391
  const button = ref(null);
379
392
  const listBox = ref(null);
@@ -384,8 +397,10 @@ function onTab(ev) {
384
397
  open.value = false;
385
398
  } else if (ev.target === null || listBox.value?.contains(ev.target)) {
386
399
  // list has focus, close and return focus to dropdown
400
+ if (button.value && typeof button.value.focus === 'function') {
401
+ button.value.focus();
402
+ }
387
403
  open.value = false;
388
- doFocus();
389
404
  ev.preventDefault();
390
405
  }
391
406
  }
@@ -394,9 +409,6 @@ function onClick(ev) {
394
409
  if (props.disabled) {
395
410
  ev.preventDefault();
396
411
  } else {
397
- open.value = !open.value;
398
- doFocus(); // open or close (Some browsers do not focus on button when clicked)
399
-
400
412
  let target = ev.target;
401
413
  while (
402
414
  !target.classList.contains(`${carbonPrefix}--dropdown-link`) &&
@@ -408,6 +420,14 @@ function onClick(ev) {
408
420
  if (target.classList.contains(`${carbonPrefix}--dropdown-link`)) {
409
421
  const targetItemEl = target.parentNode;
410
422
  dataValue.value = targetItemEl.getAttribute('data-value');
423
+ // Move focus to button before closing
424
+ if (button.value && typeof button.value.focus === 'function') {
425
+ button.value.focus();
426
+ }
427
+ open.value = false;
428
+ } else {
429
+ open.value = !open.value;
430
+ doFocus(); // open or close (Some browsers do not focus on button when clicked)
411
431
  }
412
432
  }
413
433
  }
@@ -2,11 +2,17 @@
2
2
  import { carbonPrefix } from '../../global/settings';
3
3
  defineProps({
4
4
  tagType: { type: String, default: 'main' },
5
+ id: { type: String, default: 'main-content' },
5
6
  });
6
7
  </script>
7
8
 
8
9
  <template>
9
- <component :is="tagType" :class="`${carbonPrefix}--content`">
10
+ <component
11
+ :is="tagType"
12
+ :id="id"
13
+ :class="`${carbonPrefix}--content`"
14
+ tabindex="-1"
15
+ >
10
16
  <slot />
11
17
  </component>
12
18
  </template>
@@ -192,10 +192,10 @@ function onCvPanelFocusout(payload) {
192
192
  item => item.ariaControls === srcComponent.id
193
193
  );
194
194
  if (
195
- srcComponent.el !== srcEvent.relatedTarget &&
196
- !srcComponent.el.contains(srcEvent.relatedTarget) &&
195
+ srcComponent.el.value !== srcEvent.relatedTarget &&
196
+ !srcComponent.el.value.contains(srcEvent.relatedTarget) &&
197
197
  found &&
198
- found.el !== srcEvent.relatedTarget
198
+ found.el.value !== srcEvent.relatedTarget
199
199
  ) {
200
200
  onCvPanelControlToggle(found, false);
201
201
  }
@@ -2,6 +2,7 @@
2
2
  <li
3
3
  ref="el"
4
4
  :class="`cv-header-menu ${carbonPrefix}--header__submenu`"
5
+ role="menuitem"
5
6
  @mouseenter="doHoverToggle(true)"
6
7
  @mouseleave="doHoverToggle(false)"
7
8
  >
@@ -10,9 +11,9 @@
10
11
  :aria-expanded="data.expanded ? 'true' : 'false'"
11
12
  :class="`${carbonPrefix}--header__menu-item ${carbonPrefix}--header__menu-title`"
12
13
  href="javascript:void(0)"
13
- role="menuitem"
14
14
  tabindex="0"
15
15
  :aria-label="$attrs.ariaLabel"
16
+ role="button"
16
17
  @click="doToggle"
17
18
  @keydown.space.prevent
18
19
  @keyup.space.prevent="doToggle"
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <li class="cv-header-menu-item" :role="role">
2
+ <li class="cv-header-menu-item" role="none">
3
3
  <component
4
4
  :is="tagType"
5
5
  v-bind="{ ...$attrs, ...linkProps }"
@@ -7,8 +7,9 @@
7
7
  `${carbonPrefix}--header__menu-item`,
8
8
  { [`${carbonPrefix}--header__menu-item--current`]: activePage },
9
9
  ]"
10
- role="menuitem"
11
10
  :aria-current="ariaCurrent"
11
+ :role="role"
12
+ :tabindex="tabindex"
12
13
  >
13
14
  <span :class="`${carbonPrefix}--text-truncate--end`">
14
15
  <slot />
@@ -25,7 +26,7 @@ import { computed } from 'vue';
25
26
  const props = defineProps({
26
27
  active: Boolean,
27
28
  ariaCurrent: { type: String, default: undefined },
28
- role: { type: String, default: undefined },
29
+ role: { type: String, default: 'menuitem' },
29
30
  ...propsLink,
30
31
  });
31
32
 
@@ -35,4 +36,8 @@ const linkProps = useLinkProps(props);
35
36
  const activePage = computed(() => {
36
37
  return props.active && props.ariaCurrent !== 'page';
37
38
  });
39
+
40
+ const tabindex = computed(() => {
41
+ return props.role === 'menuitem' ? '0' : undefined;
42
+ });
38
43
  </script>
@@ -3,16 +3,25 @@
3
3
  <ul
4
4
  :class="`${carbonPrefix}--header__menu-bar`"
5
5
  role="menubar"
6
- :ariaLabeldBy="ariaLabelledBy"
6
+ aria-orientation="horizontal"
7
+ :aria-label="ariaLabel || $attrs['aria-label']"
8
+ :aria-labelledby="ariaLabelledBy || $attrs['aria-labelledby']"
7
9
  >
8
10
  <slot />
9
11
  </ul>
10
12
  </nav>
11
13
  </template>
12
14
 
15
+ <script>
16
+ export default {
17
+ inheritAttrs: false,
18
+ };
19
+ </script>
20
+
13
21
  <script setup>
14
22
  import { carbonPrefix } from '../../global/settings';
15
23
  defineProps({
24
+ ariaLabel: { type: String, default: undefined },
16
25
  ariaLabelledBy: { type: String, default: undefined },
17
26
  });
18
27
  </script>
@@ -8,6 +8,7 @@
8
8
  { [`${carbonPrefix}--header-panel--expanded`]: panelExpanded },
9
9
  ]"
10
10
  :aria-hidden="!panelExpanded ? 'true' : 'false'"
11
+ :inert="!panelExpanded"
11
12
  @focusout="onFocusout"
12
13
  @mousedown="onMouseDown"
13
14
  >
@@ -20,6 +21,7 @@ import { carbonPrefix } from '../../global/settings';
20
21
  import {
21
22
  computed,
22
23
  inject,
24
+ nextTick,
23
25
  onBeforeUnmount,
24
26
  onMounted,
25
27
  reactive,
@@ -99,9 +101,49 @@ watch(
99
101
  }
100
102
  );
101
103
 
104
+ function manageFocusableElements(isExpanded) {
105
+ if (!el.value) return;
106
+
107
+ const focusableElements = el.value.querySelectorAll(
108
+ 'a, button, input, textarea, select, details, [tabindex]:not([tabindex="-1"])'
109
+ );
110
+
111
+ focusableElements.forEach(element => {
112
+ if (isExpanded) {
113
+ // Restore original tabindex when expanded
114
+ const originalTabindex = element.getAttribute('data-original-tabindex');
115
+ if (originalTabindex !== null) {
116
+ if (originalTabindex === 'null') {
117
+ element.removeAttribute('tabindex');
118
+ } else {
119
+ element.setAttribute('tabindex', originalTabindex);
120
+ }
121
+ element.removeAttribute('data-original-tabindex');
122
+ }
123
+ } else {
124
+ // Store original tabindex and set to -1 when collapsed
125
+ const currentTabindex = element.getAttribute('tabindex');
126
+ element.setAttribute('data-original-tabindex', currentTabindex || 'null');
127
+ element.setAttribute('tabindex', '-1');
128
+ }
129
+ });
130
+ }
131
+
102
132
  const emit = defineEmits(['update:expanded', 'panel-resize']);
103
133
  watch(panelExpanded, current => {
104
134
  emit('update:expanded', current);
105
135
  emit('panel-resize', { id: props.id, expanded: current });
136
+
137
+ // Manage focusable elements
138
+ nextTick(() => {
139
+ manageFocusableElements(current);
140
+ });
141
+ });
142
+
143
+ // Initial setup of focusable elements
144
+ onMounted(() => {
145
+ nextTick(() => {
146
+ manageFocusableElements(panelExpanded.value);
147
+ });
106
148
  });
107
149
  </script>
@@ -14,6 +14,7 @@
14
14
  },
15
15
  ]"
16
16
  :aria-hidden="!panelExpanded && !fixed ? 'true' : 'false'"
17
+ :inert="!panelExpanded && !fixed"
17
18
  @focusout="onFocusout"
18
19
  @mousedown="onMouseDown"
19
20
  @mouseenter="onHoverToggle(true)"
@@ -33,6 +34,7 @@
33
34
  import {
34
35
  computed,
35
36
  inject,
37
+ nextTick,
36
38
  onBeforeUnmount,
37
39
  onMounted,
38
40
  reactive,
@@ -110,6 +112,35 @@ watch(
110
112
  panelExpanded.value = props.expanded;
111
113
  }
112
114
  );
115
+ // Manage focusable elements when panel visibility changes
116
+ function manageFocusableElements(isExpanded, isFixed) {
117
+ if (!el.value || isFixed) return;
118
+
119
+ const focusableElements = el.value.querySelectorAll(
120
+ 'a, button, input, textarea, select, details, [tabindex]:not([tabindex="-1"])'
121
+ );
122
+
123
+ focusableElements.forEach(element => {
124
+ if (isExpanded) {
125
+ // Restore original tabindex when expanded
126
+ const originalTabindex = element.getAttribute('data-original-tabindex');
127
+ if (originalTabindex !== null) {
128
+ if (originalTabindex === 'null') {
129
+ element.removeAttribute('tabindex');
130
+ } else {
131
+ element.setAttribute('tabindex', originalTabindex);
132
+ }
133
+ element.removeAttribute('data-original-tabindex');
134
+ }
135
+ } else {
136
+ // Store original tabindex and set to -1 when collapsed
137
+ const currentTabindex = element.getAttribute('tabindex');
138
+ element.setAttribute('data-original-tabindex', currentTabindex || 'null');
139
+ element.setAttribute('tabindex', '-1');
140
+ }
141
+ });
142
+ }
143
+
113
144
  const emit = defineEmits(['update:expanded', 'panel-resize']);
114
145
  const isPanelExpanded = computed(
115
146
  () => panelExpanded.value || expandedViaHoverState.value
@@ -117,6 +148,11 @@ const isPanelExpanded = computed(
117
148
  watch(isPanelExpanded, current => {
118
149
  emit('update:expanded', current);
119
150
  emit('panel-resize', { id: props.id, expanded: current });
151
+
152
+ // Manage focusable elements for non-fixed nav
153
+ nextTick(() => {
154
+ manageFocusableElements(current, props.fixed);
155
+ });
120
156
  });
121
157
 
122
158
  const isChildOfHeader = computed(() => {
@@ -19,6 +19,7 @@ import CvSideNavLink from './CvSideNavLink.vue';
19
19
  import CvSideNavMenu from './CvSideNavMenu.vue';
20
20
  import CvSideNavMenuDivider from './CvSideNavMenuDivider.vue';
21
21
  import CvSideNavMenuItem from './CvSideNavMenuItem.vue';
22
+ import CvContent from './CvContent.vue';
22
23
  import {
23
24
  Notification20,
24
25
  UserAvatar20,
@@ -55,6 +56,7 @@ export const Template = args => ({
55
56
  CvSideNavMenu,
56
57
  CvSideNavMenuDivider,
57
58
  CvSideNavMenuItem,
59
+ CvContent,
58
60
  Notification20,
59
61
  UserAvatar20,
60
62
  Login20,
@@ -89,22 +91,12 @@ const defaultTemplate = `
89
91
  [Platform]
90
92
  </cv-header-name>
91
93
  <cv-header-nav aria-label="Carbon nav">
92
- <cv-header-menu-item href="javascript:void(0)">
93
- Link 1
94
- </cv-header-menu-item>
95
- <cv-header-menu-item href="javascript:void(0)">
96
- Link 2
97
- </cv-header-menu-item>
94
+ <cv-header-menu-item href="javascript:void(0)">Link 1</cv-header-menu-item>
95
+ <cv-header-menu-item href="javascript:void(0)">Link 2</cv-header-menu-item>
98
96
  <cv-header-menu aria-label="Link 3" title="Link 3">
99
- <cv-header-menu-item href="javascript:void(0)">
100
- Submenu Link 1
101
- </cv-header-menu-item>
102
- <cv-header-menu-item href="javascript:void(0)">
103
- Submenu Link 2
104
- </cv-header-menu-item>
105
- <cv-header-menu-item href="javascript:void(0)">
106
- Submenu Link 3
107
- </cv-header-menu-item>
97
+ <cv-header-menu-item href="javascript:void(0)">Submenu Link 1</cv-header-menu-item>
98
+ <cv-header-menu-item href="javascript:void(0)">Submenu Link 2</cv-header-menu-item>
99
+ <cv-header-menu-item href="javascript:void(0)">Submenu Link 3</cv-header-menu-item>
108
100
  </cv-header-menu>
109
101
  </cv-header-nav>
110
102
  <template v-slot:header-global>
@@ -162,6 +154,10 @@ const defaultTemplate = `
162
154
  </cv-header-panel>
163
155
  </template>
164
156
  </cv-header>
157
+ <cv-content>
158
+ <h1>Main Content</h1>
159
+ <p>This is the main content area that the skip link targets.</p>
160
+ </cv-content>
165
161
  `;
166
162
  const railTemplate = `
167
163
  <cv-header aria-label="Carbon header">
@@ -196,6 +192,10 @@ const railTemplate = `
196
192
  </cv-side-nav>
197
193
  </template>
198
194
  </cv-header>
195
+ <cv-content>
196
+ <h1>Main Content</h1>
197
+ <p>This is the main content area that the skip link targets.</p>
198
+ </cv-content>
199
199
  `;
200
200
  const fixedSideNavTemplate = `
201
201
  <cv-header aria-label="Carbon header">
@@ -231,6 +231,10 @@ const fixedSideNavTemplate = `
231
231
  </cv-side-nav-link>
232
232
  </cv-side-nav-items>
233
233
  </cv-side-nav>
234
+ <cv-content>
235
+ <h1>Main Content</h1>
236
+ <p>This is the main content area that the skip link targets.</p>
237
+ </cv-content>
234
238
  `;
235
239
 
236
240
  # UI Shell components
@@ -0,0 +1,47 @@
1
+ <script setup>
2
+ import CvHeader from '../CvHeader.vue';
3
+ import CvHeaderGlobalAction from '../CvHeaderGlobalAction.vue';
4
+ import CvHeaderPanel from '../CvHeaderPanel.vue';
5
+ import CvSwitcher from '../CvSwitcher.vue';
6
+ import CvSwitcherItem from '../CvSwitcherItem.vue';
7
+ import CvSwitcherItemLink from '../CvSwitcherItemLink.vue';
8
+ </script>
9
+
10
+ <template>
11
+ <cv-header>
12
+ <template #header-global>
13
+ <cv-header-global-action
14
+ data-testid="action-button"
15
+ aria-label="List links"
16
+ aria-controls="links-panel"
17
+ label="Links"
18
+ >
19
+ Links
20
+ </cv-header-global-action>
21
+ </template>
22
+ <template #right-panels>
23
+ <cv-header-panel id="links-panel" data-testid="links-panel">
24
+ <cv-switcher>
25
+ <cv-switcher-item>
26
+ <cv-switcher-item-link
27
+ href="javascript:void(0)"
28
+ data-testid="link-1"
29
+ selected
30
+ >
31
+ Selected app
32
+ </cv-switcher-item-link>
33
+ </cv-switcher-item>
34
+ <cv-switcher-item>
35
+ <cv-switcher-item-link
36
+ href="javascript:void(0)"
37
+ data-testid="link-2"
38
+ >
39
+ Other app
40
+ </cv-switcher-item-link>
41
+ </cv-switcher-item>
42
+ </cv-switcher>
43
+ </cv-header-panel>
44
+ </template>
45
+ </cv-header>
46
+ <input type="text" data-testid="outer-input" />
47
+ </template>
package/src/index.js CHANGED
@@ -158,3 +158,4 @@ export { default as CvSkipToContent } from './components/CvUIShell/CvSkipToConte
158
158
  export { default as CvSwitcher } from './components/CvUIShell/CvSwitcher.vue';
159
159
  export { default as CvSwitcherItem } from './components/CvUIShell/CvSwitcherItem.vue';
160
160
  export { default as CvSwitcherItemLink } from './components/CvUIShell/CvSwitcherItemLink.vue';
161
+ export { default as PanelFocusTestComponent } from './components/CvUIShell/__tests__/PanelFocusTestComponent.vue';