Our powerful JS Calendar component


Post by rajkasotia »

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:

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).

Post by marcio »

Hey rajkasotia,

Thanks for reaching out.

By your description, perhaps you can use a Button widget with menu property? That allows the button to have a floating popup, like the ones you shared.

https://bryntum.com/products/calendar/docs/api/Core/widget/Button#config-menu - check the explanation on our docs, so you could use something like

menu : { type : 'popup' ... }

in your project.

Best regards,
Márcio

How to ask for help? Please read our Support Policy


Post by rajkasotia »

Hi Marcio,

Thanks for your help! It took some time, but your suggestion worked perfectly.


Post Reply