@rancher/shell 3.0.5-rc.1 → 3.0.5-rc.3

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 (280) hide show
  1. package/assets/data/aws-regions.json +2 -0
  2. package/assets/images/providers/sks.svg +1 -0
  3. package/assets/styles/base/_helpers.scss +4 -0
  4. package/assets/styles/base/_variables.scss +1 -0
  5. package/assets/styles/global/_layout.scss +0 -1
  6. package/assets/translations/en-us.yaml +92 -34
  7. package/assets/translations/zh-hans.yaml +4 -13
  8. package/chart/monitoring/index.vue +4 -2
  9. package/components/ActionDropdownShell.vue +71 -0
  10. package/components/AppModal.vue +18 -4
  11. package/components/AsyncButton.vue +2 -0
  12. package/components/CodeMirror.vue +3 -3
  13. package/components/CommunityLinks.vue +3 -58
  14. package/components/CruResource.vue +109 -16
  15. package/components/ExplorerProjectsNamespaces.vue +19 -6
  16. package/components/FixedBanner.vue +19 -5
  17. package/components/GlobalRoleBindings.vue +5 -1
  18. package/components/GrowlManager.vue +1 -0
  19. package/components/LandingPagePreference.vue +2 -0
  20. package/components/LocaleSelector.vue +1 -1
  21. package/components/ModalManager.vue +55 -0
  22. package/components/PaginatedResourceTable.vue +7 -0
  23. package/components/PromptModal.vue +47 -8
  24. package/components/ResourceDetail/Masthead.vue +38 -13
  25. package/components/ResourceDetail/__tests__/Masthead.test.ts +5 -1
  26. package/components/ResourceDetail/index.vue +47 -12
  27. package/components/ResourceList/index.vue +2 -1
  28. package/components/ResourceTable.vue +54 -19
  29. package/components/SideNav.vue +5 -1
  30. package/components/SlideInPanelManager.vue +125 -0
  31. package/components/SortableTable/THead.vue +5 -2
  32. package/components/SortableTable/actions.js +1 -1
  33. package/components/SortableTable/index.vue +54 -40
  34. package/components/SortableTable/paging.js +16 -19
  35. package/components/SortableTable/selection.js +1 -12
  36. package/components/Tabbed/index.vue +6 -0
  37. package/components/Wizard.vue +2 -2
  38. package/components/__tests__/AsyncButton.test.ts +39 -0
  39. package/components/__tests__/CruResource.test.ts +63 -0
  40. package/components/__tests__/ModalManager.spec.ts +176 -0
  41. package/components/__tests__/PromptModal.test.ts +146 -0
  42. package/components/__tests__/SlideInPanelManager.spec.ts +166 -0
  43. package/components/auth/AuthBanner.vue +13 -11
  44. package/components/auth/Principal.vue +1 -0
  45. package/components/auth/login/ldap.vue +1 -1
  46. package/components/fleet/FleetResources.vue +21 -6
  47. package/components/form/ArrayList.vue +138 -118
  48. package/components/form/BannerSettings.vue +149 -85
  49. package/components/form/ColorInput.vue +35 -6
  50. package/components/form/EnvVars.vue +1 -0
  51. package/components/form/KeyValue.vue +10 -7
  52. package/components/form/LabeledSelect.vue +25 -23
  53. package/components/form/MatchExpressions.vue +9 -2
  54. package/components/form/NameNsDescription.vue +6 -2
  55. package/components/form/NotificationSettings.vue +15 -1
  56. package/components/form/Password.vue +1 -0
  57. package/components/form/Probe.vue +1 -0
  58. package/components/form/ResourceSelector.vue +26 -23
  59. package/components/form/ResourceTabs/index.vue +2 -1
  60. package/components/form/SSHKnownHosts/__tests__/KnownHostsEditDialog.test.ts +15 -34
  61. package/components/form/SSHKnownHosts/index.vue +14 -11
  62. package/components/form/Select.vue +8 -15
  63. package/components/form/UnitInput.vue +13 -0
  64. package/components/form/ValueFromResource.vue +12 -12
  65. package/components/form/__tests__/ArrayList.test.ts +34 -2
  66. package/components/form/__tests__/ColorInput.test.ts +35 -0
  67. package/components/form/__tests__/KeyValue.test.ts +36 -0
  68. package/components/form/__tests__/LabeledSelect.test.ts +73 -0
  69. package/components/form/__tests__/SSHKnownHosts.test.ts +11 -2
  70. package/components/form/__tests__/Select.test.ts +34 -1
  71. package/components/form/__tests__/UnitInput.test.ts +23 -1
  72. package/components/formatter/ClusterLink.vue +5 -8
  73. package/components/formatter/Description.vue +30 -0
  74. package/components/formatter/__tests__/ClusterLink.test.ts +2 -32
  75. package/components/nav/Group.vue +12 -4
  76. package/components/nav/Header.vue +16 -43
  77. package/components/nav/NamespaceFilter.vue +134 -86
  78. package/components/nav/TopLevelMenu.vue +4 -5
  79. package/components/nav/WindowManager/ContainerLogs.vue +87 -61
  80. package/components/nav/WindowManager/ContainerLogsActions.vue +76 -0
  81. package/components/nav/WindowManager/index.vue +1 -0
  82. package/components/templates/default.vue +6 -3
  83. package/components/templates/home.vue +6 -0
  84. package/components/templates/plain.vue +6 -3
  85. package/composables/focusTrap.ts +12 -4
  86. package/config/product/explorer.js +16 -13
  87. package/config/product/manager.js +1 -28
  88. package/config/settings.ts +11 -13
  89. package/config/store.js +4 -0
  90. package/config/table-headers.js +7 -5
  91. package/config/uiplugins.js +5 -1
  92. package/core/types.ts +7 -6
  93. package/detail/catalog.cattle.io.app.vue +5 -1
  94. package/detail/fleet.cattle.io.bundle.vue +70 -6
  95. package/detail/fleet.cattle.io.gitrepo.vue +1 -1
  96. package/detail/namespace.vue +0 -3
  97. package/detail/node.vue +17 -13
  98. package/detail/provisioning.cattle.io.cluster.vue +85 -9
  99. package/detail/service.vue +0 -1
  100. package/detail/workload/index.vue +21 -34
  101. package/dialog/AddCustomBadgeDialog.vue +0 -1
  102. package/{pages/c/_cluster/uiplugins/AddExtensionRepos.vue → dialog/AddExtensionReposDialog.vue} +72 -42
  103. package/dialog/AssignToDialog.vue +176 -0
  104. package/dialog/ChangePasswordDialog.vue +106 -0
  105. package/{pages/c/_cluster/uiplugins/DeveloperInstallDialog.vue → dialog/DeveloperLoadExtensionDialog.vue} +74 -71
  106. package/dialog/DisableAuthProviderDialog.vue +101 -0
  107. package/dialog/DrainNode.vue +1 -1
  108. package/{pages/c/_cluster/uiplugins/CatalogList/CatalogLoadDialog.vue → dialog/ExtensionCatalogInstallDialog.vue} +100 -88
  109. package/{pages/c/_cluster/uiplugins/CatalogList/CatalogUninstallDialog.vue → dialog/ExtensionCatalogUninstallDialog.vue} +83 -65
  110. package/dialog/FeatureFlagListDialog.vue +288 -0
  111. package/dialog/ForceMachineRemoveDialog.vue +1 -1
  112. package/{components/Import.vue → dialog/ImportDialog.vue} +0 -5
  113. package/{pages/c/_cluster/uiplugins/InstallDialog.vue → dialog/InstallExtensionDialog.vue} +124 -106
  114. package/{components/form/SSHKnownHosts → dialog}/KnownHostsEditDialog.vue +52 -62
  115. package/dialog/MoveNamespaceDialog.vue +157 -0
  116. package/dialog/ScalePoolDownDialog.vue +1 -1
  117. package/{components/nav/Jump.vue → dialog/SearchDialog.vue} +34 -14
  118. package/{pages/c/_cluster/uiplugins/UninstallDialog.vue → dialog/UninstallExtensionDialog.vue} +67 -58
  119. package/dialog/WechatDialog.vue +57 -0
  120. package/edit/__tests__/service.test.ts +2 -1
  121. package/edit/auth/azuread.vue +1 -1
  122. package/edit/auth/github.vue +1 -1
  123. package/edit/auth/googleoauth.vue +1 -1
  124. package/edit/auth/ldap/index.vue +1 -1
  125. package/edit/auth/oidc.vue +1 -1
  126. package/edit/auth/saml.vue +1 -1
  127. package/edit/cloudcredential.vue +24 -10
  128. package/edit/management.cattle.io.user.vue +28 -3
  129. package/edit/namespace.vue +1 -4
  130. package/edit/networking.k8s.io.networkpolicy/PolicyRule.vue +3 -14
  131. package/edit/networking.k8s.io.networkpolicy/PolicyRuleTarget.vue +57 -62
  132. package/edit/networking.k8s.io.networkpolicy/PolicyRules.vue +3 -14
  133. package/edit/networking.k8s.io.networkpolicy/__tests__/PolicyRuleTarget.test.ts +72 -41
  134. package/edit/networking.k8s.io.networkpolicy/__tests__/utils/mock.json +17 -1
  135. package/edit/networking.k8s.io.networkpolicy/index.vue +18 -30
  136. package/edit/provisioning.cattle.io.cluster/CustomCommand.vue +4 -1
  137. package/edit/provisioning.cattle.io.cluster/SelectCredential.vue +26 -10
  138. package/edit/provisioning.cattle.io.cluster/__tests__/Advanced.test.ts +8 -8
  139. package/edit/provisioning.cattle.io.cluster/__tests__/DirectoryConfig.test.ts +26 -12
  140. package/edit/provisioning.cattle.io.cluster/__tests__/rke2.test.ts +66 -0
  141. package/edit/provisioning.cattle.io.cluster/__tests__/utils/rke2-test-data.ts +58 -0
  142. package/edit/provisioning.cattle.io.cluster/index.vue +21 -73
  143. package/edit/provisioning.cattle.io.cluster/rke2.vue +24 -7
  144. package/edit/provisioning.cattle.io.cluster/tabs/DirectoryConfig.vue +5 -3
  145. package/edit/provisioning.cattle.io.cluster/tabs/MachinePool.vue +4 -1
  146. package/edit/service.vue +13 -28
  147. package/initialize/install-plugins.js +2 -1
  148. package/list/harvesterhci.io.management.cluster.vue +4 -1
  149. package/list/management.cattle.io.feature.vue +4 -288
  150. package/list/workload.vue +6 -1
  151. package/machine-config/azure.vue +16 -4
  152. package/mixins/resource-fetch-api-pagination.js +55 -43
  153. package/mixins/resource-fetch.js +14 -5
  154. package/mixins/vue-select-overrides.js +0 -4
  155. package/models/__tests__/workload.test.ts +1 -0
  156. package/models/cluster/node.js +1 -0
  157. package/models/cluster.js +32 -2
  158. package/models/fleet.cattle.io.cluster.js +8 -2
  159. package/models/fleet.cattle.io.gitrepo.js +8 -34
  160. package/models/management.cattle.io.cluster.js +0 -20
  161. package/models/management.cattle.io.feature.js +7 -1
  162. package/models/management.cattle.io.node.js +7 -22
  163. package/models/management.cattle.io.nodepool.js +12 -0
  164. package/models/namespace.js +12 -1
  165. package/models/provisioning.cattle.io.cluster.js +18 -64
  166. package/models/service.js +24 -9
  167. package/models/workload.js +70 -31
  168. package/package.json +1 -1
  169. package/pages/about.vue +13 -3
  170. package/pages/account/index.vue +12 -5
  171. package/pages/auth/login.vue +7 -4
  172. package/pages/auth/setup.vue +1 -0
  173. package/pages/auth/verify.vue +9 -7
  174. package/pages/c/_cluster/apps/charts/install.vue +25 -26
  175. package/pages/c/_cluster/auth/config/index.vue +10 -12
  176. package/pages/c/_cluster/explorer/EventsTable.vue +38 -33
  177. package/pages/c/_cluster/explorer/index.vue +28 -15
  178. package/pages/c/_cluster/istio/index.vue +2 -2
  179. package/pages/c/_cluster/longhorn/index.vue +3 -3
  180. package/pages/c/_cluster/monitoring/index.vue +1 -1
  181. package/pages/c/_cluster/monitoring/monitor/_namespace/_id.vue +4 -2
  182. package/pages/c/_cluster/monitoring/monitor/create.vue +4 -2
  183. package/pages/c/_cluster/monitoring/route-receiver/_id.vue +4 -2
  184. package/pages/c/_cluster/monitoring/route-receiver/create.vue +5 -2
  185. package/pages/c/_cluster/neuvector/index.vue +1 -1
  186. package/pages/c/_cluster/settings/banners.vue +60 -5
  187. package/pages/c/_cluster/settings/performance.vue +7 -26
  188. package/pages/c/_cluster/uiplugins/CatalogList/index.vue +8 -10
  189. package/pages/c/_cluster/uiplugins/__tests__/AddExtensionRepos.test.ts +4 -7
  190. package/pages/c/_cluster/uiplugins/index.vue +98 -55
  191. package/pages/diagnostic.vue +12 -9
  192. package/pages/fail-whale.vue +8 -5
  193. package/pages/home.vue +11 -52
  194. package/pages/prefs.vue +7 -6
  195. package/plugins/clean-html.js +2 -0
  196. package/plugins/dashboard-store/__tests__/actions.test.ts +4 -1
  197. package/plugins/dashboard-store/actions.js +122 -21
  198. package/plugins/dashboard-store/getters.js +74 -3
  199. package/plugins/dashboard-store/mutations.js +10 -5
  200. package/plugins/dashboard-store/resource-class.js +23 -3
  201. package/plugins/internal-api/index.ts +37 -0
  202. package/plugins/internal-api/shared/base-api.ts +13 -0
  203. package/plugins/internal-api/shell/shell.api.ts +108 -0
  204. package/plugins/steve/__tests__/getters.test.ts +18 -11
  205. package/plugins/steve/__tests__/steve-class.test.ts +1 -0
  206. package/plugins/steve/actions.js +34 -24
  207. package/plugins/steve/getters.js +39 -10
  208. package/plugins/steve/steve-class.js +5 -0
  209. package/plugins/steve/steve-pagination-utils.ts +199 -37
  210. package/plugins/steve/worker/web-worker.advanced.js +3 -1
  211. package/public/index.html +1 -0
  212. package/rancher-components/Banner/Banner.test.ts +51 -3
  213. package/rancher-components/Banner/Banner.vue +28 -6
  214. package/rancher-components/Card/Card.vue +1 -1
  215. package/rancher-components/Form/Checkbox/Checkbox.test.ts +59 -1
  216. package/rancher-components/Form/Checkbox/Checkbox.vue +27 -3
  217. package/rancher-components/Form/LabeledInput/LabeledInput.test.ts +51 -0
  218. package/rancher-components/Form/LabeledInput/LabeledInput.vue +20 -2
  219. package/rancher-components/Form/Radio/RadioButton.test.ts +36 -1
  220. package/rancher-components/Form/Radio/RadioButton.vue +20 -4
  221. package/rancher-components/Form/Radio/RadioGroup.test.ts +60 -0
  222. package/rancher-components/Form/Radio/RadioGroup.vue +75 -35
  223. package/rancher-components/Form/ToggleSwitch/ToggleSwitch.test.ts +17 -0
  224. package/rancher-components/Form/ToggleSwitch/ToggleSwitch.vue +26 -1
  225. package/rancher-components/LabeledTooltip/LabeledTooltip.vue +10 -1
  226. package/rancher-components/RcButton/RcButton.vue +2 -1
  227. package/rancher-components/RcButton/types.ts +1 -0
  228. package/rancher-components/RcDropdown/RcDropdown.vue +17 -6
  229. package/rancher-components/RcDropdown/RcDropdownItem.vue +3 -56
  230. package/rancher-components/RcDropdown/RcDropdownItemCheckbox.vue +68 -0
  231. package/rancher-components/RcDropdown/RcDropdownItemSelect.vue +92 -0
  232. package/rancher-components/RcDropdown/index.ts +2 -0
  233. package/rancher-components/RcDropdown/useDropdownItem.ts +63 -0
  234. package/scripts/extension/bundle +20 -0
  235. package/scripts/extension/helm/charts/ui-plugin-server/templates/cr.yaml +2 -1
  236. package/scripts/extension/helm/charts/ui-plugin-server/values.yaml +2 -0
  237. package/scripts/extension/helmpatch +44 -31
  238. package/scripts/extension/publish +12 -13
  239. package/scripts/typegen.sh +2 -4
  240. package/store/action-menu.js +26 -56
  241. package/store/features.js +0 -1
  242. package/store/index.js +5 -0
  243. package/store/modal.ts +71 -0
  244. package/store/slideInPanel.ts +47 -0
  245. package/store/type-map.js +8 -1
  246. package/store/type-map.utils.ts +49 -6
  247. package/types/fleet.d.ts +1 -1
  248. package/types/global-vue.d.ts +5 -0
  249. package/types/internal-api/shell/growl.d.ts +25 -0
  250. package/types/internal-api/shell/modal.d.ts +77 -0
  251. package/types/internal-api/shell/slideIn.d.ts +15 -0
  252. package/types/kube/kube-api.ts +22 -0
  253. package/types/resources/fleet.d.ts +0 -14
  254. package/types/resources/settings.d.ts +0 -4
  255. package/types/shell/index.d.ts +375 -306
  256. package/types/store/dashboard-store.types.ts +24 -1
  257. package/types/store/pagination.types.ts +19 -2
  258. package/types/vue-shim.d.ts +4 -1
  259. package/utils/__mocks__/tabbable.js +13 -0
  260. package/utils/__tests__/object.test.ts +38 -4
  261. package/utils/cluster.js +24 -20
  262. package/utils/fleet.ts +15 -73
  263. package/utils/grafana.js +1 -0
  264. package/utils/object.js +36 -5
  265. package/utils/pagination-utils.ts +6 -2
  266. package/utils/perf-setting.utils.ts +28 -0
  267. package/utils/selector-typed.ts +205 -0
  268. package/utils/selector.js +29 -6
  269. package/utils/uiplugins.ts +10 -6
  270. package/utils/v-sphere.ts +5 -1
  271. package/utils/validators/formRules/__tests__/index.test.ts +10 -1
  272. package/utils/validators/formRules/index.ts +27 -3
  273. package/components/AssignTo.vue +0 -199
  274. package/components/DisableAuthProviderModal.vue +0 -115
  275. package/components/MoveModal.vue +0 -167
  276. package/components/PromptChangePassword.vue +0 -123
  277. package/components/fleet/FleetBundleResources.vue +0 -86
  278. package/components/formatter/RKETemplateName.vue +0 -37
  279. package/dialog/SaveAsRKETemplateDialog.vue +0 -139
  280. package/types/vue-shim.d +0 -20
