@eturnity/eturnity_reusable_components 9.25.4 → 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 +1 -1
- package/src/components/filterComponent/viewFilter.vue +44 -12
- package/src/components/filterComponent/viewSettings.vue +5 -0
- package/src/components/filterComponent/viewSort.vue +5 -0
- package/src/components/inputs/inputText/index.vue +18 -18
- package/src/components/inputs/select/index.vue +44 -36
- package/src/components/tabsHeader/TabsHeader.spec.js +142 -0
- package/src/components/tabsHeader/TabsHeader.stories.js +63 -0
- package/src/components/tabsHeader/index.vue +164 -101
package/package.json
CHANGED
|
@@ -107,7 +107,7 @@
|
|
|
107
107
|
</SelectOption>
|
|
108
108
|
</template>
|
|
109
109
|
</SelectComponent>
|
|
110
|
-
<template v-if="item.column
|
|
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?.
|
|
205
|
-
|
|
204
|
+
selectedSort[index].selectedOptions?.some(
|
|
205
|
+
(selectedValue) =>
|
|
206
|
+
filterOptionValuesMatch(
|
|
207
|
+
selectedValue,
|
|
208
|
+
option.value
|
|
209
|
+
)
|
|
206
210
|
)
|
|
207
211
|
"
|
|
208
|
-
: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.
|
|
547
|
-
|
|
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
|
|
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
|
|
650
|
+
const option = optionsList.find((opt) =>
|
|
651
|
+
this.filterOptionValuesMatch(opt.value, optionValue)
|
|
626
652
|
)
|
|
627
|
-
return option ? option
|
|
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
|
-
|
|
129
|
-
|
|
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
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
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
|
-
|
|
197
|
-
|
|
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
|
-
|
|
212
|
-
|
|
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: '
|
|
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: '
|
|
386
|
+
default: '',
|
|
387
387
|
type: String,
|
|
388
388
|
},
|
|
389
389
|
hasFocus: {
|
|
@@ -49,10 +49,10 @@
|
|
|
49
49
|
buttonBgColor
|
|
50
50
|
? buttonBgColor
|
|
51
51
|
: colorMode == 'dark'
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
173
|
-
|
|
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
|
-
|
|
311
|
-
|
|
310
|
+
? '15px'
|
|
311
|
+
: CARET_WIDTH};
|
|
312
312
|
min-width: ${(props) =>
|
|
313
313
|
props.sidebarCaret
|
|
314
314
|
? '24px'
|
|
315
315
|
: props.colorMode === 'transparent'
|
|
316
|
-
|
|
317
|
-
|
|
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
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
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
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
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
|
-
|
|
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
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
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
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
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
|
-
|
|
490
|
-
|
|
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
|
-
|
|
537
|
-
|
|
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
|
-
<
|
|
3
|
-
<
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
:
|
|
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
|
-
<
|
|
17
|
-
v-
|
|
18
|
-
:
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
"
|
|
23
|
-
|
|
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
|
-
|
|
21
|
+
:scroll-overflow="scrollOverflow"
|
|
22
|
+
@click="onTabClick({ id: item.id, isDisabled: item.isDisabled })"
|
|
37
23
|
>
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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
|
-
|
|
156
|
-
|
|
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
|
-
|
|
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,
|