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;
}