Skip to content

Commit 32beb5d

Browse files
fix(NcAppSIdebarTabs): fix dynamic tabs registration
fix: #3461 Signed-off-by: Grigorii Shartsev <grigorii.shartsev@nextcloud.com> Co-authored-by: Raimund Schlüßler <raimund.schluessler@mailbox.org>
1 parent af25af8 commit 32beb5d

File tree

4 files changed

+162
-109
lines changed

4 files changed

+162
-109
lines changed

src/components/NcAppSidebar/NcAppSidebar.vue

Lines changed: 104 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -31,22 +31,33 @@ include a standard-header like it's used by the files app.
3131

3232
```vue
3333
<template>
34-
<NcAppSidebar
35-
title="cat-picture.jpg"
36-
subtitle="last edited 3 weeks ago">
37-
<NcAppSidebarTab name="Settings" id="settings-tab">
38-
<template #icon>
39-
<Cog :size="20" />
40-
</template>
41-
Settings tab content
42-
</NcAppSidebarTab>
43-
<NcAppSidebarTab name="Sharing" id="share-tab">
44-
<template #icon>
45-
<ShareVariant :size="20" />
46-
</template>
47-
Sharing tab content
48-
</NcAppSidebarTab>
49-
</NcAppSidebar>
34+
<div>
35+
<NcCheckboxRadioSwitch :checked.sync="hideFirstTab">
36+
The first tab is hidden
37+
</NcCheckboxRadioSwitch>
38+
<NcAppSidebar
39+
title="cat-picture.jpg"
40+
subtitle="last edited 3 weeks ago">
41+
<NcAppSidebarTab v-if="!hideFirstTab" name="Fist tab" id="first-tab">
42+
<template #icon>
43+
<Cog :size="20" />
44+
</template>
45+
New tab
46+
</NcAppSidebarTab>
47+
<NcAppSidebarTab name="Settings" id="settings-tab">
48+
<template #icon>
49+
<Cog :size="20" />
50+
</template>
51+
Settings tab content
52+
</NcAppSidebarTab>
53+
<NcAppSidebarTab name="Sharing" id="share-tab">
54+
<template #icon>
55+
<ShareVariant :size="20" />
56+
</template>
57+
Sharing tab content
58+
</NcAppSidebarTab>
59+
</NcAppSidebar>
60+
</div>
5061
</template>
5162
<script>
5263
import Cog from 'vue-material-design-icons/Cog'
@@ -57,10 +68,87 @@ include a standard-header like it's used by the files app.
5768
Cog,
5869
ShareVariant,
5970
},
71+
data() {
72+
return {
73+
hideFirstTab: true,
74+
}
75+
},
6076
}
6177
</script>
6278
```
6379

80+
### One tab
81+
82+
```vue
83+
<template>
84+
<div>
85+
<NcAppSidebar
86+
title="cat-picture.jpg"
87+
subtitle="last edited 3 weeks ago">
88+
<NcAppSidebarTab name="Settings" id="settings-tab">
89+
<template #icon>
90+
<Cog :size="20" />
91+
</template>
92+
New tab
93+
</NcAppSidebarTab>
94+
</NcAppSidebar>
95+
</div>
96+
</template>
97+
<script>
98+
import Cog from 'vue-material-design-icons/Cog'
99+
100+
export default {
101+
components: {
102+
Cog,
103+
},
104+
}
105+
</script>
106+
```
107+
108+
### One or two tabs with condition
109+
110+
```vue
111+
<template>
112+
<div>
113+
<NcCheckboxRadioSwitch :checked.sync="hideFirstTab">
114+
The first tab is hidden
115+
</NcCheckboxRadioSwitch>
116+
<NcAppSidebar
117+
title="cat-picture.jpg"
118+
subtitle="last edited 3 weeks ago">
119+
<NcAppSidebarTab v-if="!hideFirstTab" name="Settings" id="settings-tab">
120+
<template #icon>
121+
<Cog :size="20" />
122+
</template>
123+
Settings
124+
</NcAppSidebarTab>
125+
<NcAppSidebarTab name="Sharing" id="share-tab">
126+
<template #icon>
127+
<ShareVariant :size="20" />
128+
</template>
129+
Sharing tab content
130+
</NcAppSidebarTab>
131+
</NcAppSidebar>
132+
</div>
133+
</template>
134+
<script>
135+
import Cog from 'vue-material-design-icons/Cog'
136+
import ShareVariant from 'vue-material-design-icons/ShareVariant'
137+
138+
export default {
139+
components: {
140+
Cog,
141+
ShareVariant,
142+
},
143+
data() {
144+
return {
145+
hideFirstTab: true,
146+
}
147+
},
148+
}
149+
</script>
150+
```
151+
64152
### Editable title
65153

