Skip to content

Commit d0d4abb

Browse files
artem-harbourArtem Nistuley
authored andcommitted
fix: improve toolbar responsiveness
1 parent a22c0af commit d0d4abb

7 files changed

Lines changed: 103 additions & 49 deletions

File tree

packages/super-editor/src/editors/v1/components/toolbar/ButtonGroup.vue

Lines changed: 22 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -39,30 +39,37 @@ const props = defineProps({
3939
type: Boolean,
4040
default: false,
4141
},
42+
compactSideGroups: {
43+
type: Boolean,
44+
default: false,
45+
},
4246
});
4347
4448
const currentItem = ref(null);
4549
const { isHighContrastMode } = useHighContrastMode();
4650
// Matches media query from SuperDoc.vue
4751
const isMobile = window.matchMedia('(max-width: 768px)').matches;
48-
const styleMap = {
49-
left: {
50-
minWidth: '120px',
51-
justifyContent: 'flex-start',
52-
},
53-
right: {
54-
minWidth: '120px',
55-
justifyContent: 'flex-end',
56-
},
57-
default: {
52+
53+
const getPositionStyle = computed(() => {
54+
if (props.position === 'left') {
55+
return {
56+
minWidth: props.compactSideGroups ? 'auto' : '120px',
57+
justifyContent: 'flex-start',
58+
};
59+
}
60+
61+
if (props.position === 'right') {
62+
return {
63+
minWidth: props.compactSideGroups ? 'auto' : '120px',
64+
justifyContent: 'flex-end',
65+
};
66+
}
67+
68+
return {
5869
// Only grow if not on a mobile device
5970
flexGrow: isMobile ? 0 : 1,
6071
justifyContent: 'center',
61-
},
62-
};
63-
64-
const getPositionStyle = computed(() => {
65-
return styleMap[props.position] || styleMap.default;
72+
};
6673
});
6774
6875
const isButton = (item) => item.type === 'button';

packages/super-editor/src/editors/v1/components/toolbar/Toolbar.vue

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import { ref, getCurrentInstance, onMounted, onDeactivated, nextTick, computed } from 'vue';
33
import { throttle } from './helpers.js';
44
import ButtonGroup from './ButtonGroup.vue';
5+
import { RESPONSIVE_BREAKPOINTS } from './constants.js';
56
67
/**
78
* The default font-family to use for toolbar UI surfaces when no custom font is configured.
@@ -14,6 +15,8 @@ const { proxy } = getCurrentInstance();
1415
const emit = defineEmits(['command', 'toggle', 'select']);
1516
1617
let toolbarKey = ref(1);
18+
const compactSideGroups = ref(false);
19+
let containerResizeObserver = null;
1720
1821
/**
1922
* Computed property that determines the font-family to use for toolbar UI surfaces.
@@ -40,14 +43,27 @@ const getFilteredItems = (position) => {
4043
return proxy.$toolbar.getToolbarItemByGroup(position).filter((item) => !excludeButtonsList.includes(item.name.value));
4144
};
4245
46+
const updateCompactSideGroups = () => {
47+
compactSideGroups.value = proxy.$toolbar.getAvailableWidth() <= RESPONSIVE_BREAKPOINTS.lg;
48+
};
49+
4350
onMounted(() => {
4451
window.addEventListener('resize', onResizeThrottled);
4552
window.addEventListener('keydown', onKeyDown);
53+
if (typeof ResizeObserver !== 'undefined' && proxy.$toolbar.toolbarContainer) {
54+
containerResizeObserver = new ResizeObserver(() => {
55+
onResizeThrottled();
56+
});
57+
containerResizeObserver.observe(proxy.$toolbar.toolbarContainer);
58+
}
59+
updateCompactSideGroups();
4660
});
4761
4862
onDeactivated(() => {
4963
window.removeEventListener('resize', onResizeThrottled);
5064
window.removeEventListener('keydown', onKeyDown);
65+
containerResizeObserver?.disconnect();
66+
containerResizeObserver = null;
5167
});
5268
5369
const onKeyDown = async (e) => {
@@ -66,6 +82,7 @@ const onKeyDown = async (e) => {
6682
6783
const onWindowResized = async () => {
6884
await proxy.$toolbar.onToolbarResize();
85+
updateCompactSideGroups();
6986
toolbarKey.value += 1;
7087
};
7188
const onResizeThrottled = throttle(onWindowResized, 300);
@@ -107,6 +124,7 @@ const handleToolbarMousedown = (e) => {
107124
tabindex="0"
108125
v-if="showLeftSide"
109126
:toolbar-items="getFilteredItems('left')"
127+
:compact-side-groups="compactSideGroups"
110128
:ui-font-family="uiFontFamily"
111129
position="left"
112130
@command="handleCommand"
@@ -117,6 +135,7 @@ const handleToolbarMousedown = (e) => {
117135
tabindex="0"
118136
:toolbar-items="getFilteredItems('center')"
119137
:overflow-items="proxy.$toolbar.overflowItems"
138+
:compact-side-groups="compactSideGroups"
120139
:ui-font-family="uiFontFamily"
121140
position="center"
122141
@command="handleCommand"
@@ -126,6 +145,7 @@ const handleToolbarMousedown = (e) => {
126145
tabindex="0"
127146
v-if="showRightSide"
128147
:toolbar-items="getFilteredItems('right')"
148+
:compact-side-groups="compactSideGroups"
129149
:ui-font-family="uiFontFamily"
130150
position="right"
131151
@command="handleCommand"
@@ -148,12 +168,6 @@ const handleToolbarMousedown = (e) => {
148168
z-index: var(--sd-ui-toolbar-z-index, 10);
149169
}
150170
151-
@media (max-width: 1280px) {
152-
.superdoc-toolbar-group-side {
153-
min-width: auto !important;
154-
}
155-
}
156-
157171
@media (max-width: 768px) {
158172
.superdoc-toolbar {
159173
padding: 4px 10px;

packages/super-editor/src/editors/v1/components/toolbar/ToolbarButton.vue

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -291,21 +291,19 @@ const caretIcon = computed(() => {
291291
height: 10px;
292292
}
293293
294-
@media (max-width: 1280px) {
295-
.toolbar-item--doc-mode .button-label {
296-
display: none;
297-
}
294+
.toolbar-item--doc-mode-compact .button-label {
295+
display: none;
296+
}
298297
299-
.toolbar-item--doc-mode .toolbar-icon {
300-
margin-right: 5px;
301-
}
298+
.toolbar-item--doc-mode-compact .toolbar-icon {
299+
margin-right: 5px;
300+
}
302301
303-
.toolbar-item--linked-styles {
304-
width: auto !important;
305-
}
302+
.toolbar-item--linked-styles-compact {
303+
width: auto !important;
304+
}
306305
307-
.toolbar-item--linked-styles .button-label {
308-
display: none;
309-
}
306+
.toolbar-item--linked-styles-compact .button-label {
307+
display: none;
310308
}
311309
</style>

packages/super-editor/src/editors/v1/components/toolbar/constants.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,13 @@ export const TOOLBAR_FONT_SIZES = [
5454
{ label: '96', key: '96pt', props: { 'data-item': 'btn-fontSize-option' } },
5555
];
5656

57+
export const RESPONSIVE_BREAKPOINTS = {
58+
sm: 768,
59+
md: 1024,
60+
lg: 1280,
61+
xl: 1410,
62+
};
63+
5764
export const HEADLESS_ITEM_MAP = {
5865
undo: 'undo',
5966
redo: 'redo',

packages/super-editor/src/editors/v1/components/toolbar/defaultItems.js

Lines changed: 31 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import { scrollToElement } from './scroll-helpers.js';
1414

1515
import checkIconSvg from '@superdoc/common/icons/check.svg?raw';
1616
import SearchInput from './SearchInput.vue';
17-
import { TOOLBAR_FONTS, TOOLBAR_FONT_SIZES } from './constants.js';
17+
import { RESPONSIVE_BREAKPOINTS, TOOLBAR_FONTS, TOOLBAR_FONT_SIZES } from './constants.js';
1818
import { getQuickFormatList } from '@extensions/linked-styles/index.js';
1919

2020
const closeDropdown = (dropdown) => {
@@ -995,18 +995,34 @@ export const makeDefaultItems = ({
995995
}),
996996
});
997997

998-
// Responsive toolbar calculations
999-
const breakpoints = {
1000-
sm: 768,
1001-
md: 1024,
1002-
lg: 1280,
1003-
xl: 1410,
1004-
};
998+
// Responsive toolbar calculations.
999+
// `availableWidth` comes from SuperToolbar and represents either:
1000+
// - container width when `responsiveToContainer: true`
1001+
// - viewport/document width when `responsiveToContainer: false`
1002+
1003+
// Extra headroom to prevent toolbar jitter at the XL edge;
1004+
// with `<=` this effectively shifts overflow by 21px.
1005+
const XL_OVERFLOW_SAFETY_BUFFER = 20;
10051006
const stickyItemsWidth = 120;
10061007
const toolbarPadding = 32;
10071008

10081009
const itemsToHideXL = ['linkedStyles', 'clearFormatting', 'copyFormat', 'ruler'];
10091010
const itemsToHideSM = ['zoom', 'fontFamily', 'fontSize', 'redo'];
1011+
const shouldUseLgCompactStyles = availableWidth <= RESPONSIVE_BREAKPOINTS.lg;
1012+
1013+
if (shouldUseLgCompactStyles) {
1014+
documentMode.attributes.value = {
1015+
...documentMode.attributes.value,
1016+
className: `${documentMode.attributes.value.className} toolbar-item--doc-mode-compact`,
1017+
};
1018+
}
1019+
1020+
if (shouldUseLgCompactStyles) {
1021+
linkedStyles.attributes.value = {
1022+
...linkedStyles.attributes.value,
1023+
className: `${linkedStyles.attributes.value.className} toolbar-item--linked-styles-compact`,
1024+
};
1025+
}
10101026

10111027
let toolbarItems = [
10121028
undo,
@@ -1053,7 +1069,7 @@ export const makeDefaultItems = ({
10531069
}
10541070

10551071
// Hide separators on small screens
1056-
if (availableWidth <= breakpoints.md && hideButtons) {
1072+
if (availableWidth <= RESPONSIVE_BREAKPOINTS.md && hideButtons) {
10571073
toolbarItems = toolbarItems.filter((item) => item.type !== 'separator');
10581074
}
10591075

@@ -1088,7 +1104,11 @@ export const makeDefaultItems = ({
10881104
toolbarItems.forEach((item) => {
10891105
const itemWidth = controlSizes.get(item.name.value) || controlSizes.get('default');
10901106

1091-
if (availableWidth < breakpoints.xl && itemsToHideXL.includes(item.name.value) && hideButtons) {
1107+
if (
1108+
availableWidth <= RESPONSIVE_BREAKPOINTS.xl + XL_OVERFLOW_SAFETY_BUFFER &&
1109+
itemsToHideXL.includes(item.name.value) &&
1110+
hideButtons
1111+
) {
10921112
overflowItems.push(item);
10931113
if (item.name.value === 'linkedStyles') {
10941114
const linkedStylesIdx = toolbarItems.findIndex((item) => item.name.value === 'linkedStyles');
@@ -1097,7 +1117,7 @@ export const makeDefaultItems = ({
10971117
return;
10981118
}
10991119

1100-
if (availableWidth < breakpoints.sm && itemsToHideSM.includes(item.name.value) && hideButtons) {
1120+
if (availableWidth < RESPONSIVE_BREAKPOINTS.sm && itemsToHideSM.includes(item.name.value) && hideButtons) {
11011121
overflowItems.push(item);
11021122
return;
11031123
}

packages/super-editor/src/editors/v1/components/toolbar/super-toolbar.js

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -386,6 +386,16 @@ export class SuperToolbar extends EventEmitter {
386386
return this.toolbarItems.find((item) => item.name.value === name);
387387
}
388388

389+
/**
390+
* Get the width used for responsive toolbar decisions.
391+
* @returns {number} Available width in pixels
392+
*/
393+
getAvailableWidth() {
394+
const documentWidth = document.documentElement.clientWidth; // take into account the scrollbar
395+
const containerWidth = this.toolbarContainer?.offsetWidth ?? 0;
396+
return this.config.responsiveToContainer ? containerWidth : documentWidth;
397+
}
398+
389399
/**
390400
* Create toolbar items based on configuration
391401
* @private
@@ -397,9 +407,7 @@ export class SuperToolbar extends EventEmitter {
397407
* @returns {void}
398408
*/
399409
#makeToolbarItems({ superToolbar, icons, texts, fonts, hideButtons, isDev = false } = {}) {
400-
const documentWidth = document.documentElement.clientWidth; // take into account the scrollbar
401-
const containerWidth = this.toolbarContainer?.offsetWidth ?? 0;
402-
const availableWidth = this.config.responsiveToContainer ? containerWidth : documentWidth;
410+
const availableWidth = this.getAvailableWidth();
403411

404412
const { defaultItems, overflowItems } = makeDefaultItems({
405413
superToolbar,

packages/superdoc/src/dev/components/SuperdocDev.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -730,7 +730,7 @@ const init = async () => {
730730
// },
731731
// fonts: null,
732732
// hideButtons: false,
733-
// responsiveToContainer: true,
733+
responsiveToContainer: true,
734734
excludeItems: [], // ['italic', 'bold'],
735735
// texts: {},
736736
},

0 commit comments

Comments
 (0)