@eturnity/eturnity_reusable_components 9.25.3 → 9.25.5-QA-01-next.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eturnity/eturnity_reusable_components",
3
- "version": "9.25.3",
3
+ "version": "9.25.5-QA-01-next.0",
4
4
  "files": [
5
5
  "dist",
6
6
  "src"
@@ -107,7 +107,7 @@
107
107
  </SelectOption>
108
108
  </template>
109
109
  </SelectComponent>
110
- <template v-if="item.column === 'updated'">
110
+ <template v-if="isDateColumn(item.column)">
111
111
  <DatePickerContainer
112
112
  :data-id="
113
113
  rowDataId.dataId
@@ -201,11 +201,15 @@
201
201
  >
202
202
  <RcCheckbox
203
203
  :is-checked="
204
- selectedSort[index].selectedOptions?.includes(
205
- option.value
204
+ selectedSort[index].selectedOptions?.some(
205
+ (selectedValue) =>
206
+ filterOptionValuesMatch(
207
+ selectedValue,
208
+ option.value
209
+ )
206
210
  )
207
211
  "
208
- :label="$gettext(option.label)"
212
+ :label="getOptionLabel(option)"
209
213
  size="small"
210
214
  @on-event-handler="
211
215
  onSelectFilter({
@@ -508,7 +512,24 @@
508
512
  document.removeEventListener('click', this.handleClickOutside)
509
513
  },
510
514
  methods: {
515
+ isDateColumn(column) {
516
+ if (column === 'updated') {
517
+ return true
518
+ }
519
+ const category = this.filterCategories.find(
520
+ (cat) => cat.name === column
521
+ )
522
+ return ['date', 'datetime', 'date_range'].includes(
523
+ category?.filter_type
524
+ )
525
+ },
511
526
  handleClickOutside(event) {
527
+ // A target detached by a re-render (e.g. the select trigger when its
528
+ // dropdown opens) has lost its ancestry — it cannot be attributed as
529
+ // an outside click
530
+ if (!event.target.isConnected) {
531
+ return
532
+ }
512
533
  const isClickInSelect =
513
534
  event.target.closest('.sort-dropdown') ||
514
535
  event.target.closest('.rc-select-dropdown') ||
@@ -542,10 +563,9 @@
542
563
  return this.selectedSort.filter((item) => {
543
564
  const hasColumn = !!item.column
544
565
  const hasConstraint = !!item.constraint
545
- const hasSelectedOptions =
546
- item.column === 'updated'
547
- ? !!item.selectedDate
548
- : !!item.selectedOptions?.length
566
+ const hasSelectedOptions = this.isDateColumn(item.column)
567
+ ? !!item.selectedDate
568
+ : !!item.selectedOptions?.length
549
569
  return hasColumn && hasConstraint && hasSelectedOptions
550
570
  }).length
551
571
  },
@@ -584,7 +604,7 @@
584
604
  },
585
605
  ]
586
606
 
587
- if (this.selectedSort?.[index]?.column === 'updated') {
607
+ if (this.isDateColumn(this.selectedSort?.[index]?.column)) {
588
608
  return dateOptions
589
609
  }
590
610
 
@@ -602,6 +622,12 @@
602
622
  select.closeDropdown()
603
623
  })
604
624
  },
625
+ filterOptionValuesMatch(left, right) {
626
+ if (left == null || right == null) {
627
+ return left === right
628
+ }
629
+ return String(left) === String(right)
630
+ },
605
631
  getSelectedLabel({ type, value, index }) {
606
632
  if (!value) return
607
633
  if (type === 'column') {
@@ -621,10 +647,10 @@
621
647
  const optionsList = this.optionsList(index)
622
648
  const selectedLabels = selectedOptions
623
649
  .map((optionValue) => {
624
- const option = optionsList.find(
625
- (opt) => opt.value === optionValue
650
+ const option = optionsList.find((opt) =>
651
+ this.filterOptionValuesMatch(opt.value, optionValue)
626
652
  )
627
- return option ? option.label : optionValue
653
+ return option ? this.getOptionLabel(option) : optionValue
628
654
  })
629
655
  .join(', ')
630
656
 
@@ -686,9 +712,15 @@
686
712
  ...option,
687
713
  value: option[selectedItem.valueSelector],
688
714
  label: option[selectedItem.labelSelector],
715
+ optionLabelsTranslated: !!selectedItem.optionLabelsTranslated,
689
716
  })
690
717
  )
691
718
  },
719
+ getOptionLabel(option) {
720
+ return option.optionLabelsTranslated
721
+ ? option.label
722
+ : this.$gettext(option.label)
723
+ },
692
724
  },
693
725
  }
694
726
  </script>
@@ -261,6 +261,11 @@
261
261
  }
262
262
  },