66154
```vue

src/components/NcAppSidebar/NcAppSidebarTabs.vue

Lines changed: 34 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,7 @@
4747
role="tab"
4848
@click.prevent="setActive(tab.id)">
4949
<span class="app-sidebar-tabs__tab-icon">
50-
<NcVNodes v-if="hasMdIcon(tab)" :vnodes="tab.$slots.icon[0]" />
51-
<span v-else :class="tab.icon" />
50+
<NcVNodes :vnodes="tab.renderIcon()" />
5251
</span>
5352
{{ tab.name }}
5453
</a>
@@ -67,16 +66,6 @@
6766
<script>
6867
import NcVNodes from '../NcVNodes/index.js'
6968
70-
import Vue from 'vue'
71-
72-
const IsValidString = function(value) {
73-
return value && typeof value === 'string' && value.trim() !== ''
74-
}
75-
76-
const IsValidStringWithoutSpaces = function(value) {
77-
return IsValidString(value) && value.indexOf(' ') === -1
78-
}
79-
8069
export default {
8170
name: 'NcAppSidebarTabs',
8271
@@ -85,6 +74,15 @@ export default {
8574
NcVNodes,
8675
},
8776
77+
provide() {
78+
return {
79+
registerTab: this.registerTab,
80+
unregisterTab: this.unregisterTab,
81+
// Getter as an alternative to Vue 2.7 computed(() => this.activeTab)
82+
getActiveTab: () => this.activeTab,
83+
}
84+
},
85+
8886
props: {
8987
/**
9088
* Id of the tab to activate
@@ -107,10 +105,6 @@ export default {
107105
* The id of the currently active tab.
108106
*/
109107
activeTab: '',
110-
/**
111-
* Dummy array to react on slot changes.
112-
*/
113-
children: [],
114108
}
115109
},
116110
@@ -130,18 +124,6 @@ export default {
130124
this.updateActive()
131125
}
132126
},
133-
134-
children() {
135-
this.updateTabs()
136-
},
137-
},
138-
139-
mounted() {
140-
// Init the tabs list
141-
this.updateTabs()
142-
143-
// Let's make the children list reactive
144-
this.children = this.$children
145127
},
146128
147129
methods: {
@@ -216,62 +198,42 @@ export default {
216198
*/
217199
updateActive() {
218200
this.activeTab = this.active
219-
&& this.tabs.findIndex(tab => tab.id === this.active) !== -1
201+
&& this.tabs.some(tab => tab.id === this.active)
220202
? this.active
221203
: this.tabs.length > 0
222204
? this.tabs[0].id
223205
: ''
224206
},
225207
226-
hasMdIcon(tab) {
227-
return tab?.$slots?.icon
228-
},
229-
230208
/**
231-
* Manually update the sidebar tabs according to $slots.default
209+
* Register child tab in the tabs
210+
*
211+
* @param {object} tab - tab props (only the "id" is used actually)
232212
*/
233-
updateTabs() {
234-
if (!this.$slots.default) {
235-
this.tabs = []
236-
return
237-
}
238-
239-
// Find all valid children (AppSidebarTab, other components, text nodes, etc.)
240-
const children = this.$slots.default.filter(elem => elem.tag || elem.text.trim())
241-
242-
// Find all valid instances of AppSidebarTab
243-
const invalidTabs = []
244-
const tabs = children.reduce((tabs, tabNode) => {
245-
const tab = tabNode.componentInstance
246-
// Make sure all required props are provided and valid
247-
if (IsValidString(tab?.name)
248-
&& IsValidStringWithoutSpaces(tab?.id)
249-
&& (IsValidStringWithoutSpaces(tab?.icon) || tab?.$slots?.icon)) {
250-
tabs.push(tab)
251-
} else {
252-
invalidTabs.push(tabNode)
253-
}
254-
return tabs
255-
}, [])
256-
257-
// Tabs are optional, but you can use either tabs or non-tab-content only
258-
if (tabs.length !== 0 && tabs.length !== children.length) {
259-
Vue.util.warn('Mixing tabs and non-tab-content is not possible.')
260-
invalidTabs.map(invalid => console.debug('Ignoring invalid tab', invalid))
261-
}
262-
263-
// We sort the tabs by their order or by their name
264-
this.tabs = tabs.sort((a, b) => {
265-
const orderA = a.order || 0
266-
const orderB = b.order || 0
267-
if (orderA === orderB) {
213+
registerTab(tab) {
214+
this.tabs.push(tab)
215+
this.tabs.sort((a, b) => {
216+
if (a.order === b.order) {
268217
return OC.Util.naturalSortCompare(a.name, b.name)
269218
}
270-
return orderA - orderB
219+
return a.order - b.order
271220
})
221+
if (!this.activeTab) {
222+
this.updateActive()
223+
}
224+
},
272225
273-
// Init active tab if exists
274-
if (this.tabs.length > 0) {
226+
/**
227+
* Unregister child tab in the tabs
228+
*
229+
* @param {string} id - tab's id
230+
*/
231+
unregisterTab(id) {
232+
const tabIndex = this.tabs.findIndex((tab) => tab.id === id)
233+
if (tabIndex !== -1) {
234+
this.tabs.splice(tabIndex, 1)
235+
}
236+
if (this.activeTab === id) {
275237
this.updateActive()
276238
}
277239
},

src/components/NcAppSidebarTab/NcAppSidebarTab.vue

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,13 @@
4040
</template>
4141

4242
<script>
43+
import { h } from 'vue'
44+
4345
export default {
4446
name: 'NcAppSidebarTab',
4547
48+
inject: ['registerTab', 'unregisterTab', 'getActiveTab'],
49+
4650
props: {
4751
id: {
4852
type: String,
@@ -67,13 +71,22 @@ export default {
6771
'scroll',
6872
],
6973
74+
expose: ['id', 'name', 'icon', 'order', 'renderIcon'],
75+
7076
computed: {
71-
// TODO: implement a better way to force pass a prop fromm Sidebar
7277
isActive() {
73-
return this.$parent.activeTab === this.id
78+
return this.getActiveTab() === this.id
7479
},
7580
},
7681
82+
created() {
83+
this.registerTab(this)
84+
},
85+
86+
beforeDestroy() {
87+
this.unregisterTab(this.id)
88+
},
89+
7790
methods: {
7891
onScroll(event) {
7992
// Are we scrolled to the very bottom ?
@@ -85,6 +98,15 @@ export default {
8598
}
8699
this.$emit('scroll', event)
87100
},
101+
102+
/**
103+
* Render tab's icon from slot or icon prop
104+
*
105+
* @return {import('vue').VNode|import('vue').VNode[]}
106+
*/
107+
renderIcon() {
108+
return this.$slots.icon || this.$scopedSlots.icon?.() || h('span', { staticClass: this.icon })
109+
},
88110
},
89111
}
90112
</script>

tests/unit/components/NcAppSidebar/NcAppSidebarTabs.spec.js

Lines changed: 0 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -189,23 +189,4 @@ describe('NcAppSidebarTabs.vue', () => {
189189
})
190190
})
191191
})
192-
describe('when tabs and other elements are mixed', () => {
193-
it('Issues a warning and logs to console .', () => {
194-
mount(NcAppSidebarTabs, {
195-
slots: {
196-
default: [
197-
'<nc-app-sidebar-tab id="1" icon="icon-details" name="Tab1">Tab1</nc-app-sidebar-tab>',
198-
'<NcAppSidebarTab id="2" icon="icon-details" name="Tab2">Tab2</NcAppSidebarTab>',
199-
'<div>Non-tab-content</div>',
200-
'Test',
201-
],
202-
},
203-
stubs: {
204-
NcAppSidebarTab,
205-
},
206-
})
207-
expect(onWarning).toHaveBeenCalledTimes(1)
208-
expect(consoleDebug).toHaveBeenCalledTimes(2)
209-
})
210-
})
211192
})

0 commit comments

Comments
 (0)