@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.
Files changed (141) hide show
  1. package/assets/styles/base/_basic.scss +5 -7
  2. package/assets/styles/global/_button.scss +10 -0
  3. package/assets/styles/global/_tooltip.scss +2 -2
  4. package/assets/styles/themes/_dark.scss +14 -2
  5. package/assets/styles/themes/_light.scss +7 -2
  6. package/assets/styles/vendor/vue-select.scss +4 -0
  7. package/assets/translations/en-us.yaml +44 -5
  8. package/components/BannerGraphic.vue +0 -42
  9. package/components/ButtonMultiAction.vue +1 -1
  10. package/components/Carousel.vue +36 -29
  11. package/components/CommunityLinks.vue +6 -1
  12. package/components/GrowlManager.vue +9 -2
  13. package/components/LocaleSelector.vue +8 -1
  14. package/components/PaginatedResourceTable.vue +4 -7
  15. package/components/ProgressBarMulti.vue +14 -0
  16. package/components/Questions/Reference.vue +57 -28
  17. package/components/SelectIconGrid.vue +12 -1
  18. package/components/SideNav.vue +12 -38
  19. package/components/SortableTable/index.vue +1 -0
  20. package/components/Tabbed/index.vue +12 -1
  21. package/components/YamlEditor.vue +1 -0
  22. package/components/auth/Principal.vue +5 -3
  23. package/components/fleet/FleetClusters.vue +82 -1
  24. package/components/fleet/FleetRepos.vue +13 -30
  25. package/components/fleet/ForceDirectedTreeChart/index.vue +2 -2
  26. package/components/form/ChangePassword.vue +2 -0
  27. package/components/form/ColorInput.vue +24 -1
  28. package/components/form/FileSelector.vue +2 -0
  29. package/components/form/KeyValue.vue +230 -160
  30. package/components/form/LabeledSelect.vue +1 -1
  31. package/components/form/PlusMinus.vue +14 -2
  32. package/components/form/ResourceLabeledSelect.vue +13 -53
  33. package/components/form/ResourceSelector.vue +1 -0
  34. package/components/form/ResourceTabs/index.vue +79 -36
  35. package/components/form/SecretSelector.vue +2 -2
  36. package/components/form/__tests__/KeyValue.test.ts +1 -1
  37. package/components/formatter/FleetClusterSummaryGraph.vue +2 -2
  38. package/components/formatter/FleetSummaryGraph.vue +6 -7
  39. package/components/formatter/WorkloadHealthScale.vue +7 -0
  40. package/components/nav/Group.vue +30 -4
  41. package/components/nav/Header.vue +82 -114
  42. package/components/nav/HeaderPageActionMenu.vue +27 -131
  43. package/components/nav/NamespaceFilter.vue +1 -1
  44. package/components/nav/Type.vue +15 -0
  45. package/config/home-links.js +21 -13
  46. package/config/labels-annotations.js +2 -0
  47. package/config/page-actions.js +1 -0
  48. package/config/pagination-table-headers.js +15 -1
  49. package/config/product/explorer.js +7 -17
  50. package/config/table-headers.js +6 -0
  51. package/config/version.js +5 -1
  52. package/core/plugin.ts +41 -1
  53. package/core/plugins.js +125 -72
  54. package/core/types-provisioning.ts +91 -2
  55. package/core/types.ts +55 -0
  56. package/detail/__tests__/autoscaling.horizontalpodautoscaler.test.ts +12 -3
  57. package/detail/catalog.cattle.io.app.vue +1 -1
  58. package/detail/fleet.cattle.io.cluster.vue +3 -3
  59. package/detail/namespace.vue +13 -19
  60. package/detail/networking.k8s.io.ingress.vue +13 -53
  61. package/detail/provisioning.cattle.io.cluster.vue +12 -1
  62. package/detail/workload/index.vue +3 -3
  63. package/dialog/AddCustomBadgeDialog.vue +5 -1
  64. package/edit/auth/ldap/__tests__/config.test.ts +18 -0
  65. package/edit/auth/ldap/config.vue +24 -0
  66. package/edit/auth/saml.vue +8 -6
  67. package/edit/fleet.cattle.io.gitrepo.vue +7 -1
  68. package/edit/logging-flow/index.vue +4 -19
  69. package/edit/networking.k8s.io.ingress/index.vue +18 -65
  70. package/edit/networking.k8s.io.networkpolicy/index.vue +4 -5
  71. package/edit/provisioning.cattle.io.cluster/index.vue +13 -1
  72. package/edit/provisioning.cattle.io.cluster/rke2.vue +31 -115
  73. package/edit/provisioning.cattle.io.cluster/tabs/Basics.vue +2 -2
  74. package/edit/provisioning.cattle.io.cluster/tabs/networking/ACE.vue +14 -28
  75. package/edit/provisioning.cattle.io.cluster/tabs/networking/index.vue +25 -12
  76. package/edit/service.vue +1 -2
  77. package/list/networking.k8s.io.ingress.vue +1 -1
  78. package/list/node.vue +15 -8
  79. package/list/persistentvolume.vue +12 -4
  80. package/list/service.vue +1 -1
  81. package/list/workload.vue +4 -0
  82. package/mixins/chart.js +4 -1
  83. package/models/catalog.cattle.io.app.js +3 -1
  84. package/models/catalog.cattle.io.clusterrepo.js +56 -7
  85. package/models/fleet.cattle.io.bundle.js +0 -11
  86. package/models/fleet.cattle.io.cluster.js +17 -1
  87. package/models/fleet.cattle.io.gitrepo.js +86 -50
  88. package/models/provisioning.cattle.io.cluster.js +47 -2
  89. package/models/service.js +1 -0
  90. package/models/workload.js +19 -1
  91. package/package.json +5 -4
  92. package/pages/c/_cluster/apps/charts/index.vue +4 -0
  93. package/pages/c/_cluster/explorer/ConfigBadge.vue +8 -7
  94. package/pages/c/_cluster/explorer/index.vue +13 -6
  95. package/pages/c/_cluster/fleet/GitRepoGraphConfig.js +3 -3
  96. package/pages/c/_cluster/fleet/index.vue +75 -89
  97. package/pages/c/_cluster/settings/links.vue +2 -2
  98. package/pages/diagnostic.vue +17 -15
  99. package/pages/home.vue +32 -6
  100. package/plugins/clean-html.js +50 -0
  101. package/plugins/dashboard-store/resource-class.js +4 -0
  102. package/plugins/plugin.js +54 -49
  103. package/plugins/steve/mutations.js +1 -1
  104. package/plugins/steve/steve-class.js +8 -0
  105. package/plugins/steve/steve-pagination-utils.ts +3 -1
  106. package/rancher-components/Accordion/Accordion.vue +4 -4
  107. package/rancher-components/BadgeState/BadgeState.vue +7 -0
  108. package/rancher-components/Card/Card.vue +27 -1
  109. package/rancher-components/Form/Checkbox/Checkbox.vue +9 -2
  110. package/rancher-components/Form/LabeledInput/LabeledInput.test.ts +18 -1
  111. package/rancher-components/Form/LabeledInput/LabeledInput.vue +18 -1
  112. package/rancher-components/Form/ToggleSwitch/ToggleSwitch.vue +39 -2
  113. package/rancher-components/RcButton/RcButton.vue +90 -0
  114. package/rancher-components/RcButton/index.ts +2 -0
  115. package/rancher-components/RcButton/types.ts +17 -0
  116. package/rancher-components/RcDropdown/RcDropdown.vue +111 -0
  117. package/rancher-components/RcDropdown/RcDropdownItem.vue +127 -0
  118. package/rancher-components/RcDropdown/RcDropdownSeparator.vue +6 -0
  119. package/rancher-components/RcDropdown/RcDropdownTrigger.vue +43 -0
  120. package/rancher-components/RcDropdown/index.ts +4 -0
  121. package/rancher-components/RcDropdown/types.ts +22 -0
  122. package/rancher-components/RcDropdown/useDropdownCollection.ts +45 -0
  123. package/rancher-components/RcDropdown/useDropdownContext.ts +83 -0
  124. package/scripts/test-plugins-build.sh +2 -0
  125. package/scripts/typegen.sh +2 -0
  126. package/store/catalog.js +1 -1
  127. package/tsconfig.json +2 -1
  128. package/types/components/paginatedResourceTable.ts +25 -0
  129. package/types/components/resourceLabeledSelect.ts +48 -0
  130. package/types/resources/fleet.d.ts +17 -0
  131. package/types/shell/index.d.ts +61 -0
  132. package/utils/auth.js +5 -1
  133. package/utils/cluster.js +106 -0
  134. package/utils/fleet.ts +35 -3
  135. package/utils/ingress.ts +64 -0
  136. package/utils/uiplugins.ts +56 -44
  137. package/utils/validators/cron-schedule.js +7 -2
  138. package/utils/validators/formRules/__tests__/index.test.ts +53 -17
  139. package/utils/validators/formRules/index.ts +20 -5
  140. package/vue.config.js +1 -1
  141. 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 exactPartial = field.exact ? `'${ field.value }'` : field.value;
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
- <h4
50
+ <h2
51
51
  data-testid="accordion-title-slot-content"
52
52
  class="mb-0"
53
53
  >
54
54
  {{ titleKey ? t(titleKey) : title }}
55
- </h4>
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: 5px;
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: 10px;
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 mode "multiline" should emit input value correctly', () => {
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 class="slider round" />
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,2 @@
1
+ export { default as RcButton } from './RcButton.vue';
2
+ export type { RcButtonType } from './types';
@@ -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,6 @@
1
+ <template>
2
+ <hr
3
+ role="separator"
4
+ aria-orientation="horizontal"
5
+ >
6
+ </template>
@@ -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';