263
263
  handleClickOutside(event) {
264
+ // Detached targets (removed by a re-render mid-click) cannot be
265
+ // attributed as outside clicks
266
+ if (!event.target.isConnected) {
267
+ return
268
+ }
264
269
  const buttonIcon = this.$el.querySelector('.button-icon')
265
270
  const boxContainer = this.$el.querySelector('.box-container')
266
271
 
@@ -266,6 +266,11 @@
266
266
  },
267
267
  methods: {
268
268
  handleClickOutside(event) {
269
+ // Detached targets (removed by a re-render mid-click) cannot be
270
+ // attributed as outside clicks
271
+ if (!event.target.isConnected) {
272
+ return
273
+ }
269
274
  const buttonIcon = this.$el.querySelector('.button-icon')
270
275
  const boxContainer = this.$el.querySelector('.box-container')
271
276
  const isClickInSelect =
@@ -125,8 +125,8 @@
125
125
  props.theme.colors[props.labelFontColor]
126
126
  ? props.theme.colors[props.labelFontColor]
127
127
  : props.labelFontColor
128
- ? props.labelFontColor
129
- : props.theme.colors.eturnityGrey};
128
+ ? props.labelFontColor
129
+ : props.theme.colors.eturnityGrey};
130
130
 
131
131
  font-size: ${(props) =>
132
132
  props.fontSize ? props.fontSize : inputLabelFontSize};
@@ -172,20 +172,20 @@
172
172
  props.noBorder
173
173
  ? 'none'
174
174
  : props.isError
175
- ? '1px solid ' + props.theme.colors.red
176
- : props.borderColor
177
- ? props.theme.colors[props.borderColor]
178
- ? '1px solid ' + props.theme.colors[props.borderColor]
179
- : '1px solid ' + props.borderColor
180
- : '1px solid ' + props.theme.colors.grey4};
175
+ ? '1px solid ' + props.theme.colors.red
176
+ : props.borderColor
177
+ ? props.theme.colors[props.borderColor]
178
+ ? '1px solid ' + props.theme.colors[props.borderColor]
179
+ : '1px solid ' + props.borderColor
180
+ : '1px solid ' + props.theme.colors.grey4};
181
181
  padding: ${(props) =>
182
182
  props.isError
183
183
  ? '11px 25px 11px 10px'
184
184
  : props.inputType === 'password'
185
- ? '11px 25px 11px 10px'
186
- : props.defaultPadding
187
- ? '10px 35px 10px 15px'
188
- : '11px 5px 11px 10px'};
185
+ ? '11px 25px 11px 10px'
186
+ : props.defaultPadding
187
+ ? '10px 35px 10px 15px'
188
+ : '11px 5px 11px 10px'};
189
189
  border-radius: 4px;
190
190
  position: relative;
191
191
  font-size: ${(props) => (props.fontSize ? props.fontSize : '14px')};
@@ -193,8 +193,8 @@
193
193
  props.isDisabled
194
194
  ? props.theme.colors.grey2
195
195
  : props.fontColor
196
- ? props.fontColor + ' !important'
197
- : props.theme.colors.black};
196
+ ? props.fontColor + ' !important'
197
+ : props.theme.semanticColors.grey[900]};
198
198
 
199
199
  width: ${(props) => (props.inputWidth ? props.inputWidth : '100%')};
200
200
  min-width: ${(props) => (props.minWidth ? props.minWidth : 'unset')};
@@ -208,8 +208,8 @@
208
208
  ? props.disabledBackgroundColor + ' !important'
209
209
  : props.theme.colors.grey5
210
210
  : props.backgroundColor
211
- ? props.backgroundColor + ' !important'
212
- : props.theme.colors.white};
211
+ ? props.backgroundColor + ' !important'
212
+ : props.theme.colors.white};
213
213
  &::placeholder {
214
214
  color: ${(props) => props.theme.colors.grey2};
215
215
  }
@@ -369,7 +369,7 @@
369
369
  },
370
370
  labelFontColor: {
371
371
  required: false,
372
- default: 'black',
372
+ default: '',
373
373
  type: String,
374
374
  },
375
375
  backgroundColor: {
@@ -383,7 +383,7 @@
383
383
  },
384
384
  fontColor: {
385
385
  required: false,
386
- default: 'black',
386
+ default: '',
387
387
  type: String,
388
388
  },
