@rancher/shell 3.0.2-rc.2 → 3.0.2-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.
- package/assets/styles/base/_basic.scss +5 -7
- package/assets/styles/global/_button.scss +10 -0
- package/assets/styles/global/_tooltip.scss +2 -2
- package/assets/styles/themes/_dark.scss +14 -2
- package/assets/styles/themes/_light.scss +7 -2
- package/assets/styles/vendor/vue-select.scss +4 -0
- package/assets/translations/en-us.yaml +44 -5
- package/components/BannerGraphic.vue +0 -42
- package/components/ButtonMultiAction.vue +1 -1
- package/components/Carousel.vue +36 -29
- package/components/CommunityLinks.vue +6 -1
- package/components/GrowlManager.vue +9 -2
- package/components/LocaleSelector.vue +8 -1
- package/components/PaginatedResourceTable.vue +4 -7
- package/components/ProgressBarMulti.vue +14 -0
- package/components/Questions/Reference.vue +57 -28
- package/components/SelectIconGrid.vue +12 -1
- package/components/SideNav.vue +12 -38
- package/components/SortableTable/index.vue +1 -0
- package/components/Tabbed/index.vue +12 -1
- package/components/YamlEditor.vue +1 -0
- package/components/auth/Principal.vue +5 -3
- package/components/fleet/FleetClusters.vue +82 -1
- package/components/fleet/FleetRepos.vue +13 -30
- package/components/fleet/ForceDirectedTreeChart/index.vue +2 -2
- package/components/form/ChangePassword.vue +2 -0
- package/components/form/ColorInput.vue +24 -1
- package/components/form/FileSelector.vue +2 -0
- package/components/form/KeyValue.vue +230 -160
- package/components/form/LabeledSelect.vue +1 -1
- package/components/form/PlusMinus.vue +14 -2
- package/components/form/ResourceLabeledSelect.vue +13 -53
- package/components/form/ResourceSelector.vue +1 -0
- package/components/form/ResourceTabs/index.vue +79 -36
- package/components/form/SecretSelector.vue +2 -2
- package/components/form/__tests__/KeyValue.test.ts +1 -1
- package/components/formatter/FleetClusterSummaryGraph.vue +2 -2
- package/components/formatter/FleetSummaryGraph.vue +6 -7
- package/components/formatter/WorkloadHealthScale.vue +7 -0
- package/components/nav/Group.vue +30 -4
- package/components/nav/Header.vue +82 -114
- package/components/nav/HeaderPageActionMenu.vue +27 -131
- package/components/nav/NamespaceFilter.vue +1 -1
- package/components/nav/Type.vue +15 -0
- package/config/home-links.js +21 -13
- package/config/labels-annotations.js +2 -0
- package/config/page-actions.js +1 -0
- package/config/pagination-table-headers.js +15 -1
- package/config/product/explorer.js +7 -17
- package/config/table-headers.js +6 -0
- package/config/version.js +5 -1
- package/core/plugin.ts +41 -1
- package/core/plugins.js +125 -72
- package/core/types-provisioning.ts +91 -2
- package/core/types.ts +55 -0
- package/detail/__tests__/autoscaling.horizontalpodautoscaler.test.ts +12 -3
- package/detail/catalog.cattle.io.app.vue +1 -1
- package/detail/fleet.cattle.io.cluster.vue +3 -3
- package/detail/namespace.vue +13 -19
- package/detail/networking.k8s.io.ingress.vue +13 -53
- package/detail/provisioning.cattle.io.cluster.vue +12 -1
- package/detail/workload/index.vue +3 -3
- package/dialog/AddCustomBadgeDialog.vue +5 -1
- package/edit/auth/ldap/__tests__/config.test.ts +18 -0
- package/edit/auth/ldap/config.vue +24 -0
- package/edit/auth/saml.vue +8 -6
- package/edit/fleet.cattle.io.gitrepo.vue +7 -1
- package/edit/logging-flow/index.vue +4 -19
- package/edit/networking.k8s.io.ingress/index.vue +18 -65
- package/edit/networking.k8s.io.networkpolicy/index.vue +4 -5
- package/edit/provisioning.cattle.io.cluster/index.vue +13 -1
- package/edit/provisioning.cattle.io.cluster/rke2.vue +31 -115
- package/edit/provisioning.cattle.io.cluster/tabs/Basics.vue +2 -2
- package/edit/provisioning.cattle.io.cluster/tabs/networking/ACE.vue +14 -28
- package/edit/provisioning.cattle.io.cluster/tabs/networking/index.vue +25 -12
- package/edit/service.vue +1 -2
- package/list/networking.k8s.io.ingress.vue +1 -1
- package/list/node.vue +15 -8
- package/list/persistentvolume.vue +12 -4
- package/list/service.vue +1 -1
- package/list/workload.vue +4 -0
- package/mixins/chart.js +4 -1
- package/models/catalog.cattle.io.app.js +3 -1
- package/models/catalog.cattle.io.clusterrepo.js +56 -7
- package/models/fleet.cattle.io.bundle.js +0 -11
- package/models/fleet.cattle.io.cluster.js +17 -1
- package/models/fleet.cattle.io.gitrepo.js +86 -50
- package/models/provisioning.cattle.io.cluster.js +47 -2
- package/models/service.js +1 -0
- package/models/workload.js +19 -1
- package/package.json +5 -4
- package/pages/c/_cluster/apps/charts/index.vue +4 -0
- package/pages/c/_cluster/explorer/ConfigBadge.vue +8 -7
- package/pages/c/_cluster/explorer/index.vue +13 -6
- package/pages/c/_cluster/fleet/GitRepoGraphConfig.js +3 -3
- package/pages/c/_cluster/fleet/index.vue +75 -89
- package/pages/c/_cluster/settings/links.vue +2 -2
- package/pages/diagnostic.vue +17 -15
- package/pages/home.vue +32 -6
- package/plugins/clean-html.js +50 -0
- package/plugins/dashboard-store/resource-class.js +4 -0
- package/plugins/plugin.js +54 -49
- package/plugins/steve/mutations.js +1 -1
- package/plugins/steve/steve-class.js +8 -0
- package/plugins/steve/steve-pagination-utils.ts +3 -1
- package/rancher-components/Accordion/Accordion.vue +4 -4
- package/rancher-components/BadgeState/BadgeState.vue +7 -0
- package/rancher-components/Card/Card.vue +27 -1
- package/rancher-components/Form/Checkbox/Checkbox.vue +9 -2
- package/rancher-components/Form/LabeledInput/LabeledInput.test.ts +18 -1
- package/rancher-components/Form/LabeledInput/LabeledInput.vue +18 -1
- package/rancher-components/Form/ToggleSwitch/ToggleSwitch.vue +39 -2
- package/rancher-components/RcButton/RcButton.vue +90 -0
- package/rancher-components/RcButton/index.ts +2 -0
- package/rancher-components/RcButton/types.ts +17 -0
- package/rancher-components/RcDropdown/RcDropdown.vue +111 -0
- package/rancher-components/RcDropdown/RcDropdownItem.vue +127 -0
- package/rancher-components/RcDropdown/RcDropdownSeparator.vue +6 -0
- package/rancher-components/RcDropdown/RcDropdownTrigger.vue +43 -0
- package/rancher-components/RcDropdown/index.ts +4 -0
- package/rancher-components/RcDropdown/types.ts +22 -0
- package/rancher-components/RcDropdown/useDropdownCollection.ts +45 -0
- package/rancher-components/RcDropdown/useDropdownContext.ts +83 -0
- package/scripts/test-plugins-build.sh +2 -0
- package/scripts/typegen.sh +2 -0
- package/store/catalog.js +1 -1
- package/tsconfig.json +2 -1
- package/types/components/paginatedResourceTable.ts +25 -0
- package/types/components/resourceLabeledSelect.ts +48 -0
- package/types/resources/fleet.d.ts +17 -0
- package/types/shell/index.d.ts +61 -0
- package/utils/auth.js +5 -1
- package/utils/cluster.js +106 -0
- package/utils/fleet.ts +35 -3
- package/utils/ingress.ts +64 -0
- package/utils/uiplugins.ts +56 -44
- package/utils/validators/cron-schedule.js +7 -2
- package/utils/validators/formRules/__tests__/index.test.ts +53 -17
- package/utils/validators/formRules/index.ts +20 -5
- package/vue.config.js +1 -1
- package/components/RelatedWorkloadsTable.vue +0 -50
|
@@ -157,6 +157,7 @@ class StevePaginationUtils extends NamespaceProjectFilters {
|
|
|
157
157
|
{ field: '_type' },
|
|
158
158
|
{ field: 'reason' },
|
|
159
159
|
{ field: 'involvedObject.kind' },
|
|
160
|
+
// { field: 'involvedObject.uid' }, // Pending API Support - https://github.com/rancher/rancher/issues/48603
|
|
160
161
|
{ field: 'message' },
|
|
161
162
|
],
|
|
162
163
|
[CATALOG.CLUSTER_REPO]: [
|
|
@@ -431,7 +432,8 @@ class StevePaginationUtils extends NamespaceProjectFilters {
|
|
|
431
432
|
// Check if the API supports filtering by this field
|
|
432
433
|
this.validateField(validateFields, schema, field.field);
|
|
433
434
|
|
|
434
|
-
const
|
|
435
|
+
const value = encodeURIComponent(field.value);
|
|
436
|
+
const exactPartial = field.exact ? `'${ value }'` : value;
|
|
435
437
|
|
|
436
438
|
return `${ this.convertArrayPath(field.field) }${ field.equals ? '=' : '!=' }${ exactPartial }`;
|
|
437
439
|
}
|
|
@@ -47,12 +47,12 @@ export default defineComponent({
|
|
|
47
47
|
data-testid="accordion-chevron"
|
|
48
48
|
/>
|
|
49
49
|
<slot name="header">
|
|
50
|
-
<
|
|
50
|
+
<h2
|
|
51
51
|
data-testid="accordion-title-slot-content"
|
|
52
52
|
class="mb-0"
|
|
53
53
|
>
|
|
54
54
|
{{ titleKey ? t(titleKey) : title }}
|
|
55
|
-
</
|
|
55
|
+
</h2>
|
|
56
56
|
</slot>
|
|
57
57
|
</div>
|
|
58
58
|
<div
|
|
@@ -70,7 +70,7 @@ export default defineComponent({
|
|
|
70
70
|
border: 1px solid var(--border)
|
|
71
71
|
}
|
|
72
72
|
.accordion-header {
|
|
73
|
-
padding:
|
|
73
|
+
padding: 16px 16px 16px 11px;
|
|
74
74
|
display: flex;
|
|
75
75
|
align-items: center;
|
|
76
76
|
&>*{
|
|
@@ -81,6 +81,6 @@ export default defineComponent({
|
|
|
81
81
|
}
|
|
82
82
|
}
|
|
83
83
|
.accordion-body {
|
|
84
|
-
padding:
|
|
84
|
+
padding: 0px 16px 16px;
|
|
85
85
|
}
|
|
86
86
|
</style>
|
|
@@ -94,6 +94,13 @@ export default defineComponent({
|
|
|
94
94
|
background: transparent;
|
|
95
95
|
border-color: var(--success);
|
|
96
96
|
}
|
|
97
|
+
|
|
98
|
+
// Added badge-disabled instead of bg-disabled since bg-disabled is used in other places with !important styling, an investigation is needed to make the naming consistent
|
|
99
|
+
&.badge-disabled {
|
|
100
|
+
color: var(--badge-state-disabled-text);
|
|
101
|
+
background-color: var( --badge-state-disabled-bg);
|
|
102
|
+
border: 1px solid var(--badge-state-disabled-border);
|
|
103
|
+
}
|
|
97
104
|
}
|
|
98
105
|
</style>
|
|
99
106
|
<style lang="scss">
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { defineComponent, PropType } from 'vue';
|
|
3
|
+
import { createFocusTrap, FocusTrap } from 'focus-trap';
|
|
3
4
|
|
|
4
5
|
export default defineComponent({
|
|
5
6
|
name: 'Card',
|
|
@@ -50,12 +51,37 @@ export default defineComponent({
|
|
|
50
51
|
type: Boolean,
|
|
51
52
|
default: false,
|
|
52
53
|
},
|
|
53
|
-
|
|
54
|
+
triggerFocusTrap: {
|
|
55
|
+
type: Boolean,
|
|
56
|
+
default: false,
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
data() {
|
|
60
|
+
return { focusTrapInstance: {} as FocusTrap };
|
|
61
|
+
},
|
|
62
|
+
mounted() {
|
|
63
|
+
if (this.triggerFocusTrap) {
|
|
64
|
+
this.focusTrapInstance = createFocusTrap(this.$refs.cardContainer as HTMLElement, {
|
|
65
|
+
escapeDeactivates: true,
|
|
66
|
+
allowOutsideClick: true,
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
this.$nextTick(() => {
|
|
70
|
+
this.focusTrapInstance.activate();
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
beforeUnmount() {
|
|
75
|
+
if (this.focusTrapInstance && this.triggerFocusTrap) {
|
|
76
|
+
this.focusTrapInstance.deactivate();
|
|
77
|
+
}
|
|
78
|
+
},
|
|
54
79
|
});
|
|
55
80
|
</script>
|
|
56
81
|
|
|
57
82
|
<template>
|
|
58
83
|
<div
|
|
84
|
+
ref="cardContainer"
|
|
59
85
|
class="card-container"
|
|
60
86
|
:class="{'highlight-border': showHighlightBorder, 'card-sticky': sticky}"
|
|
61
87
|
data-testid="card"
|
|
@@ -264,13 +264,15 @@ export default defineComponent({
|
|
|
264
264
|
<template v-else-if="label">{{ label }}</template>
|
|
265
265
|
<i
|
|
266
266
|
v-if="tooltipKey"
|
|
267
|
-
v-clean-tooltip="t(tooltipKey)"
|
|
267
|
+
v-clean-tooltip="{content: t(tooltipKey), triggers: ['hover', 'touch', 'focus']}"
|
|
268
268
|
class="checkbox-info icon icon-info icon-lg"
|
|
269
|
+
:tabindex="isDisabled ? -1 : 0"
|
|
269
270
|
/>
|
|
270
271
|
<i
|
|
271
272
|
v-else-if="tooltip"
|
|
272
|
-
v-clean-tooltip="tooltip"
|
|
273
|
+
v-clean-tooltip="{content: tooltip, triggers: ['hover', 'touch', 'focus']}"
|
|
273
274
|
class="checkbox-info icon icon-info icon-lg"
|
|
275
|
+
:tabindex="isDisabled ? -1 : 0"
|
|
274
276
|
/>
|
|
275
277
|
</slot>
|
|
276
278
|
</span>
|
|
@@ -329,6 +331,11 @@ $fontColor: var(--input-label);
|
|
|
329
331
|
.checkbox-info {
|
|
330
332
|
line-height: normal;
|
|
331
333
|
margin-left: 2px;
|
|
334
|
+
|
|
335
|
+
&:focus-visible {
|
|
336
|
+
@include focus-outline;
|
|
337
|
+
outline-offset: 2px;
|
|
338
|
+
}
|
|
332
339
|
}
|
|
333
340
|
|
|
334
341
|
.checkbox-custom {
|
|
@@ -20,7 +20,7 @@ describe('component: LabeledInput', () => {
|
|
|
20
20
|
expect(wrapper.emitted('update:value')![0][0]).toBe(value);
|
|
21
21
|
});
|
|
22
22
|
|
|
23
|
-
it('using
|
|
23
|
+
it('using type "multiline" should emit input value correctly', () => {
|
|
24
24
|
const value = 'any-string';
|
|
25
25
|
const delay = 1;
|
|
26
26
|
const wrapper = mount(LabeledInput, {
|
|
@@ -37,4 +37,21 @@ describe('component: LabeledInput', () => {
|
|
|
37
37
|
expect(wrapper.emitted('update:value')).toHaveLength(1);
|
|
38
38
|
expect(wrapper.emitted('update:value')![0][0]).toBe(value);
|
|
39
39
|
});
|
|
40
|
+
|
|
41
|
+
describe('using type "chron"', () => {
|
|
42
|
+
it.each([
|
|
43
|
+
['0 * * * *', 'Every hour, every day'],
|
|
44
|
+
['@daily', 'At 12:00 AM, every day'],
|
|
45
|
+
['You must fail! Go!', '%generic.invalidCron%'],
|
|
46
|
+
])('passing value %p should display hint %p', (value, hint) => {
|
|
47
|
+
const wrapper = mount(LabeledInput, {
|
|
48
|
+
propsData: { value, type: 'cron' },
|
|
49
|
+
mocks: { $store: { getters: { 'i18n/t': jest.fn() } } }
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const subLabel = wrapper.find('[data-testid="sub-label"]');
|
|
53
|
+
|
|
54
|
+
expect(subLabel.text()).toBe(hint);
|
|
55
|
+
});
|
|
56
|
+
});
|
|
40
57
|
});
|
|
@@ -179,14 +179,28 @@ export default defineComponent({
|
|
|
179
179
|
if (this.type !== 'cron' || !this.value) {
|
|
180
180
|
return;
|
|
181
181
|
}
|
|
182
|
+
|
|
183
|
+
// TODO - #13202: This is required due use of 2 libraries and 3 different libraries through the code.
|
|
184
|
+
const predefined = [
|
|
185
|
+
'@yearly',
|
|
186
|
+
'@annually',
|
|
187
|
+
'@monthly',
|
|
188
|
+
'@weekly',
|
|
189
|
+
'@daily',
|
|
190
|
+
'@midnight',
|
|
191
|
+
'@hourly'
|
|
192
|
+
];
|
|
193
|
+
const isPredefined = predefined.includes(this.value as string);
|
|
194
|
+
|
|
182
195
|
// refer https://github.com/GuillaumeRochat/cron-validator#readme
|
|
183
|
-
if (!isValidCron(this.value as string, {
|
|
196
|
+
if (!isPredefined && !isValidCron(this.value as string, {
|
|
184
197
|
alias: true,
|
|
185
198
|
allowBlankDay: true,
|
|
186
199
|
allowSevenAsSunday: true,
|
|
187
200
|
})) {
|
|
188
201
|
return this.t('generic.invalidCron');
|
|
189
202
|
}
|
|
203
|
+
|
|
190
204
|
try {
|
|
191
205
|
const hint = cronstrue.toString(this.value as string || '', { verbose: true });
|
|
192
206
|
|
|
@@ -382,9 +396,12 @@ export default defineComponent({
|
|
|
382
396
|
<div
|
|
383
397
|
v-if="cronHint || subLabel"
|
|
384
398
|
class="sub-label"
|
|
399
|
+
data-testid="sub-label"
|
|
385
400
|
>
|
|
386
401
|
<div
|
|
387
402
|
v-if="cronHint"
|
|
403
|
+
role="alert"
|
|
404
|
+
:aria-label="cronHint"
|
|
388
405
|
>
|
|
389
406
|
{{ cronHint }}
|
|
390
407
|
</div>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import { defineComponent } from 'vue';
|
|
2
|
+
import { defineComponent, onMounted, onBeforeUnmount, useTemplateRef } from 'vue';
|
|
3
3
|
|
|
4
4
|
type StateType = boolean | 'true' | 'false' | undefined;
|
|
5
5
|
|
|
@@ -33,6 +33,29 @@ export default defineComponent({
|
|
|
33
33
|
|
|
34
34
|
emits: ['update:value'],
|
|
35
35
|
|
|
36
|
+
setup() {
|
|
37
|
+
const switchChrome = useTemplateRef<HTMLElement>('switchChrome');
|
|
38
|
+
const focus = () => {
|
|
39
|
+
switchChrome.value?.classList.add('focus');
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const blur = () => {
|
|
43
|
+
switchChrome.value?.classList.remove('focus');
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const switchInput = useTemplateRef<HTMLInputElement>('switchInput');
|
|
47
|
+
|
|
48
|
+
onMounted(() => {
|
|
49
|
+
switchInput.value?.addEventListener('focus', focus);
|
|
50
|
+
switchInput.value?.addEventListener('blur', blur);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
onBeforeUnmount(() => {
|
|
54
|
+
switchInput.value?.removeEventListener('focus', focus);
|
|
55
|
+
switchInput.value?.removeEventListener('blur', blur);
|
|
56
|
+
});
|
|
57
|
+
},
|
|
58
|
+
|
|
36
59
|
data() {
|
|
37
60
|
return { state: false as StateType };
|
|
38
61
|
},
|
|
@@ -64,11 +87,18 @@ export default defineComponent({
|
|
|
64
87
|
>{{ offLabel }}</span>
|
|
65
88
|
<label class="switch hand">
|
|
66
89
|
<input
|
|
90
|
+
ref="switchInput"
|
|
67
91
|
type="checkbox"
|
|
92
|
+
role="switch"
|
|
68
93
|
:checked="state"
|
|
94
|
+
:aria-label="onLabel"
|
|
69
95
|
@input="toggle(null)"
|
|
96
|
+
@keydown.enter="toggle(null)"
|
|
70
97
|
>
|
|
71
|
-
<span
|
|
98
|
+
<span
|
|
99
|
+
ref="switchChrome"
|
|
100
|
+
class="slider round"
|
|
101
|
+
/>
|
|
72
102
|
</label>
|
|
73
103
|
<span
|
|
74
104
|
class="label no-select hand"
|
|
@@ -118,6 +148,13 @@ $toggle-height: 16px;
|
|
|
118
148
|
background-color: var(--checkbox-disabled-bg);
|
|
119
149
|
-webkit-transition: .4s;
|
|
120
150
|
transition: .4s;
|
|
151
|
+
|
|
152
|
+
&.focus {
|
|
153
|
+
@include focus-outline;
|
|
154
|
+
outline-offset: 2px;
|
|
155
|
+
-webkit-transition: 0s;
|
|
156
|
+
transition: 0s;
|
|
157
|
+
}
|
|
121
158
|
}
|
|
122
159
|
|
|
123
160
|
.slider:before {
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* A button element used for performing actions, such as submitting forms or
|
|
4
|
+
* opening dialogs.
|
|
5
|
+
*
|
|
6
|
+
* Example:
|
|
7
|
+
*
|
|
8
|
+
* <rc-button primary @click="doAction">Perform an Action</rc-button>
|
|
9
|
+
*/
|
|
10
|
+
import { computed, ref, defineExpose } from 'vue';
|
|
11
|
+
import { ButtonRoleProps, ButtonSizeProps } from './types';
|
|
12
|
+
|
|
13
|
+
const buttonRoles: { role: keyof ButtonRoleProps, className: string }[] = [
|
|
14
|
+
{ role: 'primary', className: 'role-primary' },
|
|
15
|
+
{ role: 'secondary', className: 'role-secondary' },
|
|
16
|
+
{ role: 'tertiary', className: 'role-tertiary' },
|
|
17
|
+
{ role: 'link', className: 'role-link' },
|
|
18
|
+
{ role: 'ghost', className: 'role-ghost' },
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
const buttonSizes: { size: keyof ButtonSizeProps, className: string }[] = [
|
|
22
|
+
{ size: 'small', className: 'btn-sm' },
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
const props = defineProps<ButtonRoleProps & ButtonSizeProps>();
|
|
26
|
+
|
|
27
|
+
const buttonClass = computed(() => {
|
|
28
|
+
const activeRole = buttonRoles.find(({ role }) => props[role]);
|
|
29
|
+
const isButtonSmall = buttonSizes.some(({ size }) => props[size]);
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
btn: true,
|
|
33
|
+
|
|
34
|
+
[activeRole?.className || 'role-primary']: true,
|
|
35
|
+
|
|
36
|
+
'btn-sm': isButtonSmall,
|
|
37
|
+
};
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const RcFocusTarget = ref<HTMLElement | null>(null);
|
|
41
|
+
|
|
42
|
+
const focus = () => {
|
|
43
|
+
RcFocusTarget?.value?.focus();
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
defineExpose({ focus });
|
|
47
|
+
</script>
|
|
48
|
+
|
|
49
|
+
<template>
|
|
50
|
+
<button
|
|
51
|
+
ref="RcFocusTarget"
|
|
52
|
+
role="button"
|
|
53
|
+
:class="{ ...buttonClass, ...($attrs.class || { }) }"
|
|
54
|
+
>
|
|
55
|
+
<slot name="before">
|
|
56
|
+
<!-- Empty Content -->
|
|
57
|
+
</slot>
|
|
58
|
+
<slot>
|
|
59
|
+
<!-- Empty Content -->
|
|
60
|
+
</slot>
|
|
61
|
+
<slot name="after">
|
|
62
|
+
<!-- Empty Content -->
|
|
63
|
+
</slot>
|
|
64
|
+
</button>
|
|
65
|
+
</template>
|
|
66
|
+
|
|
67
|
+
<style lang="scss" scoped>
|
|
68
|
+
.role-link {
|
|
69
|
+
&:focus, &.focused {
|
|
70
|
+
outline: var(--outline-width) solid var(--border);
|
|
71
|
+
box-shadow: 0 0 0 var(--outline-width) var(--outline);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
button {
|
|
76
|
+
&.role-ghost {
|
|
77
|
+
padding: 0;
|
|
78
|
+
background-color: transparent;
|
|
79
|
+
|
|
80
|
+
&:focus, &.focused {
|
|
81
|
+
outline: 2px solid var(--primary-keyboard-focus);
|
|
82
|
+
outline-offset: 0;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
&:focus-visible {
|
|
86
|
+
outline: 2px solid var(--primary-keyboard-focus);
|
|
87
|
+
outline-offset: 0;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}</style>
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// TODO: 13211 Investigate why `InstanceType<typeof RcButton>` fails prod builds
|
|
2
|
+
// export type RcButtonType = InstanceType<typeof RcButton>
|
|
3
|
+
export type RcButtonType = {
|
|
4
|
+
focus: () => void;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export type ButtonRoleProps = {
|
|
8
|
+
primary?: boolean;
|
|
9
|
+
secondary?: boolean;
|
|
10
|
+
tertiary?: boolean;
|
|
11
|
+
link?: boolean;
|
|
12
|
+
ghost?: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export type ButtonSizeProps = {
|
|
16
|
+
small?: boolean;
|
|
17
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* Offers a list of choices to the user, such as a set of actions or functions.
|
|
4
|
+
* Opened by activating RcDropdownTrigger.
|
|
5
|
+
*
|
|
6
|
+
* Example:
|
|
7
|
+
*
|
|
8
|
+
* <rc-dropdown :aria-label="t('nav.actionMenu.label')">
|
|
9
|
+
* <rc-dropdown-trigger tertiary>
|
|
10
|
+
* <i class="icon icon-actions" />
|
|
11
|
+
* </rc-dropdown-trigger>
|
|
12
|
+
* <template #dropdownCollection>
|
|
13
|
+
* <rc-dropdown-item @click="performAction()">
|
|
14
|
+
* Action 1
|
|
15
|
+
* </rc-dropdown-item>
|
|
16
|
+
* <rc-dropdown-separator />
|
|
17
|
+
* <rc-dropdown-item @click="performAction()">
|
|
18
|
+
* Action 2
|
|
19
|
+
* </rc-dropdown-item>
|
|
20
|
+
* </template>
|
|
21
|
+
* </rc-dropdown>
|
|
22
|
+
*/
|
|
23
|
+
import { useTemplateRef } from 'vue';
|
|
24
|
+
import { useClickOutside } from '@shell/composables/useClickOutside';
|
|
25
|
+
import { useDropdownContext } from '@components/RcDropdown/useDropdownContext';
|
|
26
|
+
|
|
27
|
+
defineProps<{
|
|
28
|
+
ariaLabel?: string
|
|
29
|
+
}>();
|
|
30
|
+
|
|
31
|
+
const {
|
|
32
|
+
isMenuOpen,
|
|
33
|
+
showMenu,
|
|
34
|
+
returnFocus,
|
|
35
|
+
setFocus,
|
|
36
|
+
provideDropdownContext,
|
|
37
|
+
registerDropdownCollection,
|
|
38
|
+
} = useDropdownContext();
|
|
39
|
+
|
|
40
|
+
provideDropdownContext();
|
|
41
|
+
|
|
42
|
+
const popperContainer = useTemplateRef<HTMLElement>('popperContainer');
|
|
43
|
+
const dropdownTarget = useTemplateRef<HTMLElement>('dropdownTarget');
|
|
44
|
+
|
|
45
|
+
useClickOutside(dropdownTarget, () => showMenu(false));
|
|
46
|
+
|
|
47
|
+
const applyShow = () => {
|
|
48
|
+
registerDropdownCollection(dropdownTarget.value);
|
|
49
|
+
setFocus();
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
</script>
|
|
53
|
+
|
|
54
|
+
<template>
|
|
55
|
+
<v-dropdown
|
|
56
|
+
no-auto-focus
|
|
57
|
+
:triggers="[]"
|
|
58
|
+
:shown="isMenuOpen"
|
|
59
|
+
:auto-hide="false"
|
|
60
|
+
:container="popperContainer"
|
|
61
|
+
:placement="'bottom-end'"
|
|
62
|
+
@apply-show="applyShow"
|
|
63
|
+
>
|
|
64
|
+
<slot name="default">
|
|
65
|
+
<!--Empty slot content Trigger-->
|
|
66
|
+
</slot>
|
|
67
|
+
|
|
68
|
+
<template #popper>
|
|
69
|
+
<div
|
|
70
|
+
ref="dropdownTarget"
|
|
71
|
+
role="menu"
|
|
72
|
+
aria-orientation="vertical"
|
|
73
|
+
dropdown-menu-collection
|
|
74
|
+
:aria-label="ariaLabel || 'Dropdown Menu'"
|
|
75
|
+
>
|
|
76
|
+
<slot name="dropdownCollection">
|
|
77
|
+
<!--Empty slot content-->
|
|
78
|
+
</slot>
|
|
79
|
+
</div>
|
|
80
|
+
</template>
|
|
81
|
+
</v-dropdown>
|
|
82
|
+
<div
|
|
83
|
+
ref="popperContainer"
|
|
84
|
+
class="popperContainer"
|
|
85
|
+
@keydown.tab="showMenu(false)"
|
|
86
|
+
@keydown.escape="returnFocus"
|
|
87
|
+
>
|
|
88
|
+
<!--Empty container for mounting popper content-->
|
|
89
|
+
</div>
|
|
90
|
+
</template>
|
|
91
|
+
|
|
92
|
+
<style lang="scss" scoped>
|
|
93
|
+
.popperContainer {
|
|
94
|
+
display: contents;
|
|
95
|
+
&:deep(.v-popper__popper) {
|
|
96
|
+
|
|
97
|
+
.v-popper__wrapper {
|
|
98
|
+
box-shadow: 0px 6px 18px 0px rgba(0, 0, 0, 0.25), 0px 4px 10px 0px rgba(0, 0, 0, 0.15);
|
|
99
|
+
border-radius: var(--border-radius-lg);
|
|
100
|
+
|
|
101
|
+
.v-popper__arrow-container {
|
|
102
|
+
display: none;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
.v-popper__inner {
|
|
106
|
+
padding: 10px 0 10px 0;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
</style>
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* An item for a dropdown menu. Used in conjunction with RcDropdown.
|
|
4
|
+
*/
|
|
5
|
+
import { inject } from 'vue';
|
|
6
|
+
import { DropdownContext, defaultContext } from './types';
|
|
7
|
+
|
|
8
|
+
const props = defineProps({ disabled: Boolean });
|
|
9
|
+
const emits = defineEmits(['click']);
|
|
10
|
+
|
|
11
|
+
const { close, dropdownItems } = inject<DropdownContext>('dropdownContext') || defaultContext;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Handles keydown events to navigate between dropdown items.
|
|
15
|
+
* @param {KeyboardEvent} e - The keydown event.
|
|
16
|
+
*/
|
|
17
|
+
const handleKeydown = (e: KeyboardEvent) => {
|
|
18
|
+
const activeItem = document.activeElement;
|
|
19
|
+
|
|
20
|
+
const activeIndex = dropdownItems.value.indexOf(activeItem || new HTMLElement());
|
|
21
|
+
|
|
22
|
+
if (activeIndex < 0) {
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const shouldAdvance = e.key === 'ArrowDown';
|
|
27
|
+
|
|
28
|
+
const newIndex = findNewIndex(shouldAdvance, activeIndex, dropdownItems.value);
|
|
29
|
+
|
|
30
|
+
if (dropdownItems.value[newIndex] instanceof HTMLElement) {
|
|
31
|
+
dropdownItems.value[newIndex].focus();
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Finds the new index for the dropdown item based on the key pressed.
|
|
37
|
+
* @param shouldAdvance - Whether to advance to the next or previous item.
|
|
38
|
+
* @param activeIndex - Current active index.
|
|
39
|
+
* @param itemsArr - Array of dropdown items.
|
|
40
|
+
* @returns The new index.
|
|
41
|
+
*/
|
|
42
|
+
const findNewIndex = (shouldAdvance: boolean, activeIndex: number, itemsArr: Element[]) => {
|
|
43
|
+
const newIndex = shouldAdvance ? activeIndex + 1 : activeIndex - 1;
|
|
44
|
+
|
|
45
|
+
if (newIndex > itemsArr.length - 1) {
|
|
46
|
+
return 0;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (newIndex < 0) {
|
|
50
|
+
return itemsArr.length - 1;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return newIndex;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const handleClick = () => {
|
|
57
|
+
if (props.disabled) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
emits('click');
|
|
62
|
+
close();
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Handles keydown events to activate the dropdown item.
|
|
67
|
+
* @param e - The keydown event.
|
|
68
|
+
*/
|
|
69
|
+
const handleActivate = (e: KeyboardEvent) => {
|
|
70
|
+
if (e?.target instanceof HTMLElement) {
|
|
71
|
+
e?.target?.click();
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Handles keydown events to focus the dropdown item.
|
|
77
|
+
* @param e - The Mouse event.
|
|
78
|
+
*/
|
|
79
|
+
const handleMouseEnter = (e: MouseEvent) => {
|
|
80
|
+
if (e?.target instanceof HTMLElement) {
|
|
81
|
+
e?.target?.focus();
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
</script>
|
|
86
|
+
|
|
87
|
+
<template>
|
|
88
|
+
<div
|
|
89
|
+
ref="dropdownMenuItem"
|
|
90
|
+
dropdown-menu-item
|
|
91
|
+
tabindex="-1"
|
|
92
|
+
role="menuitem"
|
|
93
|
+
:disabled="disabled || null"
|
|
94
|
+
:aria-disabled="disabled || false"
|
|
95
|
+
@click.stop="handleClick"
|
|
96
|
+
@keydown.enter.space="handleActivate"
|
|
97
|
+
@keydown.up.down.stop="handleKeydown"
|
|
98
|
+
@mouseenter="handleMouseEnter"
|
|
99
|
+
>
|
|
100
|
+
<slot name="default">
|
|
101
|
+
<!--Empty slot content-->
|
|
102
|
+
</slot>
|
|
103
|
+
</div>
|
|
104
|
+
</template>
|
|
105
|
+
|
|
106
|
+
<style lang="scss" scoped>
|
|
107
|
+
[dropdown-menu-item] {
|
|
108
|
+
padding: 9px 8px;
|
|
109
|
+
margin: 0 9px;
|
|
110
|
+
border-radius: 4px;
|
|
111
|
+
|
|
112
|
+
&:hover {
|
|
113
|
+
cursor: pointer;
|
|
114
|
+
background-color: var(--dropdown-hover-bg);
|
|
115
|
+
}
|
|
116
|
+
&:focus-visible, &:focus {
|
|
117
|
+
@include focus-outline;
|
|
118
|
+
outline-offset: 0;
|
|
119
|
+
}
|
|
120
|
+
&[disabled] {
|
|
121
|
+
color: var(--disabled-text);
|
|
122
|
+
&:hover {
|
|
123
|
+
cursor: not-allowed;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
</style>
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* A button that opens a menu. Used in conjunction with `RcDropdown.vue`.
|
|
4
|
+
*/
|
|
5
|
+
import { inject, onMounted, useTemplateRef } from 'vue';
|
|
6
|
+
import { RcButton, RcButtonType } from '@components/RcButton';
|
|
7
|
+
import { DropdownContext, defaultContext } from './types';
|
|
8
|
+
|
|
9
|
+
const {
|
|
10
|
+
showMenu,
|
|
11
|
+
registerTrigger,
|
|
12
|
+
focusFirstElement,
|
|
13
|
+
isMenuOpen,
|
|
14
|
+
} = inject<DropdownContext>('dropdownContext') || defaultContext;
|
|
15
|
+
|
|
16
|
+
const dropdownTrigger = useTemplateRef<RcButtonType>('dropdownTrigger');
|
|
17
|
+
|
|
18
|
+
onMounted(() => {
|
|
19
|
+
registerTrigger(dropdownTrigger.value);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const focus = () => {
|
|
23
|
+
dropdownTrigger?.value?.focus();
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
defineExpose({ focus });
|
|
27
|
+
</script>
|
|
28
|
+
|
|
29
|
+
<template>
|
|
30
|
+
<RcButton
|
|
31
|
+
ref="dropdownTrigger"
|
|
32
|
+
role="button"
|
|
33
|
+
aria-haspopup="menu"
|
|
34
|
+
:aria-expanded="isMenuOpen"
|
|
35
|
+
@keydown.down="focusFirstElement"
|
|
36
|
+
@keydown.escape="showMenu(false)"
|
|
37
|
+
@click="showMenu(true)"
|
|
38
|
+
>
|
|
39
|
+
<slot name="default">
|
|
40
|
+
<!--Empty slot content-->
|
|
41
|
+
</slot>
|
|
42
|
+
</RcButton>
|
|
43
|
+
</template>
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { default as RcDropdown } from './RcDropdown.vue';
|
|
2
|
+
export { default as RcDropdownItem } from './RcDropdownItem.vue';
|
|
3
|
+
export { default as RcDropdownSeparator } from './RcDropdownSeparator.vue';
|
|
4
|
+
export { default as RcDropdownTrigger } from './RcDropdownTrigger.vue';
|