@rancher/shell 3.0.2-rc.3 → 3.0.2-rc.4

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 (49) hide show
  1. package/assets/styles/base/_basic.scss +2 -1
  2. package/assets/styles/global/_form.scss +2 -1
  3. package/assets/styles/themes/_dark.scss +1 -1
  4. package/assets/translations/en-us.yaml +22 -4
  5. package/assets/translations/zh-hans.yaml +2 -3
  6. package/components/AppModal.vue +50 -0
  7. package/components/Carousel.vue +54 -47
  8. package/components/CopyToClipboardText.vue +3 -0
  9. package/components/Dialog.vue +20 -1
  10. package/components/PromptChangePassword.vue +3 -0
  11. package/components/ResourceDetail/Masthead.vue +1 -1
  12. package/components/Tabbed/index.vue +4 -7
  13. package/components/__tests__/Carousel.test.ts +56 -27
  14. package/components/form/LabeledSelect.vue +1 -1
  15. package/components/form/SSHKnownHosts/KnownHostsEditDialog.vue +192 -0
  16. package/components/form/SSHKnownHosts/__tests__/KnownHostsEditDialog.test.ts +104 -0
  17. package/components/form/SSHKnownHosts/index.vue +101 -0
  18. package/components/form/Select.vue +1 -1
  19. package/components/form/SelectOrCreateAuthSecret.vue +43 -11
  20. package/components/form/__tests__/SSHKnownHosts.test.ts +59 -0
  21. package/composables/focusTrap.ts +68 -0
  22. package/detail/secret.vue +25 -0
  23. package/edit/fleet.cattle.io.gitrepo.vue +27 -22
  24. package/edit/provisioning.cattle.io.cluster/index.vue +26 -19
  25. package/edit/secret/index.vue +1 -1
  26. package/edit/secret/ssh.vue +21 -3
  27. package/list/provisioning.cattle.io.cluster.vue +1 -0
  28. package/models/fleet.cattle.io.gitrepo.js +2 -2
  29. package/models/provisioning.cattle.io.cluster.js +2 -12
  30. package/models/secret.js +5 -0
  31. package/package.json +1 -1
  32. package/pages/account/index.vue +4 -0
  33. package/pages/c/_cluster/explorer/ConfigBadge.vue +5 -4
  34. package/pages/c/_cluster/uiplugins/AddExtensionRepos.vue +3 -1
  35. package/pages/c/_cluster/uiplugins/CatalogList/CatalogLoadDialog.vue +3 -0
  36. package/pages/c/_cluster/uiplugins/CatalogList/CatalogUninstallDialog.vue +7 -1
  37. package/pages/c/_cluster/uiplugins/CatalogList/index.vue +3 -1
  38. package/pages/c/_cluster/uiplugins/DeveloperInstallDialog.vue +10 -7
  39. package/pages/c/_cluster/uiplugins/InstallDialog.vue +7 -0
  40. package/pages/c/_cluster/uiplugins/PluginInfoPanel.vue +181 -106
  41. package/pages/c/_cluster/uiplugins/SetupUIPlugins.vue +2 -0
  42. package/pages/c/_cluster/uiplugins/UninstallDialog.vue +9 -1
  43. package/pages/c/_cluster/uiplugins/index.vue +50 -12
  44. package/rancher-components/Card/Card.vue +7 -21
  45. package/rancher-components/Form/LabeledInput/LabeledInput.vue +1 -0
  46. package/rancher-components/RcDropdown/RcDropdown.vue +11 -0
  47. package/rancher-components/RcDropdown/RcDropdownTrigger.vue +2 -3
  48. package/rancher-components/RcDropdown/useDropdownCollection.ts +1 -0
  49. package/rancher-components/RcDropdown/useDropdownContext.ts +28 -1
