From 40956f2830bee6d71b2439bc37661820499f645e Mon Sep 17 00:00:00 2001 From: Jacob Pierce Date: Mon, 16 Feb 2026 09:30:44 -0800 Subject: [PATCH] Replace hardcoded toolbar breakpoints with KListWithOverflow Use KDS KListWithOverflow component to dynamically collapse toolbar items into a "More" dropdown based on available space instead of fixed pixel breakpoints. - Integrate KListWithOverflow for automatic overflow detection - Add overflow hidden CSS to prevent flicker during resize - Simplify toolbar logic by removing manual breakpoint calculations Fixes #5258 Co-Authored-By: Claude Opus 4.5 --- .../TipTapEditor/components/EditorToolbar.vue | 632 ++++++++---------- 1 file changed, 297 insertions(+), 335 deletions(-) diff --git a/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/components/EditorToolbar.vue b/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/components/EditorToolbar.vue index f6f095f7a2..dd68094af5 100644 --- a/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/components/EditorToolbar.vue +++ b/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/components/EditorToolbar.vue @@ -34,274 +34,288 @@ - -
- -
- - - - -
- - -
- - - - - - - - -
- -
- - - - -
+ - -
- - - - -
- -
- - -
- - - -
+ + + + + + import { defineComponent, ref, computed, onMounted, onUnmounted, nextTick } from 'vue'; + import KListWithOverflow from 'kolibri-design-system/lib/KListWithOverflow.vue'; import { useToolbarActions } from '../composables/useToolbarActions'; import { getTipTapEditorStrings } from '../TipTapEditorStrings'; import { useDropdowns } from '../composables/useDropdowns'; @@ -326,6 +341,7 @@ export default defineComponent({ name: 'EditorToolbar', components: { + KListWithOverflow, ToolbarButton, FormatDropdown, PasteDropdown, @@ -337,27 +353,6 @@ const moreDropdown = ref(null); const moreDropdownContainer = ref(null); const isMoreDropdownOpen = ref(false); - const toolbarWidth = ref(0); - - // TODO: Maybe these shouldnt be hardcoded? - const OVERFLOW_BREAKPOINTS = { - insert: 760, - script: 710, - lists: 650, - clearFormat: 560, - clipboard: 500, - textFormat: 400, - }; - - // Categories that can overflow (in order of overflow priority) - const OVERFLOW_CATEGORIES = [ - 'insert', - 'script', - 'lists', - 'clearFormat', - 'clipboard', - 'textFormat', - ]; const { handleCopy, @@ -387,48 +382,26 @@ moreButtonText$, } = getTipTapEditorStrings(); - // Compute which categories should be visible vs in overflow - const visibleCategories = computed(() => { - return OVERFLOW_CATEGORIES.filter( - category => toolbarWidth.value >= OVERFLOW_BREAKPOINTS[category], - ); - }); - - const overflowCategories = computed(() => { - return OVERFLOW_CATEGORIES.filter(category => !visibleCategories.value.includes(category)); - }); - - // Handle resize observer - let resizeObserver = null; - - const updateToolbarWidth = () => { - if (toolbarRef.value) { - // Batch layout reads in next frame - requestAnimationFrame(() => { - toolbarWidth.value = toolbarRef.value.offsetWidth; - }); - } - }; - - const handleResize = entries => { - // Use ResizeObserver data directly - no DOM reading - requestAnimationFrame(() => { - for (const entry of entries) { - toolbarWidth.value = entry.contentRect.width; - } - }); - }; - - const handleWindowResize = () => { - requestAnimationFrame(updateToolbarWidth); - }; + // Toolbar groups in visual order (left-to-right). + // KListWithOverflow will collapse items from the end first. + // Note: Don't include dividers here - KListWithOverflow renders + // them automatically via #divider slot. + const toolbarGroups = computed(() => [ + { name: 'textFormat', ariaLabel: textStyleFormatting$() }, + { name: 'clipboard', ariaLabel: copyAndPasteActions$() }, + { name: 'clearFormat' }, + { name: 'lists', ariaLabel: listFormatting$() }, + { name: 'script', ariaLabel: scriptFormatting$() }, + { name: 'insert', ariaLabel: insertTools$() }, + ]); const onToolClick = (tool, event) => { isMoreDropdownOpen.value = false; let target = event.currentTarget; - // If the tool is in the overflow menu, we center the modal - if (!visibleCategories.value.includes('insert')) target = null; + // If the tool is in the overflow menu (clicked from dropdown), center the modal + const isFromOverflow = moreDropdown.value?.contains(event.target); + if (isFromOverflow) target = null; if (tool.name === 'image') { emit('insert-image', target); @@ -452,7 +425,7 @@ isMoreDropdownOpen.value = false; // Return focus to the more button await nextTick(); - moreButton.value?.$el?.focus(); + moreButton.value?.focus(); } else if (event.key === 'ArrowDown' || event.key === 'ArrowUp') { event.preventDefault(); const menuItems = Array.from( @@ -478,32 +451,11 @@ } }; - onMounted(async () => { - await nextTick(); - - // Initial width measurement in next frame - requestAnimationFrame(() => { - updateToolbarWidth(); - }); - - // Set up resize observer - if (toolbarRef.value && window.ResizeObserver) { - resizeObserver = new ResizeObserver(handleResize); - resizeObserver.observe(toolbarRef.value); - } else { - // Fallback to window resize listener with passive flag - window.addEventListener('resize', handleWindowResize, { passive: true }); - } - + onMounted(() => { document.addEventListener('click', handleClickOutside, { passive: true }); }); onUnmounted(() => { - if (resizeObserver) { - resizeObserver.disconnect(); - } else { - window.removeEventListener('resize', handleWindowResize); - } document.removeEventListener('click', handleClickOutside); }); @@ -513,8 +465,7 @@ moreDropdown, moreDropdownContainer, isMoreDropdownOpen, - visibleCategories, - overflowCategories, + toolbarGroups, handleCopy, handleClearFormat, onToolClick, @@ -532,11 +483,6 @@ textFormattingToolbar$, historyActions$, textFormattingOptions$, - textStyleFormatting$, - copyAndPasteActions$, - listFormatting$, - scriptFormatting$, - insertTools$, clearFormatting$, moreButtonText$, }; @@ -569,6 +515,22 @@ align-items: center; } + .overflow-list { + flex: 1; + min-width: 0; + } + + /* Clip items that wrap during resize recalculation to prevent flicker */ + .overflow-list ::v-deep .list { + overflow: hidden; + } + + .toolbar-group { + display: flex; + gap: 2px; + align-items: center; + } + .more-dropdown-container { position: relative; }