389
389
  hasFocus: {
@@ -1,6 +1,10 @@
1
1
  <template>
2
2
  <FlexRadioContainer>
3
- <ComponentWrapper :color-mode="colorMode" :layout="layout">
3
+ <ComponentWrapper
4
+ :color-mode="colorMode"
5
+ :grid-gap="gridGap"
6
+ :layout="layout"
7
+ >
4
8
  <RadioWrapper
5
9
  v-for="(item, index) in options"
6
10
  :key="item.value"
@@ -120,13 +124,15 @@
120
124
  const wrapperProps = {
121
125
  layout: String,
122
126
  colorMode: String,
127
+ gridGap: String,
123
128
  }
124
129
  const ComponentWrapper = styled('div', wrapperProps)`
125
130
  display: flex;
126
131
  flex-direction: ${(props) =>
127
132
  props.layout === 'vertical' ? 'column' : 'row'};
128
133
  grid-gap: ${(props) =>
129
- props.colorMode === 'transparent' ? '16px 5px' : '10px 5px'};
134
+ props.gridGap ||
135
+ (props.colorMode === 'transparent' ? '16px 5px' : '10px 5px')};
130
136
  flex-wrap: wrap;
131
137
  `
132
138
 
@@ -361,6 +367,11 @@
361
367
  default: '',
362
368
  type: String,
363
369
  },
370
+ gridGap: {
371
+ required: false,
372
+ default: null,
373
+ type: [String, null],
374
+ },
364
375
  },
365
376
  emits: ['on-radio-change'],
366
377
  data() {
@@ -49,10 +49,10 @@
49
49
  buttonBgColor
50
50
  ? buttonBgColor
51
51
  : colorMode == 'dark'
52
- ? 'transparent'
53
- : colorMode == 'transparent'
54
- ? 'transparent'
55
- : 'white'
52
+ ? 'transparent'
53
+ : colorMode == 'transparent'
54
+ ? 'transparent'
55
+ : 'white'
56
56
  "
57
57
  class="select-button"
58
58
  :color-mode="colorMode"
@@ -169,8 +169,8 @@
169
169
  colorMode == 'dark'
170
170
  ? '#000000'
171
171
  : colorMode == 'transparent'
172
- ? 'grey6'
173
- : dropdownBgColor
172
+ ? 'grey6'
173
+ : dropdownBgColor
174
174
  "
175
175
  :hovered-index="hoveredIndex"
176
176
  :hovered-value="hoveredValueDomAttr"
@@ -307,14 +307,14 @@
307
307
  props.sidebarCaret
308
308
  ? '24px'
309
309
  : props.colorMode === 'transparent'
310
- ? '15px'
311
- : CARET_WIDTH};
310
+ ? '15px'
311
+ : CARET_WIDTH};
312
312
  min-width: ${(props) =>
313
313
  props.sidebarCaret
314
314
  ? '24px'
315
315
  : props.colorMode === 'transparent'
316
- ? '15px'
317
- : CARET_WIDTH};
316
+ ? '15px'
317
+ : CARET_WIDTH};
318
318
  height: 100%;
319
319
  align-items: center;
320
320
  cursor: pointer;
@@ -402,6 +402,9 @@
402
402
  }
403
403
  const SelectButtonWrapper = styled('div', SelectButtonWrapperAttrs)`
404
404
  ${(props) => (props.disabled ? 'cursor: not-allowed' : 'cursor: pointer')};
405
+ /* Grid item: without this, nowrap selector text sets the automatic
406
+ minimum and the wrapper blows out a constrained track. */
407
+ min-width: 0;
405
408
  `
406
409
 
407
410
  const selectButtonAttrs = {
@@ -434,23 +437,23 @@
434
437
  ? 'padding: 10px 15px 10px 5px;'
435
438
  : 'padding: 10px 15px;'
436
439
  : props.isSearchBarVisible
437
- ? ''
438
- : `padding-left: ${
439
- props.hasNoPadding
440
- ? '0'
441
- : props.tablePaddingLeft
442
- ? props.tablePaddingLeft
443
- : props.paddingLeft
444
- }`};
440
+ ? ''
441
+ : `padding-left: ${
442
+ props.hasNoPadding
443
+ ? '0'
444
+ : props.tablePaddingLeft
445
+ ? props.tablePaddingLeft
446
+ : props.paddingLeft
447
+ }`};
445
448
  text-align: ${(props) => (props.textCenter ? 'center' : 'left')};
446
449
  min-height: ${(props) =>
447
450
  props.selectHeight
448
451
  ? props.selectHeight
449
452
  : props.selectMinHeight
450
- ? props.selectMinHeight
451
- : props.height
452
- ? props.height
453
- : '36px'};
453
+ ? props.selectMinHeight
454
+ : props.height
455
+ ? props.height
456
+ : '36px'};
454
457
  display: flex;
455
458
  align-items: center;
456
459
  ${(props) => (props.selectHeight ? `height: ${props.selectHeight};` : '')}
@@ -458,8 +461,8 @@
458
461
  showBorder &&
