Skip to content

Commit 62ad771

Browse files
committed
fix(core): preserve waffle menu peek-row hint when labels wrap
Signed-off-by: Peter Ringelmann <peter.ringelmann@nextcloud.com>
1 parent f401b29 commit 62ad771

1 file changed

Lines changed: 80 additions & 5 deletions

File tree

core/src/components/AppMenu.vue

Lines changed: 80 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
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

Comments
 (0)