@rancher/shell 3.0.5-rc.1 → 3.0.5-rc.2
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.
- package/assets/images/providers/sks.svg +1 -0
- package/assets/styles/base/_helpers.scss +4 -0
- package/assets/styles/base/_variables.scss +1 -0
- package/assets/translations/en-us.yaml +31 -15
- package/assets/translations/zh-hans.yaml +4 -3
- package/chart/monitoring/index.vue +3 -1
- package/components/ActionDropdownShell.vue +71 -0
- package/components/AppModal.vue +18 -4
- package/components/CommunityLinks.vue +3 -58
- package/components/CruResource.vue +6 -1
- package/components/ExplorerProjectsNamespaces.vue +12 -4
- package/components/GlobalRoleBindings.vue +5 -1
- package/components/GrowlManager.vue +1 -0
- package/components/LandingPagePreference.vue +2 -0
- package/components/LocaleSelector.vue +1 -1
- package/components/ModalManager.vue +55 -0
- package/components/PromptModal.vue +47 -8
- package/components/ResourceDetail/Masthead.vue +38 -12
- package/components/ResourceDetail/__tests__/Masthead.test.ts +5 -1
- package/components/ResourceDetail/index.vue +47 -12
- package/components/ResourceTable.vue +54 -19
- package/components/SideNav.vue +5 -1
- package/components/SlideInPanelManager.vue +126 -0
- package/components/SortableTable/THead.vue +5 -2
- package/components/SortableTable/actions.js +1 -1
- package/components/SortableTable/index.vue +54 -40
- package/components/SortableTable/paging.js +16 -19
- package/components/SortableTable/selection.js +0 -11
- package/components/Wizard.vue +2 -2
- package/components/__tests__/ModalManager.spec.ts +176 -0
- package/components/__tests__/PromptModal.test.ts +148 -0
- package/components/__tests__/SlideInPanelManager.spec.ts +166 -0
- package/components/auth/AuthBanner.vue +13 -11
- package/components/auth/Principal.vue +1 -0
- package/components/auth/login/ldap.vue +1 -1
- package/components/fleet/FleetResources.vue +21 -6
- package/components/form/ArrayList.vue +10 -6
- package/components/form/BannerSettings.vue +17 -2
- package/components/form/ColorInput.vue +35 -6
- package/components/form/EnvVars.vue +1 -0
- package/components/form/LabeledSelect.vue +18 -23
- package/components/form/MatchExpressions.vue +4 -1
- package/components/form/NameNsDescription.vue +5 -1
- package/components/form/NotificationSettings.vue +15 -1
- package/components/form/Password.vue +1 -0
- package/components/form/Probe.vue +1 -0
- package/components/form/SSHKnownHosts/__tests__/KnownHostsEditDialog.test.ts +15 -34
- package/components/form/SSHKnownHosts/index.vue +14 -11
- package/components/form/Select.vue +1 -15
- package/components/form/ValueFromResource.vue +12 -12
- package/components/form/__tests__/ArrayList.test.ts +2 -2
- package/components/form/__tests__/ColorInput.test.ts +35 -0
- package/components/form/__tests__/LabeledSelect.test.ts +40 -0
- package/components/form/__tests__/SSHKnownHosts.test.ts +11 -2
- package/components/nav/Group.vue +12 -4
- package/components/nav/Header.vue +16 -43
- package/components/nav/NamespaceFilter.vue +134 -86
- package/components/nav/TopLevelMenu.vue +4 -5
- package/components/nav/WindowManager/ContainerLogs.vue +87 -61
- package/components/nav/WindowManager/ContainerLogsActions.vue +76 -0
- package/components/templates/default.vue +6 -3
- package/components/templates/home.vue +6 -0
- package/components/templates/plain.vue +6 -3
- package/composables/focusTrap.ts +12 -4
- package/config/store.js +4 -0
- package/config/uiplugins.js +5 -1
- package/core/types.ts +7 -6
- package/detail/catalog.cattle.io.app.vue +6 -1
- package/detail/fleet.cattle.io.bundle.vue +70 -6
- package/detail/fleet.cattle.io.gitrepo.vue +1 -1
- package/detail/namespace.vue +0 -3
- package/detail/node.vue +17 -13
- package/detail/provisioning.cattle.io.cluster.vue +72 -6
- package/dialog/AddCustomBadgeDialog.vue +0 -1
- package/{pages/c/_cluster/uiplugins/AddExtensionRepos.vue → dialog/AddExtensionReposDialog.vue} +72 -42
- package/dialog/AssignToDialog.vue +176 -0
- package/dialog/ChangePasswordDialog.vue +106 -0
- package/{pages/c/_cluster/uiplugins/DeveloperInstallDialog.vue → dialog/DeveloperLoadExtensionDialog.vue} +74 -71
- package/dialog/DisableAuthProviderDialog.vue +101 -0
- package/dialog/DrainNode.vue +1 -1
- package/{pages/c/_cluster/uiplugins/CatalogList/CatalogLoadDialog.vue → dialog/ExtensionCatalogInstallDialog.vue} +100 -88
- package/{pages/c/_cluster/uiplugins/CatalogList/CatalogUninstallDialog.vue → dialog/ExtensionCatalogUninstallDialog.vue} +69 -57
- package/dialog/FeatureFlagListDialog.vue +288 -0
- package/dialog/ForceMachineRemoveDialog.vue +1 -1
- package/{components/Import.vue → dialog/ImportDialog.vue} +0 -5
- package/{pages/c/_cluster/uiplugins/InstallDialog.vue → dialog/InstallExtensionDialog.vue} +124 -106
- package/{components/form/SSHKnownHosts → dialog}/KnownHostsEditDialog.vue +52 -62
- package/dialog/MoveNamespaceDialog.vue +157 -0
- package/dialog/ScalePoolDownDialog.vue +1 -1
- package/{components/nav/Jump.vue → dialog/SearchDialog.vue} +34 -14
- package/{pages/c/_cluster/uiplugins/UninstallDialog.vue → dialog/UninstallExtensionDialog.vue} +67 -58
- package/dialog/WechatDialog.vue +57 -0
- package/edit/auth/azuread.vue +1 -1
- package/edit/auth/github.vue +1 -1
- package/edit/auth/googleoauth.vue +1 -1
- package/edit/auth/ldap/index.vue +1 -1
- package/edit/auth/oidc.vue +1 -1
- package/edit/auth/saml.vue +1 -1
- package/edit/cloudcredential.vue +24 -10
- package/edit/management.cattle.io.user.vue +28 -3
- package/edit/namespace.vue +1 -4
- package/edit/provisioning.cattle.io.cluster/CustomCommand.vue +4 -1
- package/edit/provisioning.cattle.io.cluster/SelectCredential.vue +26 -10
- package/edit/provisioning.cattle.io.cluster/__tests__/Advanced.test.ts +8 -8
- package/edit/provisioning.cattle.io.cluster/__tests__/DirectoryConfig.test.ts +26 -12
- package/edit/provisioning.cattle.io.cluster/__tests__/rke2.test.ts +66 -0
- package/edit/provisioning.cattle.io.cluster/__tests__/utils/rke2-test-data.ts +58 -0
- package/edit/provisioning.cattle.io.cluster/rke2.vue +24 -7
- package/edit/provisioning.cattle.io.cluster/tabs/DirectoryConfig.vue +5 -3
- package/edit/provisioning.cattle.io.cluster/tabs/MachinePool.vue +4 -1
- package/initialize/install-plugins.js +2 -1
- package/list/harvesterhci.io.management.cluster.vue +4 -1
- package/list/management.cattle.io.feature.vue +4 -288
- package/machine-config/azure.vue +16 -4
- package/mixins/vue-select-overrides.js +0 -4
- package/models/fleet.cattle.io.cluster.js +8 -2
- package/models/fleet.cattle.io.gitrepo.js +8 -34
- package/models/management.cattle.io.feature.js +7 -1
- package/models/namespace.js +7 -1
- package/package.json +1 -1
- package/pages/about.vue +13 -3
- package/pages/account/index.vue +12 -5
- package/pages/auth/login.vue +7 -4
- package/pages/auth/setup.vue +1 -0
- package/pages/auth/verify.vue +9 -7
- package/pages/c/_cluster/apps/charts/install.vue +26 -26
- package/pages/c/_cluster/auth/config/index.vue +10 -12
- package/pages/c/_cluster/explorer/EventsTable.vue +38 -33
- package/pages/c/_cluster/explorer/index.vue +17 -15
- package/pages/c/_cluster/istio/index.vue +2 -2
- package/pages/c/_cluster/longhorn/index.vue +1 -1
- package/pages/c/_cluster/monitoring/index.vue +1 -1
- package/pages/c/_cluster/monitoring/monitor/_namespace/_id.vue +4 -2
- package/pages/c/_cluster/monitoring/monitor/create.vue +4 -2
- package/pages/c/_cluster/monitoring/route-receiver/_id.vue +4 -2
- package/pages/c/_cluster/monitoring/route-receiver/create.vue +5 -2
- package/pages/c/_cluster/neuvector/index.vue +1 -1
- package/pages/c/_cluster/settings/banners.vue +4 -3
- package/pages/c/_cluster/uiplugins/CatalogList/index.vue +8 -10
- package/pages/c/_cluster/uiplugins/__tests__/AddExtensionRepos.test.ts +4 -7
- package/pages/c/_cluster/uiplugins/index.vue +98 -55
- package/pages/diagnostic.vue +12 -9
- package/pages/fail-whale.vue +8 -5
- package/pages/prefs.vue +7 -6
- package/plugins/internal-api/index.ts +37 -0
- package/plugins/internal-api/shared/base-api.ts +13 -0
- package/plugins/internal-api/shell/shell.api.ts +108 -0
- package/plugins/steve/actions.js +0 -12
- package/public/index.html +1 -0
- package/rancher-components/Card/Card.vue +1 -1
- package/rancher-components/Form/Checkbox/Checkbox.test.ts +59 -1
- package/rancher-components/Form/Checkbox/Checkbox.vue +27 -3
- package/rancher-components/Form/LabeledInput/LabeledInput.test.ts +47 -0
- package/rancher-components/Form/LabeledInput/LabeledInput.vue +20 -2
- package/rancher-components/Form/Radio/RadioButton.test.ts +36 -1
- package/rancher-components/Form/Radio/RadioButton.vue +20 -4
- package/rancher-components/Form/Radio/RadioGroup.test.ts +60 -0
- package/rancher-components/Form/Radio/RadioGroup.vue +75 -35
- package/rancher-components/Form/ToggleSwitch/ToggleSwitch.test.ts +17 -0
- package/rancher-components/Form/ToggleSwitch/ToggleSwitch.vue +5 -0
- package/rancher-components/LabeledTooltip/LabeledTooltip.vue +10 -1
- package/rancher-components/RcButton/RcButton.vue +2 -1
- package/rancher-components/RcButton/types.ts +1 -0
- package/rancher-components/RcDropdown/RcDropdown.vue +17 -6
- package/rancher-components/RcDropdown/RcDropdownItem.vue +3 -56
- package/rancher-components/RcDropdown/RcDropdownItemCheckbox.vue +68 -0
- package/rancher-components/RcDropdown/RcDropdownItemSelect.vue +92 -0
- package/rancher-components/RcDropdown/index.ts +2 -0
- package/rancher-components/RcDropdown/useDropdownItem.ts +63 -0
- package/scripts/extension/bundle +20 -0
- package/scripts/extension/helm/charts/ui-plugin-server/templates/cr.yaml +2 -1
- package/scripts/extension/helm/charts/ui-plugin-server/values.yaml +2 -0
- package/scripts/extension/helmpatch +44 -31
- package/scripts/extension/publish +12 -13
- package/scripts/typegen.sh +2 -4
- package/store/action-menu.js +26 -56
- package/store/index.js +5 -0
- package/store/modal.ts +71 -0
- package/store/slideInPanel.ts +47 -0
- package/store/type-map.js +8 -1
- package/store/type-map.utils.ts +4 -4
- package/types/global-vue.d.ts +5 -0
- package/types/internal-api/shell/growl.d.ts +25 -0
- package/types/internal-api/shell/modal.d.ts +77 -0
- package/types/internal-api/shell/slideIn.d.ts +15 -0
- package/types/resources/fleet.d.ts +0 -14
- package/types/shell/index.d.ts +35 -23
- package/types/vue-shim.d.ts +4 -1
- package/utils/__mocks__/tabbable.js +13 -0
- package/utils/__tests__/object.test.ts +38 -4
- package/utils/fleet.ts +15 -73
- package/utils/object.js +48 -5
- package/utils/validators/formRules/__tests__/index.test.ts +10 -1
- package/utils/validators/formRules/index.ts +27 -3
- package/components/AssignTo.vue +0 -199
- package/components/DisableAuthProviderModal.vue +0 -115
- package/components/MoveModal.vue +0 -167
- package/components/PromptChangePassword.vue +0 -123
- package/components/fleet/FleetBundleResources.vue +0 -86
- package/types/vue-shim.d +0 -20
|
@@ -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 `${
|
|
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="
|
|
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:
|
|
401
|
+
margin-left: 4px;
|
|
378
402
|
|
|
379
403
|
&:focus-visible {
|
|
380
404
|
@include focus-outline;
|
|
@@ -54,4 +54,51 @@ 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
|
|
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
|
+
|
|
83
|
+
// validates type of input rendered
|
|
84
|
+
expect(field.exists()).toBe(true);
|
|
85
|
+
expect(ariaLabelProp).toBe(ariaLabel);
|
|
86
|
+
expect(ariaDescribedBy).toBe(`${ ariaDescribedById } ${ wrapper.vm.describedById }`);
|
|
87
|
+
expect(ariaRequired).toBe(ariaRequiredVal);
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('a11y: rendering a "label" should not render an "aria-label" prop', () => {
|
|
92
|
+
const label = 'some-label';
|
|
93
|
+
|
|
94
|
+
const wrapper = mount(LabeledInput, {
|
|
95
|
+
propsData: { type: 'text', label },
|
|
96
|
+
mocks: { $store: { getters: { 'i18n/t': jest.fn() } } }
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
const mainInput = wrapper.find('input[type="text"]');
|
|
100
|
+
|
|
101
|
+
expect(mainInput.attributes('aria-label')).toBeUndefined();
|
|
102
|
+
expect(wrapper.find('label').text()).toBe(label);
|
|
103
|
+
});
|
|
57
104
|
});
|
|
@@ -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="
|
|
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="
|
|
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 {
|
|
4
|
+
import { generateRandomAlphaString } from '@shell/utils/string';
|
|
5
5
|
|
|
6
6
|
export default defineComponent({
|
|
7
|
-
|
|
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: `${
|
|
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
|
});
|
|
@@ -2,11 +2,13 @@
|
|
|
2
2
|
import { PropType, defineComponent } from 'vue';
|
|
3
3
|
import { _VIEW } from '@shell/config/query-params';
|
|
4
4
|
import RadioButton from '@components/Form/Radio/RadioButton.vue';
|
|
5
|
+
import { generateRandomAlphaString } from '@shell/utils/string';
|
|
5
6
|
|
|
6
7
|
interface Option {
|
|
7
8
|
value: unknown,
|
|
8
9
|
label: string,
|
|
9
10
|
description?: string,
|
|
11
|
+
radioOptionId?: string,
|
|
10
12
|
}
|
|
11
13
|
|
|
12
14
|
export default defineComponent({
|
|
@@ -106,7 +108,10 @@ export default defineComponent({
|
|
|
106
108
|
emits: ['update:value'],
|
|
107
109
|
|
|
108
110
|
data() {
|
|
109
|
-
return {
|
|
111
|
+
return {
|
|
112
|
+
currFocusedElem: undefined as undefined | EventTarget | null,
|
|
113
|
+
radioOptionsIdPrefix: `radio-option-${ generateRandomAlphaString(12) }-`
|
|
114
|
+
};
|
|
110
115
|
},
|
|
111
116
|
|
|
112
117
|
computed: {
|
|
@@ -120,16 +125,21 @@ export default defineComponent({
|
|
|
120
125
|
const opt = this.options[i];
|
|
121
126
|
|
|
122
127
|
if (typeof opt === 'object' && opt) {
|
|
123
|
-
out.push(
|
|
128
|
+
out.push({
|
|
129
|
+
...opt,
|
|
130
|
+
radioOptionId: `${ this.radioOptionsIdPrefix }${ i }`
|
|
131
|
+
});
|
|
124
132
|
} else if (this.labels) {
|
|
125
133
|
out.push({
|
|
126
|
-
label:
|
|
127
|
-
value:
|
|
134
|
+
label: this.labels[i],
|
|
135
|
+
value: opt,
|
|
136
|
+
radioOptionId: `${ this.radioOptionsIdPrefix }${ i }`
|
|
128
137
|
});
|
|
129
138
|
} else {
|
|
130
139
|
out.push({
|
|
131
|
-
label:
|
|
132
|
-
value:
|
|
140
|
+
label: opt,
|
|
141
|
+
value: opt,
|
|
142
|
+
radioOptionId: `${ this.radioOptionsIdPrefix }${ i }`
|
|
133
143
|
});
|
|
134
144
|
}
|
|
135
145
|
}
|
|
@@ -150,8 +160,36 @@ export default defineComponent({
|
|
|
150
160
|
isDisabled(): boolean {
|
|
151
161
|
return (this.disabled || this.isView);
|
|
152
162
|
},
|
|
153
|
-
|
|
154
|
-
|
|
163
|
+
/**
|
|
164
|
+
* Radio Group Aria Label based on the label present on this input
|
|
165
|
+
*/
|
|
166
|
+
radioGroupAriaLabel(): string | undefined {
|
|
167
|
+
// seems like VoiceOver screen reader isn't really picking up aria-labelledby
|
|
168
|
+
// let's just gather the label that comes in and assign it.
|
|
169
|
+
// We allow override with $attrs['aria-label'] for more control
|
|
170
|
+
if (this.$attrs['aria-label']) {
|
|
171
|
+
return this.$attrs['aria-label'] as string || undefined;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return this.labelKey ? this.t(this.labelKey) : this.label ? this.label : undefined;
|
|
175
|
+
},
|
|
176
|
+
/**
|
|
177
|
+
* Radio Group Aria DescribedBy parent attribute for extendability
|
|
178
|
+
*/
|
|
179
|
+
radioGroupAriaDescribedBy(): string | undefined {
|
|
180
|
+
return this.$attrs['aria-describedby'] as string || undefined;
|
|
181
|
+
},
|
|
182
|
+
/**
|
|
183
|
+
* Radio Group value for aria-activedescendant HTML prop
|
|
184
|
+
*/
|
|
185
|
+
ariaActiveDescendant(): string | undefined {
|
|
186
|
+
const activeOpt = this.normalizedOptions.find((opt) => opt.value === this.value);
|
|
187
|
+
|
|
188
|
+
if (this.value && activeOpt) {
|
|
189
|
+
return activeOpt.radioOptionId;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return '';
|
|
155
193
|
}
|
|
156
194
|
},
|
|
157
195
|
|
|
@@ -199,45 +237,46 @@ export default defineComponent({
|
|
|
199
237
|
</script>
|
|
200
238
|
|
|
201
239
|
<template>
|
|
202
|
-
<
|
|
240
|
+
<div>
|
|
203
241
|
<!-- Label -->
|
|
204
242
|
<div
|
|
205
243
|
v-if="label || labelKey || tooltip || tooltipKey || $slots.label"
|
|
206
244
|
class="radio-group label"
|
|
207
245
|
>
|
|
208
|
-
<
|
|
209
|
-
<
|
|
210
|
-
<
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
</slot>
|
|
230
|
-
</legend>
|
|
246
|
+
<slot name="label">
|
|
247
|
+
<h3>
|
|
248
|
+
<t
|
|
249
|
+
v-if="labelKey"
|
|
250
|
+
:k="labelKey"
|
|
251
|
+
/>
|
|
252
|
+
<template v-else-if="label">
|
|
253
|
+
{{ label }}
|
|
254
|
+
</template>
|
|
255
|
+
<i
|
|
256
|
+
v-if="tooltipKey"
|
|
257
|
+
v-clean-tooltip="t(tooltipKey)"
|
|
258
|
+
class="icon icon-info icon-lg"
|
|
259
|
+
/>
|
|
260
|
+
<i
|
|
261
|
+
v-else-if="tooltip"
|
|
262
|
+
v-clean-tooltip="tooltip"
|
|
263
|
+
class="icon icon-info icon-lg"
|
|
264
|
+
/>
|
|
265
|
+
</h3>
|
|
266
|
+
</slot>
|
|
231
267
|
</div>
|
|
232
268
|
|
|
233
269
|
<!-- Group -->
|
|
234
270
|
<div
|
|
235
271
|
ref="radioGroup"
|
|
236
272
|
role="radiogroup"
|
|
237
|
-
:aria-label="
|
|
273
|
+
:aria-label="radioGroupAriaLabel"
|
|
274
|
+
:aria-describedby="radioGroupAriaDescribedBy"
|
|
275
|
+
:aria-activedescendant="ariaActiveDescendant"
|
|
238
276
|
class="radio-group"
|
|
239
277
|
:class="{'row':row}"
|
|
240
|
-
tabindex="0"
|
|
278
|
+
:tabindex="isDisabled ? -1 : 0"
|
|
279
|
+
:aria-disabled="isDisabled"
|
|
241
280
|
@keydown.down.prevent.stop="clickNext(1)"
|
|
242
281
|
@keydown.up.prevent.stop="clickNext(-1)"
|
|
243
282
|
@keydown.space.enter.stop.prevent
|
|
@@ -257,6 +296,7 @@ export default defineComponent({
|
|
|
257
296
|
:name="name"
|
|
258
297
|
:value="value"
|
|
259
298
|
:label="option.label"
|
|
299
|
+
:radio-option-id="option.radioOptionId"
|
|
260
300
|
:description="option.description"
|
|
261
301
|
:val="option.value"
|
|
262
302
|
:disabled="isDisabled"
|
|
@@ -268,7 +308,7 @@ export default defineComponent({
|
|
|
268
308
|
</slot>
|
|
269
309
|
</div>
|
|
270
310
|
</div>
|
|
271
|
-
</
|
|
311
|
+
</div>
|
|
272
312
|
</template>
|
|
273
313
|
|
|
274
314
|
<style lang='scss'>
|
|
@@ -91,4 +91,21 @@ describe('toggleSwitch.vue', () => {
|
|
|
91
91
|
expect(wrapper.emitted('update:value')).toHaveLength(1);
|
|
92
92
|
expect(wrapper.emitted('update:value')[0][0]).toBe(offValue);
|
|
93
93
|
});
|
|
94
|
+
|
|
95
|
+
it('adds focus class when input is focused', async() => {
|
|
96
|
+
const wrapper = shallowMount(ToggleSwitch);
|
|
97
|
+
|
|
98
|
+
await wrapper.find('input').trigger('focus');
|
|
99
|
+
|
|
100
|
+
expect(wrapper.find('.slider').classes()).toContain('focus');
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('removes focus class when input is blurred', async() => {
|
|
104
|
+
const wrapper = shallowMount(ToggleSwitch);
|
|
105
|
+
|
|
106
|
+
await wrapper.find('input').trigger('focus');
|
|
107
|
+
await wrapper.find('input').trigger('blur');
|
|
108
|
+
|
|
109
|
+
expect(wrapper.find('.slider').classes()).not.toContain('focus');
|
|
110
|
+
});
|
|
94
111
|
});
|
|
@@ -26,7 +26,15 @@ export default defineComponent({
|
|
|
26
26
|
hover: {
|
|
27
27
|
type: Boolean,
|
|
28
28
|
default: true
|
|
29
|
-
}
|
|
29
|
+
},
|
|
30
|
+
/**
|
|
31
|
+
* Inherited global identifier prefix for tests
|
|
32
|
+
* Define a term based on the parent component to avoid conflicts on multiple components
|
|
33
|
+
*/
|
|
34
|
+
componentTestid: {
|
|
35
|
+
type: String,
|
|
36
|
+
default: 'labeledTooltip-info-icon'
|
|
37
|
+
},
|
|
30
38
|
},
|
|
31
39
|
computed: {
|
|
32
40
|
iconClass(): string {
|
|
@@ -64,6 +72,7 @@ export default defineComponent({
|
|
|
64
72
|
:class="{'hover':!value, [iconClass]: true}"
|
|
65
73
|
class="icon status-icon"
|
|
66
74
|
tabindex="0"
|
|
75
|
+
:data-testid="componentTestid"
|
|
67
76
|
/>
|
|
68
77
|
</template>
|
|
69
78
|
<template v-else>
|
|
@@ -15,6 +15,7 @@ const buttonRoles: { role: keyof ButtonRoleProps, className: string }[] = [
|
|
|
15
15
|
{ role: 'secondary', className: 'role-secondary' },
|
|
16
16
|
{ role: 'tertiary', className: 'role-tertiary' },
|
|
17
17
|
{ role: 'link', className: 'role-link' },
|
|
18
|
+
{ role: 'multiAction', className: 'role-multi-action' },
|
|
18
19
|
{ role: 'ghost', className: 'role-ghost' },
|
|
19
20
|
];
|
|
20
21
|
|
|
@@ -50,7 +51,7 @@ defineExpose({ focus });
|
|
|
50
51
|
<button
|
|
51
52
|
ref="RcFocusTarget"
|
|
52
53
|
role="button"
|
|
53
|
-
:class="{ ...buttonClass
|
|
54
|
+
:class="{ ...buttonClass }"
|
|
54
55
|
>
|
|
55
56
|
<slot name="before">
|
|
56
57
|
<!-- Empty Content -->
|