459
462
  `
460
463
  border: ${BORDER_WIDTH} solid ${
461
- hasError ? theme.colors.red : theme.colors.grey4
462
- }
464
+ hasError ? theme.colors.red : theme.colors.grey4
465
+ }
463
466
  `}
464
467
  opacity: ${(props) =>
465
468
  props.colorMode === 'transparent' && props.disabled ? '0.4' : '1'};
@@ -467,18 +470,18 @@
467
470
  props.colorMode === 'transparent'
468
471
  ? 'transparent'
469
472
  : props.disabled && props.showDisabledBackground
470
- ? props.theme.colors.grey5
471
- : props.theme.colors[props.bgColor]
472
- ? props.theme.colors[props.bgColor]
473
- : props.bgColor} !important;
473
+ ? props.theme.colors.grey5
474
+ : props.theme.colors[props.bgColor]
475
+ ? props.theme.colors[props.bgColor]
476
+ : props.bgColor} !important;
474
477
  color: ${(props) =>
475
478
  props.colorMode === 'transparent'
476
479
  ? props.theme.colors.white
477
480
  : props.disabled && props.showDisabledBackground
478
- ? props.theme.colors.black
479
- : props.theme.colors[props.fontColor]
480
- ? props.theme.colors[props.fontColor]
481
- : props.fontColor};
481
+ ? props.theme.colors.black
482
+ : props.theme.colors[props.fontColor]
483
+ ? props.theme.colors[props.fontColor]
484
+ : props.fontColor};
482
485
  ${(props) => (props.disabled ? 'pointer-events: none' : '')};
483
486
  overflow: hidden;
484
487
  & > .handle {
@@ -486,8 +489,8 @@
486
489
  props.hasError
487
490
  ? props.theme.colors.red
488
491
  : props.colorMode === 'light'
489
- ? props.theme.colors.grey4
490
- : props.theme.colors.white}
492
+ ? props.theme.colors.grey4
493
+ : props.theme.colors.white}
491
494
  1px solid;
492
495
  }
