@@ -403,6 +403,152 @@ function NoDataTable(props) {
403403
404404</details >
405405
406+ ## Context Menu
407+
408+ The ` onRowContextMenu ` callback fires when a row is right-clicked. It provides the ` row ` and ` column ` (if the click targeted a specific cell) in ` e.detail ` . The native browser context menu is ** not** suppressed — call ` e.preventDefault() ` in your callback to replace it with a custom menu.
409+
410+ This example shows two tables with products that can be moved between them via buttons or a right-click context menu.
411+
412+ <Canvas sourceState = " none" of = { ComponentStories .ContextMenu } />
413+
414+ ### Code
415+
416+ <details >
417+
418+ <summary >Show Code</summary >
419+
420+ ``` tsx
421+ const productData = [
422+ { id: ' 1' , product: ' Laptop Pro 15' , category: ' Electronics' , price: 1299 },
423+ { id: ' 2' , product: ' Wireless Mouse' , category: ' Accessories' , price: 49 },
424+ // ...
425+ ];
426+
427+ type Product = (typeof productData )[number ];
428+
429+ const productColumns = [
430+ { Header: ' Product' , accessor: ' product' },
431+ { Header: ' Category' , accessor: ' category' },
432+ { Header: ' Price' , accessor: ' price' , hAlign: TextAlign .End },
433+ ];
434+
435+ function ContextMenuExample() {
436+ const [availableProducts, setAvailableProducts] = useState (productData );
437+ const [selectedProducts, setSelectedProducts] = useState <Product []>([]);
438+ const [checkedAvailable, setCheckedAvailable] = useState <Product []>([]);
439+ const [checkedSelected, setCheckedSelected] = useState <Product []>([]);
440+ const [menuOpen, setMenuOpen] = useState (false );
441+ const [menuTarget, setMenuTarget] = useState <' available' | ' selected' >(' available' );
442+ const [contextRow, setContextRow] = useState <Product | null >(null );
443+ const anchorRef = useRef <HTMLDivElement >(null );
444+ const rafId = useRef (0 );
445+ useEffect (() => {
446+ return () => {
447+ cancelAnimationFrame (rafId .current );
448+ };
449+ }, []);
450+
451+ const moveToSelected = (rows : Product []) => {
452+ const ids = new Set (rows .map ((r ) => r .id ));
453+ setAvailableProducts ((prev ) => prev .filter ((p ) => ! ids .has (p .id )));
454+ setSelectedProducts ((prev ) => [... prev , ... rows .filter ((r ) => ! prev .some ((p ) => p .id === r .id ))]);
455+ setCheckedAvailable ([]);
456+ };
457+
458+ const moveToAvailable = (rows : Product []) => {
459+ const ids = new Set (rows .map ((r ) => r .id ));
460+ setSelectedProducts ((prev ) => prev .filter ((p ) => ! ids .has (p .id )));
461+ setAvailableProducts ((prev ) => [... prev , ... rows .filter ((r ) => ! prev .some ((p ) => p .id === r .id ))]);
462+ setCheckedSelected ([]);
463+ };
464+
465+ const handleRowSelect: (
466+ setter : typeof setCheckedAvailable
467+ ) => AnalyticalTablePropTypes [' onRowSelect' ] = (setter ) => (e ) => {
468+ const rows = Object .values (e .detail .rowsById )
469+ .filter ((r ) => e .detail .selectedRowIds [r .id ])
470+ .map ((r ) => r .original as Product );
471+ setter (rows );
472+ };
473+
474+ const handleContextMenu: (
475+ target : ' available' | ' selected'
476+ ) => AnalyticalTablePropTypes [' onRowContextMenu' ] = (target ) => (e ) => {
477+ e .preventDefault ();
478+ setContextRow (e .detail .row .original as Product );
479+ setMenuTarget (target );
480+ if (anchorRef .current ) {
481+ anchorRef .current .style .left = ` ${e .clientX }px ` ;
482+ anchorRef .current .style .top = ` ${e .clientY }px ` ;
483+ }
484+ // Defer open so it runs after the menu's onClose from the previous right-click.
485+ setMenuOpen (false );
486+ rafId .current = requestAnimationFrame (() => setMenuOpen (true ));
487+ };
488+
489+ const handleMenuItemClick = () => {
490+ if (! contextRow ) {
491+ return ;
492+ }
493+ if (menuTarget === ' available' ) {
494+ moveToSelected ([contextRow ]);
495+ } else {
496+ moveToAvailable ([contextRow ]);
497+ }
498+ setMenuOpen (false );
499+ setContextRow (null );
500+ };
501+
502+ return (
503+ <>
504+ <FlexBox alignItems = { FlexBoxAlignItems .Start } style = { { gap: ' 0.5rem' }} >
505+ <AnalyticalTable
506+ header = " Available Products"
507+ columns = { productColumns }
508+ data = { availableProducts }
509+ selectionMode = " Multiple"
510+ onRowContextMenu = { handleContextMenu (' available' )}
511+ onRowSelect = { handleRowSelect (setCheckedAvailable )}
512+ style = { { flex: 1 }}
513+ />
514+ <FlexBox
515+ direction = { FlexBoxDirection .Column }
516+ justifyContent = { FlexBoxJustifyContent .Center }
517+ style = { { alignSelf: ' center' }}
518+ >
519+ <Button icon = " navigation-right-arrow" onClick = { () => moveToSelected (checkedAvailable )} />
520+ <Button icon = " navigation-left-arrow" onClick = { () => moveToAvailable (checkedSelected )} />
521+ </FlexBox >
522+ <AnalyticalTable
523+ header = " Selected Products"
524+ columns = { productColumns }
525+ data = { selectedProducts }
526+ selectionMode = " Multiple"
527+ onRowContextMenu = { handleContextMenu (' selected' )}
528+ onRowSelect = { handleRowSelect (setCheckedSelected )}
529+ style = { { flex: 1 }}
530+ />
531+ </FlexBox >
532+ { /* Hidden anchor for Menu positioning */ }
533+ <div
534+ ref = { anchorRef }
535+ style = { { position: ' fixed' , width: 0 , height: 0 , pointerEvents: ' none' }}
536+ />
537+ { menuOpen && (
538+ <Menu open opener = { anchorRef .current } onClose = { () => setMenuOpen (false )} onItemClick = { handleMenuItemClick } >
539+ <MenuItem
540+ text = { ` Move to ${menuTarget === ' available' ? ' Selected Products' : ' Available Products' } ` }
541+ icon = { menuTarget === ' available' ? ' navigation-right-arrow' : ' navigation-left-arrow' }
542+ />
543+ </Menu >
544+ )}
545+ </>
546+ );
547+ }
548+ ```
549+
550+ </details >
551+
406552## Kitchen Sink
407553
408554A comprehensive example combining many AnalyticalTable features: sorting, filtering, grouping, custom cells, row and navigation highlighting, infinite scrolling, column reordering, vertical alignment, ` scaleWidthModeOptions ` for custom renderers, ` retainColumnWidth ` , ` sortDescFirst ` , and more.
0 commit comments