@@ -32,6 +32,7 @@ import { tableTranslateWithId } from '../../../utils/componentUtilityFunctions';
3232import { settings } from '../../../constants/Settings' ;
3333import { RuleGroupPropType } from '../../RuleBuilder/RuleBuilderPropTypes' ;
3434import useDynamicOverflowMenuItems from '../../../hooks/useDynamicOverflowMenuItems' ;
35+ import useResponsiveInlineCount from '../../../hooks/useResponsiveInlineCount' ;
3536import { renderTableOverflowItemText } from '../tableUtilities' ;
3637
3738import TableToolbarAdvancedFilterFlyout from './TableToolbarAdvancedFilterFlyout' ;
@@ -255,10 +256,13 @@ const TableToolbar = ({
255256} ) => {
256257 const shouldShowBatchActions = hasRowSelection === 'multi' && totalSelected > 0 ;
257258 const langDir = useLangDirection ( ) ;
259+ const tableToolbarRef = useRef ( null ) ;
258260 const batchActionsRef = useRef ( null ) ;
259261 const previousFocusedElement = useRef ( null ) ;
260262 const toolbarContentRef = useRef ( null ) ;
261263 const batchOverflowMenuRef = useRef ( null ) ;
264+ const batchActionsVisibleRef = useRef ( null ) ;
265+ const batchActionsLayoutRef = useRef ( null ) ;
262266 const [ isBatchOverflowOpen , setIsBatchOverflowOpen ] = React . useState ( false ) ;
263267
264268 // Function to restore focus to the previous element
@@ -468,16 +472,48 @@ const TableToolbar = ({
468472 } ;
469473 } , [ customToolbarContent ] ) ;
470474
471- const visibleBatchActions = batchActions . filter (
472- ( action ) => ! action . isOverflow && action . hidden !== true
475+ const visibleBatchActionCandidates = useMemo (
476+ ( ) => batchActions . filter ( ( action ) => ! action . isOverflow && action . hidden !== true ) ,
477+ [ batchActions ]
478+ ) ;
479+
480+ const staticOverflowBatchActions = useMemo (
481+ ( ) => batchActions . filter ( ( action ) => action . isOverflow && action . hidden !== true ) ,
482+ [ batchActions ]
473483 ) ;
474484
475- const visibleOverflowBatchActions = batchActions . filter (
476- ( action ) => action . isOverflow && action . hidden !== true
485+ const batchActionExcludedWidthSelectors = useMemo (
486+ ( ) => [ '.cds--batch-summary' , '.cds--batch-summary__cancel' ] ,
487+ [ ]
477488 ) ;
478489
479- const hasVisibleBatchActions = visibleBatchActions . length > 0 ;
490+ const responsiveBatchActionCount = useResponsiveInlineCount ( {
491+ enabled :
492+ hasBatchActionToolbar &&
493+ ( visibleBatchActionCandidates . length > 0 || staticOverflowBatchActions . length > 0 ) ,
494+ items : visibleBatchActionCandidates ,
495+ containerRef : batchActionsVisibleRef ,
496+ overflowTriggerRef : batchOverflowMenuRef ,
497+ itemSelector : '[data-batch-action-visible="true"]' ,
498+ staticOverflowCount : staticOverflowBatchActions . length ,
499+ layoutRef : tableToolbarRef ,
500+ excludedWidthSelector : batchActionExcludedWidthSelectors ,
501+ } ) ;
502+
503+ const visibleOverflowBatchActions = [
504+ ...visibleBatchActionCandidates . slice (
505+ responsiveBatchActionCount ?? visibleBatchActionCandidates . length
506+ ) ,
507+ ...staticOverflowBatchActions ,
508+ ] ;
509+
480510 const hasVisibleOverflowBatchActions = visibleOverflowBatchActions . length > 0 ;
511+ const hasStaticOverflowBatchActions = staticOverflowBatchActions . length > 0 ;
512+ const shouldRenderBatchOverflowTrigger =
513+ hasStaticOverflowBatchActions ||
514+ ( responsiveBatchActionCount !== null &&
515+ responsiveBatchActionCount < visibleBatchActionCandidates . length ) ||
516+ hasVisibleOverflowBatchActions ;
481517
482518 const totalSelectedText = useMemo ( ( ) => {
483519 if ( totalSelected > 1 ) {
@@ -513,6 +549,7 @@ const TableToolbar = ({
513549
514550 return (
515551 < CarbonTableToolbar
552+ ref = { tableToolbarRef }
516553 // TODO: remove deprecated 'testID' in v3
517554 data-testid = { testID || testId }
518555 className = { classnames ( `${ iotPrefix } --table-toolbar` , className ) }
@@ -755,13 +792,23 @@ const TableToolbar = ({
755792 ) }
756793 { hasBatchActionToolbar ? (
757794 < TableBatchActions
758- ref = { batchActionsRef }
795+ ref = { ( node ) => {
796+ batchActionsRef . current = node ;
797+ batchActionsLayoutRef . current = node ;
798+ } }
759799 role = "region"
760800 aria-live = "polite"
761801 aria-label = { totalSelectedText }
762802 // TODO: remove deprecated 'testID' in v3
763803 data-testid = { `${ testID || testId } -batch-actions` }
804+ id = { `${ tableId } -batch-actions-root` }
764805 className = { `${ iotPrefix } --table-batch-actions` }
806+ style = { {
807+ width : '100%' ,
808+ maxWidth : '100%' ,
809+ minWidth : 0 ,
810+ overflow : 'hidden' ,
811+ } }
765812 onCancel = { ( ) => {
766813 if ( onCancelBatchAction ) {
767814 onCancelBatchAction ( ) ;
@@ -772,129 +819,166 @@ const TableToolbar = ({
772819 totalSelected = { totalSelected }
773820 translateWithId = { ( ...args ) => tableTranslateWithId ( i18n , ...args ) }
774821 >
775- { hasVisibleBatchActions &&
776- visibleBatchActions . map ( ( { id, labelText, disabled, ...others } ) => (
777- < TableBatchAction
778- key = { id }
779- onClick = { ( ) => {
780- onApplyBatchAction ( id ) ;
781- restoreFocus ( ) ;
782- } }
783- tabIndex = { shouldShowBatchActions ? 0 : - 1 }
784- disabled = { ! shouldShowBatchActions || disabled }
785- { ...others }
786- >
787- { labelText }
788- </ TableBatchAction >
789- ) ) }
790- { hasVisibleOverflowBatchActions ? (
791- < div
792- role = "presentation"
793- ref = { batchOverflowMenuRef }
794- onKeyDown = { ( e ) => {
795- if ( ! shouldShowBatchActions ) return ;
796-
797- // Handle Shift+Tab from within the batch overflow menu
798- if ( e . shiftKey && e . key === 'Tab' ) {
799- const menuItems = batchOverflowMenuRef . current ?. querySelectorAll (
800- '[role="menuitem"]:not([disabled])'
801- ) ;
802- const firstMenuItem = menuItems ?. [ 0 ] ;
803-
804- // If we're on the first menu item, move focus to previous batch action
805- if ( firstMenuItem && document . activeElement === firstMenuItem ) {
806- e . preventDefault ( ) ;
807- // Find previous focusable element in batch actions
808- if ( batchActionsRef . current ) {
809- const focusableElements = batchActionsRef . current . querySelectorAll (
810- 'button:not([disabled]), [tabindex]:not([tabindex="-1"]):not([disabled])'
811- ) ;
812- const overflowButton = batchOverflowMenuRef . current ?. querySelector ( 'button' ) ;
813- const currentIndex = Array . from ( focusableElements ) . indexOf ( overflowButton ) ;
814- const prevElement = focusableElements [ currentIndex - 1 ] ;
815-
816- if ( prevElement ) {
817- prevElement . focus ( ) ;
822+ < div
823+ id = { `${ tableId } -batch-actions-visible` }
824+ ref = { batchActionsVisibleRef }
825+ style = { {
826+ display : 'flex' ,
827+ alignItems : 'center' ,
828+ justifyContent : 'flex-end' ,
829+ minWidth : 0 ,
830+ flex : '1 1 auto' ,
831+ overflow : 'visible' ,
832+ } }
833+ >
834+ { visibleBatchActionCandidates . map ( ( { id, labelText, disabled, ...others } , index ) => {
835+ const isVisibleInline =
836+ responsiveBatchActionCount === null || index < responsiveBatchActionCount ;
837+
838+ return (
839+ < TableBatchAction
840+ key = { id }
841+ id = { `${ tableId } -batch-action-${ id } ` }
842+ data-batch-action-visible = "true"
843+ onClick = { ( ) => {
844+ onApplyBatchAction ( id ) ;
845+ restoreFocus ( ) ;
846+ } }
847+ tabIndex = { isVisibleInline && shouldShowBatchActions ? 0 : - 1 }
848+ disabled = { ! shouldShowBatchActions || disabled }
849+ { ...others }
850+ aria-hidden = { ! isVisibleInline }
851+ style = {
852+ isVisibleInline
853+ ? {
854+ flex : '0 0 auto' ,
855+ }
856+ : {
857+ flex : '0 0 auto' ,
858+ position : 'absolute' ,
859+ visibility : 'hidden' ,
860+ pointerEvents : 'none' ,
861+ }
862+ }
863+ >
864+ { labelText }
865+ </ TableBatchAction >
866+ ) ;
867+ } ) }
868+ { shouldRenderBatchOverflowTrigger ? (
869+ < div
870+ id = { `${ tableId } -batch-actions-overflow-trigger` }
871+ role = "presentation"
872+ ref = { batchOverflowMenuRef }
873+ data-batch-overflow-visible = "true"
874+ style = { { display : 'inline-flex' , flexShrink : 0 } }
875+ onKeyDown = { ( e ) => {
876+ if ( ! shouldShowBatchActions ) return ;
877+
878+ // Handle Shift+Tab from within the batch overflow menu
879+ if ( e . shiftKey && e . key === 'Tab' ) {
880+ const menuItems = batchOverflowMenuRef . current ?. querySelectorAll (
881+ '[role="menuitem"]:not([disabled])'
882+ ) ;
883+ const firstMenuItem = menuItems ?. [ 0 ] ;
884+
885+ // If we're on the first menu item, move focus to previous batch action
886+ if ( firstMenuItem && document . activeElement === firstMenuItem ) {
887+ e . preventDefault ( ) ;
888+ // Find previous focusable element in batch actions
889+ if ( batchActionsRef . current ) {
890+ const focusableElements = batchActionsRef . current . querySelectorAll (
891+ 'button:not([disabled]), [tabindex]:not([tabindex="-1"]):not([disabled])'
892+ ) ;
893+ const overflowButton =
894+ batchOverflowMenuRef . current ?. querySelector ( 'button' ) ;
895+ const currentIndex = Array . from ( focusableElements ) . indexOf ( overflowButton ) ;
896+ const prevElement = focusableElements [ currentIndex - 1 ] ;
897+
898+ if ( prevElement ) {
899+ prevElement . focus ( ) ;
900+ }
818901 }
819902 }
820903 }
821- }
822- // Handle Tab from the last menu item
823- else if ( e . key === 'Tab' && ! e . shiftKey ) {
824- const menuItems = batchOverflowMenuRef . current ?. querySelectorAll (
825- '[role="menuitem"]:not([disabled])'
826- ) ;
827- const lastMenuItem = menuItems ?. [ menuItems . length - 1 ] ;
828-
829- // If we're on the last menu item, move to Clear selections button
830- if ( lastMenuItem && document . activeElement === lastMenuItem ) {
831- e . preventDefault ( ) ;
832- // Find the Clear selections button or next focusable element
833- if ( batchActionsRef . current ) {
834- const focusableElements = batchActionsRef . current . querySelectorAll (
835- 'button:not([disabled]), [tabindex]:not([tabindex="-1"]):not([disabled])'
836- ) ;
837- const overflowButton = batchOverflowMenuRef . current ?. querySelector ( 'button' ) ;
838- const currentIndex = Array . from ( focusableElements ) . indexOf ( overflowButton ) ;
839- const nextElement = focusableElements [ currentIndex + 1 ] ;
840-
841- if ( nextElement ) {
842- nextElement . focus ( ) ;
904+ // Handle Tab from the last menu item
905+ else if ( e . key === 'Tab' && ! e . shiftKey ) {
906+ const menuItems = batchOverflowMenuRef . current ?. querySelectorAll (
907+ '[role="menuitem"]:not([disabled])'
908+ ) ;
909+ const lastMenuItem = menuItems ?. [ menuItems . length - 1 ] ;
910+
911+ // If we're on the last menu item, move to Clear selections button
912+ if ( lastMenuItem && document . activeElement === lastMenuItem ) {
913+ e . preventDefault ( ) ;
914+ // Find the Clear selections button or next focusable element
915+ if ( batchActionsRef . current ) {
916+ const focusableElements = batchActionsRef . current . querySelectorAll (
917+ 'button:not([disabled]), [tabindex]:not([tabindex="-1"]):not([disabled])'
918+ ) ;
919+ const overflowButton =
920+ batchOverflowMenuRef . current ?. querySelector ( 'button' ) ;
921+ const currentIndex = Array . from ( focusableElements ) . indexOf ( overflowButton ) ;
922+ const nextElement = focusableElements [ currentIndex + 1 ] ;
923+
924+ if ( nextElement ) {
925+ nextElement . focus ( ) ;
926+ }
843927 }
844928 }
845929 }
846- }
847- } }
848- >
849- < OverflowMenu
850- data-testid = { `${ testID || testId } -batch-actions-overflow-menu` }
851- className = { `${ iotPrefix } --table-overflow-batch-actions` }
852- flipped = { langDir === 'ltr' }
853- direction = "bottom"
854- onClick = { ( e ) => e . stopPropagation ( ) }
855- renderIcon = { ( props ) => < OverflowMenuVertical size = { 16 } { ...props } /> }
856- tabIndex = { shouldShowBatchActions ? 0 : - 1 }
857- size = "md"
858- menuOptionsClass = { `${ iotPrefix } --table-overflow-batch-actions__menu` }
859- withCarbonTooltip
860- tooltipPosition = "bottom"
861- buttonLabel = { i18n . batchActionsOverflowMenuText }
862- open = { isBatchOverflowOpen }
863- onOpen = { ( ) => setIsBatchOverflowOpen ( true ) }
864- onClose = { ( ) => setIsBatchOverflowOpen ( false ) }
930+ } }
865931 >
866- { visibleOverflowBatchActions . map (
867- ( {
868- id,
869- labelText,
870- disabled,
871- hasDivider,
872- isDelete,
873- renderIcon,
874- iconDescription,
875- } ) => (
876- < OverflowMenuItem
877- data-testid = { `${ testID || testId } -batch-actions-overflow-menu-item-${ id } ` }
878- itemText = { renderTableOverflowItemText ( {
879- action : { renderIcon, labelText : labelText || iconDescription } ,
880- className : `${ iotPrefix } --table-toolbar-aggregations__overflow-menu-content` ,
881- } ) }
882- disabled = { ! shouldShowBatchActions || disabled }
883- onClick = { ( ) => {
884- onApplyBatchAction ( id ) ;
885- restoreFocus ( ) ;
886- } }
887- key = { `table-batch-actions-overflow-menu-${ id } ` }
888- requireTitle = { ! renderIcon }
889- hasDivider = { hasDivider }
890- isDelete = { isDelete }
891- aria-label = { labelText }
892- />
893- )
894- ) }
895- </ OverflowMenu >
896- </ div >
897- ) : null }
932+ < OverflowMenu
933+ data-testid = { `${ testID || testId } -batch-actions-overflow-menu` }
934+ className = { `${ iotPrefix } --table-overflow-batch-actions` }
935+ flipped = { langDir === 'ltr' }
936+ direction = "bottom"
937+ onClick = { ( e ) => e . stopPropagation ( ) }
938+ renderIcon = { ( props ) => < OverflowMenuVertical size = { 16 } { ...props } /> }
939+ tabIndex = { shouldShowBatchActions ? 0 : - 1 }
940+ size = "md"
941+ menuOptionsClass = { `${ iotPrefix } --table-overflow-batch-actions__menu` }
942+ withCarbonTooltip
943+ tooltipPosition = "bottom"
944+ buttonLabel = { i18n . batchActionsOverflowMenuText }
945+ open = { isBatchOverflowOpen }
946+ onOpen = { ( ) => setIsBatchOverflowOpen ( true ) }
947+ onClose = { ( ) => setIsBatchOverflowOpen ( false ) }
948+ >
949+ { visibleOverflowBatchActions . map (
950+ ( {
951+ id,
952+ labelText,
953+ disabled,
954+ hasDivider,
955+ isDelete,
956+ renderIcon,
957+ iconDescription,
958+ } ) => (
959+ < OverflowMenuItem
960+ data-testid = { `${ testID || testId } -batch-actions-overflow-menu-item-${ id } ` }
961+ itemText = { renderTableOverflowItemText ( {
962+ action : { renderIcon, labelText : labelText || iconDescription } ,
963+ className : `${ iotPrefix } --table-toolbar-aggregations__overflow-menu-content` ,
964+ } ) }
965+ disabled = { ! shouldShowBatchActions || disabled }
966+ onClick = { ( ) => {
967+ onApplyBatchAction ( id ) ;
968+ restoreFocus ( ) ;
969+ } }
970+ key = { `table-batch-actions-overflow-menu-${ id } ` }
971+ requireTitle = { ! renderIcon }
972+ hasDivider = { hasDivider }
973+ isDelete = { isDelete }
974+ aria-label = { labelText }
975+ />
976+ )
977+ ) }
978+ </ OverflowMenu >
979+ </ div >
980+ ) : null }
981+ </ div >
898982 </ TableBatchActions >
899983 ) : null }
900984 </ CarbonTableToolbar >
0 commit comments