493
496
  `
@@ -533,8 +536,8 @@
533
536
  props.minWidth
534
537
  ? props.minWidth
535
538
  : props.optionWidth
536
- ? props.optionWidth
537
- : '100%'};
539
+ ? props.optionWidth
540
+ : '100%'};
538
541
  background-color: ${(props) =>
539
542
  props.theme.colors[props.bgColor]
540
543
  ? props.theme.colors[props.bgColor]
@@ -1506,6 +1509,11 @@
1506
1509
  }
1507
1510
  },
1508
1511
  getDistanceBetweenSelectAndDropdownMenu() {
1512
+ // Runs from a delayed watcher callback; the select may have closed or
1513
+ // unmounted by the time it fires
1514
+ if (!this.$refs.select?.$el) {
1515
+ return
1516
+ }
1509
1517
  const wholeSelectTopPosition =
1510
1518
  this.selectTopPosition + this.$refs.select.$el.clientHeight
1511
1519
  this.selectAndDropdownDistance =
@@ -98,4 +98,146 @@ describe('TabsHeader', () => {
98
98
  const tabs = wrapper.findAll('[data-test-id="tab-item"]')
99
99
  expect(tabs.length).toBeGreaterThan(0)
100
100
  })
101
+
102
+ it('renders tab-menu slot when tab is active and showTabMenu', () => {
103
+ const wrapper = mount(TabsHeader, {
104
+ props: {
105
+ tabsData: [
106
+ { text: 'A', id: 'a', dataId: 'a', showTabMenu: true },
107
+ { text: 'B', id: 'b', dataId: 'b' },
108
+ ],
109
+ activeTab: 'a',
110
+ fullSize: false,
111
+ },
112
+ slots: {
113
+ 'tab-menu': '<div data-slot-mark="tab-menu">menu</div>',
114
+ },
115
+ global: {
116
+ provide: { theme },
117
+ },
118
+ })
119
+
120
+ expect(wrapper.find('[data-test-id="tab-menu-slot"]').exists()).toBe(true)
121
+ expect(wrapper.find('[data-slot-mark="tab-menu"]').exists()).toBe(true)
122
+ })
123
+
124
+ it('renders tab-menu slot for inactive tabs that have showTabMenu (revealed via hover styling)', () => {
125
+ const wrapper = mount(TabsHeader, {
126
+ props: {
127
+ tabsData: [
128
+ { text: 'A', id: 'a', dataId: 'a', showTabMenu: true },
129
+ { text: 'B', id: 'b', dataId: 'b' },
130
+ ],
131
+ activeTab: 'b',
132
+ fullSize: false,
133
+ },
134
+ slots: {
135
+ 'tab-menu': '<div data-slot-mark="tab-menu">menu</div>',
136
+ },
137
+ global: {
138
+ provide: { theme },
139
+ },
140
+ })
141
+
142
+ expect(wrapper.find('[data-test-id="tab-menu-slot"]').exists()).toBe(true)
143
+ expect(wrapper.find('[data-slot-mark="tab-menu"]').exists()).toBe(true)
144
+ })
145
+
146
+ it('does not render tab-menu slot when showTabMenu is false', () => {
147
+ const wrapper = mount(TabsHeader, {
148
+ props: {
149
+ tabsData: [
150
+ { text: 'A', id: 'a', dataId: 'a' },
151
+ { text: 'B', id: 'b', dataId: 'b' },
152
+ ],
153
+ activeTab: 'a',
154
+ fullSize: false,
155
+ },
156
+ slots: {
157
+ 'tab-menu': '<div data-slot-mark="tab-menu">menu</div>',
158
+ },
159
+ global: {
160
+ provide: { theme },
161
+ },
162
+ })
163
+
164
+ expect(wrapper.find('[data-test-id="tab-menu-slot"]').exists()).toBe(false)
165
+ expect(wrapper.find('[data-slot-mark="tab-menu"]').exists()).toBe(false)
166
+ })
167
+
168
+ it('passes is-active=true to the tab-menu slot scope when the slot tab is the active one', () => {
169
+ const wrapper = mount(TabsHeader, {
170
+ props: {
171
+ tabsData: [
172
+ { text: 'A', id: 'a', dataId: 'a', showTabMenu: true },
173
+ { text: 'B', id: 'b', dataId: 'b', showTabMenu: true },
174
+ ],
175
+ activeTab: 'a',
176
+ fullSize: false,
177
+ },
178
+ slots: {
179
+ 'tab-menu': `
180
+ <template #default="{ item, isActive }">
181
+ <span :data-slot-mark="item.id" :data-active="isActive">menu</span>
182
+ </template>
183
+ `,
184
+ },
185
+ global: {
186
+ provide: { theme },
187
+ },
188
+ })
189
+
190
+ expect(wrapper.find('[data-slot-mark="a"]').attributes('data-active')).toBe(
191
+ 'true'
192
+ )
193
+ expect(wrapper.find('[data-slot-mark="b"]').attributes('data-active')).toBe(
194
+ 'false'
195
+ )
196
+ })
197
+
198
+ it('renders after-tabs slot when provided', () => {
199
+ const wrapper = mount(TabsHeader, {
200
+ props: {
201
+ tabsData: mockTabsData,
202
+ activeTab: 0,
203
+ fullSize: false,
204
+ },
205
+ slots: {
206
+ 'after-tabs': '<aside data-slot-mark="after-tabs">trail</aside>',
207
+ },
208
+ global: {
209
+ provide: { theme },
210
+ },
211
+ })
212
+
213
+ expect(wrapper.find('[data-test-id="after-tabs-slot"]').exists()).toBe(
214
+ true
215
+ )
216
+ expect(wrapper.find('[data-slot-mark="after-tabs"]').exists()).toBe(true)
217
+ })
218
+
219
+ it('keeps after-tabs outside the scroll area when scrollOverflow is enabled', () => {
220
+ const wrapper = mount(TabsHeader, {
221
+ props: {
222
+ tabsData: mockTabsData,
223
+ activeTab: 0,
224
+ fullSize: false,
225
+ scrollOverflow: true,
226
+ },
227
+ slots: {
228
+ 'after-tabs': '<aside data-slot-mark="after-tabs">trail</aside>',
229
+ },
230
+ global: {
231
+ provide: { theme },
232
+ },
233
+ })
234
+
235
+ const scrollArea = wrapper.find('[data-test-id="tabs-scroll-area"]')
236
+ const afterTabs = wrapper.find('[data-test-id="after-tabs-slot"]')
237
+ expect(scrollArea.exists()).toBe(true)
238
+ expect(afterTabs.exists()).toBe(true)
239
+ expect(scrollArea.find('[data-test-id="after-tabs-slot"]').exists()).toBe(
240
+ false
241
+ )
242
+ })
101
243
  })
@@ -140,3 +140,66 @@ StringIds.args = {
140
140
  ],
141
141
  activeTab: 'tab1',
142
142
  }
143
+
144
+ /**
145
+ * Tab menu (#tab-menu) and trailing “Create view” (#after-tabs) — consumes
146
+ * slots like ProjectList. The active tab shows the kebab persistently; inactive
147
+ * tabs flagged `showTabMenu: true` reveal it on hover.
148
+ */
149
+ export const WithTabMenuAndAfterTabs = () => ({
150
+ components: { TabsHeader },
151
+ data() {
152
+ return {
153
+ activeTab: 'saved',
154
+ tabsData: [
155
+ {
156
+ text: 'Table view',
157
+ id: 'table',
158
+ icon: 'table_view',
159
+ dataId: 'kanban_tab_story_table',
160
+ },
161
+ {
162
+ text: 'My saved view',
163
+ id: 'saved',
164
+ icon: 'kanban_view',
165
+ dataId: 'kanban_tab_story_saved',
166
+ showTabMenu: true,
167
+ },
168
+ {
169
+ text: 'Inactive saved view (hover me)',
170
+ id: 'saved-2',
171
+ icon: 'table_view',
172
+ dataId: 'kanban_tab_story_saved_2',
173
+ showTabMenu: true,
174
+ },
175
+ ],
176
+ }
177
+ },
178
+ methods: {
179
+ onTabChange(id) {
180
+ this.activeTab = id
181
+ },
182
+ },
183
+ template: `
184
+ <TabsHeader
185
+ :tabs-data="tabsData"
186
+ :active-tab="activeTab"
187
+ :full-size="false"
188
+ @on-tab-change="onTabChange"
189
+ >
190
+ <template #tab-menu>
191
+ <span
192
+ data-test-id="story_tab_menu_trigger"
193
+ style="width: 26px; height: 26px; border-radius: 4px; background: #e8d8fe; display: inline-flex; align-items: center; justify-content: center; font-weight: bold; font-size: 12px; color: #6f20dc;"
194
+ >
195
+
196
+ </span>
197
+ </template>
198
+ <template #after-tabs>
199
+ <span style="margin-left: 4px; margin-bottom: 2px; color: #6f20dc; font-size: 14px;">
200
+ + Create view (story)
201
+ </span>
202
+ </template>
203
+ </TabsHeader>
204
+ `,
205
+ })
@@ -1,100 +1,94 @@
1
1
  <template>
2
- <PageContainer data-test-id="tabs-header-container">
3
- <TabsContainer data-test-id="tabs-container">
4
- <TabItem
5
- v-for="item in tabsData"
6
- :key="item.id"
7
- :data-id="item.dataId"
8
- :data-qa-id="item.dataId"
9
- :data-test-active="activeTab === item.id"
10
- data-test-id="tab-item"
11
- :full-size="fullSize"
12
- :is-active="activeTab === item.id"
13
- :is-disabled="item.isDisabled"
14
- @click="onTabClick({ id: item.id, isDisabled: item.isDisabled })"
2
+ <RootContainer data-test-id="tabs-header-container">
3
+ <TabsScrollArea
4
+ data-test-id="tabs-scroll-area"
5
+ :scroll-overflow="scrollOverflow"
6
+ >
7
+ <TabsContainer
8
+ data-test-id="tabs-container"
9
+ :scroll-overflow="scrollOverflow"
15
10
  >
16
- <RCIcon
17
- v-if="item.icon"
18
- :color="
19
- activeTab === item.id
20
- ? theme.semanticColors.purple[500]
21
- : theme.semanticColors.teal[800]
22
- "
23
- data-test-id="tab-icon"
24
- :hovered-color="
25
- activeTab === item.id
26
- ? theme.semanticColors.purple[500]
27
- : theme.semanticColors.teal[800]
28
- "
29
- :name="item.icon"
30
- size="14px"
31
- />
32
- <TabText
33
- data-test-id="tab-text"
11
+ <TabItem
12
+ v-for="item in tabsData"
13
+ :key="item.id"
14
+ :data-id="item.dataId"
15
+ :data-qa-id="item.dataId"
16
+ :data-test-active="activeTab === item.id"
17
+ data-test-id="tab-item"
18
+ :full-size="fullSize"
34
19
  :is-active="activeTab === item.id"
35
20
  :is-disabled="item.isDisabled"
36
- >{{ item.text }}</TabText
21
+ :scroll-overflow="scrollOverflow"
22
+ @click="onTabClick({ id: item.id, isDisabled: item.isDisabled })"
37
23
  >
38
- <DotIcon
39
- v-if="item.subText"
40
- data-test-id="dot-icon"
41
- :is-active="activeTab === item.id"
42
- />
43
- <SubText v-if="item.subText" data-test-id="tab-subtext">{{
44
- item.subText
45
- }}</SubText>
46
- <RCIcon
47
- v-if="item.hasError"
48
- data-test-id="warning-icon"
49
- name="warning"
50
- size="14px"
51
- />
52
- <RCIcon
53
- v-if="item.closable"
54
- :color="
55
- activeTab === item.id
56
- ? theme.semanticColors.red[500]
57
- : theme.semanticColors.red[300]
58
- "
59
- data-test-id="tab-close-icon"
60
- :hovered-color="theme.semanticColors.red[600]"
61
- name="close"
62
- size="12px"
63
- @click.stop="$emit('on-tab-close', item.id)"
64
- />
65
- <ConversionTag
66
- v-if="item.showUpgradeTag"
67
- :data-id="
68
- item.isTrialEnded || item.isTrialActive
69
- ? `${item.upgradeName}_upgrade_only`
70
- : `conversion_tag_import_${item.upgradeName}_available_trial_${item.isTrialAvailable}`
71
- "
72
- :is-capitalized="true"
73
- :text="item.upgradeTagText"
74
- />
75
- </TabItem>
76
- </TabsContainer>
77
- </PageContainer>
24
+ <RCIcon
25
+ v-if="item.icon"
26
+ :color="
27
+ activeTab === item.id
28
+ ? theme.semanticColors.purple[500]
29
+ : theme.semanticColors.teal[800]
30
+ "
31
+ data-test-id="tab-icon"
32
+ :hovered-color="
33
+ activeTab === item.id
34
+ ? theme.semanticColors.purple[500]
35
+ : theme.semanticColors.teal[800]
36
+ "
37
+ :name="item.icon"
38
+ size="14px"
39
+ />
40
+ <TabText
41
+ data-test-id="tab-text"
42
+ :is-active="activeTab === item.id"
43
+ :is-disabled="item.isDisabled"
44
+ >{{ item.text }}</TabText
45
+ >
46
+ <TabMenuSlot
47
+ v-if="item.showTabMenu && $slots['tab-menu']"
48
+ class="rc-tabs-header__menu-slot"
49
+ data-test-id="tab-menu-slot"
50
+ :is-active="activeTab === item.id"
51
+ >
52
+ <slot
53
+ name="tab-menu"
54
+ :item="item"
55
+ :is-active="activeTab === item.id"
56
+ />
57
+ </TabMenuSlot>
58
+ <DotIcon
59
+ v-if="item.subText"
60
+ data-test-id="dot-icon"
61
+ :is-active="activeTab === item.id"
62
+ />
63
+ <SubText v-if="item.subText" data-test-id="tab-subtext">{{
64
+ item.subText
65
+ }}</SubText>
66
+ <RCIcon
67
+ v-if="item.hasError"
68
+ data-test-id="warning-icon"
69
+ name="warning"
70
+ size="14px"
71
+ />
72
+ <ConversionTag
73
+ v-if="item.showUpgradeTag"
74
+ :data-id="
75
+ item.isTrialEnded || item.isTrialActive
76
+ ? `${item.upgradeName}_upgrade_only`
77
+ : `conversion_tag_import_${item.upgradeName}_available_trial_${item.isTrialAvailable}`
78
+ "
79
+ :is-capitalized="true"
80
+ :text="item.upgradeTagText"
81
+ />
82
+ </TabItem>
83
+ </TabsContainer>
84
+ </TabsScrollArea>
85
+ <AfterTabsSlot v-if="$slots['after-tabs']" data-test-id="after-tabs-slot">
86
+ <slot name="after-tabs" />
87
+ </AfterTabsSlot>
88
+ </RootContainer>
78
89
  </template>
79
90
 
80
91
  <script>
81
- // import RCTabsHeader from "@eturnity/eturnity_reusable_components/src/components/tabsHeader"
82
- // To use:
83
- // <RCTabsHeader
84
- // :activeTab="activeTabIndex" // should match the 'id'
85
- // :tabsData="[
86
- // {
87
- // text: 'Tab 1',
88
- // id: 0,
89
- // hasError: true // optional
90
- // },
91
- // {
92
- // text: 'Tab 1',
93
- // id: 1,
94
- // hasError: false // optional
95
- // }
96
- // ]"
97
- // />
98
92
  import styled from 'vue3-styled-components'
99
93
  import RCIcon from '../icon'
100
94
  import theme from '@/assets/theme'
@@ -114,15 +108,60 @@
114
108
  color: ${(props) => props.theme.colors.black};
115
109
  `
