2929 </template >
3030 </NcEmptyContent >
3131
32- <div v-else class =" member-grid" >
32+ <VList
33+ v-else
34+ v-slot =" { item }"
35+ class =" member-list__virtual"
36+ :style =" virtualListStyle"
37+ :data =" flatList" >
3338 <MemberGridItem
34- v-for =" member in flatList"
35- :key =" `member-grid-item-${member.id}`"
36- :member =" member"
37- :is-team =" !member.isUser" />
38- </div >
39+ :key =" `member-grid-item-${item.id}`"
40+ :member =" item"
41+ :is-team =" !item.isUser" />
42+ </VList >
3943
4044 <!-- member picker -->
4145 <EntityPicker
@@ -59,6 +63,7 @@ import { showError, showWarning } from '@nextcloud/dialogs'
5963import { subscribe } from ' @nextcloud/event-bus'
6064import { t } from ' @nextcloud/l10n'
6165import { NcEmptyContent } from ' @nextcloud/vue'
66+ import { VList } from ' virtua/vue'
6267import { defineComponent } from ' vue'
6368import IconContact from ' vue-material-design-icons/AccountMultipleOutline.vue'
6469import EntityPicker from ' ../EntityPicker/EntityPicker.vue'
@@ -76,6 +81,7 @@ export default defineComponent({
7681 IconContact ,
7782 MemberGridItem ,
7883 NcEmptyContent ,
84+ VList ,
7985 },
8086
8187 mixins: [IsMobileMixin , RouterMixin ],
@@ -103,6 +109,8 @@ export default defineComponent({
103109 pickerData: [],
104110 pickerSelection: {},
105111 pickerTypes: CIRCLES_MEMBER_GROUPING ,
112+
113+ circleHeaderHeight: 0 ,
106114 }
107115 },
108116
@@ -137,14 +145,47 @@ export default defineComponent({
137145 hasMembers() {
138146 return this .flatList .length > 0
139147 },
148+
149+ virtualListStyle() {
150+ const gridBaseline = parseInt (getComputedStyle (document .documentElement ).getPropertyValue (' --default-grid-baseline' )) || 4
151+ const headerHeight = parseInt (getComputedStyle (document .documentElement ).getPropertyValue (' --header-height' )) || 50
152+ const padding = gridBaseline * 32
153+ const availableHeight = window .innerHeight - headerHeight - this .circleHeaderHeight - padding
154+ return {
155+ height: ` ${Math .max (availableHeight , 200 )}px ` ,
156+ }
157+ },
140158 },
141159
142160 mounted() {
143161 subscribe (' contacts:circles:append' , this .onShowPicker )
144162 subscribe (' guests:user:created' , this .onGuestCreated )
163+ this .measureCircleHeader ()
164+ },
165+
166+ beforeUnmount() {
167+ this .resizeObserver ?.disconnect ()
145168 },
146169
147170 methods: {
171+ /**
172+ * Measure the circle details header height from the DOM
173+ * and keep it updated via ResizeObserver.
174+ */
175+ measureCircleHeader() {
176+ const header = document .querySelector (' .circle-details__header-wrapper' )
177+ if (! header ) {
178+ return
179+ }
180+ this .circleHeaderHeight = header .getBoundingClientRect ().height
181+ this .resizeObserver = new ResizeObserver ((entries ) => {
182+ for (const entry of entries ) {
183+ this .circleHeaderHeight = entry .contentRect .height
184+ }
185+ })
186+ this .resizeObserver .observe (header )
187+ },
188+
148189 /**
149190 * Show picker and fetch for recommendations
150191 * Cache the circleId in case the url change or something
@@ -250,30 +291,13 @@ export default defineComponent({
250291
251292<style lang="scss" scoped>
252293.member-list {
253- // Make virtual scroller scrollable
254- max-height : 100% ;
255294 max-width : 900px ;
256- overflow : auto ;
257295
258296 :deep (.empty-content ) {
259297 margin : auto ;
260298 }
261299}
262300
263- .member-grid {
264- display : grid ;
265- grid-template-columns : repeat (2 , 1fr );
266- gap : 8px ;
267-
268- @media (max-width : 768px ) {
269- grid-template-columns : 1fr ;
270- }
271-
272- @media (min-width : 1200px ) {
273- grid-template-columns : repeat (3 , 1fr );
274- }
275- }
276-
277301.empty-content {
278302 height : 100% ;
279303}
0 commit comments