@eturnity/eturnity_reusable_components 9.25.3 → 9.25.5

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",
4
4
  "files": [
5
5
  "dist",
6
6
  "src"
@@ -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({
@@ -602,6 +606,12 @@
602
606
  select.closeDropdown()
603
607
  })
604
608
  },
609
+ filterOptionValuesMatch(left, right) {
610
+ if (left == null || right == null) {
611
+ return left === right
612
+ }
613
+ return String(left) === String(right)
614
+ },
605
615
  getSelectedLabel({ type, value, index }) {
606
616
  if (!value) return
607
617
  if (type === 'column') {
@@ -621,10 +631,10 @@
621
631
  const optionsList = this.optionsList(index)
622
632
  const selectedLabels = selectedOptions
623
633
  .map((optionValue) => {
624
- const option = optionsList.find(
625
- (opt) => opt.value === optionValue
634
+ const option = optionsList.find((opt) =>
635
+ this.filterOptionValuesMatch(opt.value, optionValue)
626
636
  )
627
- return option ? option.label : optionValue
637
+ return option ? this.getOptionLabel(option) : optionValue
628
638
  })
629
639
  .join(', ')
630
640
 
@@ -686,9 +696,15 @@
686
696
  ...option,
687
697
  value: option[selectedItem.valueSelector],
688
698
  label: option[selectedItem.labelSelector],
699
+ optionLabelsTranslated: !!selectedItem.optionLabelsTranslated,
689
700
  })
690
701
  )
691
702
  },
703
+ getOptionLabel(option) {
704
+ return option.optionLabelsTranslated
705
+ ? option.label
706
+ : this.$gettext(option.label)
707
+ },
692
708
  },
693
709
  }
694
710
  </script>
@@ -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;
@@ -434,23 +434,23 @@
434
434
  ? 'padding: 10px 15px 10px 5px;'
435
435
  : 'padding: 10px 15px;'
436
436
  : props.isSearchBarVisible
437
- ? ''
438
- : `padding-left: ${
439
- props.hasNoPadding
440
- ? '0'
441
- : props.tablePaddingLeft
442
- ? props.tablePaddingLeft
443
- : props.paddingLeft
444
- }`};
437
+ ? ''
438
+ : `padding-left: ${
439
+ props.hasNoPadding
440
+ ? '0'
441
+ : props.tablePaddingLeft
442
+ ? props.tablePaddingLeft
443
+ : props.paddingLeft
444
+ }`};
445
445
  text-align: ${(props) => (props.textCenter ? 'center' : 'left')};
446
446
  min-height: ${(props) =>
447
447
  props.selectHeight
448
448
  ? props.selectHeight
449
449
  : props.selectMinHeight
450
- ? props.selectMinHeight
451
- : props.height
452
- ? props.height
453
- : '36px'};
450
+ ? props.selectMinHeight
451
+ : props.height
452
+ ? props.height
453
+ : '36px'};
454
454
  display: flex;
455
455
  align-items: center;
456
456
  ${(props) => (props.selectHeight ? `height: ${props.selectHeight};` : '')}
@@ -458,8 +458,8 @@
458
458
  showBorder &&
459
459
  `
460
460
  border: ${BORDER_WIDTH} solid ${
461
- hasError ? theme.colors.red : theme.colors.grey4
462
- }
461
+ hasError ? theme.colors.red : theme.colors.grey4
462
+ }
463
463
  `}
464
464
  opacity: ${(props) =>
465
465
  props.colorMode === 'transparent' && props.disabled ? '0.4' : '1'};
@@ -467,18 +467,18 @@
467
467
  props.colorMode === 'transparent'
468
468
  ? 'transparent'
469
469
  : 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;
