Description / Background:
We have implemented a Bryntum Calendar with a sidebar containing filters like Locations, Staffs, and Time Ranges.
Requirement:
We needed to move all sidebar filters to the toolbar while retaining full existing functionality. To achieve this:
- The sidebar widgets are now displayed as floating popups when clicking corresponding toolbar buttons.
- All filters (Locations, Time Ranges) are working correctly in the toolbar.
Issue:
- The Staff filter button in the toolbar does not open the floating popup for the sidebar widget.
- No errors appear in the console.
- All other filters and calendar functionalities remain unaffected.
- We cannot identify the root cause of the problem.
Expected Behavior:
- Clicking the Staff filter button in the toolbar should open the floating sidebar widget, identical to how it works in the original sidebar.
Code Snippet (Simplified):
const sidebarConfig = useMemo(
() => ({
items: {
timeRange: {
type: "panel",
title: "Time Range",
weight: 150,
collapsible: true,
collapsed: true,
hidden: true,
bodyCls: "tight-panel",
listeners: {
expand: ({ source }: any) => {
try {
const sidebar = source?.owner;
const items = sidebar?.items || {};
Object.keys(items).forEach((k) => { if (items[k] !== source && items[k]?.collapse) items[k].collapse(); });
} catch {}
}
},
items: {
timeContainer: {
type: "container",
cls: "time-range-container",
layout: {
type: "vbox",
align: "stretch",
},
items: {
startTimeField: {},
endTimeField: {}
}
}
}
},
saveFilter: {
type: "panel",
title: "Save Filter",
weight: 200,
collapsible: true,
collapsed: true,
hidden: true,
bodyCls: "tight-panel",
listeners: {
expand: ({ source }: any) => {
try {
const sidebar = source?.owner;
const items = sidebar?.items || {};
Object.keys(items).forEach((k) => { if (items[k] !== source && items[k]?.collapse) items[k].collapse(); });
} catch {}
}
},
items: {
btnRow: {
type: "container",
cls: "my-custom-list",
layout: {
type: "hbox",
align: "start",
pack: "start",
wrap: true,
},
defaults: { margin: 0 },
style: { gap: "8px 10px" },
items: {
saveBtn: {},
resetBtn: {}
},
},
},
} as any,
locationPositionList: {
type: "panel",
title: "Locations",
weight: 300,
collapsible: true,
allowGroupSelect: true,
collapsed: true,
hidden: true,
listeners: {
expand: ({ source }: any) => {
try {
const sidebar = source?.owner;
const items = sidebar?.items || {};
Object.keys(items).forEach((k) => { if (items[k] !== source && items[k]?.collapse) items[k].collapse(); });
} catch {}
}
},
items: {
locationList: {
type: "list",
cls: "my-custom-list",
maxHeight: "30em",
displayField: "text",
valueField: "id",
multiSelect: true,
showCheckboxes: true,
allowGroupSelect: true,
selected: [],
itemTpl: (data: any) => {
const record = data?.record || data;
return record?.text || record?.name || "";
},
store: locationStoreRef.current,
onBeforeItem: ({ event }: any) => {
// Only allow checkbox clicks to select/deselect items
// This prevents the entire row from toggling selection
if (!event.target.closest('.b-icon')) {
return false;
}
},
onItem: ({ source, item, record }: any) => {
console.info("Clicked item:", record);
const { store } = source;
const { collapsed } = record.instanceMeta(store);
store.toggleCollapse(record, !collapsed);
},
},
},
} as any,
resourceFilter: {
type: "panel",
title: "Staffs",
weight: 400,
collapsible: true,
collapsed: true,
hidden: true,
listeners: {
expand: ({ source }: any) => {
try {
const sidebar = source?.owner;
const items = sidebar?.items || {};
Object.keys(items).forEach((k) => { if (items[k] !== source && items[k]?.collapse) items[k].collapse(); });
} catch {}
}
},
items: {
resourceFilter: {
type: "resourceFilter",
listeners: {
selectionChange: ({ selected }: any) => {
const ids = selected.map((rec: any) => rec.id);
setChoosenResourceIds(ids);
},
},
},
},
} as any,
},
}),
[handleBryntumDateSelect, savedFiltered]
);
const sidebarConfig = useMemo(
() => ({
items: {
timeRange: {
type: "panel",
title: "Time Range",
weight: 150,
collapsible: true,
collapsed: true,
hidden: true,
bodyCls: "tight-panel",
listeners: {
expand: ({ source }: any) => {
try {
const sidebar = source?.owner;
const items = sidebar?.items || {};
Object.keys(items).forEach((k) => { if (items[k] !== source && items[k]?.collapse) items[k].collapse(); });
} catch {}
}
},
items: {
timeContainer: {
type: "container",
cls: "time-range-container",
layout: {
type: "vbox",
align: "stretch",
},
items: {
startTimeField: {},
endTimeField: {}
}
}
}
},
saveFilter: {
type: "panel",
title: "Save Filter",
weight: 200,
collapsible: true,
collapsed: true,
hidden: true,
bodyCls: "tight-panel",
listeners: {
expand: ({ source }: any) => {
try {
const sidebar = source?.owner;
const items = sidebar?.items || {};
Object.keys(items).forEach((k) => { if (items[k] !== source && items[k]?.collapse) items[k].collapse(); });
} catch {}
}
},
items: {
btnRow: {
type: "container",
cls: "my-custom-list",
layout: {
type: "hbox",
align: "start",
pack: "start",
wrap: true,
},
defaults: { margin: 0 },
style: { gap: "8px 10px" },
items: {
saveBtn: {},
resetBtn: {}
},
},
},
} as any,
locationPositionList: {
type: "panel",
title: "Locations",
weight: 300,
collapsible: true,
allowGroupSelect: true,
collapsed: true,
hidden: true,
listeners: {
expand: ({ source }: any) => {
try {
const sidebar = source?.owner;
const items = sidebar?.items || {};
Object.keys(items).forEach((k) => { if (items[k] !== source && items[k]?.collapse) items[k].collapse(); });
} catch {}
}
},
items: {
locationList: {
type: "list",
cls: "my-custom-list",
maxHeight: "30em",
displayField: "text",
valueField: "id",
multiSelect: true,
showCheckboxes: true,
allowGroupSelect: true,
selected: [],
itemTpl: (data: any) => {
const record = data?.record || data;
return record?.text || record?.name || "";
},
store: locationStoreRef.current,
onBeforeItem: ({ event }: any) => {
// Only allow checkbox clicks to select/deselect items
// This prevents the entire row from toggling selection
if (!event.target.closest('.b-icon')) {
return false;
}
},
onItem: ({ source, item, record }: any) => {
console.info("Clicked item:", record);
const { store } = source;
const { collapsed } = record.instanceMeta(store);
store.toggleCollapse(record, !collapsed);
},
},
},
} as any,
resourceFilter: {
type: "panel",
title: "Staffs",
weight: 400,
collapsible: true,
collapsed: true,
hidden: true,
listeners: {
expand: ({ source }: any) => {
try {
const sidebar = source?.owner;
const items = sidebar?.items || {};
Object.keys(items).forEach((k) => { if (items[k] !== source && items[k]?.collapse) items[k].collapse(); });
} catch {}
}
},
items: {
resourceFilter: {
type: "resourceFilter",
listeners: {
selectionChange: ({ selected }: any) => {
const ids = selected.map((rec: any) => rec.id);
setChoosenResourceIds(ids);
},
},
},
},
} as any,
},
}),
[handleBryntumDateSelect, savedFiltered]
);
function showSidebarPanelAsPopup(key: 'timeRange'|'saveFilter'|'locationPositionList'|'resourceFilter', btnRefKey: string) {
try {
const cal: any = calendarRef.current?.instance;
if (!cal) {
console.warn('Calendar instance not found');
return;
}
const originalPanel: any = cal?.widgetMap?.sidebar?.widgetMap?.[key];
if (!originalPanel) {
console.warn(`Panel ${key} not found in sidebar widgetMap`);
return;
}
// Toggle: if same panel already active, close it and return
if (activePopupKey === key || (originalPanel as any)._isFloating) {
try {
const st = (originalPanel as any)._originalState || {};
originalPanel.setConfig?.({
floating: false,
hidden: true,
collapsible: st.collapsible !== undefined ? st.collapsible : true
});
try {
if (st.parentEl && originalPanel.element?.parentElement !== st.parentEl) {
st.parentEl.appendChild(originalPanel.element);
}
} catch {}
try {
if (st.collapsed === true) originalPanel.collapse?.(); else originalPanel.expand?.();
} catch {}
originalPanel.hide?.();
(originalPanel as any)._isFloating = false;
} catch {}
setActivePopupKey(null);
return;
}
// Close and restore any other panel first
(['timeRange','saveFilter','locationPositionList','resourceFilter'] as const).forEach(k => {
const p: any = cal?.widgetMap?.sidebar?.widgetMap?.[k];
if (p && k !== key) {
try {
p.setConfig?.({ floating: false, hidden: true });
p.hide?.();
p.collapse?.();
} catch {}
}
});
// Anchor element
const btn = cal?.tbar?.widgetMap?.[btnRefKey];
const anchorEl = btn?.element || cal.element;
if (!anchorEl) {
console.warn(`Button ${btnRefKey} not found in toolbar`);
return;
}
// Capture original state once
if (!(originalPanel as any)._originalState) {
try {
(originalPanel as any)._originalState = {
collapsible: originalPanel.collapsible,
collapsed: originalPanel.collapsed,
hidden: originalPanel.hidden,
parentEl: originalPanel.element?.parentElement || null
};
} catch {}
}
// Reconfigure the original panel to float and show by button
const width = key === 'saveFilter' ? 360 : 320;
// While floating, hard-disable collapsing to keep panel expanded
originalPanel.setConfig?.({
floating: {
anchor: true,
align: 't-b',
constrainTo: document.body,
draggable: false,
autoClose: false
},
modal: false,
closable: false,
hidden: false,
// collapse/expand are controlled explicitly to avoid recursion
collapsible: false,
width,
maxWidth: width,
minWidth: Math.min(260, width)
});
// Install one-time guards to prevent collapse while floating
if (!(originalPanel as any)._preventCollapseHandlersInstalled) {
try {
// Cancel any attempt to collapse while floating
originalPanel.on?.('beforeCollapse', () => {
return (originalPanel as any)._isFloating ? false : undefined;
});
} catch {}
(originalPanel as any)._preventCollapseHandlersInstalled = true;
}
// Ensure element is in body to detach from sidebar layout
if (originalPanel.element && originalPanel.element.parentElement !== document.body) {
try { originalPanel.appendTo?.(document.body); } catch {}
}
// Mark floating
(originalPanel as any)._isFloating = true;
originalPanel.showBy?.(anchorEl);
try {
if (originalPanel.collapsed) originalPanel.expand?.();
} catch {}
originalPanel.show?.();
// Force layout so content renders fully on first open
try {
// Any of these that exist will help trigger layout
(originalPanel as any).remeasure?.();
(originalPanel as any).updateLayout?.();
(originalPanel as any).refresh?.();
// Read to force reflow
void originalPanel.element?.offsetHeight;
} catch {}
// Extra passes to ensure inner widgets render (handles first-open blank-content case)
setTimeout(() => {
try {
(originalPanel as any).updateLayout?.();
(originalPanel as any).refresh?.();
void originalPanel.element?.offsetHeight;
} catch {}
}, 50);
setTimeout(() => {
try {
(originalPanel as any).updateLayout?.();
(originalPanel as any).refresh?.();
void originalPanel.element?.offsetHeight;
} catch {}
}, 150);
// Panel-specific first-open fixes
try {
if (key === 'locationPositionList') {
const locList = originalPanel.widgetMap?.locationList || cal?.widgetMap?.sidebar?.widgetMap?.locationPositionList?.widgetMap?.locationList;
if (locList) {
// Ensure checkboxes and tree rows render on first open
try { locList.showCheckboxes = true; } catch {}
try { locList.element?.classList?.add('b-has-checkboxes'); } catch {}
try { locList.refresh?.(); } catch {}
try { locList.updateLayout?.(); } catch {}
try { void locList.element?.offsetHeight; } catch {}
// Two RAF passes to let Bryntum paint the rows
requestAnimationFrame(() => {
try { locList.refresh?.(); locList.updateLayout?.(); } catch {}
requestAnimationFrame(() => {
try { locList.refresh?.(); locList.updateLayout?.(); } catch {}
});
});
}
} else if (key === 'resourceFilter') {
const rf = originalPanel.widgetMap?.resourceFilter || cal?.widgetMap?.sidebar?.widgetMap?.resourceFilter?.widgetMap?.resourceFilter;
if (rf) {
try { rf.refresh?.(); } catch {}
try { rf.updateLayout?.(); } catch {}
}
}
} catch {}
// Generic widget layout pass for any children inside the panel
try {
const runChildLayout = () => {
try {
originalPanel.eachWidget?.((w: any) => {
try { w.refresh?.(); } catch {}
try { w.updateLayout?.(); } catch {}
});
} catch {}
};
runChildLayout();
setTimeout(runChildLayout, 60);
setTimeout(runChildLayout, 180);
} catch {}
// Reposition helper: stick to button and constrain to viewport
const reposition = () => {
try {
originalPanel.showBy?.(anchorEl);
const el = originalPanel.element;
if (!el) return;
const rect = el.getBoundingClientRect();
const vw = window.innerWidth;
const vh = window.innerHeight;
let left = rect.left;
let top = rect.top;
if (rect.right > vw) left = vw - rect.width - 10;
if (left < 10) left = 10;
if (rect.bottom > vh) top = vh - rect.height - 10;
if (top < 10) top = 10;
if (left !== rect.left || top !== rect.top) {
el.style.left = `${left}px`;
el.style.top = `${top}px`;
}
} catch {}
};
// Initial clamp after layout
setTimeout(reposition, 50);
// Attach scroll/resize listeners (capture phase to catch container scrolls)
const scrollTargets: any[] = [window, document, cal?.element].filter(Boolean);
const onScrollOrResize = () => reposition();
scrollTargets.forEach(t => {
try { t.addEventListener('scroll', onScrollOrResize, true); } catch {}
try { t.addEventListener('resize', onScrollOrResize, true); } catch {}
});
activeAnchorRef.current = anchorEl;
setActivePopupKey(key);
const handleClickOutside = (e: MouseEvent) => {
if (!originalPanel.element?.contains(e.target as Node) && !anchorEl.contains(e.target as Node)) {
try {
// Restore sidebar behavior on close
const st = (originalPanel as any)._originalState || {};
originalPanel.setConfig?.({
floating: false,
hidden: true,
collapsible: st.collapsible !== undefined ? st.collapsible : true
});
// Move back to original parent if known
try {
if (st.parentEl && originalPanel.element?.parentElement !== st.parentEl) {
st.parentEl.appendChild(originalPanel.element);
}
} catch {}
// Restore collapse state
try {
if (st.collapsed === true) {
originalPanel.collapse?.();
} else {
originalPanel.expand?.();
}
} catch {}
originalPanel.hide?.();
(originalPanel as any)._isFloating = false;
} catch {}
setActivePopupKey(null);
document.removeEventListener('mousedown', handleClickOutside);
// Detach listeners
scrollTargets.forEach(t => {
try { t.removeEventListener('scroll', onScrollOrResize, true); } catch {}
try { t.removeEventListener('resize', onScrollOrResize, true); } catch {}
});
}
};
setTimeout(() => {
document.addEventListener('mousedown', handleClickOutside);
}, 100);
} catch (e) {
console.error('Error in showSidebarPanelAsPopup:', e);
}
}
Additional Context / Notes:
- Screenshot attached showing the toolbar layout with floating popup setup.
- All other filters (Locations, Time Ranges) are working perfectly in the toolbar.
- The issue is isolated to the Staff filter popup.
Images:
- Working another toolbar option perfectly:
https://snipboard.io/f9MLS1.jpg - Staff toolbar button no changes after click:
https://snipboard.io/S9YGmp.jpg
Request / Assistance Needed:
- Guidance on why the Staff popup does not display when using the toolbar button.
- Recommended approach to ensure floating sidebar widgets fully work for all filters, including Staff.
- Example or reference for toolbar-triggered floating sidebar widgets in React (if available).