@@ -61,13 +61,13 @@ BODY {
61
61
  INPUT,
62
62
  SELECT,
63
63
  TEXTAREA,
64
- .labeled-input,
65
64
  .checkbox-custom {
66
65
  &:focus, &.focused {
67
66
  @include form-focus;
68
67
  }
69
68
  }
70
69
 
70
+ .labeled-input,
71
71
  .radio-custom,
72
72
  .labeled-select,
73
73
  .unlabeled-select {
@@ -76,6 +76,7 @@ TEXTAREA,
76
76
  }
77
77
  }
78
78
 
79
+ .labeled-input,
79
80
  .labeled-select,
80
81
  .unlabeled-select {
81
82
  &.focused {
@@ -27,7 +27,8 @@ TEXTAREA,
27
27
 
28
28
  @include input-status-color;
29
29
 
30
- &:focus:not(.unlabeled-select):not(.labeled-select), &.focused:not(.unlabeled-select):not(.labeled-select) {
30
+ &:focus:not(.labeled-input):not(.unlabeled-select):not(.labeled-select),
31
+ &.focused:not(.labeled-input):not(.unlabeled-select):not(.labeled-select) {
31
32
  @include form-focus;
32
33
  }
33
34
 
@@ -72,7 +72,7 @@
72
72
  --banner-text-color : #{$lightest};
73
73
 
74
74
  --nav-bg : #{$darkest};
75
- --nav-active : var(--primary-active-bg);
75
+ --nav-active : #333;
76
76
  --nav-border : #{$medium};
77
77
  --nav-hover : var(--primary);
78
78
  --nav-expander-hover : var(--primary-banner-bg);
@@ -16,6 +16,7 @@ generic:
16
16
  comingSoon: Coming Soon
17
17
  comma: ', '
18
18
  copy: Copy
19
+ copyToClipboard: Copy text to Clipboard
19
20
  create: Create
20
21
  created: Created
21
22
  customize: Customize
@@ -2440,6 +2441,7 @@ fleet:
2440
2441
  resources: Resources
2441
2442
  unready: Non-Ready
2442
2443
  auth:
2444
+ title: Authentication
2443
2445
  label: Authentication
2444
2446
  git: Git Authentication
2445
2447
  helm: Helm Authentication
@@ -2450,20 +2452,20 @@ fleet:
2450
2452
  label: Paths
2451
2453
  placeholder: e.g. /directory/in/your/repo
2452
2454
  addLabel: Add Path
2453
- empty: The root of the repo is used by default. To use one or more different directories, add them here.
2455
+ empty: The root of the repo is used by default. Multiple different directories can be provided.
2454
2456
  repo:
2457
+ title: Source
2455
2458
  label: Repository URL
2456
2459
  placeholder: e.g. https://github.com/rancher/fleet-examples.git or git@github.com:rancher/fleet-examples.git
2457
2460
  addRepo: Add Repository
2458
2461
  noRepos: No repositories have been added
2459
2462
  noWorkspaces: There are no workspaces. <br/> Please create a workspace before adding repositories
2460
- protocolBanner: Enter a valid HTTPS or SSH URL to a git repository.
2461
2463
  resources:
2462
2464
  label: 'Resource Handling'
2463
2465
  keepResources: Always Keep Resources
2464
- keepResourcesBanner: When enabled above, resources will be kept when deleting a GitRepo or Bundle - only Helm release secrets will be deleted.
2466
+ keepResourcesTooltip: When enabled, resources will be kept when deleting a GitRepo or Bundle - only Helm release secrets will be deleted.
2465
2467
  correctDrift: Enable Self-Healing
2466
- correctDriftBanner: When enabled, Fleet will ensure that the cluster resources are kept in sync with the git repository. All resource changes made on the cluster will be lost.
2468
+ correctDriftTooltip: When enabled, Fleet will ensure that the cluster resources are kept in sync with the git repository. All resource changes made on the cluster will be lost.
2467
2469
  add:
2468
2470
  steps:
2469
2471
  repoInfo:
@@ -4380,6 +4382,8 @@ plugins:
4380
4382
  incompatibleUiExtensionsApiVersion: "The latest version of this extension ({ version }) is not compatible with the current Extensions API version ({ required })."
4381
4383
  incompatibleHost: 'The latest version of this extension ({ version }) has a host of "{ required }" which is not compatible with this application "{ mainHost }".'
4382
4384
  currentInstalledVersionBlockedByKubeVersion: "This version is not compatible with the current Kubernetes version ({ kubeVersion } Vs { kubeVersionToCheck })."
4385
+ closePluginPanel: Close plugin description panel
4386
+ viewVersionDetails: View extension {name} version {version} details/Readme
4383
4387
  labels:
4384
4388
  builtin: Built-in
4385
4389
  experimental: Experimental
@@ -4387,6 +4391,8 @@ plugins:
4387
4391
  image: Image
4388
4392
  installing: Installing ...
4389
4393
  uninstalling: Uninstalling ...
4394
+ menu: Extensions menu
4395
+ reloadRancher: Reload Rancher
4390
4396
  descriptions:
4391
4397
  experimental: This Extension is marked as experimental
4392
4398
  third-party: This Extension is provided by a Third-Party
@@ -5139,10 +5145,22 @@ secret:
5139
5145
  username: Username
5140
5146
  ssh:
5141
5147
  keys: Keys
5148
+ keysAndHosts: Keys and Known Hosts
5142
5149
  public: Public Key
5143
5150
  publicPlaceholder: "Paste in your public key"
5144
5151
  private: Private Key
5145
5152
  privatePlaceholder: "Paste in your private key"
5153
+ knownHosts: Known Hosts
5154
+ knownHostsPlaceholder: "Known hosts metadata, one per line"
5155
+ editKnownHosts:
5156
+ title: SSH Known Hosts Configuration
5157
+ entries: |-
5158
+ {entries, plural,
5159
+ =0 {Empty}
5160
+ =1 {1 Entry}
5161
+ other {{entries} Entries}
5162
+ }
5163
+
5146
5164
  serviceAcct:
5147
5165
  ca: CA Certificate
5148
5166
  token: Token
@@ -2153,13 +2153,12 @@ fleet:
2153
2153
  addRepo: 添加仓库
2154
2154
  noRepos: 未添加任何仓库
2155
2155
  noWorkspaces: 没有工作空间。<br/>请在添加仓库之前创建一个工作空间
2156
- protocolBanner: 输入指向 git 仓库的有效 HTTPS 或 SSH URL。
2157
2156
  resources:
2158
2157
  label: '资源处理'
2159
2158
  keepResources: 永远保留资源
2160
- keepResourcesBanner: 启用时,删除 GitRepo 或 Bundle 后将保留资源,只会删除 Helm Release Secret。
2159
+ keepResourcesTooltip: 启用时,删除 GitRepo 或 Bundle 后将保留资源,只会删除 Helm Release Secret。
2161
2160
  correctDrift: 启用自我修复
2162
- correctDriftBanner: 启用后,Fleet 将确保集群资源与 Git 仓库保持同步。在 ecluster 上进行的所有资源更改都将丢失。
2161
+ correctDriftTooltip: 启用后,Fleet 将确保集群资源与 Git 仓库保持同步。在 ecluster 上进行的所有资源更改都将丢失。
2163
2162
  add:
2164
2163
  steps:
2165
2164
  repoInfo:
@@ -1,5 +1,8 @@
1
1
  <script lang="ts">
2
2
  import { defineComponent } from 'vue';
3
+ import { DEFAULT_FOCUS_TRAP_OPTS, useBasicSetupFocusTrap, getFirstFocusableElement } from '@shell/composables/focusTrap';
4
+
5
+ export const DEFAULT_ITERABLE_NODE_SELECTOR = 'body;';
3
6
 
4
7
  export default defineComponent({
5
8
  name: 'AppModal',
@@ -56,6 +59,27 @@ export default defineComponent({
56
59
  name: {
57
60
  type: String,
58
61
  default: '',
62
+ },
63
+ /**
64
+ * trigger focus trap
65
+ */
66
+ triggerFocusTrap: {
67
+ type: Boolean,
68
+ default: false,
69
+ },
70
+ /**
71
+ * forcefully set return focus element based on this selector
72
+ */
73
+ returnFocusSelector: {
74
+ type: String,
75
+ default: '',
76
+ },
77
+ /**
78
+ * will return focus to the first iterable node of this container select
79
+ */
80
+ returnFocusFirstIterableNodeSelector: {
81
+ type: String,
82
+ default: DEFAULT_ITERABLE_NODE_SELECTOR,
59
83
  }
60
84
  },
61
85
  computed: {
@@ -85,6 +109,31 @@ export default defineComponent({
85
109
  };
86
110
  }
87
111
  },
112
+ setup(props) {
113
+ if (props.triggerFocusTrap) {
114
+ let opts:any = DEFAULT_FOCUS_TRAP_OPTS;
115
+
116
+ // if we have a "returnFocusFirstIterableNodeSelector" on top of "returnFocusSelector"
117
+ // then we will use "returnFocusFirstIterableNodeSelector" as a fallback of "returnFocusSelector"
118
+ if (props.returnFocusFirstIterableNodeSelector && props.returnFocusFirstIterableNodeSelector !== DEFAULT_ITERABLE_NODE_SELECTOR && props.returnFocusSelector) {
119
+ opts = {
120
+ ...DEFAULT_FOCUS_TRAP_OPTS,
121
+ setReturnFocus: () => {
122
+ return document.querySelector(props.returnFocusSelector) ? props.returnFocusSelector : getFirstFocusableElement(document.querySelector(props.returnFocusFirstIterableNodeSelector));
123
+ }
124
+ };
125
+ // otherwise, if we are sure of permanent existance of "returnFocusSelector"
126
+ // we just return to that element
127
+ } else if (props.returnFocusSelector) {
128
+ opts = {
129
+ ...DEFAULT_FOCUS_TRAP_OPTS,
130
+ setReturnFocus: props.returnFocusSelector
131
+ };
132
+ }
133
+
134
+ useBasicSetupFocusTrap('#modal-container-element', opts);
135
+ }
136
+ },
88
137
  mounted() {
89
138
  document.addEventListener('keydown', this.handleEscapeKey);
90
139
  },
@@ -134,6 +183,7 @@ export default defineComponent({
134
183
  >
135
184
  <div
136
185
  v-bind="$attrs"
186
+ id="modal-container-element"
137
187
  ref="modalRef"
138
188
  :class="customClass"
139
189
  class="modal-container"
@@ -43,24 +43,29 @@ export default {
43
43
  activeItemId: 0,
44
44
  autoScroll: true,
45
45
  autoScrollSlideInterval: null,
46
+ isTransitionning: false, // prevents showing empty spaces caused by aggressive clicking
47
+ shouldDisableTransition: false // smoothes the move from the first/last slides to the previous/next slide
46
48
  };
47
49
  },
48
50
 
49
51
  computed: {
50
52
  ...mapGetters(['clusterId']),
51
53
  trackStyle() {
52
- let sliderItem = ( this.activeItemId + 1) * 100 / (this.slider.length + 2);
53
- const width = 60 * (this.slider.length + 2);
54
-
55
54
  if (this.slider.length === 1) {
56
- sliderItem = 0;
55
+ return `
56
+ width: 100%;
57
+ left: 0%;
58
+ `;
57
59
  }
58
60
 
59
- return `transform: translateX(-${ sliderItem }%); width: ${ width }%`;
60
- },
61
+ const width = 60 * (this.slider.length + 2);
62
+ const left = -(40 + this.activeItemId * 60);
61
63
 
62
- test() {
63
- return 'test';
64
+ return `
65
+ width: ${ width }%;
66
+ left: ${ left }%;
67
+ transition: ${ this.shouldDisableTransition ? 'none' : '700ms ease-in-out' };
68
+ `;
64
69
  }
65
70
  },
66
71
 
@@ -77,10 +82,14 @@ export default {
77
82
  },
78
83
 
79
84
  nextPrev(direction) {
80
- this.autoScroll = false;
81
- const slideTrack = document.getElementById('slide-track');
85
+ if (this.isTransitionning) {
86
+ return;
87
+ }
82
88
 
83
- slideTrack.style.transition = `transform 450ms ease-in-out`;
89
+ this.isTransitionning = true;
90
+ this.autoScroll = false;
91
+ this.shouldDisableTransition = false;
92
+ const slideTrack = this.$refs.slider;
84
93
 
85
94
  direction !== 'prev' ? (this.activeItemId++) : (this.activeItemId--);
86
95
 
@@ -88,21 +97,27 @@ export default {
88
97
  },
89
98
 
90
99
  slideTransition() {
91
- const slideTrack = document.getElementById('slide-track');
92
100
  const slidesArray = this.slider.length + 2;
93
101
 
94
102
  if (this.activeItemId === -1) {
95
- slideTrack.style.transition = 'none';
103
+ this.shouldDisableTransition = true;
96
104
  this.activeItemId = this.slider.length - 1;
97
105
  }
106
+
98
107
  if (this.activeItemId === slidesArray - 2) {
99
- slideTrack.style.transition = 'none';
108
+ this.shouldDisableTransition = true;
100
109
  this.activeItemId = 0;
101
110
  }
111
+
112
+ this.isTransitionning = false;
102
113
  },
103
114
 
104
115
  autoScrollSlide() {
105
- if (this.activeItemId < (this.slider.length + 1) && this.autoScroll ) {
116
+ if (!this.autoScroll) {
117
+ return;
118
+ }
119
+
120
+ if (this.activeItemId < (this.slider.length + 1)) {
106
121
  this.activeItemId++;
107
122
  }
108
123
 
@@ -120,23 +135,21 @@ export default {
120
135
  },
121
136
 
122
137
  mounted() {
123
- const slideTrack = document.getElementById('slide-track');
138
+ const slideTrack = this.$refs.slider;
124
139
 
125
- if (this.slider.length === 1) {
126
- slideTrack.style = 'transform:translateX(0%); width:100%; left:0';
127
- } else {
128
- const node = document.getElementById('slide0');
140
+ if (this.slider.length > 1) {
141
+ const firstSlide = this.$refs['slide0']?.[0];
129
142
 
130
- if (node) {
131
- const clone = node.cloneNode(true);
143
+ if (firstSlide) {
144
+ const clone = firstSlide.cloneNode(true);
132
145
 
133
146
  slideTrack.appendChild(clone);
134
147
  }
135
148
 
136
- const nodeLast = document.getElementById(`slide${ this.slider.length - 1 }`);
149
+ const lastSlide = this.$refs[`slide${ this.slider.length - 1 }`]?.[0];
137
150
 
138
- if (nodeLast) {
139
- const cloneLast = nodeLast.cloneNode(true);
151
+ if (lastSlide) {
152
+ const cloneLast = lastSlide.cloneNode(true);
140
153
 
141
154
  slideTrack.insertBefore(cloneLast, slideTrack.children[0]);
142
155
  }
@@ -159,7 +172,7 @@ export default {
159
172
  <template>
160
173
  <div
161
174
  class="slider"
162
- :class="{'disable': sliders.length === 1}"
175
+ :class="{'disabled': sliders.length === 1}"
163
176
  >
164
177
  <div
165
178
  id="slide-track"
@@ -170,8 +183,8 @@ export default {
170
183
  <component
171
184
  :is="asLink ? 'a' : 'div'"
172
185
  v-for="(slide, i) in sliders"
173
- :id="`slide` + i"
174
- ref="slide"
186
+ :id="`slide${i}`"
187
+ :ref="`slide${i}`"
175
188
  :key="get(slide, keyField)"
176
189
  class="slide"
177
190
  :class="{'singleSlide': sliders.length === 1}"
@@ -187,7 +200,7 @@ export default {
187
200
  <div class="slide-content-right">
188
201
  <BadgeState
189
202
  :label="slide.repoName"
190
- color="slider-badge mb-20"
203
+ color="slide-badge mb-20"
191
204
  />
192
205
  <h1>{{ slide.chartNameDisplay }}</h1>
193
206
  <p>{{ slide.chartDescription }}</p>
@@ -196,12 +209,11 @@ export default {
196
209
  </component>
197
210
  </div>
198
211
  <div
199
- ref="prev"
200
212
  role="button"
201
213
  class="prev"
202
214
  :aria-label="t('carousel.previous')"
203
215
  :aria-disabled="sliders.length === 1"
204
- :class="{'disable': sliders.length === 1}"
216
+ :class="{'disabled': sliders.length === 1}"
205
217
  tabindex="0"
206
218
  @click="nextPrev('prev')"
207
219
  @keyup.enter.space="nextPrev('prev')"
@@ -211,12 +223,11 @@ export default {
211
223
  />
212
224
  </div>
213
225
  <div
214
- ref="next"
215
226
  role="button"
216
227
  class="next"
217
228
  :aria-label="t('carousel.next')"
218
229
  :aria-disabled="sliders.length === 1"
219
- :class="{'disable': sliders.length === 1}"
230
+ :class="{'disabled': sliders.length === 1}"
220
231
  tabindex="0"
221
232
  @click="nextPrev('next')"
222
233
  @keyup.enter.space="nextPrev('next')"
@@ -226,8 +237,8 @@ export default {
226
237
  />
227
238
  </div>
228
239
  <div
240
+ v-if="sliders.length > 1"
229
241
  class="controls"
230
- :class="{'disable': sliders.length === 1}"
231
242
  >
232
243
  <div
233
244
  v-for="(slide, i) in slider"
@@ -252,23 +263,21 @@ export default {
252
263
  place-items: center;
253
264
  overflow: hidden;
254
265
  margin-bottom: 30px;
255
- // min-width: 700px;
266
+ height: 245px;
256
267
 
257
- &.disable::before,
258
- &.disable::after {
268
+ &.disabled::before,
269
+ &.disabled::after {
259
270
  display: none;
260
271
  }
261
272
  }
262
273
 
263
274
  .slide-track {
264
275
  display: flex;
265
- animation: scrolls 10s ;
266
- position: relative;
267
- transition: 1s ease-in-out;
268
- left: 21%;
276
+ position: absolute;
277
+ top: 0;
269
278
  }
270
279
 
271
- .slider-badge {
280
+ .slide-badge {
272
281
  background: var(--app-partner-accent);
273
282
  color: var(--body-bg);
274
283
  }
@@ -333,7 +342,7 @@ export default {
333
342
  .slider::before {
334
343
  left: 0;
335
344
  top: 0;
336
- &.disable {
345
+ &.disabled {
337
346
  display: none;
338
347
  }
339
348
  }
@@ -344,15 +353,13 @@ export default {
344
353
  }
345
354
 
346
355
  .controls {
356
+ position: absolute;
357
+ bottom: 0;
347
358
  width: 100%;
348
359
  display: flex;
349
360
  justify-content: center;
350
361
  margin-top: 10px;
351
362
 
352
- &.disable {
353
- display: none;
354
- }
355
-
356
363
  .control-item {
357
364
  width: 10px;
358
365
  height: 10px;
@@ -48,9 +48,12 @@ export default {
48
48
  <a
49
49
  v-if="text"
50
50
  class="copy-to-clipboard-text"
51
+ role="button"
52
+ :aria-label="t('generic.copyToClipboard')"
51
53
  :class="{ 'copied': copied, 'plain': plain}"
52
54
  href="#"
53
55
  @click="clicked"
56
+ @keyup.space="clicked"
54
57
  >
55
58
  {{ text }} <i
56
59
  class="icon"
@@ -1,6 +1,6 @@
1
1
  <script>
2
2
  import AsyncButton from '@shell/components/AsyncButton';
3
- import AppModal from '@shell/components/AppModal.vue';
3
+ import AppModal, { DEFAULT_ITERABLE_NODE_SELECTOR } from '@shell/components/AppModal.vue';
4
4
 
5
5
  export default {
6
6
  emits: ['okay', 'closed'],
@@ -21,6 +21,22 @@ export default {
21
21
  mode: {
22
22
  type: String,
23
23
  default: '',
24
+ },
25
+
26
+ /**
27
+ * forcefully set return focus element based on this selector
28
+ */
29
+ returnFocusSelector: {
30
+ type: String,
31
+ default: '',
32
+ },
33
+
34
+ /**
35
+ * will return focus to the first iterable node of this container select
36
+ */
37
+ returnFocusFirstIterableNodeSelector: {
38
+ type: String,
39
+ default: DEFAULT_ITERABLE_NODE_SELECTOR,
24
40
  }
25
41
  },
26
42
 
@@ -60,6 +76,9 @@ export default {
60
76
  :name="name"
61
77
  height="auto"
62
78
  :scrollable="true"
79
+ :trigger-focus-trap="true"
80
+ :return-focus-selector="returnFocusSelector"
81
+ :return-focus-first-iterable-node-selector="returnFocusFirstIterableNodeSelector"
63
82
  @close="closeDialog(false)"
64
83
  @before-open="beforeOpen"
65
84
  >
@@ -43,6 +43,7 @@ export default {
43
43
  name="password-modal"
44
44
  :width="500"
45
45
  :height="465"
46
+ :trigger-focus-trap="true"
46
47
  @close="show(false)"
47
48
  >
48
49
  <Card
@@ -68,6 +69,8 @@ export default {
68
69
  <!-- type reset is required by lastpass -->
69
70
  <button
70
71
  class="btn role-secondary"
72
+ role="button"
73
+ :aria-label="t('changePassword.cancel')"
71
74
  type="reset"
72
75
  @click="show(false)"
73
76
  >
@@ -609,7 +609,7 @@ export default {
609
609
  .masthead-resource-title {
610
610
  padding: 0 8px;
611
611
  text-overflow: ellipsis;
612
- overflow-x: hidden;
612
+ overflow: hidden;
613
613
  white-space: nowrap;
614
614
  }
615
615
 
@@ -276,12 +276,11 @@ export default {
276
276
  :data-testid="`btn-${tab.name}`"
277
277
  :aria-controls="'#' + tab.name"
278
278
  :aria-selected="tab.active"
279
- :aria-label="tab.labelDisplay"
279
+ :aria-label="tab.labelDisplay || ''"
280
280
  role="tab"
281
281
  tabindex="0"
282
282
  @click.prevent="select(tab.name, $event)"
283
- @keyup.enter="select(tab.name, $event)"
284
- @keyup.space="select(tab.name, $event)"
283
+ @keyup.enter.space="select(tab.name, $event)"
285
284
  >
286
285
  <span>{{ tab.labelDisplay }}</span>
287
286
  <span
@@ -409,10 +408,8 @@ export default {
409
408
 
410
409
  &:focus-visible {
411
410
  @include focus-outline;
412
-
413
- span {
414
- text-decoration: underline;
415
- }
411
+ outline-offset: -4px;
412
+ text-decoration: none;
416
413
  }
417
414
  }
418
415
 
@@ -1,35 +1,41 @@
1
1
  import { mount } from '@vue/test-utils';
2
2
  import Carousel from '@shell/components/Carousel.vue';
3
3
 
4
+ const sliders = [
5
+ {
6
+ key: 'key-0',
7
+ repoName: 'some-repo-name-0',
8
+ chartNameDisplay: 'chart-name-display-0',
9
+ chartDescription: 'chart-description-0'
10
+ },
11
+ {
12
+ key: 'key-1',
13
+ repoName: 'some-repo-name-1',
14
+ chartNameDisplay: 'chart-name-display-1',
15
+ chartDescription: 'chart-description-1'
16
+ },
17
+ {
18
+ key: 'key-2',
19
+ repoName: 'some-repo-name-2',
20
+ chartNameDisplay: 'chart-name-display-2',
21
+ chartDescription: 'chart-description-2'
22
+ },
23
+ {
24
+ key: 'key-3',
25
+ repoName: 'some-repo-name-3',
26
+ chartNameDisplay: 'chart-name-display-3',
27
+ chartDescription: 'chart-description-3'
28
+ },
29
+ {
30
+ key: 'key-4',
31
+ repoName: 'some-repo-name-4',
32
+ chartNameDisplay: 'chart-name-display-4',
33
+ chartDescription: 'chart-description-4'
34
+ }
35
+ ];
36
+
4
37
  describe('component: Carousel', () => {
5
38
  it('should render component with the correct data applied', async() => {
6
- const sliders = [
7
- {
8
- key: 'key-0',
9
- repoName: 'some-repo-name-0',
10
- chartNameDisplay: 'chart-name-display-0',
11
- chartDescription: 'chart-description-0'
12
- },
13
- {
14
- key: 'key-1',
15
- repoName: 'some-repo-name-1',
16
- chartNameDisplay: 'chart-name-display-1',
17
- chartDescription: 'chart-description-1'
18
- },
19
- {
20
- key: 'key-2',
21
- repoName: 'some-repo-name-2',
22
- chartNameDisplay: 'chart-name-display-2',
23
- chartDescription: 'chart-description-2'
24
- },
25
- {
26
- key: 'key-3',
27
- repoName: 'some-repo-name-3',
28
- chartNameDisplay: 'chart-name-display-3',
29
- chartDescription: 'chart-description-3'
30
- }
31
- ];
32
-
33
39
  const wrapper = mount(Carousel, {
34
40
  props: { sliders },
35
41
  global: { mocks: { $store: { getters: { clusterId: () => 'some-cluster-id' } } } }
@@ -40,4 +46,27 @@ describe('component: Carousel', () => {
40
46
  expect(wrapper.find(`#slide${ index } h1`).text()).toContain(slider.chartNameDisplay);
41
47
  });
42
48
  });
49
+
50
+ it.each([
51
+ [sliders.slice(0, 2)],
52
+ [sliders.slice(0, 3)],
53
+ [sliders.slice(0, 4)],
54
+ [sliders.slice(0, 5)]
55
+ ])('should have the correct width and left position', async(sliders) => {
56
+ const wrapper = mount(Carousel, {
57
+ props: { sliders },
58
+ global: { mocks: { $store: { getters: { clusterId: () => 'some-cluster-id' } } } }
59
+ });
60
+
61
+ const width = 60 * (wrapper.vm.slider.length + 2);
62
+ const initialLeft = -(40 + wrapper.vm.activeItemId * 60);
63
+
64
+ expect(wrapper.vm.trackStyle).toContain(`width: ${ width }%`);
65
+ expect(wrapper.vm.trackStyle).toContain(`left: ${ initialLeft }`);
66
+
67
+ wrapper.vm.activeItemId = wrapper.vm.activeItemId + 1; // next slide
68
+ expect(wrapper.vm.trackStyle).toContain(`left: ${ -(40 + wrapper.vm.activeItemId * 60) }`);
69
+ wrapper.vm.activeItemId = wrapper.vm.activeItemId - 1; // previous slide
70
+ expect(wrapper.vm.trackStyle).toContain(`left: ${ initialLeft }`);
71
+ });
43
72
  });
@@ -291,7 +291,7 @@ export default {
291
291
  ]"
292
292
  :tabindex="isView || disabled ? -1 : 0"
293
293
  @click="focusSearch"
294
- @keyup.enter.space.down="focusSearch"
294
+ @keydown.enter.space.down="focusSearch"
295
295
  >
296
296
  <div
297
297
  :class="{ 'labeled-container': true, raised, empty, [mode]: true }"