116
110
 
117
- const PageContainer = styled.div``
111
+ const RootContainer = styled.div`
112
+ display: flex;
113
+ align-items: stretch;
114
+ width: 100%;
115
+ min-width: 0;
116
+ `
117
+
118
+ const TabsScrollAreaAttrs = { scrollOverflow: Boolean }
119
+ const TabsScrollArea = styled('div', TabsScrollAreaAttrs)`
120
+ flex: 1 1 auto;
121
+ min-width: 0;
122
+ ${(props) =>
123
+ props.scrollOverflow
124
+ ? `
125
+ overflow-x: auto;
126
+ overflow-y: hidden;
127
+ -webkit-overflow-scrolling: touch;
128
+ padding-bottom: 1px;
129
+ margin-bottom: -1px;
130
+ `
131
+ : ''}
132
+ `
118
133
 
119
- const TabsContainer = styled.div`
134
+ const TabsContainerAttrs = { scrollOverflow: Boolean }
135
+ const TabsContainer = styled('div', TabsContainerAttrs)`
120
136
  display: flex;
137
+ flex-wrap: ${(props) => (props.scrollOverflow ? 'nowrap' : 'wrap')};
138
+ align-items: stretch;
121
139
  cursor: pointer;
122
- width: 100%;
140
+ width: ${(props) => (props.scrollOverflow ? 'max-content' : '100%')};
141
+ `
142
+
143
+ const AfterTabsSlot = styled.div`
144
+ flex-shrink: 0;
145
+ display: flex;
146
+ align-items: center;
147
+ align-self: stretch;
123
148
  `
124
149
 
125
- const TabAttrs = { isActive: Boolean, fullSize: Boolean, isDisabled: Boolean }
150
+ const TabMenuSlotAttrs = { isActive: Boolean }
151
+ const TabMenuSlot = styled('div', TabMenuSlotAttrs)`
152
+ display: inline-flex;
153
+ align-items: center;
154
+ opacity: ${(props) => (props.isActive ? 1 : 0)};
155
+ transition: opacity 0.15s ease;
156
+ pointer-events: ${(props) => (props.isActive ? 'auto' : 'none')};
157
+ `
158
+
159
+ const TabAttrs = {
160
+ isActive: Boolean,
161
+ fullSize: Boolean,
162
+ isDisabled: Boolean,
163
+ scrollOverflow: Boolean,
164
+ }
126
165
  const TabItem = styled('div', TabAttrs)`