470
+ ? props.theme.colors.grey5
471
+ : props.theme.colors[props.bgColor]
472
+ ? props.theme.colors[props.bgColor]
473
+ : props.bgColor} !important;
474
474
  color: ${(props) =>
475
475
  props.colorMode === 'transparent'
476
476
  ? props.theme.colors.white
477
477
  : props.disabled && props.showDisabledBackground
478
- ? props.theme.colors.black
479
- : props.theme.colors[props.fontColor]
480
- ? props.theme.colors[props.fontColor]
481
- : props.fontColor};
478
+ ? props.theme.colors.black
479
+ : props.theme.colors[props.fontColor]
480
+ ? props.theme.colors[props.fontColor]
481
+ : props.fontColor};
482
482
  ${(props) => (props.disabled ? 'pointer-events: none' : '')};
483
483
  overflow: hidden;
484
484
  & > .handle {
@@ -486,8 +486,8 @@
486
486
  props.hasError
487
487
  ? props.theme.colors.red
488
488
  : props.colorMode === 'light'
489
- ? props.theme.colors.grey4
490
- : props.theme.colors.white}
489
+ ? props.theme.colors.grey4
490
+ : props.theme.colors.white}
491
491
  1px solid;
492
492
  }
493
493
  `
@@ -533,8 +533,8 @@
533
533
  props.minWidth
534
534
  ? props.minWidth
535
535
  : props.optionWidth
536
- ? props.optionWidth
537
- : '100%'};
536
+ ? props.optionWidth
537
+ : '100%'};
538
538
  background-color: ${(props) =>
539
539
  props.theme.colors[props.bgColor]
540
540
  ? props.theme.colors[props.bgColor]
@@ -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,40 @@
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 '1px solid transparent'
187
+ }
188
+ if (props.isActive) {
189
+ return `2px solid ${props.theme.semanticColors.purple[400]}`
190
+ }
191
+ return `2px solid ${props.theme.semanticColors.grey[400]}`
192
+ }};
193
+ ${(props) =>
194
+ props.scrollOverflow && props.isActive
195
+ ? `
196
+ &::after {
197
+ content: '';
198
+ position: absolute;
199
+ left: 0;
200
+ right: 0;
201
+ bottom: -1px;
202
+ height: 2px;
203
+ background-color: ${props.theme.semanticColors.purple[400]};
204
+ z-index: 1;
205
+ }
206
+ `
207
+ : ''}
208
+ &:hover .rc-tabs-header__menu-slot {
209
+ opacity: 1;
210
+ pointer-events: auto;
211
+ }
145
212
  `
146
213
 
147
214
  const TabTextAttrs = { isActive: Boolean, isDisabled: Boolean }
@@ -152,17 +219,20 @@
152
219
  props.isActive
153
220
  ? props.theme.semanticColors.purple[500]
154
221
  : props.isDisabled
155
- ? props.theme.semanticColors.grey[600]
156
- : props.theme.semanticColors.teal[700]};
222
+ ? props.theme.semanticColors.grey[600]
223
+ : props.theme.semanticColors.teal[700]};
157
224
  cursor: ${(props) => (props.isDisabled ? 'not-allowed' : 'pointer')};
158
225
  `
159
226
 
160
227
  export default {
161
228
  name: 'RCTabsHeader',
162
229
  components: {
163
- PageContainer,
230
+ RootContainer,
231
+ TabsScrollArea,
164
232
  TabsContainer,
233
+ AfterTabsSlot,
165
234
  TabItem,
235
+ TabMenuSlot,
166
236
  SubText,
167
237
  DotIcon,
168
238
  RCIcon,
@@ -182,6 +252,11 @@
182
252
  type: Boolean,
183
253
  default: true,
184
254
  },
255
+ /** Horizontal scroll when tabs overflow; keeps tabs on one row. */
256
+ scrollOverflow: {
257
+ type: Boolean,
258
+ default: false,
259
+ },
185
260
  /** When true, emit on-tab-change even when clicking the currently active tab */
186
261
  emitWhenActiveTabClicked: {
187
262
  type: Boolean,