3333 class =" app-menu__popover"
3434 role =" menu"
3535 :aria-label =" t('core', 'Apps')" >
36- <div class =" app-menu__grid" @keydown =" onGridKeydown" >
36+ <div ref = " grid " class =" app-menu__grid" @keydown =" onGridKeydown" >
3737 <AppItem
3838 v-for =" (item , i ) in gridItems "
3939 :key =" item .id "
@@ -149,6 +149,8 @@ export default defineComponent({
149149 // skidding sign isn't auto-mirrored, so we flip it here. Snapshot
150150 // at init: Nextcloud's language doesn't change at runtime.
151151 popoverSkidding: isRTL () ? 82 : - 82 ,
152+ // Re-fires recomputeGridMaxHeight on layout changes.
153+ gridResizeObserver: null as ResizeObserver | null ,
152154 }
153155 },
154156
@@ -182,6 +184,9 @@ export default defineComponent({
182184 opened(isOpen : boolean ) {
183185 if (isOpen ) {
184186 this .focusedIndex = this .activeGridIndex ()
187+ this .$nextTick (() => this .attachGridObserver ())
188+ } else {
189+ this .detachGridObserver ()
185190 }
186191 },
187192 },
@@ -199,6 +204,7 @@ export default defineComponent({
199204 beforeUnmount() {
200205 unsubscribe (' nextcloud:app-menu.refresh' , this .setApps )
201206 ;(this .$refs .popover as { $off: (e : string , fn : () => void ) => void } | undefined )?.$off (' after-hide' , this .onPopoverAfterHide )
207+ this .detachGridObserver ()
202208 },
203209
204210 methods: {
@@ -237,6 +243,57 @@ export default defineComponent({
237243 }
238244 },
239245
246+ // NcPopover renders the slot lazily; poll for the grid ref via rAF.
247+ attachGridObserver(retries = 30 ) {
248+ if (! this .opened || retries <= 0 ) {
249+ return
250+ }
251+ const grid = this .$refs .grid as HTMLElement | undefined
252+ if (! grid ) {
253+ requestAnimationFrame (() => this .attachGridObserver (retries - 1 ))
254+ return
255+ }
256+ this .detachGridObserver ()
257+ this .gridResizeObserver = new ResizeObserver (() => this .recomputeGridMaxHeight ())
258+ this .gridResizeObserver .observe (grid )
259+ this .recomputeGridMaxHeight ()
260+ },
261+
262+ detachGridObserver() {
263+ this .gridResizeObserver ?.disconnect ()
264+ this .gridResizeObserver = null
265+ },
266+
267+ // Cap = sum of first 6 row heights + baseline × 6, so the peek of
268+ // row 7 stays constant when wraps grow rows.
269+ recomputeGridMaxHeight() {
270+ const grid = this .$refs .grid as HTMLElement | undefined
271+ if (! grid ) {
272+ return
273+ }
274+ const VISIBLE_CELLS = 24 // 4 cols × 6 visible rows
275+ const cells = grid .children
276+ if (cells .length <= VISIBLE_CELLS ) {
277+ if (grid .style .maxHeight !== ' ' ) {
278+ grid .style .maxHeight = ' '
279+ }
280+ return
281+ }
282+ const firstHidden = cells [VISIBLE_CELLS ] as HTMLElement | undefined
283+ const firstCell = cells [0 ] as HTMLElement | undefined
284+ if (! firstHidden || ! firstCell ) {
285+ return
286+ }
287+ const sumOfFirstRows = firstHidden .getBoundingClientRect ().top
288+ - firstCell .getBoundingClientRect ().top
289+ const baseline = parseFloat (getComputedStyle (grid ).getPropertyValue (' --default-grid-baseline' )) || 4
290+ const cap = ` ${sumOfFirstRows + baseline * 6 }px `
291+ // Skip identical writes — they re-fire the ResizeObserver.
292+ if (grid .style .maxHeight !== cap ) {
293+ grid .style .maxHeight = cap
294+ }
295+ },
296+
240297 // Index of the active app within `gridItems`, or 0 if none is active.
241298 activeGridIndex(): number {
242299 const idx = this .gridItems .findIndex ((app ) => app .active )
@@ -385,6 +442,16 @@ export default defineComponent({
385442 outline : none !important ;
386443 box-shadow : inset 0 0 0 2px var (--color-background-plain-text ) !important ;
387444 }
445+
446+ // Inner text slot needs min-width: 0 so the label can ellipsize.
447+ :deep (.button-vue__text ) {
448+ min-width : 0 ;
449+ }
450+
451+ // Hide on small screens (matches $breakpoint-small-mobile in @nextcloud/vue).
452+ @media only screen and (max-width : 512px ) {
453+ display : none !important ;
454+ }
388455 }
389456
390457 & __current-app-icon {
@@ -402,10 +469,18 @@ export default defineComponent({
402469 }
403470
404471 & __current-app-name {
472+ // inline-block: inline elements ignore max-width + overflow.
473+ display : inline-block ;
474+ vertical-align : middle ;
405475 font-size : var (--default-font-size );
406476 font-weight : 500 ;
407477 white-space : nowrap ;
408478 letter-spacing : -0.5px ;
479+ overflow : hidden ;
480+ text-overflow : ellipsis ;
481+ // Cap width so long titles ellipsize instead of pushing the header
482+ // icons off-screen (.header-start doesn't shrink).
483+ max-width : clamp (80px , 22vw , 320px );
409484 }
410485
411486 & __popover {
@@ -416,14 +491,14 @@ export default defineComponent({
416491 & __grid {
417492 --app-item-col-width : 69px ;
418493 --app-item-row-height : 64px ;
419- --app-menu-rows-visible : 6 ;
494+ // border-box: the JS-set max-height (see recomputeGridMaxHeight)
495+ // needs to include padding for the peek math to hold.
496+ box-sizing : border-box ;
420497 padding : calc (var (--default-grid-baseline ) * 2 );
421498 display : grid ;
422499 grid-template-columns : repeat (4 , var (--app-item-col-width ));
423500 grid-auto-rows : minmax (var (--app-item-row-height ), max-content );
424- // + baseline * 5: peek-row hint so users see that content continues
425- // below the fold.
426- max-height : calc (var (--app-item-row-height ) * var (--app-menu-rows-visible ) + var (--default-grid-baseline ) * 7 );
501+ // max-height set inline by recomputeGridMaxHeight(); CSS just owns the scroll.
427502 overflow-y : auto ;
428503
429504 // Extra top padding on first-row tiles so the hover bg reads
0 commit comments