@linzjs/step-ag-grid 15.0.2 → 15.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -59,6 +59,9 @@ const GridContext = React.createContext({
59
59
  prePopupOps: () => {
60
60
  console.error("no context provider for prePopupOps");
61
61
  },
62
+ postPopupOps: () => {
63
+ console.error("no context provider for prePopupOps");
64
+ },
62
65
  externallySelectedItemsAreInSync: false,
63
66
  setApis: () => {
64
67
  console.error("no context provider for setApis");
@@ -475,1007 +478,378 @@ const GridHeaderSelect = ({ api }) => {
475
478
  } }) }));
476
479
  };
477
480
 
478
- /**
479
- * Wrapper for AgGrid to add commonly used functionality.
480
- */
481
- const Grid = ({ "data-testid": dataTestId, rowSelection = "multiple", suppressColumnVirtualization = true, theme = "ag-theme-alpine", sizeColumns = "auto", selectColumnPinned = null, ...params }) => {
482
- const { gridReady, setApis, ensureRowVisible, getFirstRowId, selectRowsById, focusByRowById, ensureSelectedRowIsVisible, autoSizeColumns, sizeColumnsToFit, externallySelectedItemsAreInSync, setExternallySelectedItemsAreInSync, isExternalFilterPresent, doesExternalFilterPass, setOnCellEditingComplete, getColDef, } = React.useContext(GridContext);
483
- const { checkUpdating, updatedDep, updatingCols } = React.useContext(GridUpdatingContext);
484
- const { prePopupOps } = React.useContext(GridContext);
485
- const gridDivRef = React.useRef(null);
486
- const lastSelectedIds = React.useRef([]);
487
- const [staleGrid, setStaleGrid] = React.useState(false);
488
- const postSortRows = usePostSortRowsHook({ setStaleGrid });
489
- /**
490
- * onContentSize should only be called at maximum twice.
491
- * Once when an empty grid is loaded.
492
- * And again when the grid has content.
493
- */
494
- const hasSetContentSize = React.useRef(false);
495
- const hasSetContentSizeEmpty = React.useRef(false);
496
- const needsAutoSize = React.useRef(false);
497
- const setInitialContentSize = React.useCallback(() => {
498
- if (!gridDivRef.current?.clientWidth) {
499
- // Don't resize grids if they are offscreen as it doesn't work.
500
- needsAutoSize.current = true;
501
- return;
502
- }
503
- const headerCellCount = gridDivRef.current?.getElementsByClassName("ag-header-cell-label")?.length;
504
- if (headerCellCount < 2) {
505
- // Don't resize grids until all the columns are visible
506
- // as `autoSizeColumns` will fail silently in this case
507
- needsAutoSize.current = true;
508
- return;
509
- }
510
- const skipHeader = sizeColumns === "auto-skip-headers" && !lodashEs.isEmpty(params.rowData);
511
- if (sizeColumns === "auto" || skipHeader) {
512
- const result = autoSizeColumns({ skipHeader, userSizedColIds: userSizedColIds.current });
513
- if (lodashEs.isEmpty(params.rowData)) {
514
- if (!hasSetContentSizeEmpty.current && result && !hasSetContentSize.current) {
515
- hasSetContentSizeEmpty.current = true;
516
- params.onContentSize && params.onContentSize(result);
517
- }
481
+ // Generate className following BEM methodology: http://getbem.com/naming/
482
+ // Modifier value can be one of the following types: boolean, string, undefined
483
+ const useBEM = ({ block, element, modifiers, className }) => React.useMemo(() => {
484
+ const blockElement = element ? `${block}__${element}` : block;
485
+ let classString = blockElement;
486
+ modifiers &&
487
+ Object.keys(modifiers).forEach((name) => {
488
+ const value = modifiers[name];
489
+ if (value)
490
+ classString += ` ${blockElement}--${value === true ? name : `${name}-${value}`}`;
491
+ });
492
+ let expandedClassName = typeof className === "function" ? className(modifiers) : className;
493
+ if (typeof expandedClassName === "string") {
494
+ expandedClassName = expandedClassName.trim();
495
+ if (expandedClassName)
496
+ classString += ` ${expandedClassName}`;
497
+ }
498
+ return classString;
499
+ }, [block, element, modifiers, className]);
500
+
501
+ // Adapted from material-ui
502
+ // https://github.com/mui-org/material-ui/blob/f996027d00e7e4bff3fc040786c1706f9c6c3f82/packages/material-ui-utils/src/useForkRef.ts
503
+ const setRef = (ref, instance) => {
504
+ if (typeof ref === "function") {
505
+ ref(instance);
506
+ }
507
+ else if (ref) {
508
+ ref.current = instance;
509
+ }
510
+ };
511
+ const useCombinedRef = (refA, refB) => {
512
+ return React.useMemo(() => {
513
+ return (instance) => {
514
+ setRef(refA, instance);
515
+ setRef(refB, instance);
516
+ };
517
+ }, [refA, refB]);
518
+ };
519
+
520
+ // Get around a warning when using useLayoutEffect on the server.
521
+ // https://github.com/reduxjs/react-redux/blob/b48d087d76f666e1c6c5a9713bbec112a1631841/src/utils/useIsomorphicLayoutEffect.js#L12
522
+ // https://gist.github.com/gaearon/e7d97cdf38a2907924ea12e4ebdf3c85
523
+ // https://github.com/facebook/react/issues/14927#issuecomment-549457471
524
+ const useIsomorphicLayoutEffect = typeof window !== "undefined" &&
525
+ typeof window.document !== "undefined" &&
526
+ typeof window.document.createElement !== "undefined"
527
+ ? React.useLayoutEffect
528
+ : React.useEffect;
529
+
530
+ const menuContainerClass = "szh-menu-container";
531
+ const menuClass = "szh-menu";
532
+ const menuButtonClass = "szh-menu-button";
533
+ const menuArrowClass = "arrow";
534
+ const menuItemClass = "item";
535
+ const menuDividerClass = "divider";
536
+ const menuHeaderClass = "header";
537
+ const menuGroupClass = "group";
538
+ const subMenuClass = "submenu";
539
+ const radioGroupClass = "radio-group";
540
+ const Keys = Object.freeze({
541
+ ENTER: "Enter",
542
+ TAB: "Tab",
543
+ ESC: "Escape",
544
+ SPACE: " ",
545
+ HOME: "Home",
546
+ END: "End",
547
+ LEFT: "ArrowLeft",
548
+ RIGHT: "ArrowRight",
549
+ UP: "ArrowUp",
550
+ DOWN: "ArrowDown",
551
+ });
552
+ const HoverActionTypes = Object.freeze({
553
+ RESET: 0,
554
+ SET: 1,
555
+ UNSET: 2,
556
+ INCREASE: 3,
557
+ DECREASE: 4,
558
+ FIRST: 5,
559
+ LAST: 6,
560
+ SET_INDEX: 7,
561
+ });
562
+ const CloseReason = Object.freeze({
563
+ CLICK: "click",
564
+ CANCEL: "cancel",
565
+ BLUR: "blur",
566
+ SCROLL: "scroll",
567
+ TAB_FORWARD: "tab_forward",
568
+ TAB_BACKWARD: "tab_backward",
569
+ });
570
+ const FocusPositions = Object.freeze({
571
+ FIRST: "first",
572
+ LAST: "last",
573
+ });
574
+ const MenuStateMap = Object.freeze({
575
+ entering: "opening",
576
+ entered: "open",
577
+ exiting: "closing",
578
+ exited: "closed",
579
+ });
580
+
581
+ const isMenuOpen = (state) => !!state && state[0] === "o";
582
+ const batchedUpdates = ReactDOM.unstable_batchedUpdates || ((callback) => callback());
583
+ const floatEqual = (a, b, diff = 0.0001) => Math.abs(a - b) < diff;
584
+ const getTransition = (transition, name) => transition === true || !!(transition && transition[name]);
585
+ function safeCall(fn, arg) {
586
+ return typeof fn === "function" ? fn(arg) : fn;
587
+ }
588
+ const internalKey = "_szhsinMenu";
589
+ const getName = (component) => component[internalKey];
590
+ const mergeProps = (target, source) => {
591
+ source &&
592
+ Object.keys(source).forEach((key) => {
593
+ const targetProp = target[key];
594
+ const sourceProp = source[key];
595
+ if (typeof sourceProp === "function" && targetProp) {
596
+ target[key] = (...arg) => {
597
+ sourceProp(...arg);
598
+ targetProp(...arg);
599
+ };
518
600
  }
519
601
  else {
520
- if (result && !hasSetContentSize.current) {
521
- hasSetContentSize.current = true;
522
- params.onContentSize && params.onContentSize(result);
523
- }
602
+ target[key] = sourceProp;
524
603
  }
604
+ });
605
+ return target;
606
+ };
607
+ const parsePadding = (paddingStr) => {
608
+ if (paddingStr == null)
609
+ return { top: 0, right: 0, bottom: 0, left: 0 };
610
+ const padding = paddingStr.trim().split(/\s+/, 4).map(parseFloat);
611
+ const top = !isNaN(padding[0]) ? padding[0] : 0;
612
+ const right = !isNaN(padding[1]) ? padding[1] : top;
613
+ return {
614
+ top,
615
+ right,
616
+ bottom: !isNaN(padding[2]) ? padding[2] : top,
617
+ left: !isNaN(padding[3]) ? padding[3] : right,
618
+ };
619
+ };
620
+ // Adapted from https://github.com/popperjs/popper-core/tree/v2.9.1/src/dom-utils
621
+ const getScrollAncestor = (node) => {
622
+ const thisWindow = (node?.ownerDocument ?? document).defaultView ?? window;
623
+ while (node) {
624
+ node = node.parentNode;
625
+ if (!node || node === thisWindow?.document?.body)
626
+ return null;
627
+ if (node instanceof Element) {
628
+ const { overflow, overflowX, overflowY } = thisWindow.getComputedStyle(node);
629
+ if (/auto|scroll|overlay|hidden/.test(overflow + overflowY + overflowX))
630
+ return node;
525
631
  }
526
- if (sizeColumns !== "none") {
527
- sizeColumnsToFit();
528
- }
529
- needsAutoSize.current = false;
530
- }, [autoSizeColumns, params, sizeColumns, sizeColumnsToFit]);
531
- const lastOwnerDocumentRef = React.useRef();
532
- /**
533
- * Auto-size windows that had deferred auto-size
534
- */
535
- useIntervalHook({
536
- callback: () => {
537
- // Check if window has been popped out and needs resize
538
- const currentDocument = gridDivRef.current?.ownerDocument;
539
- if (currentDocument !== lastOwnerDocumentRef.current) {
540
- lastOwnerDocumentRef.current = currentDocument;
541
- if (currentDocument) {
542
- needsAutoSize.current = true;
543
- }
544
- }
545
- if (needsAutoSize.current) {
546
- needsAutoSize.current = false;
547
- setInitialContentSize();
548
- }
549
- },
550
- timeoutMs: 1000,
632
+ }
633
+ return null;
634
+ };
635
+ function commonProps(isDisabled, isHovering) {
636
+ return {
637
+ "aria-disabled": isDisabled || undefined,
638
+ tabIndex: isHovering ? 0 : -1,
639
+ };
640
+ }
641
+ const indexOfNode = (nodeList, node) => lodashEs.findIndex(nodeList, node);
642
+ const focusFirstInput = (container) => {
643
+ // We can't use instanceof Element in portals, so I use querySelectorAll as a proxy here
644
+ if (!container || !("querySelectorAll" in container))
645
+ return false;
646
+ const inputs = container.querySelectorAll("input[type='text'],input:not([type]),textarea");
647
+ const input = inputs[0];
648
+ // Using focus as proxy for HTMLElement
649
+ if (!input || !("focus" in input))
650
+ return false;
651
+ input.focus();
652
+ // Text areas should start at end
653
+ // this is a proxy for instanceof HTMLTextAreaElement
654
+ if (["textarea", "text"].includes(input.type)) {
655
+ input.setSelectionRange(0, input.value.length);
656
+ }
657
+ return true;
658
+ };
659
+
660
+ const HoverItemContext = React.createContext(undefined);
661
+
662
+ const withHovering = (name, WrappedComponent) => {
663
+ const Component = React.memo(WrappedComponent);
664
+ const WithHovering = React.forwardRef((props, ref) => {
665
+ const menuItemRef = React.useRef(null);
666
+ return (jsxRuntime.jsx(Component, { ...props, menuItemRef: menuItemRef, externalRef: ref, isHovering: React.useContext(HoverItemContext) === menuItemRef.current }));
551
667
  });
552
- const previousGridReady = React.useRef(gridReady);
553
- React.useEffect(() => {
554
- if (!previousGridReady.current && gridReady) {
555
- previousGridReady.current = true;
556
- setInitialContentSize();
668
+ WithHovering.displayName = `WithHovering(${name})`;
669
+ return WithHovering;
670
+ };
671
+
672
+ const useItems = (menuRef, focusRef) => {
673
+ const [hoverItem, setHoverItem] = React.useState();
674
+ const stateRef = React.useRef({
675
+ items: [],
676
+ hoverIndex: -1,
677
+ sorted: false,
678
+ });
679
+ const mutableState = stateRef.current;
680
+ const updateItems = React.useCallback((item, isMounted) => {
681
+ const { items } = mutableState;
682
+ if (!item) {
683
+ mutableState.items = [];
557
684
  }
558
- }, [gridReady, setInitialContentSize]);
559
- /**
560
- * On data load select the first row of the grid if required.
561
- */
562
- const hasSelectedFirstItem = React.useRef(false);
563
- React.useEffect(() => {
564
- if (!gridReady || hasSelectedFirstItem.current || !params.rowData || !externallySelectedItemsAreInSync)
565
- return;
566
- hasSelectedFirstItem.current = true;
567
- if (isNotEmpty(params.rowData) && lodashEs.isEmpty(params.externalSelectedItems)) {
568
- const firstRowId = getFirstRowId();
569
- if (params.autoSelectFirstRow) {
570
- selectRowsById([firstRowId]);
685
+ else if (isMounted) {
686
+ items.push(item);
687
+ }
688
+ else {
689
+ const index = items.indexOf(item);
690
+ if (index > -1) {
691
+ items.splice(index, 1);
692
+ if (item.contains(document.activeElement)) {
693
+ focusRef?.current?.focus();
694
+ setHoverItem(undefined);
695
+ }
571
696
  }
572
- else {
573
- focusByRowById(firstRowId);
697
+ }
698
+ mutableState.hoverIndex = -1;
699
+ mutableState.sorted = false;
700
+ }, [mutableState, focusRef]);
701
+ const dispatch = React.useCallback((actionType, item, nextIndex) => {
702
+ const { items, hoverIndex } = mutableState;
703
+ const sortItems = () => {
704
+ if (mutableState.sorted)
705
+ return;
706
+ const orderedNodes = menuRef.current.querySelectorAll(".szh-menu__item");
707
+ items.sort((a, b) => indexOfNode(orderedNodes, a) - indexOfNode(orderedNodes, b));
708
+ mutableState.sorted = true;
709
+ };
710
+ let index = -1;
711
+ let newItem = undefined;
712
+ let newItemFn = undefined;
713
+ switch (actionType) {
714
+ case HoverActionTypes.RESET:
715
+ break;
716
+ case HoverActionTypes.SET:
717
+ newItem = item;
718
+ break;
719
+ case HoverActionTypes.UNSET:
720
+ newItemFn = (prevItem) => (prevItem === item ? undefined : prevItem);
721
+ break;
722
+ case HoverActionTypes.FIRST:
723
+ sortItems();
724
+ index = 0;
725
+ newItem = items[index];
726
+ break;
727
+ case HoverActionTypes.LAST:
728
+ sortItems();
729
+ index = items.length - 1;
730
+ newItem = items[index];
731
+ break;
732
+ case HoverActionTypes.SET_INDEX:
733
+ if (typeof nextIndex !== "number")
734
+ break;
735
+ sortItems();
736
+ index = nextIndex;
737
+ newItem = items[index];
738
+ lodashEs.defer(() => newItem.scrollIntoView({ behavior: "smooth", block: "nearest", inline: "center" }));
739
+ break;
740
+ case HoverActionTypes.INCREASE:
741
+ sortItems();
742
+ index = hoverIndex;
743
+ if (index < 0)
744
+ index = items.indexOf(item);
745
+ index++;
746
+ if (index >= items.length)
747
+ index = 0;
748
+ newItem = items[index];
749
+ focusFirstInput(newItem);
750
+ break;
751
+ case HoverActionTypes.DECREASE: {
752
+ sortItems();
753
+ index = hoverIndex;
754
+ if (index < 0)
755
+ index = items.indexOf(item);
756
+ index--;
757
+ if (index < 0)
758
+ index = items.length - 1;
759
+ newItem = items[index];
760
+ focusFirstInput(newItem);
761
+ break;
574
762
  }
763
+ default:
764
+ if (process.env.NODE_ENV !== "production")
765
+ throw new Error(`[React-Menu] Unknown hover action type: ${actionType}`);
575
766
  }
576
- }, [
577
- externallySelectedItemsAreInSync,
578
- focusByRowById,
579
- gridReady,
580
- params.externalSelectedItems,
581
- params.autoSelectFirstRow,
582
- params.rowData,
583
- selectRowsById,
584
- getFirstRowId,
585
- ]);
586
- /**
587
- * AgGrid checkbox select does not pass clicks within cell but not on the checkbox to checkbox.
588
- * This passes the event to the checkbox when you click anywhere in the cell.
589
- */
590
- const clickSelectorCheckboxWhenContainingCellClicked = React.useCallback(({ event }) => {
591
- if (!event)
592
- return;
593
- const input = event.target.querySelector("input");
594
- input?.dispatchEvent(event);
595
- }, []);
596
- /**
597
- * Ensure external selected items list is in sync with panel.
598
- */
599
- const synchroniseExternalStateToGridSelection = React.useCallback(({ api }) => {
600
- if (!params.externalSelectedItems || !params.setExternalSelectedItems) {
601
- setExternallySelectedItemsAreInSync(true);
767
+ if (!newItem && !newItemFn)
768
+ index = -1;
769
+ setHoverItem(newItem ?? newItemFn);
770
+ mutableState.hoverIndex = index;
771
+ }, [menuRef, mutableState]);
772
+ return { hoverItem, dispatch, updateItems };
773
+ };
774
+
775
+ const useItemEffect = (isDisabled, menuItemRef, updateItems) => {
776
+ useIsomorphicLayoutEffect(() => {
777
+ if (!menuItemRef)
602
778
  return;
779
+ if (process.env.NODE_ENV !== "production" && !updateItems) {
780
+ throw new Error(`[React-Menu] This menu item or submenu should be rendered under a menu: ${menuItemRef.current.outerHTML}`);
603
781
  }
604
- const selectedRows = api.getSelectedRows();
605
- // We don't want to update selected Items if it hasn't changed to prevent excess renders
606
- if (params.externalSelectedItems.length != selectedRows.length ||
607
- isNotEmpty(lodashEs.xorBy(selectedRows, params.externalSelectedItems, (row) => row.id))) {
608
- setExternallySelectedItemsAreInSync(false);
609
- params.setExternalSelectedItems([...selectedRows]);
782
+ if (isDisabled)
783
+ return;
784
+ const item = menuItemRef.current;
785
+ updateItems(item, true);
786
+ return () => {
787
+ updateItems(item);
788
+ };
789
+ }, [isDisabled, menuItemRef, updateItems]);
790
+ };
791
+
792
+ const ItemSettingsContext = React.createContext({
793
+ submenuOpenDelay: 0,
794
+ submenuCloseDelay: 0,
795
+ });
796
+
797
+ const MenuListItemContext = React.createContext({
798
+ dispatch: () => { },
799
+ updateItems: () => { },
800
+ setOpenSubmenuCount: () => 0,
801
+ });
802
+
803
+ // This hook includes some common stateful logic in MenuItem and FocusableItem
804
+ const useItemState = (menuItemRef, focusRef, isHovering, isDisabled) => {
805
+ const { submenuCloseDelay } = React.useContext(ItemSettingsContext);
806
+ const { isParentOpen, isSubmenuOpen, dispatch, updateItems } = React.useContext(MenuListItemContext);
807
+ const timeoutId = React.useRef();
808
+ const setHover = () => {
809
+ !isHovering && !isDisabled && dispatch(HoverActionTypes.SET, menuItemRef?.current, 0);
810
+ };
811
+ const unsetHover = () => {
812
+ !isDisabled && dispatch(HoverActionTypes.UNSET, menuItemRef?.current, 0);
813
+ };
814
+ const onBlur = (e) => {
815
+ // Focus has moved out of the entire item
816
+ // It handles situation such as clicking on a sibling disabled menu item
817
+ if (isHovering && !e.currentTarget.contains(e.relatedTarget))
818
+ unsetHover();
819
+ };
820
+ const onPointerMove = () => {
821
+ if (isSubmenuOpen) {
822
+ if (!timeoutId.current)
823
+ timeoutId.current = setTimeout(() => {
824
+ timeoutId.current = undefined;
825
+ setHover();
826
+ }, submenuCloseDelay);
610
827
  }
611
828
  else {
612
- setExternallySelectedItemsAreInSync(true);
829
+ setHover();
613
830
  }
614
- }, [params, setExternallySelectedItemsAreInSync]);
615
- /**
616
- * Synchronise externally selected items to grid.
617
- * If new ids are selected scroll them into view.
618
- */
619
- const synchroniseExternallySelectedItemsToGrid = React.useCallback(() => {
620
- if (!gridReady)
621
- return;
622
- if (!params.externalSelectedItems) {
623
- setExternallySelectedItemsAreInSync(true);
624
- return;
831
+ };
832
+ const onPointerLeave = (_, keepHover) => {
833
+ if (timeoutId.current) {
834
+ clearTimeout(timeoutId.current);
835
+ timeoutId.current = undefined;
625
836
  }
626
- const selectedIds = params.externalSelectedItems.map((row) => row.id);
627
- const lastNewId = lodashEs.last(lodashEs.difference(selectedIds, lastSelectedIds.current));
628
- if (lastNewId != null) {
629
- ensureRowVisible(lastNewId);
837
+ !keepHover && unsetHover();
838
+ };
839
+ useItemEffect(isDisabled, menuItemRef, updateItems);
840
+ React.useEffect(() => () => clearTimeout(timeoutId.current), []);
841
+ React.useEffect(() => {
842
+ // Don't set focus when parent menu is closed, otherwise focus will be lost
843
+ // and onBlur event will be fired with relatedTarget setting as null.
844
+ if (isHovering && isParentOpen) {
845
+ focusRef?.current && focusRef.current.focus();
630
846
  }
631
- lastSelectedIds.current = selectedIds;
632
- selectRowsById(selectedIds);
633
- setExternallySelectedItemsAreInSync(true);
634
- }, [gridReady, params.externalSelectedItems, ensureRowVisible, selectRowsById, setExternallySelectedItemsAreInSync]);
635
- /**
636
- * Combine grid and cell editable into one function
637
- */
638
- const combineEditables = (...editables) => (params) => {
639
- const results = editables.map((editable) => fnOrVar(editable, params));
640
- // If editable is not set anywhere then it's non-editable
641
- if (results.every((v) => v == null))
642
- return false;
643
- // If any editable value is or returns false then it's non-editable
644
- return !results.some((v) => v == false);
645
- };
646
- /**
647
- * Synchronise externally selected items to grid on externalSelectedItems change
648
- */
649
- React.useEffect(synchroniseExternallySelectedItemsToGrid, [synchroniseExternallySelectedItemsToGrid]);
650
- /**
651
- * Add selectable column to colDefs. Adjust column defs to block fit for auto sized columns.
652
- */
653
- const columnDefs = React.useMemo(() => {
654
- const adjustColDefs = params.columnDefs.map((colDef) => {
655
- const colDefEditable = colDef.editable;
656
- const editable = combineEditables(params.readOnly !== true, params.defaultColDef?.editable, colDefEditable);
657
- return {
658
- ...colDef,
659
- editable,
660
- cellClassRules: {
661
- ...colDef.cellClassRules,
662
- "GridCell-readonly": (ccp) => !editable(ccp),
663
- },
664
- };
665
- });
666
- return params.selectable
667
- ? [
668
- {
669
- colId: "selection",
670
- editable: false,
671
- minWidth: 42,
672
- maxWidth: 42,
673
- pinned: selectColumnPinned,
674
- headerComponentParams: {
675
- exportable: false,
676
- },
677
- checkboxSelection: true,
678
- headerComponent: rowSelection === "multiple" ? GridHeaderSelect : null,
679
- suppressHeaderKeyboardEvent: (e) => {
680
- if ((e.event.key === "Enter" || e.event.key === " ") && !e.event.repeat) {
681
- if (lodashEs.isEmpty(e.api.getSelectedRows())) {
682
- e.api.selectAllFiltered();
683
- }
684
- else {
685
- e.api.deselectAll();
686
- }
687
- return true;
688
- }
689
- return false;
690
- },
691
- onCellClicked: clickSelectorCheckboxWhenContainingCellClicked,
692
- },
693
- ...adjustColDefs,
694
- ]
695
- : adjustColDefs;
696
- }, [
697
- params.columnDefs,
698
- params.selectable,
699
- params.readOnly,
700
- params.defaultColDef?.editable,
701
- selectColumnPinned,
702
- rowSelection,
703
- clickSelectorCheckboxWhenContainingCellClicked,
704
- ]);
705
- /**
706
- * When grid is ready set the apis to the grid context and sync selected items to grid.
707
- */
708
- const onGridReady = React.useCallback((event) => {
709
- setApis(event.api, event.columnApi, dataTestId);
710
- synchroniseExternallySelectedItemsToGrid();
711
- }, [dataTestId, setApis, synchroniseExternallySelectedItemsToGrid]);
712
- /**
713
- * When the grid is being initialized the data may be empty.
714
- * This will resize columns when we have at least one row.
715
- */
716
- const previousRowDataLength = React.useRef(0);
717
- const onRowDataChanged = React.useCallback(() => {
718
- const length = params.rowData?.length ?? 0;
719
- if (previousRowDataLength.current !== length) {
720
- setInitialContentSize();
721
- previousRowDataLength.current = length;
722
- }
723
- if (lastUpdatedDep.current === updatedDep || lodashEs.isEmpty(colIdsEdited.current))
724
- return;
725
- lastUpdatedDep.current = updatedDep;
726
- // Don't update while there are spinners
727
- if (!lodashEs.isEmpty(updatingCols()))
728
- return;
729
- const skipHeader = sizeColumns === "auto-skip-headers";
730
- autoSizeColumns({ skipHeader, userSizedColIds: userSizedColIds.current, colIds: colIdsEdited.current });
731
- colIdsEdited.current.clear();
732
- }, [autoSizeColumns, params.rowData?.length, setInitialContentSize, sizeColumns, updatedDep, updatingCols]);
733
- /**
734
- * Show/hide no rows overlay when model changes.
735
- */
736
- const isShowingNoRowsOverlay = React.useRef(false);
737
- const onModelUpdated = React.useCallback((event) => {
738
- if (event.api.getDisplayedRowCount() === 0) {
739
- if (!isShowingNoRowsOverlay.current) {
740
- event.api.showNoRowsOverlay();
741
- isShowingNoRowsOverlay.current = true;
742
- }
743
- }
744
- else {
745
- if (isShowingNoRowsOverlay.current) {
746
- event.api.hideOverlay();
747
- isShowingNoRowsOverlay.current = false;
748
- }
749
- }
750
- }, []);
751
- /**
752
- * Force-refresh all selected rows to re-run class function, to update selection highlighting
753
- */
754
- const refreshSelectedRows = React.useCallback((event) => {
755
- event.api.refreshCells({
756
- force: true,
757
- rowNodes: event.api.getSelectedNodes(),
758
- });
759
- }, []);
760
- /**
761
- * Make sure node is selected for editing and start edit
762
- */
763
- const startCellEditing = React.useCallback((event) => {
764
- prePopupOps();
765
- if (!event.node.isSelected()) {
766
- event.node.setSelected(true, true);
767
- }
768
- // Cell already being edited, so don't re-edit until finished
769
- if (checkUpdating([event.colDef.field ?? ""], event.data.id)) {
770
- return;
771
- }
772
- if (event.rowIndex !== null) {
773
- event.api.startEditingCell({
774
- rowIndex: event.rowIndex,
775
- colKey: event.column.getColId(),
776
- });
777
- }
778
- }, [checkUpdating, prePopupOps]);
779
- /**
780
- * Handle double click edit
781
- */
782
- const onCellDoubleClick = React.useCallback((event) => {
783
- if (!invokeEditAction(event))
784
- startCellEditing(event);
785
- }, [startCellEditing]);
786
- /**
787
- * Handle single click edits
788
- */
789
- const onCellClicked = React.useCallback((event) => {
790
- if (event.colDef?.cellRendererParams?.singleClickEdit) {
791
- startCellEditing(event);
792
- }
793
- }, [startCellEditing]);
794
- /**
795
- * If cell has an edit action invoke it (if editable)
796
- */
797
- const invokeEditAction = (e) => {
798
- const editAction = e.colDef?.cellRendererParams?.editAction;
799
- if (!editAction)
800
- return false;
801
- const editable = fnOrVar(e.colDef?.editable, e);
802
- if (editable) {
803
- if (!e.node.isSelected()) {
804
- e.node.setSelected(true, true);
805
- }
806
- editAction([e.data, ...e.api.getSelectedRows().filter((row) => row.id !== e.data.id)]);
807
- }
808
- return true;
809
- };
810
- /**
811
- * Start editing on pressing Enter
812
- */
813
- const onCellKeyPress = React.useCallback((e) => {
814
- if (e.event.key === "Enter") {
815
- if (!invokeEditAction(e))
816
- startCellEditing(e);
817
- }
818
- }, [startCellEditing]);
819
- /**
820
- * Once the grid has auto-sized we want to run fit to fit the grid in its container,
821
- * but we don't want the non-flex auto-sized columns to "fit" size, so suppressSizeToFit is set to true.
822
- */
823
- const columnDefsAdjusted = React.useMemo(() => {
824
- const adjustColDefOrGroup = (colDef) => "children" in colDef ? adjustGroupColDef(colDef) : adjustColDef(colDef);
825
- const adjustGroupColDef = (colDef) => ({
826
- ...colDef,
827
- children: colDef.children.map((colDef) => adjustColDefOrGroup(colDef)),
828
- });
829
- const adjustColDef = (colDef) => ({
830
- ...colDef,
831
- suppressSizeToFit: (sizeColumns === "auto" || sizeColumns === "auto-skip-headers") && !colDef.flex,
832
- sortable: colDef.sortable && params.defaultColDef?.sortable !== false,
833
- });
834
- return columnDefs.map((colDef) => adjustColDefOrGroup(colDef));
835
- }, [columnDefs, params.defaultColDef?.sortable, sizeColumns]);
836
- /**
837
- * Set of colIds that need auto-sizing.
838
- */
839
- const colIdsEdited = React.useRef(new Set());
840
- const lastUpdatedDep = React.useRef(updatedDep);
841
- /**
842
- * When cell editing has completed the colId as needing auto-sizing
843
- */
844
- React.useEffect(() => {
845
- if (lastUpdatedDep.current === updatedDep)
846
- return;
847
- lastUpdatedDep.current = updatedDep;
848
- const colIds = updatingCols();
849
- // Updating possibly completed
850
- if (lodashEs.isEmpty(colIds)) {
851
- // Columns to resize?
852
- if (!lodashEs.isEmpty(colIdsEdited.current)) {
853
- const skipHeader = sizeColumns === "auto-skip-headers";
854
- if (sizeColumns === "auto" || skipHeader) {
855
- lodashEs.defer(() => autoSizeColumns({
856
- skipHeader,
857
- userSizedColIds: userSizedColIds.current,
858
- colIds: colIdsEdited.current,
859
- }));
860
- }
861
- colIdsEdited.current.clear();
862
- }
863
- }
864
- else {
865
- // Updates not complete, add them to a list of columns needing resize
866
- colIds.forEach((colId) => {
867
- if (colId && !getColDef(colId)?.flex) {
868
- colIdsEdited.current.add(colId);
869
- }
870
- });
871
- }
872
- }, [autoSizeColumns, getColDef, sizeColumns, updatedDep, updatingCols]);
873
- /**
874
- * Resize columns to fit if required on window/container resize
875
- */
876
- const onGridSizeChanged = React.useCallback(() => {
877
- if (sizeColumns !== "none") {
878
- sizeColumnsToFit();
879
- }
880
- }, [sizeColumns, sizeColumnsToFit]);
881
- /**
882
- * Set of column Id's that are prevented from auto-sizing as they are user set
883
- */
884
- const userSizedColIds = React.useRef(new Set());
885
- /**
886
- * Lock/unlock column width on user edit/reset.
887
- */
888
- const onColumnResized = React.useCallback((e) => {
889
- const colId = e.column?.getColId();
890
- if (colId == null)
891
- return;
892
- switch (e.source) {
893
- case "uiColumnDragged":
894
- userSizedColIds.current.add(colId);
895
- break;
896
- case "autosizeColumns":
897
- userSizedColIds.current.delete(colId);
898
- break;
899
- }
900
- }, []);
901
- setOnCellEditingComplete(params.onCellEditingComplete);
902
- return (jsxRuntime.jsx("div", { "data-testid": dataTestId, className: clsx("Grid-container", theme, staleGrid && "Grid-sortIsStale", gridReady && params.rowData && "Grid-ready"), children: jsxRuntime.jsx("div", { style: { flex: 1 }, ref: gridDivRef, children: jsxRuntime.jsx(agGridReact.AgGridReact, { rowHeight: params.rowHeight, animateRows: params.animateRows, rowClassRules: params.rowClassRules, getRowId: (params) => `${params.data.id}`, suppressRowClickSelection: true, rowSelection: rowSelection, suppressBrowserResizeObserver: true, onGridSizeChanged: onGridSizeChanged, suppressColumnVirtualisation: suppressColumnVirtualization, suppressClickEdit: true, onColumnVisible: () => {
903
- setInitialContentSize();
904
- }, onRowDataChanged: onRowDataChanged, onCellKeyPress: onCellKeyPress, onCellClicked: onCellClicked, onCellDoubleClicked: onCellDoubleClick, onCellEditingStarted: refreshSelectedRows, domLayout: params.domLayout, onColumnResized: onColumnResized, defaultColDef: { minWidth: 48, ...lodashEs.omit(params.defaultColDef, ["editable"]) }, columnDefs: columnDefsAdjusted, rowData: params.rowData, noRowsOverlayComponent: GridNoRowsOverlay, noRowsOverlayComponentParams: {
905
- rowData: params.rowData,
906
- noRowsOverlayText: params.noRowsOverlayText,
907
- }, onModelUpdated: onModelUpdated, onGridReady: onGridReady, onSortChanged: ensureSelectedRowIsVisible, postSortRows: params.postSortRows ?? postSortRows, onSelectionChanged: synchroniseExternalStateToGridSelection, onColumnMoved: params.onColumnMoved, alwaysShowVerticalScroll: params.alwaysShowVerticalScroll, isExternalFilterPresent: isExternalFilterPresent, doesExternalFilterPass: doesExternalFilterPass, maintainColumnOrder: true }) }) }));
908
- };
909
-
910
- const GridPopoverContext = React.createContext({
911
- anchorRef: { current: null },
912
- saving: false,
913
- setSaving: () => { },
914
- field: "",
915
- value: null,
916
- data: {},
917
- selectedRows: [],
918
- updateValue: async () => false,
919
- formatValue: () => "! No gridPopoverContextProvider !",
920
- });
921
- const useGridPopoverContext = () => React.useContext(GridPopoverContext);
922
-
923
- const GridPopoverContextProvider = ({ props, children }) => {
924
- const { getFilteredSelectedRows, updatingCells } = React.useContext(GridContext);
925
- const anchorRef = React.useRef(props.eGridCell);
926
- const hasSaved = React.useRef(false);
927
- const [saving, setSaving] = React.useState(false);
928
- const { colDef } = props;
929
- const { cellEditorParams } = colDef;
930
- const multiEdit = cellEditorParams?.multiEdit ?? false;
931
- // Then item that is clicked on will always be first in the list
932
- const selectedRows = React.useMemo(() => multiEdit ? lodashEs.sortBy(getFilteredSelectedRows(), (row) => row.id !== props.data.id) : [props.data], [getFilteredSelectedRows, multiEdit, props.data]);
933
- const field = props.colDef?.field ?? "";
934
- const updateValue = React.useCallback(async (saveFn, tabDirection) => {
935
- if (hasSaved.current)
936
- return true;
937
- hasSaved.current = true;
938
- return saving ? false : await updatingCells({ selectedRows, field }, saveFn, setSaving, tabDirection);
939
- }, [field, saving, selectedRows, updatingCells]);
940
- return (jsxRuntime.jsx(GridPopoverContext.Provider, { value: {
941
- anchorRef: anchorRef,
942
- saving,
943
- setSaving,
944
- selectedRows,
945
- field,
946
- data: props.data,
947
- value: props.value,
948
- updateValue,
949
- formatValue: props.formatValue,
950
- }, children: children }));
951
- };
952
-
953
- const GridCellMultiSelectClassRules = {
954
- "ag-selected-for-edit": (params) => {
955
- const { api, node, colDef } = params;
956
- const cep = colDef.cellEditorSelector
957
- ? colDef.cellEditorSelector(params)?.params
958
- : colDef.cellEditorParams;
959
- return (cep?.multiEdit &&
960
- api
961
- .getSelectedNodes()
962
- .map((row) => row.id)
963
- .includes(node.id) &&
964
- api.getEditingCells().some((cell) => cell.column.getColDef() === colDef));
965
- },
966
- };
967
-
968
- const GridIcon = (props) => (jsxRuntime.jsx(lui.LuiIcon, { name: props.icon, title: props.title, alt: props.title, size: props.size ?? "md", className: clsx(`AgGridGenericCellRenderer-${props.icon}Icon`, props.className, props.disabled && "GridIcon-disabled") }));
969
-
970
- const GridLoadableCell = () => (jsxRuntime.jsx(lui.LuiMiniSpinner, { size: 22, divProps: { className: "GridLoadableCell-container", role: "status", "aria-label": "Loading" } }));
971
-
972
- const GridCellRenderer = (props) => {
973
- const { checkUpdating } = React.useContext(GridUpdatingContext);
974
- const colDef = props.colDef;
975
- const rendererParams = colDef.cellRendererParams;
976
- const warningFn = rendererParams?.warning;
977
- let warningText = warningFn ? warningFn(props) : undefined;
978
- const infoFn = rendererParams?.info;
979
- let infoText = infoFn ? infoFn(props) : undefined;
980
- if (Array.isArray(warningText))
981
- warningText = warningText.join("\n");
982
- if (Array.isArray(infoText))
983
- infoText = infoText.join("\n");
984
- return checkUpdating(colDef.field ?? colDef.colId ?? "", props.data.id) ? (jsxRuntime.jsx(GridLoadableCell, {})) : (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [!!warningText && (jsxRuntime.jsx(GridIcon, { icon: "ic_warning_outline", title: typeof warningText === "string" ? warningText : "Warning" })), !!infoText && jsxRuntime.jsx(GridIcon, { icon: "ic_info_outline", title: typeof infoText === "string" ? infoText : "Info" }), jsxRuntime.jsx("div", { className: "GridCell-container", children: colDef.cellRendererParams?.originalCellRenderer ? (jsxRuntime.jsx(colDef.cellRendererParams.originalCellRenderer, { ...props })) : (jsxRuntime.jsx("span", { title: props.valueFormatted ?? undefined, children: props.valueFormatted })) }), fnOrVar(colDef.editable, props) && rendererParams?.rightHoverElement && (jsxRuntime.jsx("div", { className: "GridCell-hoverRight", children: rendererParams?.rightHoverElement }))] }));
985
- };
986
- const suppressCellKeyboardEvents = (e) => {
987
- const shortcutKeys = e.colDef.cellRendererParams?.shortcutKeys ?? {};
988
- const exec = shortcutKeys[e.event.key];
989
- if (!e.editing && !e.event.repeat && e.event.type === "keypress" && exec) {
990
- const editable = fnOrVar(e.colDef?.editable, e);
991
- return editable ? exec(e) ?? true : true;
992
- }
993
- // It's important that aggrid doesn't trigger edit on enter
994
- // as the incorrect selected rows will be returned
995
- return !["ArrowLeft", "ArrowRight", "ArrowDown", "ArrowUp", "Tab", " ", "Home", "End", "PageUp", "PageDown"].includes(e.event.key);
996
- };
997
- const generateFilterGetter = (field, filterValueGetter, valueFormatter) => {
998
- if (filterValueGetter)
999
- return filterValueGetter;
1000
- // aggrid will default to valueGetter
1001
- if (typeof valueFormatter !== "function" || !field)
1002
- return undefined;
1003
- return (params) => {
1004
- const value = params.getValue(field);
1005
- let formattedValue = valueFormatter({ ...params, value });
1006
- // Search for null values using standard dash
1007
- if (formattedValue === "–")
1008
- formattedValue += " -";
1009
- // Search by raw value as well as formatted
1010
- const gotValue = ["string", "number"].includes(typeof value) ? value : undefined;
1011
- return (formattedValue + (gotValue != null && formattedValue != gotValue ? " " + gotValue : "")) //
1012
- .replaceAll(/\s+/g, " ")
1013
- .trim();
1014
- };
1015
- };
1016
- /*
1017
- * All cells should use this.
1018
- */
1019
- const GridCell = (props, custom) => {
1020
- // Generate a default filter value getter which uses the formatted value plus
1021
- // the editable value if it's a string and different from the formatted value.
1022
- // This is so that e.g. bearings can be searched for by DMS or raw number.
1023
- const valueFormatter = props.valueFormatter;
1024
- const filterValueGetter = generateFilterGetter(props.field, props.filterValueGetter, valueFormatter);
1025
- const exportable = props.exportable;
1026
- // Can't leave this here ag-grid will complain
1027
- delete props.exportable;
1028
- return {
1029
- colId: props.field ?? props.field,
1030
- headerTooltip: props.headerName,
1031
- sortable: !!(props?.field || props?.valueGetter),
1032
- resizable: true,
1033
- editable: props.editable ?? false,
1034
- ...(custom?.editor && {
1035
- cellClassRules: GridCellMultiSelectClassRules,
1036
- editable: props.editable ?? true,
1037
- cellEditor: GenericCellEditorComponentWrapper(custom?.editor),
1038
- }),
1039
- suppressKeyboardEvent: suppressCellKeyboardEvents,
1040
- ...(custom?.editorParams && {
1041
- cellEditorParams: {
1042
- ...custom.editorParams,
1043
- multiEdit: custom.multiEdit,
1044
- preventAutoEdit: custom.preventAutoEdit ?? false,
1045
- },
1046
- }),
1047
- // If there's a valueFormatter and no filterValueGetter then create a filterValueGetter
1048
- filterValueGetter,
1049
- // Default value formatter, otherwise react freaks out on objects
1050
- valueFormatter: (params) => {
1051
- if (params.value == null)
1052
- return "–";
1053
- const types = ["number", "boolean", "string"];
1054
- if (types.includes(typeof params.value))
1055
- return `${params.value}`;
1056
- else
1057
- return JSON.stringify(params.value);
1058
- },
1059
- ...props,
1060
- cellRenderer: GridCellRenderer,
1061
- cellRendererParams: {
1062
- originalCellRenderer: props.cellRenderer,
1063
- ...props.cellRendererParams,
1064
- },
1065
- headerComponentParams: {
1066
- exportable,
1067
- ...props.headerComponentParams,
1068
- },
1069
- };
1070
- };
1071
- const GenericCellEditorComponentWrapper = (editor) => {
1072
- const obj = { editor };
1073
- return React.forwardRef(function GenericCellEditorComponentFr(cellEditorParams, _) {
1074
- const valueFormatted = cellEditorParams.formatValue
1075
- ? cellEditorParams.formatValue(cellEditorParams.value)
1076
- : "Missing formatter";
1077
- return (jsxRuntime.jsxs(GridPopoverContextProvider, { props: cellEditorParams, children: [jsxRuntime.jsx(cellEditorParams.colDef.cellRenderer, { ...cellEditorParams, valueFormatted: valueFormatted, ...cellEditorParams.colDef.cellRendererParams }), obj.editor && jsxRuntime.jsx(obj.editor, { ...cellEditorParams })] }));
1078
- });
1079
- };
1080
-
1081
- const GridCellFillerColId = "gridCellFiller";
1082
- const isGridCellFiller = (col) => col.colId === GridCellFillerColId;
1083
- const GridCellFiller = () => ({
1084
- colId: GridCellFillerColId,
1085
- headerName: "",
1086
- flex: 1,
1087
- });
1088
-
1089
- const Editor = (props) => ({
1090
- component: GenericCellEditorComponentWrapper(props.editor),
1091
- params: { ...props.editorParams, multiEdit: props.multiEdit },
1092
- });
1093
- /**
1094
- * Used to choose between cell editors based in data.
1095
- */
1096
- const GridCellMultiEditor = (props, cellEditorSelector) => GridCell({
1097
- cellClassRules: GridCellMultiSelectClassRules,
1098
- cellEditorSelector,
1099
- editable: props.editable ?? true,
1100
- ...props,
1101
- });
1102
-
1103
- const GridFilterHeaderIconButton = React.forwardRef(function columnsButton({ icon, title, onClick, buttonProps, disabled = false, size = "md" }, ref) {
1104
- return (jsxRuntime.jsx(lui.LuiButton, { ...buttonProps, type: "button", level: "tertiary", className: "lui-button-icon-only", ref: ref, "aria-label": title, title: title, onClick: onClick, disabled: disabled, children: jsxRuntime.jsx(lui.LuiIcon, { name: icon, alt: "Menu", size: size }) }));
1105
- });
1106
-
1107
- // Generate className following BEM methodology: http://getbem.com/naming/
1108
- // Modifier value can be one of the following types: boolean, string, undefined
1109
- const useBEM = ({ block, element, modifiers, className }) => React.useMemo(() => {
1110
- const blockElement = element ? `${block}__${element}` : block;
1111
- let classString = blockElement;
1112
- modifiers &&
1113
- Object.keys(modifiers).forEach((name) => {
1114
- const value = modifiers[name];
1115
- if (value)
1116
- classString += ` ${blockElement}--${value === true ? name : `${name}-${value}`}`;
1117
- });
1118
- let expandedClassName = typeof className === "function" ? className(modifiers) : className;
1119
- if (typeof expandedClassName === "string") {
1120
- expandedClassName = expandedClassName.trim();
1121
- if (expandedClassName)
1122
- classString += ` ${expandedClassName}`;
1123
- }
1124
- return classString;
1125
- }, [block, element, modifiers, className]);
1126
-
1127
- // Adapted from material-ui
1128
- // https://github.com/mui-org/material-ui/blob/f996027d00e7e4bff3fc040786c1706f9c6c3f82/packages/material-ui-utils/src/useForkRef.ts
1129
- const setRef = (ref, instance) => {
1130
- if (typeof ref === "function") {
1131
- ref(instance);
1132
- }
1133
- else if (ref) {
1134
- ref.current = instance;
1135
- }
1136
- };
1137
- const useCombinedRef = (refA, refB) => {
1138
- return React.useMemo(() => {
1139
- return (instance) => {
1140
- setRef(refA, instance);
1141
- setRef(refB, instance);
1142
- };
1143
- }, [refA, refB]);
1144
- };
1145
-
1146
- // Get around a warning when using useLayoutEffect on the server.
1147
- // https://github.com/reduxjs/react-redux/blob/b48d087d76f666e1c6c5a9713bbec112a1631841/src/utils/useIsomorphicLayoutEffect.js#L12
1148
- // https://gist.github.com/gaearon/e7d97cdf38a2907924ea12e4ebdf3c85
1149
- // https://github.com/facebook/react/issues/14927#issuecomment-549457471
1150
- const useIsomorphicLayoutEffect = typeof window !== "undefined" &&
1151
- typeof window.document !== "undefined" &&
1152
- typeof window.document.createElement !== "undefined"
1153
- ? React.useLayoutEffect
1154
- : React.useEffect;
1155
-
1156
- const menuContainerClass = "szh-menu-container";
1157
- const menuClass = "szh-menu";
1158
- const menuButtonClass = "szh-menu-button";
1159
- const menuArrowClass = "arrow";
1160
- const menuItemClass = "item";
1161
- const menuDividerClass = "divider";
1162
- const menuHeaderClass = "header";
1163
- const menuGroupClass = "group";
1164
- const subMenuClass = "submenu";
1165
- const radioGroupClass = "radio-group";
1166
- const Keys = Object.freeze({
1167
- ENTER: "Enter",
1168
- TAB: "Tab",
1169
- ESC: "Escape",
1170
- SPACE: " ",
1171
- HOME: "Home",
1172
- END: "End",
1173
- LEFT: "ArrowLeft",
1174
- RIGHT: "ArrowRight",
1175
- UP: "ArrowUp",
1176
- DOWN: "ArrowDown",
1177
- });
1178
- const HoverActionTypes = Object.freeze({
1179
- RESET: 0,
1180
- SET: 1,
1181
- UNSET: 2,
1182
- INCREASE: 3,
1183
- DECREASE: 4,
1184
- FIRST: 5,
1185
- LAST: 6,
1186
- SET_INDEX: 7,
1187
- });
1188
- const CloseReason = Object.freeze({
1189
- CLICK: "click",
1190
- CANCEL: "cancel",
1191
- BLUR: "blur",
1192
- SCROLL: "scroll",
1193
- TAB_FORWARD: "tab_forward",
1194
- TAB_BACKWARD: "tab_backward",
1195
- });
1196
- const FocusPositions = Object.freeze({
1197
- FIRST: "first",
1198
- LAST: "last",
1199
- });
1200
- const MenuStateMap = Object.freeze({
1201
- entering: "opening",
1202
- entered: "open",
1203
- exiting: "closing",
1204
- exited: "closed",
1205
- });
1206
-
1207
- const isMenuOpen = (state) => !!state && state[0] === "o";
1208
- const batchedUpdates = ReactDOM.unstable_batchedUpdates || ((callback) => callback());
1209
- const floatEqual = (a, b, diff = 0.0001) => Math.abs(a - b) < diff;
1210
- const getTransition = (transition, name) => transition === true || !!(transition && transition[name]);
1211
- function safeCall(fn, arg) {
1212
- return typeof fn === "function" ? fn(arg) : fn;
1213
- }
1214
- const internalKey = "_szhsinMenu";
1215
- const getName = (component) => component[internalKey];
1216
- const mergeProps = (target, source) => {
1217
- source &&
1218
- Object.keys(source).forEach((key) => {
1219
- const targetProp = target[key];
1220
- const sourceProp = source[key];
1221
- if (typeof sourceProp === "function" && targetProp) {
1222
- target[key] = (...arg) => {
1223
- sourceProp(...arg);
1224
- targetProp(...arg);
1225
- };
1226
- }
1227
- else {
1228
- target[key] = sourceProp;
1229
- }
1230
- });
1231
- return target;
1232
- };
1233
- const parsePadding = (paddingStr) => {
1234
- if (paddingStr == null)
1235
- return { top: 0, right: 0, bottom: 0, left: 0 };
1236
- const padding = paddingStr.trim().split(/\s+/, 4).map(parseFloat);
1237
- const top = !isNaN(padding[0]) ? padding[0] : 0;
1238
- const right = !isNaN(padding[1]) ? padding[1] : top;
1239
- return {
1240
- top,
1241
- right,
1242
- bottom: !isNaN(padding[2]) ? padding[2] : top,
1243
- left: !isNaN(padding[3]) ? padding[3] : right,
1244
- };
1245
- };
1246
- // Adapted from https://github.com/popperjs/popper-core/tree/v2.9.1/src/dom-utils
1247
- const getScrollAncestor = (node) => {
1248
- const thisWindow = (node?.ownerDocument ?? document).defaultView ?? window;
1249
- while (node) {
1250
- node = node.parentNode;
1251
- if (!node || node === thisWindow?.document?.body)
1252
- return null;
1253
- if (node instanceof Element) {
1254
- const { overflow, overflowX, overflowY } = thisWindow.getComputedStyle(node);
1255
- if (/auto|scroll|overlay|hidden/.test(overflow + overflowY + overflowX))
1256
- return node;
1257
- }
1258
- }
1259
- return null;
1260
- };
1261
- function commonProps(isDisabled, isHovering) {
1262
- return {
1263
- "aria-disabled": isDisabled || undefined,
1264
- tabIndex: isHovering ? 0 : -1,
1265
- };
1266
- }
1267
- const indexOfNode = (nodeList, node) => lodashEs.findIndex(nodeList, node);
1268
- const focusFirstInput = (container) => {
1269
- // We can't use instanceof Element in portals, so I use querySelectorAll as a proxy here
1270
- if (!container || !("querySelectorAll" in container))
1271
- return false;
1272
- const inputs = container.querySelectorAll("input[type='text'],input:not([type]),textarea");
1273
- const input = inputs[0];
1274
- // Using focus as proxy for HTMLElement
1275
- if (!input || !("focus" in input))
1276
- return false;
1277
- input.focus();
1278
- // Text areas should start at end
1279
- // this is a proxy for instanceof HTMLTextAreaElement
1280
- if (["textarea", "text"].includes(input.type)) {
1281
- input.setSelectionRange(0, input.value.length);
1282
- }
1283
- return true;
1284
- };
1285
-
1286
- const HoverItemContext = React.createContext(undefined);
1287
-
1288
- const withHovering = (name, WrappedComponent) => {
1289
- const Component = React.memo(WrappedComponent);
1290
- const WithHovering = React.forwardRef((props, ref) => {
1291
- const menuItemRef = React.useRef(null);
1292
- return (jsxRuntime.jsx(Component, { ...props, menuItemRef: menuItemRef, externalRef: ref, isHovering: React.useContext(HoverItemContext) === menuItemRef.current }));
1293
- });
1294
- WithHovering.displayName = `WithHovering(${name})`;
1295
- return WithHovering;
1296
- };
1297
-
1298
- const useItems = (menuRef, focusRef) => {
1299
- const [hoverItem, setHoverItem] = React.useState();
1300
- const stateRef = React.useRef({
1301
- items: [],
1302
- hoverIndex: -1,
1303
- sorted: false,
1304
- });
1305
- const mutableState = stateRef.current;
1306
- const updateItems = React.useCallback((item, isMounted) => {
1307
- const { items } = mutableState;
1308
- if (!item) {
1309
- mutableState.items = [];
1310
- }
1311
- else if (isMounted) {
1312
- items.push(item);
1313
- }
1314
- else {
1315
- const index = items.indexOf(item);
1316
- if (index > -1) {
1317
- items.splice(index, 1);
1318
- if (item.contains(document.activeElement)) {
1319
- focusRef?.current?.focus();
1320
- setHoverItem(undefined);
1321
- }
1322
- }
1323
- }
1324
- mutableState.hoverIndex = -1;
1325
- mutableState.sorted = false;
1326
- }, [mutableState, focusRef]);
1327
- const dispatch = React.useCallback((actionType, item, nextIndex) => {
1328
- const { items, hoverIndex } = mutableState;
1329
- const sortItems = () => {
1330
- if (mutableState.sorted)
1331
- return;
1332
- const orderedNodes = menuRef.current.querySelectorAll(".szh-menu__item");
1333
- items.sort((a, b) => indexOfNode(orderedNodes, a) - indexOfNode(orderedNodes, b));
1334
- mutableState.sorted = true;
1335
- };
1336
- let index = -1;
1337
- let newItem = undefined;
1338
- let newItemFn = undefined;
1339
- switch (actionType) {
1340
- case HoverActionTypes.RESET:
1341
- break;
1342
- case HoverActionTypes.SET:
1343
- newItem = item;
1344
- break;
1345
- case HoverActionTypes.UNSET:
1346
- newItemFn = (prevItem) => (prevItem === item ? undefined : prevItem);
1347
- break;
1348
- case HoverActionTypes.FIRST:
1349
- sortItems();
1350
- index = 0;
1351
- newItem = items[index];
1352
- break;
1353
- case HoverActionTypes.LAST:
1354
- sortItems();
1355
- index = items.length - 1;
1356
- newItem = items[index];
1357
- break;
1358
- case HoverActionTypes.SET_INDEX:
1359
- if (typeof nextIndex !== "number")
1360
- break;
1361
- sortItems();
1362
- index = nextIndex;
1363
- newItem = items[index];
1364
- lodashEs.defer(() => newItem.scrollIntoView({ behavior: "smooth", block: "nearest", inline: "center" }));
1365
- break;
1366
- case HoverActionTypes.INCREASE:
1367
- sortItems();
1368
- index = hoverIndex;
1369
- if (index < 0)
1370
- index = items.indexOf(item);
1371
- index++;
1372
- if (index >= items.length)
1373
- index = 0;
1374
- newItem = items[index];
1375
- focusFirstInput(newItem);
1376
- break;
1377
- case HoverActionTypes.DECREASE: {
1378
- sortItems();
1379
- index = hoverIndex;
1380
- if (index < 0)
1381
- index = items.indexOf(item);
1382
- index--;
1383
- if (index < 0)
1384
- index = items.length - 1;
1385
- newItem = items[index];
1386
- focusFirstInput(newItem);
1387
- break;
1388
- }
1389
- default:
1390
- if (process.env.NODE_ENV !== "production")
1391
- throw new Error(`[React-Menu] Unknown hover action type: ${actionType}`);
1392
- }
1393
- if (!newItem && !newItemFn)
1394
- index = -1;
1395
- setHoverItem(newItem ?? newItemFn);
1396
- mutableState.hoverIndex = index;
1397
- }, [menuRef, mutableState]);
1398
- return { hoverItem, dispatch, updateItems };
1399
- };
1400
-
1401
- const useItemEffect = (isDisabled, menuItemRef, updateItems) => {
1402
- useIsomorphicLayoutEffect(() => {
1403
- if (!menuItemRef)
1404
- return;
1405
- if (process.env.NODE_ENV !== "production" && !updateItems) {
1406
- throw new Error(`[React-Menu] This menu item or submenu should be rendered under a menu: ${menuItemRef.current.outerHTML}`);
1407
- }
1408
- if (isDisabled)
1409
- return;
1410
- const item = menuItemRef.current;
1411
- updateItems(item, true);
1412
- return () => {
1413
- updateItems(item);
1414
- };
1415
- }, [isDisabled, menuItemRef, updateItems]);
1416
- };
1417
-
1418
- const ItemSettingsContext = React.createContext({
1419
- submenuOpenDelay: 0,
1420
- submenuCloseDelay: 0,
1421
- });
1422
-
1423
- const MenuListItemContext = React.createContext({
1424
- dispatch: () => { },
1425
- updateItems: () => { },
1426
- setOpenSubmenuCount: () => 0,
1427
- });
1428
-
1429
- // This hook includes some common stateful logic in MenuItem and FocusableItem
1430
- const useItemState = (menuItemRef, focusRef, isHovering, isDisabled) => {
1431
- const { submenuCloseDelay } = React.useContext(ItemSettingsContext);
1432
- const { isParentOpen, isSubmenuOpen, dispatch, updateItems } = React.useContext(MenuListItemContext);
1433
- const timeoutId = React.useRef();
1434
- const setHover = () => {
1435
- !isHovering && !isDisabled && dispatch(HoverActionTypes.SET, menuItemRef?.current, 0);
1436
- };
1437
- const unsetHover = () => {
1438
- !isDisabled && dispatch(HoverActionTypes.UNSET, menuItemRef?.current, 0);
1439
- };
1440
- const onBlur = (e) => {
1441
- // Focus has moved out of the entire item
1442
- // It handles situation such as clicking on a sibling disabled menu item
1443
- if (isHovering && !e.currentTarget.contains(e.relatedTarget))
1444
- unsetHover();
1445
- };
1446
- const onPointerMove = () => {
1447
- if (isSubmenuOpen) {
1448
- if (!timeoutId.current)
1449
- timeoutId.current = setTimeout(() => {
1450
- timeoutId.current = undefined;
1451
- setHover();
1452
- }, submenuCloseDelay);
1453
- }
1454
- else {
1455
- setHover();
1456
- }
1457
- };
1458
- const onPointerLeave = (_, keepHover) => {
1459
- if (timeoutId.current) {
1460
- clearTimeout(timeoutId.current);
1461
- timeoutId.current = undefined;
1462
- }
1463
- !keepHover && unsetHover();
1464
- };
1465
- useItemEffect(isDisabled, menuItemRef, updateItems);
1466
- React.useEffect(() => () => clearTimeout(timeoutId.current), []);
1467
- React.useEffect(() => {
1468
- // Don't set focus when parent menu is closed, otherwise focus will be lost
1469
- // and onBlur event will be fired with relatedTarget setting as null.
1470
- if (isHovering && isParentOpen) {
1471
- focusRef?.current && focusRef.current.focus();
1472
- }
1473
- }, [focusRef, isHovering, isParentOpen]);
1474
- return {
1475
- setHover,
1476
- onBlur,
1477
- onPointerMove,
1478
- onPointerLeave,
847
+ }, [focusRef, isHovering, isParentOpen]);
848
+ return {
849
+ setHover,
850
+ onBlur,
851
+ onPointerMove,
852
+ onPointerLeave,
1479
853
  };
1480
854
  };
1481
855
 
@@ -2585,339 +1959,1007 @@ function MenuFr({ "aria-label": ariaLabel, menuButton, instanceRef, onMenuChange
2585
1959
  // Focus (hover) the first menu item when onClick event is trigger by keyboard
2586
1960
  openMenu(e.detail === 0 ? FocusPositions.FIRST : undefined);
2587
1961
  };
2588
- const onKeyDown = (e) => {
1962
+ const onKeyDown = (e) => {
1963
+ switch (e.key) {
1964
+ case Keys.UP:
1965
+ openMenu(FocusPositions.LAST);
1966
+ break;
1967
+ case Keys.DOWN:
1968
+ openMenu(FocusPositions.FIRST);
1969
+ break;
1970
+ default:
1971
+ return;
1972
+ }
1973
+ e.preventDefault();
1974
+ };
1975
+ // Too many mixed types here to figure out what to pick for button. Bad code.
1976
+ const button = safeCall(menuButton, { open: isOpen });
1977
+ if (!button || !button.type)
1978
+ throw new Error("Menu requires a menuButton prop.");
1979
+ const buttonProps = {
1980
+ ref: useCombinedRef(button.ref, buttonRef),
1981
+ ...mergeProps({ onClick, onKeyDown }, button.props),
1982
+ isOpen: false,
1983
+ };
1984
+ if (getName(button.type) === "MenuButton") {
1985
+ buttonProps.isOpen = isOpen;
1986
+ }
1987
+ const renderButton = React.cloneElement(button, buttonProps);
1988
+ useMenuChange(onMenuChange, isOpen);
1989
+ React.useImperativeHandle(instanceRef, () => ({
1990
+ openMenu,
1991
+ closeMenu: () => toggleMenu(false),
1992
+ }));
1993
+ return (jsxRuntime.jsxs(React.Fragment, { children: [renderButton, jsxRuntime.jsx(ControlledMenu, { ...restProps, ...stateProps, "aria-label": ariaLabel || (typeof button.props.children === "string" ? button.props.children : "Menu"), anchorRef: buttonRef, ref: externalRef, onClose: handleClose, skipOpen: skipOpen })] }));
1994
+ }
1995
+ const Menu = React.forwardRef(MenuFr);
1996
+
1997
+ const SubMenuFr = ({ "aria-label": ariaLabel, className, disabled, direction, label, openTrigger, onMenuChange, isHovering, instanceRef, menuItemRef, itemProps = {}, ...restProps }) => {
1998
+ const settings = React.useContext(SettingsContext);
1999
+ const { rootMenuRef } = settings;
2000
+ const { submenuOpenDelay, submenuCloseDelay } = React.useContext(ItemSettingsContext);
2001
+ const { parentMenuRef, parentDir, overflow: parentOverflow } = React.useContext(MenuListContext);
2002
+ const { isParentOpen, isSubmenuOpen, setOpenSubmenuCount, dispatch, updateItems } = React.useContext(MenuListItemContext);
2003
+ const isPortal = parentOverflow !== "visible";
2004
+ const [stateProps, toggleMenu, _openMenu] = useMenuStateAndFocus(settings);
2005
+ const { state } = stateProps;
2006
+ const isDisabled = !!disabled;
2007
+ const isOpen = isMenuOpen(state);
2008
+ const containerRef = React.useRef(null);
2009
+ const timeoutId = React.useRef();
2010
+ const stopTimer = () => {
2011
+ if (timeoutId.current) {
2012
+ clearTimeout(timeoutId.current);
2013
+ timeoutId.current = undefined;
2014
+ }
2015
+ };
2016
+ const openMenu = (...args) => {
2017
+ stopTimer();
2018
+ setHover();
2019
+ !isDisabled && _openMenu(...args);
2020
+ };
2021
+ const setHover = () => !isHovering && !isDisabled && dispatch(HoverActionTypes.SET, menuItemRef?.current, 0);
2022
+ const delayOpen = (delay) => {
2023
+ setHover();
2024
+ if (!openTrigger)
2025
+ timeoutId.current = setTimeout(() => batchedUpdates(openMenu), Math.max(delay, 0));
2026
+ };
2027
+ const handlePointerMove = () => {
2028
+ if (timeoutId.current || isOpen || isDisabled)
2029
+ return;
2030
+ if (isSubmenuOpen) {
2031
+ timeoutId.current = setTimeout(() => delayOpen(submenuOpenDelay - submenuCloseDelay), submenuCloseDelay);
2032
+ }
2033
+ else {
2034
+ delayOpen(submenuOpenDelay);
2035
+ }
2036
+ };
2037
+ const handlePointerLeave = () => {
2038
+ stopTimer();
2039
+ if (!isOpen)
2040
+ dispatch(HoverActionTypes.UNSET, menuItemRef?.current, 0);
2041
+ };
2042
+ const handleKeyDown = (e) => {
2043
+ let handled = false;
2044
+ switch (e.key) {
2045
+ // LEFT key is bubbled up from submenu items
2046
+ case Keys.LEFT:
2047
+ if (isOpen) {
2048
+ menuItemRef?.current && menuItemRef.current.focus();
2049
+ toggleMenu(false);
2050
+ handled = true;
2051
+ }
2052
+ break;
2053
+ // prevent browser from scrolling page to the right
2054
+ case Keys.RIGHT:
2055
+ if (!isOpen)
2056
+ handled = true;
2057
+ break;
2058
+ }
2059
+ if (handled) {
2060
+ e.preventDefault();
2061
+ e.stopPropagation();
2062
+ }
2063
+ };
2064
+ const handleItemKeyDown = (e) => {
2065
+ if (!isHovering)
2066
+ return;
2067
+ switch (e.key) {
2068
+ case Keys.ENTER:
2069
+ case Keys.SPACE:
2070
+ case Keys.RIGHT:
2071
+ openTrigger !== "none" && openMenu(FocusPositions.FIRST);
2072
+ break;
2073
+ }
2074
+ };
2075
+ useItemEffect(isDisabled, menuItemRef, updateItems);
2076
+ useMenuChange(onMenuChange, isOpen);
2077
+ React.useEffect(() => () => clearTimeout(timeoutId.current), []);
2078
+ React.useEffect(() => {
2079
+ // Don't set focus when parent menu is closed, otherwise focus will be lost
2080
+ // and onBlur event will be fired with relatedTarget setting as null.
2081
+ if (isHovering && isParentOpen) {
2082
+ menuItemRef?.current && menuItemRef.current.focus();
2083
+ }
2084
+ else {
2085
+ toggleMenu(false);
2086
+ }
2087
+ }, [isHovering, isParentOpen, toggleMenu, menuItemRef]);
2088
+ React.useEffect(() => {
2089
+ setOpenSubmenuCount((count) => (isOpen ? count + 1 : Math.max(count - 1, 0)));
2090
+ }, [setOpenSubmenuCount, isOpen]);
2091
+ React.useImperativeHandle(instanceRef, () => ({
2092
+ openMenu: (...args) => {
2093
+ isParentOpen && openMenu(...args);
2094
+ },
2095
+ closeMenu: () => {
2096
+ if (isOpen) {
2097
+ menuItemRef?.current && menuItemRef.current.focus();
2098
+ toggleMenu(false);
2099
+ }
2100
+ },
2101
+ }));
2102
+ const modifiers = React.useMemo(() => ({
2103
+ open: isOpen,
2104
+ hover: isHovering,
2105
+ disabled: isDisabled,
2106
+ submenu: true,
2107
+ }), [isOpen, isHovering, isDisabled]);
2108
+ const { ref: externalItemRef, className: itemClassName, ...restItemProps } = itemProps;
2109
+ const mergedItemProps = mergeProps({
2110
+ onPointerMove: handlePointerMove,
2111
+ onPointerLeave: handlePointerLeave,
2112
+ onKeyDown: handleItemKeyDown,
2113
+ onClick: () => openTrigger !== "none" && openMenu(),
2114
+ }, restItemProps);
2115
+ const getMenuList = () => {
2116
+ const menuList = (jsxRuntime.jsx(MenuList, { ...restProps, ...stateProps, ariaLabel: ariaLabel || (typeof label === "string" ? label : "Submenu"), anchorRef: menuItemRef, containerRef: isPortal ? rootMenuRef : containerRef, direction: direction || (parentDir === "right" || parentDir === "left" ? parentDir : "right"), parentScrollingRef: isPortal && parentMenuRef, isDisabled: isDisabled }));
2117
+ const container = rootMenuRef?.current;
2118
+ return isPortal && container ? ReactDOM.createPortal(menuList, container) : menuList;
2119
+ };
2120
+ return (jsxRuntime.jsxs("li", { className: useBEM({ block: menuClass, element: subMenuClass, className }), style: { position: "relative" }, role: "presentation", ref: containerRef, onKeyDown: handleKeyDown, children: [jsxRuntime.jsx("div", { role: "menuitem", "aria-haspopup": true, "aria-expanded": isOpen, ...mergedItemProps, ...commonProps(isDisabled, isHovering), ref: useCombinedRef(externalItemRef, menuItemRef), className: useBEM({
2121
+ block: menuClass,
2122
+ element: menuItemClass,
2123
+ modifiers,
2124
+ className: itemClassName,
2125
+ }), children: React.useMemo(() => safeCall(label, modifiers), [label, modifiers]) }), state && getMenuList()] }));
2126
+ };
2127
+ const SubMenu = withHovering("SubMenu", SubMenuFr);
2128
+
2129
+ const RadioGroupContext = React.createContext({});
2130
+
2131
+ const MenuItemFr = ({ className, value, href, type, checked, disabled, children, onClick, isHovering, menuItemRef, externalRef, ...restProps }) => {
2132
+ const isDisabled = !!disabled;
2133
+ const { setHover, ...restStateProps } = useItemState(menuItemRef, menuItemRef, isHovering, isDisabled);
2134
+ const eventHandlers = React.useContext(EventHandlersContext);
2135
+ const radioGroup = React.useContext(RadioGroupContext);
2136
+ const isRadio = type === "radio";
2137
+ const isCheckBox = type === "checkbox";
2138
+ const isAnchor = !!href && !isDisabled && !isRadio && !isCheckBox;
2139
+ const isChecked = isRadio ? radioGroup.value === value : isCheckBox ? !!checked : false;
2140
+ // handle click seems to be a combination of multiple event types, bad code.
2141
+ const handleClick = (e) => {
2142
+ if (isDisabled) {
2143
+ if (e.syntheticEvent) {
2144
+ e.syntheticEvent.stopPropagation();
2145
+ e.syntheticEvent.preventDefault();
2146
+ }
2147
+ else {
2148
+ e.stopPropagation();
2149
+ e.preventDefault();
2150
+ }
2151
+ return;
2152
+ }
2153
+ const event = {
2154
+ value,
2155
+ syntheticEvent: e,
2156
+ };
2157
+ if (e.key !== undefined) {
2158
+ const ke = e;
2159
+ event.key = ke.key;
2160
+ event.shiftKey = ke.shiftKey;
2161
+ }
2162
+ if (isCheckBox)
2163
+ event.checked = !isChecked;
2164
+ if (isRadio)
2165
+ event.name = radioGroup.name;
2166
+ safeCall(onClick, event);
2167
+ if (isRadio)
2168
+ safeCall(radioGroup.onRadioChange, event);
2169
+ eventHandlers.handleClick(event, isCheckBox || isRadio);
2170
+ };
2171
+ const handleKeyDown = (e) => {
2172
+ // if tab is allowed the handleKeyUp event can't process the tab
2173
+ if (e.key === "Tab") {
2174
+ e.preventDefault();
2175
+ e.stopPropagation();
2176
+ }
2177
+ };
2178
+ /**
2179
+ * Keyboard events are triggered on up, otherwise subcomponents get spaces and enters typed in them
2180
+ */
2181
+ const handleKeyUp = (e) => {
2182
+ if (!isHovering)
2183
+ return;
2589
2184
  switch (e.key) {
2590
- case Keys.UP:
2591
- openMenu(FocusPositions.LAST);
2592
- break;
2593
- case Keys.DOWN:
2594
- openMenu(FocusPositions.FIRST);
2185
+ case Keys.ENTER:
2186
+ case Keys.TAB:
2187
+ case Keys.SPACE:
2188
+ e.preventDefault();
2189
+ e.stopPropagation();
2190
+ if (isAnchor) {
2191
+ menuItemRef?.current && menuItemRef.current.click();
2192
+ }
2193
+ else {
2194
+ handleClick(e);
2195
+ }
2595
2196
  break;
2596
- default:
2597
- return;
2598
2197
  }
2599
- e.preventDefault();
2600
2198
  };
2601
- // Too many mixed types here to figure out what to pick for button. Bad code.
2602
- const button = safeCall(menuButton, { open: isOpen });
2603
- if (!button || !button.type)
2604
- throw new Error("Menu requires a menuButton prop.");
2605
- const buttonProps = {
2606
- ref: useCombinedRef(button.ref, buttonRef),
2607
- ...mergeProps({ onClick, onKeyDown }, button.props),
2608
- isOpen: false,
2199
+ const modifiers = React.useMemo(() => ({
2200
+ type,
2201
+ disabled: isDisabled,
2202
+ hover: isHovering,
2203
+ checked: isChecked,
2204
+ anchor: isAnchor,
2205
+ }), [type, isDisabled, isHovering, isChecked, isAnchor]);
2206
+ const mergedProps = mergeProps({
2207
+ ...restStateProps,
2208
+ onPointerDown: setHover,
2209
+ onKeyDown: handleKeyDown,
2210
+ onKeyUp: handleKeyUp,
2211
+ onClick: handleClick,
2212
+ }, restProps);
2213
+ // Order of props overriding (same in all components):
2214
+ // 1. Preset props adhering to WAI-ARIA Authoring Practices.
2215
+ // 2. Merged outer and local props
2216
+ // 3. ref, className
2217
+ const menuItemProps = {
2218
+ role: isRadio ? "menuitemradio" : isCheckBox ? "menuitemcheckbox" : "menuitem",
2219
+ "aria-checked": isRadio || isCheckBox ? isChecked : undefined,
2220
+ ...mergedProps,
2221
+ ...commonProps(isDisabled, isHovering),
2222
+ ref: useCombinedRef(externalRef, menuItemRef),
2223
+ className: useBEM({ block: menuClass, element: menuItemClass, modifiers, className }),
2224
+ children: React.useMemo(() => safeCall(children, modifiers), [children, modifiers]),
2609
2225
  };
2610
- if (getName(button.type) === "MenuButton") {
2611
- buttonProps.isOpen = isOpen;
2226
+ if (isAnchor) {
2227
+ return (jsxRuntime.jsx("li", { role: "presentation", children: jsxRuntime.jsx("a", { href: href, ...menuItemProps }) }));
2612
2228
  }
2613
- const renderButton = React.cloneElement(button, buttonProps);
2614
- useMenuChange(onMenuChange, isOpen);
2615
- React.useImperativeHandle(instanceRef, () => ({
2616
- openMenu,
2617
- closeMenu: () => toggleMenu(false),
2618
- }));
2619
- return (jsxRuntime.jsxs(React.Fragment, { children: [renderButton, jsxRuntime.jsx(ControlledMenu, { ...restProps, ...stateProps, "aria-label": ariaLabel || (typeof button.props.children === "string" ? button.props.children : "Menu"), anchorRef: buttonRef, ref: externalRef, onClose: handleClose, skipOpen: skipOpen })] }));
2620
- }
2621
- const Menu = React.forwardRef(MenuFr);
2229
+ else {
2230
+ return jsxRuntime.jsx("li", { ...menuItemProps });
2231
+ }
2232
+ };
2233
+ const MenuItem = withHovering("MenuItem", MenuItemFr);
2622
2234
 
2623
- const SubMenuFr = ({ "aria-label": ariaLabel, className, disabled, direction, label, openTrigger, onMenuChange, isHovering, instanceRef, menuItemRef, itemProps = {}, ...restProps }) => {
2624
- const settings = React.useContext(SettingsContext);
2625
- const { rootMenuRef } = settings;
2626
- const { submenuOpenDelay, submenuCloseDelay } = React.useContext(ItemSettingsContext);
2627
- const { parentMenuRef, parentDir, overflow: parentOverflow } = React.useContext(MenuListContext);
2628
- const { isParentOpen, isSubmenuOpen, setOpenSubmenuCount, dispatch, updateItems } = React.useContext(MenuListItemContext);
2629
- const isPortal = parentOverflow !== "visible";
2630
- const [stateProps, toggleMenu, _openMenu] = useMenuStateAndFocus(settings);
2631
- const { state } = stateProps;
2235
+ const FocusableItemFr = ({ className, disabled, children, isHovering, menuItemRef, externalRef, ...restProps }) => {
2632
2236
  const isDisabled = !!disabled;
2633
- const isOpen = isMenuOpen(state);
2634
- const containerRef = React.useRef(null);
2635
- const timeoutId = React.useRef();
2636
- const stopTimer = () => {
2637
- if (timeoutId.current) {
2638
- clearTimeout(timeoutId.current);
2639
- timeoutId.current = undefined;
2237
+ const ref = React.useRef(null);
2238
+ const { setHover, onPointerLeave, ...restStateProps } = useItemState(menuItemRef, ref, isHovering, isDisabled);
2239
+ const { handleClose } = React.useContext(EventHandlersContext);
2240
+ const modifiers = React.useMemo(() => ({
2241
+ disabled: isDisabled,
2242
+ hover: isHovering,
2243
+ focusable: true,
2244
+ }), [isDisabled, isHovering]);
2245
+ const renderChildren = React.useMemo(() => safeCall(children, {
2246
+ ...modifiers,
2247
+ ref,
2248
+ closeMenu: handleClose,
2249
+ }), [children, modifiers, handleClose]);
2250
+ const mergedProps = mergeProps({
2251
+ ...restStateProps,
2252
+ onPointerLeave: (e) => onPointerLeave(e, true),
2253
+ onFocus: setHover,
2254
+ }, restProps);
2255
+ return (jsxRuntime.jsx("li", { role: "menuitem", ...mergedProps, ...commonProps(isDisabled), ref: useCombinedRef(externalRef, menuItemRef), className: useBEM({ block: menuClass, element: menuItemClass, modifiers, className }), children: renderChildren }));
2256
+ };
2257
+ const FocusableItem = withHovering("FocusableItem", FocusableItemFr);
2258
+
2259
+ const MenuDividerFr = ({ className, ...restProps }, externalRef) => {
2260
+ return (jsxRuntime.jsx("li", { role: "separator", ...restProps, ref: externalRef, className: useBEM({ block: menuClass, element: menuDividerClass, className }) }));
2261
+ };
2262
+ const MenuDivider = React.memo(React.forwardRef(MenuDividerFr));
2263
+
2264
+ const MenuHeaderFr = ({ className, ...restProps }, externalRef) => {
2265
+ return (jsxRuntime.jsx("li", { role: "presentation", ...restProps, ref: externalRef, className: useBEM({ block: menuClass, element: menuHeaderClass, className }) }));
2266
+ };
2267
+ const MenuHeader = React.memo(React.forwardRef(MenuHeaderFr));
2268
+
2269
+ const MenuGroupFr = ({ className, style, takeOverflow, ...restProps }, externalRef) => {
2270
+ const ref = React.useRef(null);
2271
+ const [overflowStyle, setOverflowStyle] = React.useState();
2272
+ const { overflow, overflowAmt } = React.useContext(MenuListContext);
2273
+ useIsomorphicLayoutEffect(() => {
2274
+ let maxHeight;
2275
+ if (takeOverflow && overflowAmt != null && overflowAmt >= 0 && ref.current) {
2276
+ maxHeight = ref.current.getBoundingClientRect().height - overflowAmt;
2277
+ if (maxHeight < 0)
2278
+ maxHeight = 0;
2640
2279
  }
2280
+ setOverflowStyle(maxHeight != null && maxHeight >= 0 ? { maxHeight, overflow } : undefined);
2281
+ }, [takeOverflow, overflow, overflowAmt]);
2282
+ useIsomorphicLayoutEffect(() => {
2283
+ if (overflowStyle && ref.current)
2284
+ ref.current.scrollTop = 0;
2285
+ }, [overflowStyle]);
2286
+ return (jsxRuntime.jsx("div", { ...restProps, ref: useCombinedRef(externalRef, ref), className: useBEM({ block: menuClass, element: menuGroupClass, className }), style: { ...style, ...overflowStyle } }));
2287
+ };
2288
+ const MenuGroup = React.forwardRef(MenuGroupFr);
2289
+
2290
+ const MenuRadioGroupFr = ({ "aria-label": ariaLabel, className, name, value, onRadioChange, ...restProps }, externalRef) => {
2291
+ const contextValue = React.useMemo(() => ({ name, value, onRadioChange }), [name, value, onRadioChange]);
2292
+ return (jsxRuntime.jsx(RadioGroupContext.Provider, { value: contextValue, children: jsxRuntime.jsx("li", { role: "presentation", children: jsxRuntime.jsx("ul", { role: "group", "aria-label": ariaLabel || name || "Radio group", ...restProps, ref: externalRef, className: useBEM({ block: menuClass, element: radioGroupClass, className }) }) }) }));
2293
+ };
2294
+ const MenuRadioGroup = React.forwardRef(MenuRadioGroupFr);
2295
+
2296
+ const useGridContextMenu = ({ contextMenuSelectRow, contextMenu: ContextMenu, }) => {
2297
+ const { redrawRows, prePopupOps, postPopupOps, getSelectedRows } = React.useContext(GridContext);
2298
+ const [isOpen, setOpen] = React.useState(false);
2299
+ const [anchorPoint, setAnchorPoint] = React.useState({ x: 0, y: 0 });
2300
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2301
+ const clickedColDefRef = React.useRef(null);
2302
+ const selectedRowsRef = React.useRef([]);
2303
+ const clickedRowRef = React.useRef(null);
2304
+ const openMenu = React.useCallback((e) => {
2305
+ if (!e)
2306
+ return;
2307
+ prePopupOps();
2308
+ setAnchorPoint({ x: e.clientX, y: e.clientY });
2309
+ setOpen(true);
2310
+ }, [prePopupOps]);
2311
+ const closeMenu = React.useCallback(() => {
2312
+ setOpen(false);
2313
+ redrawRows();
2314
+ postPopupOps();
2315
+ }, [postPopupOps, redrawRows]);
2316
+ const cellContextMenu = React.useCallback((event) => {
2317
+ if (contextMenuSelectRow && !event.node.isSelected()) {
2318
+ event.node.setSelected(true, true);
2319
+ }
2320
+ clickedColDefRef.current = event.colDef;
2321
+ selectedRowsRef.current = getSelectedRows();
2322
+ clickedRowRef.current = event.data;
2323
+ // This is actually a pointer event
2324
+ openMenu(event.event);
2325
+ }, [contextMenuSelectRow, getSelectedRows, openMenu]);
2326
+ return {
2327
+ openMenu,
2328
+ cellContextMenu,
2329
+ component: ContextMenu ? (jsxRuntime.jsx(jsxRuntime.Fragment, { children: jsxRuntime.jsx(ControlledMenu, { anchorPoint: anchorPoint, state: isOpen ? "open" : "closed", direction: "right", onClose: closeMenu, children: isOpen && (jsxRuntime.jsx(ContextMenu, { selectedRows: selectedRowsRef.current, clickedRow: clickedRowRef.current, colDef: clickedColDefRef.current, close: closeMenu })) }) })) : null,
2641
2330
  };
2642
- const openMenu = (...args) => {
2643
- stopTimer();
2644
- setHover();
2645
- !isDisabled && _openMenu(...args);
2646
- };
2647
- const setHover = () => !isHovering && !isDisabled && dispatch(HoverActionTypes.SET, menuItemRef?.current, 0);
2648
- const delayOpen = (delay) => {
2649
- setHover();
2650
- if (!openTrigger)
2651
- timeoutId.current = setTimeout(() => batchedUpdates(openMenu), Math.max(delay, 0));
2652
- };
2653
- const handlePointerMove = () => {
2654
- if (timeoutId.current || isOpen || isDisabled)
2331
+ };
2332
+
2333
+ /**
2334
+ * Wrapper for AgGrid to add commonly used functionality.
2335
+ */
2336
+ const Grid = ({ "data-testid": dataTestId, rowSelection = "multiple", suppressColumnVirtualization = true, theme = "ag-theme-alpine", sizeColumns = "auto", selectColumnPinned = null, contextMenuSelectRow = false, ...params }) => {
2337
+ const { gridReady, setApis, ensureRowVisible, getFirstRowId, selectRowsById, focusByRowById, ensureSelectedRowIsVisible, autoSizeColumns, sizeColumnsToFit, externallySelectedItemsAreInSync, setExternallySelectedItemsAreInSync, isExternalFilterPresent, doesExternalFilterPass, setOnCellEditingComplete, getColDef, } = React.useContext(GridContext);
2338
+ const { checkUpdating, updatedDep, updatingCols } = React.useContext(GridUpdatingContext);
2339
+ const { prePopupOps } = React.useContext(GridContext);
2340
+ const gridDivRef = React.useRef(null);
2341
+ const lastSelectedIds = React.useRef([]);
2342
+ const [staleGrid, setStaleGrid] = React.useState(false);
2343
+ const postSortRows = usePostSortRowsHook({ setStaleGrid });
2344
+ /**
2345
+ * onContentSize should only be called at maximum twice.
2346
+ * Once when an empty grid is loaded.
2347
+ * And again when the grid has content.
2348
+ */
2349
+ const hasSetContentSize = React.useRef(false);
2350
+ const hasSetContentSizeEmpty = React.useRef(false);
2351
+ const needsAutoSize = React.useRef(false);
2352
+ const setInitialContentSize = React.useCallback(() => {
2353
+ if (!gridDivRef.current?.clientWidth) {
2354
+ // Don't resize grids if they are offscreen as it doesn't work.
2355
+ needsAutoSize.current = true;
2655
2356
  return;
2656
- if (isSubmenuOpen) {
2657
- timeoutId.current = setTimeout(() => delayOpen(submenuOpenDelay - submenuCloseDelay), submenuCloseDelay);
2658
2357
  }
2659
- else {
2660
- delayOpen(submenuOpenDelay);
2358
+ const headerCellCount = gridDivRef.current?.getElementsByClassName("ag-header-cell-label")?.length;
2359
+ if (headerCellCount < 2) {
2360
+ // Don't resize grids until all the columns are visible
2361
+ // as `autoSizeColumns` will fail silently in this case
2362
+ needsAutoSize.current = true;
2363
+ return;
2661
2364
  }
2662
- };
2663
- const handlePointerLeave = () => {
2664
- stopTimer();
2665
- if (!isOpen)
2666
- dispatch(HoverActionTypes.UNSET, menuItemRef?.current, 0);
2667
- };
2668
- const handleKeyDown = (e) => {
2669
- let handled = false;
2670
- switch (e.key) {
2671
- // LEFT key is bubbled up from submenu items
2672
- case Keys.LEFT:
2673
- if (isOpen) {
2674
- menuItemRef?.current && menuItemRef.current.focus();
2675
- toggleMenu(false);
2676
- handled = true;
2365
+ const skipHeader = sizeColumns === "auto-skip-headers" && !lodashEs.isEmpty(params.rowData);
2366
+ if (sizeColumns === "auto" || skipHeader) {
2367
+ const result = autoSizeColumns({ skipHeader, userSizedColIds: userSizedColIds.current });
2368
+ if (lodashEs.isEmpty(params.rowData)) {
2369
+ if (!hasSetContentSizeEmpty.current && result && !hasSetContentSize.current) {
2370
+ hasSetContentSizeEmpty.current = true;
2371
+ params.onContentSize && params.onContentSize(result);
2372
+ }
2373
+ }
2374
+ else {
2375
+ if (result && !hasSetContentSize.current) {
2376
+ hasSetContentSize.current = true;
2377
+ params.onContentSize && params.onContentSize(result);
2378
+ }
2379
+ }
2380
+ }
2381
+ if (sizeColumns !== "none") {
2382
+ sizeColumnsToFit();
2383
+ }
2384
+ needsAutoSize.current = false;
2385
+ }, [autoSizeColumns, params, sizeColumns, sizeColumnsToFit]);
2386
+ const lastOwnerDocumentRef = React.useRef();
2387
+ /**
2388
+ * Auto-size windows that had deferred auto-size
2389
+ */
2390
+ useIntervalHook({
2391
+ callback: () => {
2392
+ // Check if window has been popped out and needs resize
2393
+ const currentDocument = gridDivRef.current?.ownerDocument;
2394
+ if (currentDocument !== lastOwnerDocumentRef.current) {
2395
+ lastOwnerDocumentRef.current = currentDocument;
2396
+ if (currentDocument) {
2397
+ needsAutoSize.current = true;
2677
2398
  }
2678
- break;
2679
- // prevent browser from scrolling page to the right
2680
- case Keys.RIGHT:
2681
- if (!isOpen)
2682
- handled = true;
2683
- break;
2399
+ }
2400
+ if (needsAutoSize.current) {
2401
+ needsAutoSize.current = false;
2402
+ setInitialContentSize();
2403
+ }
2404
+ },
2405
+ timeoutMs: 1000,
2406
+ });
2407
+ const previousGridReady = React.useRef(gridReady);
2408
+ React.useEffect(() => {
2409
+ if (!previousGridReady.current && gridReady) {
2410
+ previousGridReady.current = true;
2411
+ setInitialContentSize();
2684
2412
  }
2685
- if (handled) {
2686
- e.preventDefault();
2687
- e.stopPropagation();
2413
+ }, [gridReady, setInitialContentSize]);
2414
+ /**
2415
+ * On data load select the first row of the grid if required.
2416
+ */
2417
+ const hasSelectedFirstItem = React.useRef(false);
2418
+ React.useEffect(() => {
2419
+ if (!gridReady || hasSelectedFirstItem.current || !params.rowData || !externallySelectedItemsAreInSync)
2420
+ return;
2421
+ hasSelectedFirstItem.current = true;
2422
+ if (isNotEmpty(params.rowData) && lodashEs.isEmpty(params.externalSelectedItems)) {
2423
+ const firstRowId = getFirstRowId();
2424
+ if (params.autoSelectFirstRow) {
2425
+ selectRowsById([firstRowId]);
2426
+ }
2427
+ else {
2428
+ focusByRowById(firstRowId);
2429
+ }
2688
2430
  }
2689
- };
2690
- const handleItemKeyDown = (e) => {
2691
- if (!isHovering)
2431
+ }, [
2432
+ externallySelectedItemsAreInSync,
2433
+ focusByRowById,
2434
+ gridReady,
2435
+ params.externalSelectedItems,
2436
+ params.autoSelectFirstRow,
2437
+ params.rowData,
2438
+ selectRowsById,
2439
+ getFirstRowId,
2440
+ ]);
2441
+ /**
2442
+ * AgGrid checkbox select does not pass clicks within cell but not on the checkbox to checkbox.
2443
+ * This passes the event to the checkbox when you click anywhere in the cell.
2444
+ */
2445
+ const clickSelectorCheckboxWhenContainingCellClicked = React.useCallback(({ event }) => {
2446
+ if (!event)
2447
+ return;
2448
+ const input = event.target.querySelector("input");
2449
+ input?.dispatchEvent(event);
2450
+ }, []);
2451
+ /**
2452
+ * Ensure external selected items list is in sync with panel.
2453
+ */
2454
+ const synchroniseExternalStateToGridSelection = React.useCallback(({ api }) => {
2455
+ if (!params.externalSelectedItems || !params.setExternalSelectedItems) {
2456
+ setExternallySelectedItemsAreInSync(true);
2692
2457
  return;
2693
- switch (e.key) {
2694
- case Keys.ENTER:
2695
- case Keys.SPACE:
2696
- case Keys.RIGHT:
2697
- openTrigger !== "none" && openMenu(FocusPositions.FIRST);
2698
- break;
2699
2458
  }
2700
- };
2701
- useItemEffect(isDisabled, menuItemRef, updateItems);
2702
- useMenuChange(onMenuChange, isOpen);
2703
- React.useEffect(() => () => clearTimeout(timeoutId.current), []);
2704
- React.useEffect(() => {
2705
- // Don't set focus when parent menu is closed, otherwise focus will be lost
2706
- // and onBlur event will be fired with relatedTarget setting as null.
2707
- if (isHovering && isParentOpen) {
2708
- menuItemRef?.current && menuItemRef.current.focus();
2459
+ const selectedRows = api.getSelectedRows();
2460
+ // We don't want to update selected Items if it hasn't changed to prevent excess renders
2461
+ if (params.externalSelectedItems.length != selectedRows.length ||
2462
+ isNotEmpty(lodashEs.xorBy(selectedRows, params.externalSelectedItems, (row) => row.id))) {
2463
+ setExternallySelectedItemsAreInSync(false);
2464
+ params.setExternalSelectedItems([...selectedRows]);
2709
2465
  }
2710
2466
  else {
2711
- toggleMenu(false);
2467
+ setExternallySelectedItemsAreInSync(true);
2712
2468
  }
2713
- }, [isHovering, isParentOpen, toggleMenu, menuItemRef]);
2714
- React.useEffect(() => {
2715
- setOpenSubmenuCount((count) => (isOpen ? count + 1 : Math.max(count - 1, 0)));
2716
- }, [setOpenSubmenuCount, isOpen]);
2717
- React.useImperativeHandle(instanceRef, () => ({
2718
- openMenu: (...args) => {
2719
- isParentOpen && openMenu(...args);
2720
- },
2721
- closeMenu: () => {
2722
- if (isOpen) {
2723
- menuItemRef?.current && menuItemRef.current.focus();
2724
- toggleMenu(false);
2725
- }
2726
- },
2727
- }));
2728
- const modifiers = React.useMemo(() => ({
2729
- open: isOpen,
2730
- hover: isHovering,
2731
- disabled: isDisabled,
2732
- submenu: true,
2733
- }), [isOpen, isHovering, isDisabled]);
2734
- const { ref: externalItemRef, className: itemClassName, ...restItemProps } = itemProps;
2735
- const mergedItemProps = mergeProps({
2736
- onPointerMove: handlePointerMove,
2737
- onPointerLeave: handlePointerLeave,
2738
- onKeyDown: handleItemKeyDown,
2739
- onClick: () => openTrigger !== "none" && openMenu(),
2740
- }, restItemProps);
2741
- const getMenuList = () => {
2742
- const menuList = (jsxRuntime.jsx(MenuList, { ...restProps, ...stateProps, ariaLabel: ariaLabel || (typeof label === "string" ? label : "Submenu"), anchorRef: menuItemRef, containerRef: isPortal ? rootMenuRef : containerRef, direction: direction || (parentDir === "right" || parentDir === "left" ? parentDir : "right"), parentScrollingRef: isPortal && parentMenuRef, isDisabled: isDisabled }));
2743
- const container = rootMenuRef?.current;
2744
- return isPortal && container ? ReactDOM.createPortal(menuList, container) : menuList;
2469
+ }, [params, setExternallySelectedItemsAreInSync]);
2470
+ /**
2471
+ * Synchronise externally selected items to grid.
2472
+ * If new ids are selected scroll them into view.
2473
+ */
2474
+ const synchroniseExternallySelectedItemsToGrid = React.useCallback(() => {
2475
+ if (!gridReady)
2476
+ return;
2477
+ if (!params.externalSelectedItems) {
2478
+ setExternallySelectedItemsAreInSync(true);
2479
+ return;
2480
+ }
2481
+ const selectedIds = params.externalSelectedItems.map((row) => row.id);
2482
+ const lastNewId = lodashEs.last(lodashEs.difference(selectedIds, lastSelectedIds.current));
2483
+ if (lastNewId != null) {
2484
+ ensureRowVisible(lastNewId);
2485
+ }
2486
+ lastSelectedIds.current = selectedIds;
2487
+ selectRowsById(selectedIds);
2488
+ setExternallySelectedItemsAreInSync(true);
2489
+ }, [gridReady, params.externalSelectedItems, ensureRowVisible, selectRowsById, setExternallySelectedItemsAreInSync]);
2490
+ /**
2491
+ * Combine grid and cell editable into one function
2492
+ */
2493
+ const combineEditables = (...editables) => (params) => {
2494
+ const results = editables.map((editable) => fnOrVar(editable, params));
2495
+ // If editable is not set anywhere then it's non-editable
2496
+ if (results.every((v) => v == null))
2497
+ return false;
2498
+ // If any editable value is or returns false then it's non-editable
2499
+ return !results.some((v) => v == false);
2745
2500
  };
2746
- return (jsxRuntime.jsxs("li", { className: useBEM({ block: menuClass, element: subMenuClass, className }), style: { position: "relative" }, role: "presentation", ref: containerRef, onKeyDown: handleKeyDown, children: [jsxRuntime.jsx("div", { role: "menuitem", "aria-haspopup": true, "aria-expanded": isOpen, ...mergedItemProps, ...commonProps(isDisabled, isHovering), ref: useCombinedRef(externalItemRef, menuItemRef), className: useBEM({
2747
- block: menuClass,
2748
- element: menuItemClass,
2749
- modifiers,
2750
- className: itemClassName,
2751
- }), children: React.useMemo(() => safeCall(label, modifiers), [label, modifiers]) }), state && getMenuList()] }));
2752
- };
2753
- const SubMenu = withHovering("SubMenu", SubMenuFr);
2754
-
2755
- const RadioGroupContext = React.createContext({});
2756
-
2757
- const MenuItemFr = ({ className, value, href, type, checked, disabled, children, onClick, isHovering, menuItemRef, externalRef, ...restProps }) => {
2758
- const isDisabled = !!disabled;
2759
- const { setHover, ...restStateProps } = useItemState(menuItemRef, menuItemRef, isHovering, isDisabled);
2760
- const eventHandlers = React.useContext(EventHandlersContext);
2761
- const radioGroup = React.useContext(RadioGroupContext);
2762
- const isRadio = type === "radio";
2763
- const isCheckBox = type === "checkbox";
2764
- const isAnchor = !!href && !isDisabled && !isRadio && !isCheckBox;
2765
- const isChecked = isRadio ? radioGroup.value === value : isCheckBox ? !!checked : false;
2766
- // handle click seems to be a combination of multiple event types, bad code.
2767
- const handleClick = (e) => {
2768
- if (isDisabled) {
2769
- if (e.syntheticEvent) {
2770
- e.syntheticEvent.stopPropagation();
2771
- e.syntheticEvent.preventDefault();
2501
+ /**
2502
+ * Synchronise externally selected items to grid on externalSelectedItems change
2503
+ */
2504
+ React.useEffect(synchroniseExternallySelectedItemsToGrid, [synchroniseExternallySelectedItemsToGrid]);
2505
+ /**
2506
+ * Add selectable column to colDefs. Adjust column defs to block fit for auto sized columns.
2507
+ */
2508
+ const columnDefs = React.useMemo(() => {
2509
+ const adjustColDefs = params.columnDefs.map((colDef) => {
2510
+ const colDefEditable = colDef.editable;
2511
+ const editable = combineEditables(params.readOnly !== true, params.defaultColDef?.editable, colDefEditable);
2512
+ return {
2513
+ ...colDef,
2514
+ editable,
2515
+ cellClassRules: {
2516
+ ...colDef.cellClassRules,
2517
+ "GridCell-readonly": (ccp) => !editable(ccp),
2518
+ },
2519
+ };
2520
+ });
2521
+ return params.selectable
2522
+ ? [
2523
+ {
2524
+ colId: "selection",
2525
+ editable: false,
2526
+ minWidth: 42,
2527
+ maxWidth: 42,
2528
+ pinned: selectColumnPinned,
2529
+ headerComponentParams: {
2530
+ exportable: false,
2531
+ },
2532
+ checkboxSelection: true,
2533
+ headerComponent: rowSelection === "multiple" ? GridHeaderSelect : null,
2534
+ suppressHeaderKeyboardEvent: (e) => {
2535
+ if ((e.event.key === "Enter" || e.event.key === " ") && !e.event.repeat) {
2536
+ if (lodashEs.isEmpty(e.api.getSelectedRows())) {
2537
+ e.api.selectAllFiltered();
2538
+ }
2539
+ else {
2540
+ e.api.deselectAll();
2541
+ }
2542
+ return true;
2543
+ }
2544
+ return false;
2545
+ },
2546
+ onCellClicked: clickSelectorCheckboxWhenContainingCellClicked,
2547
+ },
2548
+ ...adjustColDefs,
2549
+ ]
2550
+ : adjustColDefs;
2551
+ }, [
2552
+ params.columnDefs,
2553
+ params.selectable,
2554
+ params.readOnly,
2555
+ params.defaultColDef?.editable,
2556
+ selectColumnPinned,
2557
+ rowSelection,
2558
+ clickSelectorCheckboxWhenContainingCellClicked,
2559
+ ]);
2560
+ /**
2561
+ * When grid is ready set the apis to the grid context and sync selected items to grid.
2562
+ */
2563
+ const onGridReady = React.useCallback((event) => {
2564
+ setApis(event.api, event.columnApi, dataTestId);
2565
+ synchroniseExternallySelectedItemsToGrid();
2566
+ }, [dataTestId, setApis, synchroniseExternallySelectedItemsToGrid]);
2567
+ /**
2568
+ * When the grid is being initialized the data may be empty.
2569
+ * This will resize columns when we have at least one row.
2570
+ */
2571
+ const previousRowDataLength = React.useRef(0);
2572
+ const onRowDataChanged = React.useCallback(() => {
2573
+ const length = params.rowData?.length ?? 0;
2574
+ if (previousRowDataLength.current !== length) {
2575
+ setInitialContentSize();
2576
+ previousRowDataLength.current = length;
2577
+ }
2578
+ if (lastUpdatedDep.current === updatedDep || lodashEs.isEmpty(colIdsEdited.current))
2579
+ return;
2580
+ lastUpdatedDep.current = updatedDep;
2581
+ // Don't update while there are spinners
2582
+ if (!lodashEs.isEmpty(updatingCols()))
2583
+ return;
2584
+ const skipHeader = sizeColumns === "auto-skip-headers";
2585
+ autoSizeColumns({ skipHeader, userSizedColIds: userSizedColIds.current, colIds: colIdsEdited.current });
2586
+ colIdsEdited.current.clear();
2587
+ }, [autoSizeColumns, params.rowData?.length, setInitialContentSize, sizeColumns, updatedDep, updatingCols]);
2588
+ /**
2589
+ * Show/hide no rows overlay when model changes.
2590
+ */
2591
+ const isShowingNoRowsOverlay = React.useRef(false);
2592
+ const onModelUpdated = React.useCallback((event) => {
2593
+ if (event.api.getDisplayedRowCount() === 0) {
2594
+ if (!isShowingNoRowsOverlay.current) {
2595
+ event.api.showNoRowsOverlay();
2596
+ isShowingNoRowsOverlay.current = true;
2772
2597
  }
2773
- else {
2774
- e.stopPropagation();
2775
- e.preventDefault();
2598
+ }
2599
+ else {
2600
+ if (isShowingNoRowsOverlay.current) {
2601
+ event.api.hideOverlay();
2602
+ isShowingNoRowsOverlay.current = false;
2776
2603
  }
2604
+ }
2605
+ }, []);
2606
+ /**
2607
+ * Force-refresh all selected rows to re-run class function, to update selection highlighting
2608
+ */
2609
+ const refreshSelectedRows = React.useCallback((event) => {
2610
+ event.api.refreshCells({
2611
+ force: true,
2612
+ rowNodes: event.api.getSelectedNodes(),
2613
+ });
2614
+ }, []);
2615
+ /**
2616
+ * Make sure node is selected for editing and start edit
2617
+ */
2618
+ const startCellEditing = React.useCallback((event) => {
2619
+ prePopupOps();
2620
+ if (!event.node.isSelected()) {
2621
+ event.node.setSelected(true, true);
2622
+ }
2623
+ // Cell already being edited, so don't re-edit until finished
2624
+ if (checkUpdating([event.colDef.field ?? ""], event.data.id)) {
2777
2625
  return;
2778
2626
  }
2779
- const event = {
2780
- value,
2781
- syntheticEvent: e,
2782
- };
2783
- if (e.key !== undefined) {
2784
- const ke = e;
2785
- event.key = ke.key;
2786
- event.shiftKey = ke.shiftKey;
2627
+ if (event.rowIndex !== null) {
2628
+ event.api.startEditingCell({
2629
+ rowIndex: event.rowIndex,
2630
+ colKey: event.column.getColId(),
2631
+ });
2787
2632
  }
2788
- if (isCheckBox)
2789
- event.checked = !isChecked;
2790
- if (isRadio)
2791
- event.name = radioGroup.name;
2792
- safeCall(onClick, event);
2793
- if (isRadio)
2794
- safeCall(radioGroup.onRadioChange, event);
2795
- eventHandlers.handleClick(event, isCheckBox || isRadio);
2796
- };
2797
- const handleKeyDown = (e) => {
2798
- // if tab is allowed the handleKeyUp event can't process the tab
2799
- if (e.key === "Tab") {
2800
- e.preventDefault();
2801
- e.stopPropagation();
2633
+ }, [checkUpdating, prePopupOps]);
2634
+ /**
2635
+ * Handle double click edit
2636
+ */
2637
+ const onCellDoubleClick = React.useCallback((event) => {
2638
+ if (!invokeEditAction(event))
2639
+ startCellEditing(event);
2640
+ }, [startCellEditing]);
2641
+ /**
2642
+ * Handle single click edits
2643
+ */
2644
+ const onCellClicked = React.useCallback((event) => {
2645
+ if (event.colDef?.cellRendererParams?.singleClickEdit) {
2646
+ startCellEditing(event);
2647
+ }
2648
+ }, [startCellEditing]);
2649
+ /**
2650
+ * If cell has an edit action invoke it (if editable)
2651
+ */
2652
+ const invokeEditAction = (e) => {
2653
+ const editAction = e.colDef?.cellRendererParams?.editAction;
2654
+ if (!editAction)
2655
+ return false;
2656
+ const editable = fnOrVar(e.colDef?.editable, e);
2657
+ if (editable) {
2658
+ if (!e.node.isSelected()) {
2659
+ e.node.setSelected(true, true);
2660
+ }
2661
+ editAction([e.data, ...e.api.getSelectedRows().filter((row) => row.id !== e.data.id)]);
2802
2662
  }
2663
+ return true;
2803
2664
  };
2804
2665
  /**
2805
- * Keyboard events are triggered on up, otherwise subcomponents get spaces and enters typed in them
2666
+ * Start editing on pressing Enter
2806
2667
  */
2807
- const handleKeyUp = (e) => {
2808
- if (!isHovering)
2668
+ const onCellKeyPress = React.useCallback((e) => {
2669
+ if (e.event.key === "Enter") {
2670
+ if (!invokeEditAction(e))
2671
+ startCellEditing(e);
2672
+ }
2673
+ }, [startCellEditing]);
2674
+ /**
2675
+ * Once the grid has auto-sized we want to run fit to fit the grid in its container,
2676
+ * but we don't want the non-flex auto-sized columns to "fit" size, so suppressSizeToFit is set to true.
2677
+ */
2678
+ const columnDefsAdjusted = React.useMemo(() => {
2679
+ const adjustColDefOrGroup = (colDef) => "children" in colDef ? adjustGroupColDef(colDef) : adjustColDef(colDef);
2680
+ const adjustGroupColDef = (colDef) => ({
2681
+ ...colDef,
2682
+ children: colDef.children.map((colDef) => adjustColDefOrGroup(colDef)),
2683
+ });
2684
+ const adjustColDef = (colDef) => ({
2685
+ ...colDef,
2686
+ suppressSizeToFit: (sizeColumns === "auto" || sizeColumns === "auto-skip-headers") && !colDef.flex,
2687
+ sortable: colDef.sortable && params.defaultColDef?.sortable !== false,
2688
+ });
2689
+ return columnDefs.map((colDef) => adjustColDefOrGroup(colDef));
2690
+ }, [columnDefs, params.defaultColDef?.sortable, sizeColumns]);
2691
+ /**
2692
+ * Set of colIds that need auto-sizing.
2693
+ */
2694
+ const colIdsEdited = React.useRef(new Set());
2695
+ const lastUpdatedDep = React.useRef(updatedDep);
2696
+ /**
2697
+ * When cell editing has completed the colId as needing auto-sizing
2698
+ */
2699
+ React.useEffect(() => {
2700
+ if (lastUpdatedDep.current === updatedDep)
2809
2701
  return;
2810
- switch (e.key) {
2811
- case Keys.ENTER:
2812
- case Keys.TAB:
2813
- case Keys.SPACE:
2814
- e.preventDefault();
2815
- e.stopPropagation();
2816
- if (isAnchor) {
2817
- menuItemRef?.current && menuItemRef.current.click();
2702
+ lastUpdatedDep.current = updatedDep;
2703
+ const colIds = updatingCols();
2704
+ // Updating possibly completed
2705
+ if (lodashEs.isEmpty(colIds)) {
2706
+ // Columns to resize?
2707
+ if (!lodashEs.isEmpty(colIdsEdited.current)) {
2708
+ const skipHeader = sizeColumns === "auto-skip-headers";
2709
+ if (sizeColumns === "auto" || skipHeader) {
2710
+ lodashEs.defer(() => autoSizeColumns({
2711
+ skipHeader,
2712
+ userSizedColIds: userSizedColIds.current,
2713
+ colIds: colIdsEdited.current,
2714
+ }));
2818
2715
  }
2819
- else {
2820
- handleClick(e);
2716
+ colIdsEdited.current.clear();
2717
+ }
2718
+ }
2719
+ else {
2720
+ // Updates not complete, add them to a list of columns needing resize
2721
+ colIds.forEach((colId) => {
2722
+ if (colId && !getColDef(colId)?.flex) {
2723
+ colIdsEdited.current.add(colId);
2821
2724
  }
2725
+ });
2726
+ }
2727
+ }, [autoSizeColumns, getColDef, sizeColumns, updatedDep, updatingCols]);
2728
+ /**
2729
+ * Resize columns to fit if required on window/container resize
2730
+ */
2731
+ const onGridSizeChanged = React.useCallback(() => {
2732
+ if (sizeColumns !== "none") {
2733
+ sizeColumnsToFit();
2734
+ }
2735
+ }, [sizeColumns, sizeColumnsToFit]);
2736
+ /**
2737
+ * Set of column Id's that are prevented from auto-sizing as they are user set
2738
+ */
2739
+ const userSizedColIds = React.useRef(new Set());
2740
+ /**
2741
+ * Lock/unlock column width on user edit/reset.
2742
+ */
2743
+ const onColumnResized = React.useCallback((e) => {
2744
+ const colId = e.column?.getColId();
2745
+ if (colId == null)
2746
+ return;
2747
+ switch (e.source) {
2748
+ case "uiColumnDragged":
2749
+ userSizedColIds.current.add(colId);
2750
+ break;
2751
+ case "autosizeColumns":
2752
+ userSizedColIds.current.delete(colId);
2822
2753
  break;
2823
2754
  }
2824
- };
2825
- const modifiers = React.useMemo(() => ({
2826
- type,
2827
- disabled: isDisabled,
2828
- hover: isHovering,
2829
- checked: isChecked,
2830
- anchor: isAnchor,
2831
- }), [type, isDisabled, isHovering, isChecked, isAnchor]);
2832
- const mergedProps = mergeProps({
2833
- ...restStateProps,
2834
- onPointerDown: setHover,
2835
- onKeyDown: handleKeyDown,
2836
- onKeyUp: handleKeyUp,
2837
- onClick: handleClick,
2838
- }, restProps);
2839
- // Order of props overriding (same in all components):
2840
- // 1. Preset props adhering to WAI-ARIA Authoring Practices.
2841
- // 2. Merged outer and local props
2842
- // 3. ref, className
2843
- const menuItemProps = {
2844
- role: isRadio ? "menuitemradio" : isCheckBox ? "menuitemcheckbox" : "menuitem",
2845
- "aria-checked": isRadio || isCheckBox ? isChecked : undefined,
2846
- ...mergedProps,
2847
- ...commonProps(isDisabled, isHovering),
2848
- ref: useCombinedRef(externalRef, menuItemRef),
2849
- className: useBEM({ block: menuClass, element: menuItemClass, modifiers, className }),
2850
- children: React.useMemo(() => safeCall(children, modifiers), [children, modifiers]),
2851
- };
2852
- if (isAnchor) {
2853
- return (jsxRuntime.jsx("li", { role: "presentation", children: jsxRuntime.jsx("a", { href: href, ...menuItemProps }) }));
2854
- }
2855
- else {
2856
- return jsxRuntime.jsx("li", { ...menuItemProps });
2755
+ }, []);
2756
+ const gridContextMenu = useGridContextMenu({ contextMenu: params.contextMenu, contextMenuSelectRow });
2757
+ // This is setting a ref in the GridContext so won't be triggering an update loop
2758
+ setOnCellEditingComplete(params.onCellEditingComplete);
2759
+ return (jsxRuntime.jsxs("div", { "data-testid": dataTestId, className: clsx("Grid-container", theme, staleGrid && "Grid-sortIsStale", gridReady && params.rowData && "Grid-ready"), children: [gridContextMenu.component, jsxRuntime.jsx("div", { style: { flex: 1 }, ref: gridDivRef, children: jsxRuntime.jsx(agGridReact.AgGridReact, { rowHeight: params.rowHeight, animateRows: params.animateRows, rowClassRules: params.rowClassRules, getRowId: (params) => `${params.data.id}`, suppressRowClickSelection: true, rowSelection: rowSelection, suppressBrowserResizeObserver: true, onGridSizeChanged: onGridSizeChanged, suppressColumnVirtualisation: suppressColumnVirtualization, suppressClickEdit: true, onColumnVisible: () => {
2760
+ setInitialContentSize();
2761
+ }, onRowDataChanged: onRowDataChanged, onCellKeyPress: onCellKeyPress, onCellClicked: onCellClicked, onCellDoubleClicked: onCellDoubleClick, onCellEditingStarted: refreshSelectedRows, domLayout: params.domLayout, onColumnResized: onColumnResized, defaultColDef: { minWidth: 48, ...lodashEs.omit(params.defaultColDef, ["editable"]) }, columnDefs: columnDefsAdjusted, rowData: params.rowData, noRowsOverlayComponent: GridNoRowsOverlay, noRowsOverlayComponentParams: {
2762
+ rowData: params.rowData,
2763
+ noRowsOverlayText: params.noRowsOverlayText,
2764
+ }, onModelUpdated: onModelUpdated, onGridReady: onGridReady, onSortChanged: ensureSelectedRowIsVisible, postSortRows: params.postSortRows ?? postSortRows, onSelectionChanged: synchroniseExternalStateToGridSelection, onColumnMoved: params.onColumnMoved, alwaysShowVerticalScroll: params.alwaysShowVerticalScroll, isExternalFilterPresent: isExternalFilterPresent, doesExternalFilterPass: doesExternalFilterPass, maintainColumnOrder: true, preventDefaultOnContextMenu: true, onCellContextMenu: gridContextMenu.cellContextMenu }) })] }));
2765
+ };
2766
+
2767
+ const GridPopoverContext = React.createContext({
2768
+ anchorRef: { current: null },
2769
+ saving: false,
2770
+ setSaving: () => { },
2771
+ field: "",
2772
+ value: null,
2773
+ data: {},
2774
+ selectedRows: [],
2775
+ updateValue: async () => false,
2776
+ formatValue: () => "! No gridPopoverContextProvider !",
2777
+ });
2778
+ const useGridPopoverContext = () => React.useContext(GridPopoverContext);
2779
+
2780
+ const GridPopoverContextProvider = ({ props, children }) => {
2781
+ const { getFilteredSelectedRows, updatingCells } = React.useContext(GridContext);
2782
+ const anchorRef = React.useRef(props.eGridCell);
2783
+ const hasSaved = React.useRef(false);
2784
+ const [saving, setSaving] = React.useState(false);
2785
+ const { colDef } = props;
2786
+ const { cellEditorParams } = colDef;
2787
+ const multiEdit = cellEditorParams?.multiEdit ?? false;
2788
+ // Then item that is clicked on will always be first in the list
2789
+ const selectedRows = React.useMemo(() => multiEdit ? lodashEs.sortBy(getFilteredSelectedRows(), (row) => row.id !== props.data.id) : [props.data], [getFilteredSelectedRows, multiEdit, props.data]);
2790
+ const field = props.colDef?.field ?? "";
2791
+ const updateValue = React.useCallback(async (saveFn, tabDirection) => {
2792
+ if (hasSaved.current)
2793
+ return true;
2794
+ hasSaved.current = true;
2795
+ return saving ? false : await updatingCells({ selectedRows, field }, saveFn, setSaving, tabDirection);
2796
+ }, [field, saving, selectedRows, updatingCells]);
2797
+ return (jsxRuntime.jsx(GridPopoverContext.Provider, { value: {
2798
+ anchorRef: anchorRef,
2799
+ saving,
2800
+ setSaving,
2801
+ selectedRows,
2802
+ field,
2803
+ data: props.data,
2804
+ value: props.value,
2805
+ updateValue,
2806
+ formatValue: props.formatValue,
2807
+ }, children: children }));
2808
+ };
2809
+
2810
+ const GridCellMultiSelectClassRules = {
2811
+ "ag-selected-for-edit": (params) => {
2812
+ const { api, node, colDef } = params;
2813
+ const cep = colDef.cellEditorSelector
2814
+ ? colDef.cellEditorSelector(params)?.params
2815
+ : colDef.cellEditorParams;
2816
+ return (cep?.multiEdit &&
2817
+ api
2818
+ .getSelectedNodes()
2819
+ .map((row) => row.id)
2820
+ .includes(node.id) &&
2821
+ api.getEditingCells().some((cell) => cell.column.getColDef() === colDef));
2822
+ },
2823
+ };
2824
+
2825
+ const GridIcon = (props) => (jsxRuntime.jsx(lui.LuiIcon, { name: props.icon, title: props.title, alt: props.title, size: props.size ?? "md", className: clsx(`AgGridGenericCellRenderer-${props.icon}Icon`, props.className, props.disabled && "GridIcon-disabled") }));
2826
+
2827
+ const GridLoadableCell = () => (jsxRuntime.jsx(lui.LuiMiniSpinner, { size: 22, divProps: { className: "GridLoadableCell-container", role: "status", "aria-label": "Loading" } }));
2828
+
2829
+ const GridCellRenderer = (props) => {
2830
+ const { checkUpdating } = React.useContext(GridUpdatingContext);
2831
+ const colDef = props.colDef;
2832
+ const rendererParams = colDef.cellRendererParams;
2833
+ const warningFn = rendererParams?.warning;
2834
+ let warningText = warningFn ? warningFn(props) : undefined;
2835
+ const infoFn = rendererParams?.info;
2836
+ let infoText = infoFn ? infoFn(props) : undefined;
2837
+ if (Array.isArray(warningText))
2838
+ warningText = warningText.join("\n");
2839
+ if (Array.isArray(infoText))
2840
+ infoText = infoText.join("\n");
2841
+ return checkUpdating(colDef.field ?? colDef.colId ?? "", props.data.id) ? (jsxRuntime.jsx(GridLoadableCell, {})) : (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [!!warningText && (jsxRuntime.jsx(GridIcon, { icon: "ic_warning_outline", title: typeof warningText === "string" ? warningText : "Warning" })), !!infoText && jsxRuntime.jsx(GridIcon, { icon: "ic_info_outline", title: typeof infoText === "string" ? infoText : "Info" }), jsxRuntime.jsx("div", { className: "GridCell-container", children: colDef.cellRendererParams?.originalCellRenderer ? (jsxRuntime.jsx(colDef.cellRendererParams.originalCellRenderer, { ...props })) : (jsxRuntime.jsx("span", { title: props.valueFormatted ?? undefined, children: props.valueFormatted })) }), fnOrVar(colDef.editable, props) && rendererParams?.rightHoverElement && (jsxRuntime.jsx("div", { className: "GridCell-hoverRight", children: rendererParams?.rightHoverElement }))] }));
2842
+ };
2843
+ const suppressCellKeyboardEvents = (e) => {
2844
+ const shortcutKeys = e.colDef.cellRendererParams?.shortcutKeys ?? {};
2845
+ const exec = shortcutKeys[e.event.key];
2846
+ if (!e.editing && !e.event.repeat && e.event.type === "keypress" && exec) {
2847
+ const editable = fnOrVar(e.colDef?.editable, e);
2848
+ return editable ? exec(e) ?? true : true;
2857
2849
  }
2850
+ // It's important that aggrid doesn't trigger edit on enter
2851
+ // as the incorrect selected rows will be returned
2852
+ return !["ArrowLeft", "ArrowRight", "ArrowDown", "ArrowUp", "Tab", " ", "Home", "End", "PageUp", "PageDown"].includes(e.event.key);
2858
2853
  };
2859
- const MenuItem = withHovering("MenuItem", MenuItemFr);
2860
-
2861
- const FocusableItemFr = ({ className, disabled, children, isHovering, menuItemRef, externalRef, ...restProps }) => {
2862
- const isDisabled = !!disabled;
2863
- const ref = React.useRef(null);
2864
- const { setHover, onPointerLeave, ...restStateProps } = useItemState(menuItemRef, ref, isHovering, isDisabled);
2865
- const { handleClose } = React.useContext(EventHandlersContext);
2866
- const modifiers = React.useMemo(() => ({
2867
- disabled: isDisabled,
2868
- hover: isHovering,
2869
- focusable: true,
2870
- }), [isDisabled, isHovering]);
2871
- const renderChildren = React.useMemo(() => safeCall(children, {
2872
- ...modifiers,
2873
- ref,
2874
- closeMenu: handleClose,
2875
- }), [children, modifiers, handleClose]);
2876
- const mergedProps = mergeProps({
2877
- ...restStateProps,
2878
- onPointerLeave: (e) => onPointerLeave(e, true),
2879
- onFocus: setHover,
2880
- }, restProps);
2881
- return (jsxRuntime.jsx("li", { role: "menuitem", ...mergedProps, ...commonProps(isDisabled), ref: useCombinedRef(externalRef, menuItemRef), className: useBEM({ block: menuClass, element: menuItemClass, modifiers, className }), children: renderChildren }));
2854
+ const generateFilterGetter = (field, filterValueGetter, valueFormatter) => {
2855
+ if (filterValueGetter)
2856
+ return filterValueGetter;
2857
+ // aggrid will default to valueGetter
2858
+ if (typeof valueFormatter !== "function" || !field)
2859
+ return undefined;
2860
+ return (params) => {
2861
+ const value = params.getValue(field);
2862
+ let formattedValue = valueFormatter({ ...params, value });
2863
+ // Search for null values using standard dash
2864
+ if (formattedValue === "–")
2865
+ formattedValue += " -";
2866
+ // Search by raw value as well as formatted
2867
+ const gotValue = ["string", "number"].includes(typeof value) ? value : undefined;
2868
+ return (formattedValue + (gotValue != null && formattedValue != gotValue ? " " + gotValue : "")) //
2869
+ .replaceAll(/\s+/g, " ")
2870
+ .trim();
2871
+ };
2882
2872
  };
2883
- const FocusableItem = withHovering("FocusableItem", FocusableItemFr);
2884
-
2885
- const MenuDividerFr = ({ className, ...restProps }, externalRef) => {
2886
- return (jsxRuntime.jsx("li", { role: "separator", ...restProps, ref: externalRef, className: useBEM({ block: menuClass, element: menuDividerClass, className }) }));
2873
+ /*
2874
+ * All cells should use this.
2875
+ */
2876
+ const GridCell = (props, custom) => {
2877
+ // Generate a default filter value getter which uses the formatted value plus
2878
+ // the editable value if it's a string and different from the formatted value.
2879
+ // This is so that e.g. bearings can be searched for by DMS or raw number.
2880
+ const valueFormatter = props.valueFormatter;
2881
+ const filterValueGetter = generateFilterGetter(props.field, props.filterValueGetter, valueFormatter);
2882
+ const exportable = props.exportable;
2883
+ // Can't leave this here ag-grid will complain
2884
+ delete props.exportable;
2885
+ return {
2886
+ colId: props.field ?? props.field,
2887
+ headerTooltip: props.headerName,
2888
+ sortable: !!(props?.field || props?.valueGetter),
2889
+ resizable: true,
2890
+ editable: props.editable ?? false,
2891
+ ...(custom?.editor && {
2892
+ cellClassRules: GridCellMultiSelectClassRules,
2893
+ editable: props.editable ?? true,
2894
+ cellEditor: GenericCellEditorComponentWrapper(custom?.editor),
2895
+ }),
2896
+ suppressKeyboardEvent: suppressCellKeyboardEvents,
2897
+ ...(custom?.editorParams && {
2898
+ cellEditorParams: {
2899
+ ...custom.editorParams,
2900
+ multiEdit: custom.multiEdit,
2901
+ preventAutoEdit: custom.preventAutoEdit ?? false,
2902
+ },
2903
+ }),
2904
+ // If there's a valueFormatter and no filterValueGetter then create a filterValueGetter
2905
+ filterValueGetter,
2906
+ // Default value formatter, otherwise react freaks out on objects
2907
+ valueFormatter: (params) => {
2908
+ if (params.value == null)
2909
+ return "–";
2910
+ const types = ["number", "boolean", "string"];
2911
+ if (types.includes(typeof params.value))
2912
+ return `${params.value}`;
2913
+ else
2914
+ return JSON.stringify(params.value);
2915
+ },
2916
+ ...props,
2917
+ cellRenderer: GridCellRenderer,
2918
+ cellRendererParams: {
2919
+ originalCellRenderer: props.cellRenderer,
2920
+ ...props.cellRendererParams,
2921
+ },
2922
+ headerComponentParams: {
2923
+ exportable,
2924
+ ...props.headerComponentParams,
2925
+ },
2926
+ };
2887
2927
  };
2888
- const MenuDivider = React.memo(React.forwardRef(MenuDividerFr));
2928
+ const GenericCellEditorComponentWrapper = (editor) => {
2929
+ const obj = { editor };
2930
+ return React.forwardRef(function GenericCellEditorComponentFr(cellEditorParams, _) {
2931
+ const valueFormatted = cellEditorParams.formatValue
2932
+ ? cellEditorParams.formatValue(cellEditorParams.value)
2933
+ : "Missing formatter";
2934
+ return (jsxRuntime.jsxs(GridPopoverContextProvider, { props: cellEditorParams, children: [jsxRuntime.jsx(cellEditorParams.colDef.cellRenderer, { ...cellEditorParams, valueFormatted: valueFormatted, ...cellEditorParams.colDef.cellRendererParams }), obj.editor && jsxRuntime.jsx(obj.editor, { ...cellEditorParams })] }));
2935
+ });
2936
+ };
2889
2937
 
2890
- const MenuHeaderFr = ({ className, ...restProps }, externalRef) => {
2891
- return (jsxRuntime.jsx("li", { role: "presentation", ...restProps, ref: externalRef, className: useBEM({ block: menuClass, element: menuHeaderClass, className }) }));
2892
- };
2893
- const MenuHeader = React.memo(React.forwardRef(MenuHeaderFr));
2938
+ const GridCellFillerColId = "gridCellFiller";
2939
+ const isGridCellFiller = (col) => col.colId === GridCellFillerColId;
2940
+ const GridCellFiller = () => ({
2941
+ colId: GridCellFillerColId,
2942
+ headerName: "",
2943
+ flex: 1,
2944
+ });
2894
2945
 
2895
- const MenuGroupFr = ({ className, style, takeOverflow, ...restProps }, externalRef) => {
2896
- const ref = React.useRef(null);
2897
- const [overflowStyle, setOverflowStyle] = React.useState();
2898
- const { overflow, overflowAmt } = React.useContext(MenuListContext);
2899
- useIsomorphicLayoutEffect(() => {
2900
- let maxHeight;
2901
- if (takeOverflow && overflowAmt != null && overflowAmt >= 0 && ref.current) {
2902
- maxHeight = ref.current.getBoundingClientRect().height - overflowAmt;
2903
- if (maxHeight < 0)
2904
- maxHeight = 0;
2905
- }
2906
- setOverflowStyle(maxHeight != null && maxHeight >= 0 ? { maxHeight, overflow } : undefined);
2907
- }, [takeOverflow, overflow, overflowAmt]);
2908
- useIsomorphicLayoutEffect(() => {
2909
- if (overflowStyle && ref.current)
2910
- ref.current.scrollTop = 0;
2911
- }, [overflowStyle]);
2912
- return (jsxRuntime.jsx("div", { ...restProps, ref: useCombinedRef(externalRef, ref), className: useBEM({ block: menuClass, element: menuGroupClass, className }), style: { ...style, ...overflowStyle } }));
2913
- };
2914
- const MenuGroup = React.forwardRef(MenuGroupFr);
2946
+ const Editor = (props) => ({
2947
+ component: GenericCellEditorComponentWrapper(props.editor),
2948
+ params: { ...props.editorParams, multiEdit: props.multiEdit },
2949
+ });
2950
+ /**
2951
+ * Used to choose between cell editors based in data.
2952
+ */
2953
+ const GridCellMultiEditor = (props, cellEditorSelector) => GridCell({
2954
+ cellClassRules: GridCellMultiSelectClassRules,
2955
+ cellEditorSelector,
2956
+ editable: props.editable ?? true,
2957
+ ...props,
2958
+ });
2915
2959
 
2916
- const MenuRadioGroupFr = ({ "aria-label": ariaLabel, className, name, value, onRadioChange, ...restProps }, externalRef) => {
2917
- const contextValue = React.useMemo(() => ({ name, value, onRadioChange }), [name, value, onRadioChange]);
2918
- return (jsxRuntime.jsx(RadioGroupContext.Provider, { value: contextValue, children: jsxRuntime.jsx("li", { role: "presentation", children: jsxRuntime.jsx("ul", { role: "group", "aria-label": ariaLabel || name || "Radio group", ...restProps, ref: externalRef, className: useBEM({ block: menuClass, element: radioGroupClass, className }) }) }) }));
2919
- };
2920
- const MenuRadioGroup = React.forwardRef(MenuRadioGroupFr);
2960
+ const GridFilterHeaderIconButton = React.forwardRef(function columnsButton({ icon, title, onClick, buttonProps, disabled = false, size = "md" }, ref) {
2961
+ return (jsxRuntime.jsx(lui.LuiButton, { ...buttonProps, type: "button", level: "tertiary", className: "lui-button-icon-only", ref: ref, "aria-label": title, title: title, onClick: onClick, disabled: disabled, children: jsxRuntime.jsx(lui.LuiIcon, { name: icon, alt: "Menu", size: size }) }));
2962
+ });
2921
2963
 
2922
2964
  const GridFilterColumnsToggle = ({ saveState = true }) => {
2923
2965
  const [loaded, setLoaded] = React.useState(false);
@@ -4527,6 +4569,16 @@ const GridContextProvider = (props) => {
4527
4569
  const prePopupOps = React.useCallback(() => {
4528
4570
  prePopupFocusedCell.current = gridApi?.getFocusedCell() ?? undefined;
4529
4571
  }, [gridApi]);
4572
+ /**
4573
+ * After a popup refocus the cell.
4574
+ */
4575
+ const postPopupOps = React.useCallback(() => {
4576
+ if (!gridApi)
4577
+ return;
4578
+ if (prePopupFocusedCell.current) {
4579
+ gridApi?.setFocusedCell(prePopupFocusedCell.current.rowIndex, prePopupFocusedCell.current.column);
4580
+ }
4581
+ }, [gridApi]);
4530
4582
  /**
4531
4583
  * Get all row id's in grid.
4532
4584
  */
@@ -4899,6 +4951,7 @@ const GridContextProvider = (props) => {
4899
4951
  setInvisibleColumnIds,
4900
4952
  gridReady,
4901
4953
  prePopupOps,
4954
+ postPopupOps,
4902
4955
  setApis,
4903
4956
  setQuickFilter,
4904
4957
  selectRowsById,
@@ -28628,6 +28681,7 @@ exports.typeOtherTextArea = typeOtherTextArea;
28628
28681
  exports.useDeferredPromise = useDeferredPromise;
28629
28682
  exports.useGenerateOrDefaultId = useGenerateOrDefaultId;
28630
28683
  exports.useGridContext = useGridContext;
28684
+ exports.useGridContextMenu = useGridContextMenu;
28631
28685
  exports.useGridFilter = useGridFilter;
28632
28686
  exports.useGridPopoverContext = useGridPopoverContext;
28633
28687
  exports.useGridPopoverHook = useGridPopoverHook;