127
166
  display: flex;
128
167
  align-items: center;
@@ -136,12 +175,28 @@
136
175
  props.isActive
137
176
  ? props.theme.semanticColors.purple[500]
138
177
  : props.theme.semanticColors.teal[800]};
139
- flex-grow: ${(props) => (props.fullSize ? 1 : 0)};
178
+ flex-grow: ${(props) => (props.fullSize && !props.scrollOverflow ? 1 : 0)};
179
+ flex-shrink: ${(props) => (props.scrollOverflow ? 0 : 1)};
180
+ white-space: ${(props) => (props.scrollOverflow ? 'nowrap' : 'normal')};
140
181
  background-color: ${(props) => props.theme.colors.white};
141
- border-bottom: ${(props) =>
142
- props.isActive
143
- ? '2px solid' + props.theme.semanticColors.purple[400]
144
- : '2px solid' + props.theme.semanticColors.grey[400]};
182
+ position: relative;
183
+ z-index: ${(props) => (props.isActive ? 1 : 0)};
184
+ border-bottom: ${(props) => {
185
+ if (props.scrollOverflow) {
186
+ return props.isActive
187
+ ? `1px solid ${props.theme.semanticColors.purple[400]}`
188
+ : '1px solid transparent'
189
+ }
190
+ if (props.isActive) {
191
+ return `2px solid ${props.theme.semanticColors.purple[400]}`
192
+ }
193
+ return `2px solid ${props.theme.semanticColors.grey[400]}`
194
+ }};
195
+ margin-bottom: ${(props) => (props.scrollOverflow && props.isActive ? '-1px' : '0')};
196
+ &:hover .rc-tabs-header__menu-slot {
197
+ opacity: 1;
198
+ pointer-events: auto;
199
+ }
145
200
  `
146
201
 
147
202
  const TabTextAttrs = { isActive: Boolean, isDisabled: Boolean }
@@ -152,17 +207,20 @@
152
207
  props.isActive
153
208
  ? props.theme.semanticColors.purple[500]
154
209
  : props.isDisabled
155
- ? props.theme.semanticColors.grey[600]
156
- : props.theme.semanticColors.teal[700]};
210
+ ? props.theme.semanticColors.grey[600]
211
+ : props.theme.semanticColors.teal[700]};
157
212
  cursor: ${(props) => (props.isDisabled ? 'not-allowed' : 'pointer')};
158
213
  `
159
214
 
160
215
  export default {
161
216
  name: 'RCTabsHeader',
162
217
  components: {
163
- PageContainer,
218
+ RootContainer,
219
+ TabsScrollArea,
164
220
  TabsContainer,
221
+ AfterTabsSlot,
165
222
  TabItem,
223
+ TabMenuSlot,
166
224
  SubText,
167
225
  DotIcon,
168
226
  RCIcon,
@@ -182,6 +240,11 @@
182
240
  type: Boolean,
183
241
  default: true,
184
242
  },
243
+ /** Horizontal scroll when tabs overflow; keeps tabs on one row. */
244
+ scrollOverflow: {
245
+ type: Boolean,
246
+ default: false,
247
+ },
185
248
  /** When true, emit on-tab-change even when clicking the currently active tab */
186
249
  emitWhenActiveTabClicked: {
187
250
  type: Boolean,