@@ -314,7 +314,9 @@ self.onmessage = (e) => {
314
314
  if (workerActions[action]) {
315
315
  workerActions[action](e?.data[action]);
316
316
  } else {
317
- console.warn('no associated action for:', action); // eslint-disable-line no-console
317
+ // This catches any window sendMessage event. We're hitting this on hot-reload of code where somehow this file is loaded
318
+ // Could be related to extensions, which have their own version of this
319
+ console.debug('no associated action for:', action); // eslint-disable-line no-console
318
320
  }
319
321
  });
320
322
  }; // bind everything to the worker's onmessage handler via the workerActions
package/public/index.html CHANGED
@@ -16,6 +16,7 @@
16
16
  </div>
17
17
  </div>
18
18
  <div id="modals"><!--Portal content here--></div>
19
+ <div id="slides"></div>
19
20
 
20
21
  <script>
21
22
  (() => {
@@ -3,14 +3,23 @@ import { Banner } from './index';
3
3
 
4
4
  describe('component: Banner', () => {
5
5
  it('should display text based on label', () => {
6
- const label = 'test';
6
+ const label = 'some-label-test';
7
7
  const wrapper = mount(
8
8
  Banner,
9
9
  { propsData: { label } });
10
10
 
11
- const element = wrapper.find('span').element;
11
+ expect(wrapper.html()).toContain(label);
12
+ });
13
+
14
+ it('should display text based on default slot', () => {
15
+ const slotText = 'some-test';
16
+
17
+ const wrapper = mount(
18
+ Banner,
19
+ { slots: { default: slotText } }
20
+ );
12
21
 
13
- expect(element.textContent).toBe(label);
22
+ expect(wrapper.html()).toContain(slotText);
14
23
  });
15
24
 
16
25
  it('should display an icon', () => {
@@ -56,4 +65,43 @@ describe('component: Banner', () => {
56
65
 
57
66
  expect(element.classList).toContain('stacked');
58
67
  });
68
+
69
+ it('a11y: adding ARIA props should correctly fill out the appropriate fields on the component', () => {
70
+ const label = 'test';
71
+ const icon = 'my-icon';
72
+ const closable = true;
73
+
74
+ const wrapper = mount(
75
+ Banner,
76
+ {
77
+ propsData: {
78
+ label, icon, closable
79
+ }
80
+ });
81
+
82
+ const mainContainer = wrapper.find('.banner');
83
+ const bannerIcon = wrapper.find('.banner__icon i');
84
+ const bannerContent = wrapper.find('.banner__content');
85
+ const bannerCloseBtn = wrapper.find('.banner__content__closer');
86
+ const bannerCloseIcon = wrapper.find('.icon-close.closer-icon');
87
+
88
+ const mainContainerRole = mainContainer.attributes('role');
89
+ const mainContainerAriaLabelledBy = mainContainer.attributes('aria-labelledby');
90
+
91
+ const bannerIconAlt = bannerIcon.attributes('alt');
92
+
93
+ const bannerContentId = bannerContent.attributes('id');
94
+
95
+ const bannerCloseBtnRole = bannerCloseBtn.attributes('role');
96
+ const bannerCloseBtnAriaLabel = bannerCloseBtn.attributes('aria-label');
97
+
98
+ const bannerCloseIconAlt = bannerCloseIcon.attributes('alt');
99
+
100
+ expect(mainContainerRole).toBe('region');
101
+ expect(mainContainerAriaLabelledBy).toBe(bannerContentId);
102
+ expect(bannerIconAlt).toBeDefined();
103
+ expect(bannerCloseIconAlt).toBeDefined();
104
+ expect(bannerCloseBtnRole).toBe('button');
105
+ expect(bannerCloseBtnAriaLabel).toBeDefined();
106
+ });
59
107
  });
@@ -1,6 +1,6 @@
1
1
  <script lang="ts">
2
2
  import { defineComponent } from 'vue';
3
- import { nlToBr } from '@shell/utils/string';
3
+ import { nlToBr, generateRandomAlphaString } from '@shell/utils/string';
4
4
  import { stringify } from '@shell/utils/error';
5
5
 
6
6
  export default defineComponent({
@@ -47,16 +47,26 @@ export default defineComponent({
47
47
  stacked: {
48
48
  type: Boolean,
49
49
  default: false
50
- }
50
+ },
51
+ /**
52
+ * Disabled banner - banner is shown greyed out
53
+ */
54
+ disabled: {
55
+ type: Boolean,
56
+ default: false
57
+ },
58
+ },
59
+ emits: ['close'],
60
+ data() {
61
+ return { labelledbyId: `banner-labelledby-${ generateRandomAlphaString(12) }` };
51
62
  },
52
- emits: ['close'],
53
63
  computed: {
54
64
  /**
55
65
  * Return message text as label.
56
66
  */
57
67
  messageLabel(): string | void {
58
68
  return !(typeof this.label === 'string') ? stringify(this.label) : undefined;
59
- }
69
+ },
60
70
  },
61
71
  methods: { nlToBr }
62
72
  });
@@ -66,8 +76,11 @@ export default defineComponent({
66
76
  class="banner"
67
77
  :class="{
68
78
  [color]: true,
79
+ 'banner-disabled': disabled
69
80
  }"
70
- role="banner"
81
+ role="region"
82
+ :aria-labelledby="labelledbyId"
83
+ tabindex="0"
71
84
  >
72
85
  <div
73
86
  v-if="icon"
@@ -77,9 +90,11 @@ export default defineComponent({
77
90
  <i
78
91
  class="icon icon-2x"
79
92
  :class="icon"
93
+ :alt="t('generic.banners.bannerIcon')"
80
94
  />
81
95
  </div>
82
96
  <div
97
+ :id="labelledbyId"
83
98
  class="banner__content"
84
99
  data-testid="banner-content"
85
100
  :class="{
@@ -94,7 +109,9 @@ export default defineComponent({
94
109
  :k="labelKey"
95
110
  :raw="true"
96
111
  />
97
- <span v-else-if="messageLabel">{{ messageLabel }}</span>
112
+ <span
113
+ v-else-if="messageLabel"
114
+ >{{ messageLabel }}</span>
98
115
  <span
99
116
  v-else
100
117
  v-clean-html="nlToBr(label)"
@@ -113,6 +130,7 @@ export default defineComponent({
113
130
  <i
114
131
  data-testid="banner-close"
115
132
  class="icon icon-close closer-icon"
133
+ :alt="t('generic.banners.altCloseBanner')"
116
134
  />
117
135
  </div>
118
136
  </div>
@@ -164,6 +182,10 @@ $icon-size: 24px;
164
182
  }
165
183
  }
166
184
 
185
+ &.banner-disabled {
186
+ filter: grayscale(1);
187
+ }
188
+
167
189
  &__content {
168
190
  padding: 10px;
169
191
  transition: all 0.2s ease;
@@ -89,7 +89,7 @@ export default defineComponent({
89
89
  {{ title }}
90
90
  </slot>
91
91
  </div>
92
- <hr>
92
+ <hr role="none">
93
93
  <div
94
94
  class="card-body"
95
95
  data-testid="card-body-slot"
@@ -1,4 +1,4 @@
1
- import { shallowMount, Wrapper } from '@vue/test-utils';
1
+ import { shallowMount, Wrapper, mount } from '@vue/test-utils';
2
2
  import { Checkbox } from './index';
3
3
 
4
4
  describe('checkbox.vue', () => {
@@ -65,4 +65,62 @@ describe('checkbox.vue', () => {
65
65
 
66
66
  expect(wrapper.emitted('update:value')[0][0]).toBeNull();
67
67
  });
68
+
69
+ it('a11y: adding ARIA props should correctly fill out the appropriate fields on the component', async() => {
70
+ const alternateLabel = 'some-alternate-aria-label';
71
+ const description = 'some-description';
72
+ const ariaDescribedById = 'some-external-id';
73
+
74
+ const wrapper: Wrapper<InstanceType<typeof Checkbox>> = mount(
75
+ Checkbox,
76
+ {
77
+ propsData: {
78
+ value: false, alternateLabel, description
79
+ },
80
+ attrs: { 'aria-describedby': ariaDescribedById },
81
+ }
82
+ );
83
+
84
+ const field = wrapper.find('.checkbox-custom');
85
+ const ariaChecked = field.attributes('aria-checked');
86
+ const ariaLabel = field.attributes('aria-label');
87
+ const ariaLabelledBy = field.attributes('aria-labelledby');
88
+ const ariaDescribedBy = field.attributes('aria-describedby');
89
+
90
+ // validates type of input rendered
91
+ expect(ariaChecked).toBe('false');
92
+ expect(ariaLabelledBy).toBeUndefined();
93
+ expect(ariaLabel).toBe(alternateLabel);
94
+ expect(ariaDescribedBy).toBe(`${ ariaDescribedById } ${ wrapper.vm.describedById }`);
95
+ });
96
+
97
+ it('a11y: having a label should not render "aria-label" prop and have "aria-labelledby"', async() => {
98
+ const label = 'some-label';
99
+
100
+ const wrapper: Wrapper<InstanceType<typeof Checkbox>> = mount(
101
+ Checkbox,
102
+ {
103
+ propsData: {
104
+ value: true, label, disabled: true
105
+ }
106
+ }
107
+ );
108
+
109
+ const field = wrapper.find('.checkbox-custom');
110
+ const ariaChecked = field.attributes('aria-checked');
111
+ const ariaLabel = field.attributes('aria-label');
112
+ const ariaLabelledBy = field.attributes('aria-labelledby');
113
+ const ariaDisabled = field.attributes('aria-disabled');
114
+ const tabIndex = field.attributes('tabindex');
115
+
116
+ // validates type of input rendered
117
+ expect(field.exists()).toBe(true);
118
+ expect(ariaChecked).toBe('true');
119
+ expect(ariaLabelledBy).toBe(wrapper.vm.idForLabel);
120
+ expect(ariaLabel).toBeUndefined();
121
+ expect(wrapper.find(`#${ wrapper.vm.idForLabel }`).text()).toBe(label);
122
+
123
+ expect(ariaDisabled).toBe('true');
124
+ expect(tabIndex).toBe('-1');
125
+ });
68
126
  });
@@ -124,6 +124,15 @@ export default defineComponent({
124
124
  type: String,
125
125
  default: undefined
126
126
  },
127
+
128
+ /**
129
+ * Inherited global identifier prefix for tests
130
+ * Define a term based on the parent component to avoid conflicts on multiple components
131
+ */
132
+ componentTestid: {
133
+ type: String,
134
+ default: 'checkbox'
135
+ },
127
136
  },
128
137
 
129
138
  emits: ['update:value'],
@@ -133,6 +142,18 @@ export default defineComponent({
133
142
  },
134
143
 
135
144
  computed: {
145
+ ariaDescribedBy(): string | undefined {
146
+ const inheritedDescribedBy = this.$attrs['aria-describedby'];
147
+ const internalDescribedBy = this.descriptionKey || this.description ? this.describedById : undefined;
148
+
149
+ if (inheritedDescribedBy && internalDescribedBy) {
150
+ return `${ inheritedDescribedBy } ${ internalDescribedBy }`;
151
+ } else if (inheritedDescribedBy || internalDescribedBy) {
152
+ return `${ inheritedDescribedBy || internalDescribedBy }`;
153
+ }
154
+
155
+ return undefined;
156
+ },
136
157
  /**
137
158
  * Determines if the checkbox is disabled.
138
159
  * @returns boolean: True when the disabled prop is true or when mode is
@@ -167,7 +188,7 @@ export default defineComponent({
167
188
  },
168
189
 
169
190
  idForLabel():string {
170
- return `${ this.id }-label`;
191
+ return `${ generateRandomAlphaString(12) }-checkbox-label`;
171
192
  }
172
193
  },
173
194
 
@@ -271,10 +292,11 @@ export default defineComponent({
271
292
  class="checkbox-custom"
272
293
  :class="{indeterminate: indeterminate}"
273
294
  :tabindex="isDisabled ? -1 : 0"
295
+ :aria-disabled="isDisabled"
274
296
  :aria-label="replacementLabel"
275
297
  :aria-checked="!!value"
276
298
  :aria-labelledby="labelKey || label ? idForLabel : undefined"
277
- :aria-describedby="descriptionKey || description ? describedById : undefined"
299
+ :aria-describedby="ariaDescribedBy"
278
300
  role="checkbox"
279
301
  />
280
302
  <span
@@ -298,6 +320,7 @@ export default defineComponent({
298
320
  v-clean-tooltip="{content: t(tooltipKey), triggers: ['hover', 'touch', 'focus']}"
299
321
  v-stripped-aria-label="t(tooltipKey)"
300
322
  class="checkbox-info icon icon-info icon-lg"
323
+ :data-testid="componentTestid + '-info-icon'"
301
324
  :tabindex="isDisabled ? -1 : 0"
302
325
  />
303
326
  <i
@@ -305,6 +328,7 @@ export default defineComponent({
305
328
  v-clean-tooltip="{content: tooltip, triggers: ['hover', 'touch', 'focus']}"
306
329
  v-stripped-aria-label="tooltip"
307
330
  class="checkbox-info icon icon-info icon-lg"
331
+ :data-testid="componentTestid + '-info-icon'"
308
332
  :tabindex="isDisabled ? -1 : 0"
309
333
  />
310
334
  </slot>
@@ -374,7 +398,7 @@ $fontColor: var(--input-label);
374
398
 
375
399
  .checkbox-info {
376
400
  line-height: normal;
377
- margin-left: 2px;
401
+ margin-left: 4px;
378
402
 
379
403
  &:focus-visible {
380
404
  @include focus-outline;
@@ -54,4 +54,55 @@ describe('component: LabeledInput', () => {
54
54
  expect(subLabel.text()).toBe(hint);
55
55
  });
56
56
  });
57
+
58
+ describe('a11y: adding ARIA props', () => {
59
+ const ariaLabelVal = 'some-aria-label';
60
+ const subLabelVal = 'some-sublabel';
61
+ const ariaDescribedByIdVal = 'some-external-id';
62
+ const ariaRequiredVal = 'true';
63
+
64
+ it.each([
65
+ ['text', 'input', ariaLabelVal, subLabelVal, ariaDescribedByIdVal],
66
+ ['cron', 'input', ariaLabelVal, subLabelVal, ariaDescribedByIdVal],
67
+ ['multiline', 'textarea', ariaLabelVal, subLabelVal, ariaDescribedByIdVal],
68
+ ['multiline-password', 'textarea', ariaLabelVal, subLabelVal, ariaDescribedByIdVal],
69
+ ])('for type %p should correctly fill out the appropriate fields on the component', (type, validationType, ariaLabel, subLabel, ariaDescribedById) => {
70
+ const wrapper = mount(LabeledInput, {
71
+ propsData: {
72
+ value: '', type, ariaLabel, subLabel, required: true, mode: 'view'
73
+ },
74
+ attrs: { 'aria-describedby': ariaDescribedById },
75
+ mocks: { $store: { getters: { 'i18n/t': jest.fn() } } }
76
+ });
77
+
78
+ const field = wrapper.find(validationType);
79
+ const ariaLabelProp = field.attributes('aria-label');
80
+ const ariaDescribedBy = field.attributes('aria-describedby');
81
+ const ariaRequired = field.attributes('aria-required');
82
+ const ariaDisabled = field.attributes('aria-disabled');
83
+ const disabledAttr = field.attributes('disabled');
84
+
85
+ // validates type of input rendered
86
+ expect(field.exists()).toBe(true);
87
+ expect(ariaLabelProp).toBe(ariaLabel);
88
+ expect(ariaDescribedBy).toBe(`${ ariaDescribedById } ${ wrapper.vm.describedById }`);
89
+ expect(ariaRequired).toBe(ariaRequiredVal);
90
+ expect(ariaDisabled).toBe('true');
91
+ expect(disabledAttr).toBeDefined();
92
+ });
93
+ });
94
+
95
+ it('a11y: rendering a "label" should not render an "aria-label" prop', () => {
96
+ const label = 'some-label';
97
+
98
+ const wrapper = mount(LabeledInput, {
99
+ propsData: { type: 'text', label },
100
+ mocks: { $store: { getters: { 'i18n/t': jest.fn() } } }
101
+ });
102
+
103
+ const mainInput = wrapper.find('input[type="text"]');
104
+
105
+ expect(mainInput.attributes('aria-label')).toBeUndefined();
106
+ expect(wrapper.find('label').text()).toBe(label);
107
+ });
57
108
  });
@@ -161,6 +161,19 @@ export default defineComponent({
161
161
  return this.isCompact ? false : !!this.label || !!this.labelKey || !!this.$slots.label;
162
162
  },
163
163
 
164
+ ariaDescribedBy(): string | undefined {
165
+ const inheritedDescribedBy = this.$attrs['aria-describedby'];
166
+ const internalDescribedBy = this.cronHint || this.subLabel ? this.describedById : undefined;
167
+
168
+ if (inheritedDescribedBy && internalDescribedBy) {
169
+ return `${ inheritedDescribedBy } ${ internalDescribedBy }`;
170
+ } else if (inheritedDescribedBy || internalDescribedBy) {
171
+ return `${ inheritedDescribedBy || internalDescribedBy }`;
172
+ }
173
+
174
+ return undefined;
175
+ },
176
+
164
177
  /**
165
178
  * Determines if the Labeled Input should display a tooltip.
166
179
  */
@@ -362,6 +375,7 @@ export default defineComponent({
362
375
  <span
363
376
  v-if="requiredField"
364
377
  class="required"
378
+ :aria-hidden="true"
365
379
  >*</span>
366
380
  </label>
367
381
  </slot>
@@ -377,11 +391,13 @@ export default defineComponent({
377
391
  v-stripped-aria-label="!hasLabel && ariaLabel ? ariaLabel : undefined"
378
392
  :maxlength="_maxlength"
379
393
  :disabled="isDisabled"
394
+ :aria-disabled="isDisabled"
380
395
  :value="value || ''"
381
396
  :placeholder="_placeholder"
382
397
  autocapitalize="off"
383
398
  :class="{ conceal: type === 'multiline-password' }"
384
- :aria-describedby="cronHint || subLabel ? describedById : undefined"
399
+ :aria-describedby="ariaDescribedBy"
400
+ :aria-required="requiredField"
385
401
  @update:value="onInput"
386
402
  @focus="onFocus"
387
403
  @blur="onBlur"
@@ -396,13 +412,15 @@ export default defineComponent({
396
412
  v-bind="$attrs"
397
413
  :maxlength="_maxlength"
398
414
  :disabled="isDisabled"
415
+ :aria-disabled="isDisabled"
399
416
  :type="type === 'cron' ? 'text' : type"
400
417
  :value="value"
401
418
  :placeholder="_placeholder"
402
419
  autocomplete="off"
403
420
  autocapitalize="off"
404
421
  :data-lpignore="ignorePasswordManagers"
405
- :aria-describedby="cronHint || subLabel ? describedById : undefined"
422
+ :aria-describedby="ariaDescribedBy"
423
+ :aria-required="requiredField"
406
424
  @input="onInput"
407
425
  @focus="onFocus"
408
426
  @blur="onBlur"
@@ -1,4 +1,4 @@
1
- import { shallowMount } from '@vue/test-utils';
1
+ import { shallowMount, mount } from '@vue/test-utils';
2
2
  import { RadioButton } from './index';
3
3
 
4
4
  describe('radioButton.vue', () => {
@@ -30,4 +30,39 @@ describe('radioButton.vue', () => {
30
30
 
31
31
  expect(wrapper.find('.radio-label').text()).toBe('Test Label - Slot');
32
32
  });
33
+
34
+ it('a11y: adding ARIA props should correctly fill out the appropriate fields on the component', async() => {
35
+ const val = 'foo';
36
+ const value = 'foo';
37
+ const description = 'some-description';
38
+ const itemLabel = 'some-label';
39
+ const radioOptionId = 'some-id-from-parent';
40
+
41
+ const wrapper = mount(
42
+ RadioButton,
43
+ {
44
+ propsData: {
45
+ label: itemLabel,
46
+ val,
47
+ value,
48
+ description,
49
+ radioOptionId
50
+ }
51
+ });
52
+
53
+ const radioInputElem = wrapper.find('span[role="radio"]');
54
+ const role = radioInputElem.attributes('role');
55
+ const ariaLabel = radioInputElem.attributes('aria-label');
56
+ const ariaChecked = radioInputElem.attributes('aria-checked');
57
+ const ariaDisabled = radioInputElem.attributes('aria-disabled');
58
+ const ariaDescribedBy = radioInputElem.attributes('aria-describedby');
59
+ const itemId = radioInputElem.attributes('id');
60
+
61
+ expect(role).toBe('radio');
62
+ expect(ariaLabel).toBe(itemLabel);
63
+ expect(ariaChecked).toBe('true');
64
+ expect(ariaDisabled).toBe('false');
65
+ expect(ariaDescribedBy).toBe(wrapper.vm.describeById);
66
+ expect(itemId).toBe(radioOptionId);
67
+ });
33
68
  });
@@ -1,10 +1,12 @@
1
1
  <script lang="ts">
2
2
  import { defineComponent } from 'vue';
3
3
  import { _VIEW } from '@shell/config/query-params';
4
- import { randomStr } from '@shell/utils/string';
4
+ import { generateRandomAlphaString } from '@shell/utils/string';
5
5
 
6
6
  export default defineComponent({
7
- props: {
7
+
8
+ inheritAttrs: false,
9
+ props: {
8
10
  /**
9
11
  * The name of the input, for grouping.
10
12
  */
@@ -76,7 +78,16 @@ export default defineComponent({
76
78
  preventFocusOnRadioGroups: {
77
79
  type: Boolean,
78
80
  default: false
79
- }
81
+ },
82
+
83
+ /**
84
+ * Radio option Id - used to link to aria-activedescendant
85
+ * when using inside of the context of a Radio Group
86
+ */
87
+ radioOptionId: {
88
+ type: String,
89
+ default: undefined
90
+ },
80
91
  },
81
92
 
82
93
  emits: ['update:value'],
@@ -84,7 +95,8 @@ export default defineComponent({
84
95
  data() {
85
96
  return {
86
97
  isChecked: this.value === this.val,
87
- randomString: `${ randomStr() }-radio`,
98
+ randomString: `${ generateRandomAlphaString(12) }-radio`,
99
+ describeById: `${ generateRandomAlphaString(12) }-radio-described-id`,
88
100
  };
89
101
  },
90
102
 
@@ -165,11 +177,14 @@ export default defineComponent({
165
177
  @click.stop.prevent
166
178
  >
167
179
  <span
180
+ :id="radioOptionId"
168
181
  ref="custom"
169
182
  :class="[ isDisabled ? 'text-muted' : '', 'radio-custom']"
170
183
  :tabindex="isDisabled || preventFocusOnRadioGroups ? -1 : 0"
171
184
  :aria-label="label"
172
185
  :aria-checked="isChecked"
186
+ :aria-disabled="isDisabled"
187
+ :aria-describedby="descriptionKey || description ? describeById : undefined"
173
188
  role="radio"
174
189
  />
175
190
  <div class="labeling">
@@ -190,6 +205,7 @@ export default defineComponent({
190
205
  </label>
191
206
  <div
192
207
  v-if="descriptionKey || description"
208
+ :id="describeById"
193
209
  class="radio-button-outer-container-description"
194
210
  >
195
211
  <t
@@ -24,4 +24,64 @@ describe('component: RadioGroup', () => {
24
24
  expect(slot.disabled).toBe(disabled);
25
25
  });
26
26
  });
27
+
28
+ it('a11y: adding ARIA props should correctly fill out the appropriate fields on the component', async() => {
29
+ const inputLabel = 'some-label';
30
+ const ariaDescribedById = 'some-external-id';
31
+ const currValue = 'whatever';
32
+
33
+ const wrapper = mount(RadioGroup, {
34
+ propsData: {
35
+ name: 'some-name',
36
+ label: inputLabel,
37
+ value: currValue,
38
+ options: [{ label: currValue, value: currValue }]
39
+ },
40
+ attrs: { 'aria-describedby': ariaDescribedById }
41
+ });
42
+
43
+ const field = wrapper.find('[role="radiogroup"]');
44
+ const role = field.attributes('role');
45
+ const ariaLabel = field.attributes('aria-label');
46
+ const ariaDescribedBy = field.attributes('aria-describedby');
47
+ const ariaActiveDescendant = field.attributes('aria-activedescendant');
48
+
49
+ expect(ariaLabel).toBe(inputLabel);
50
+ expect(role).toBe('radiogroup');
51
+ expect(ariaActiveDescendant).toBe(`${ wrapper.vm.radioOptionsIdPrefix }0`);
52
+ expect(ariaDescribedBy).toBe(ariaDescribedById);
53
+
54
+ const radioOption = wrapper.find(`.radio-custom`);
55
+
56
+ // make sure we validate when using RadioGroup without custom slot data
57
+ // we do assign an ID that is important to get 'aria-activedescendant' working
58
+ expect(radioOption.attributes('id')).toBe(`${ wrapper.vm.radioOptionsIdPrefix }0`);
59
+ });
60
+
61
+ it('a11y: adding aria-label ($attrs) from parent should override label-based aria-label', async() => {
62
+ const inputLabel = 'some-label';
63
+ const overrideLabel = 'some-override-label';
64
+ const currValue = 'whatever';
65
+
66
+ const wrapper = mount(RadioGroup, {
67
+ propsData: {
68
+ name: 'some-name',
69
+ label: inputLabel,
70
+ value: currValue,
71
+ disabled: true,
72
+ options: [{ label: currValue, value: currValue }]
73
+ },
74
+ attrs: { 'aria-label': overrideLabel }
75
+ });
76
+
77
+ const field = wrapper.find('[role="radiogroup"]');
78
+ const ariaLabel = field.attributes('aria-label');
79
+ const ariaDisabled = field.attributes('aria-disabled');
80
+ const tabIndex = field.attributes('tabindex');
81
+
82
+ expect(ariaLabel).toBe(overrideLabel);
83
+ expect(ariaLabel).not.toBe(inputLabel);
84
+ expect(ariaDisabled).toBe('true');
85
+ expect(tabIndex).toBe('-1');
86
+ });
27
87
  });