Skip to content

Commit fc50314

Browse files
MAXUIF-2515 [Table] Multi-select row actions (blue bar) smart truncation of actions (#4101)
* Multi-select row actions (blue bar) smart truncation of actions * Multi-select row actions (blue bar) smart truncation of actions * lint issue fixed * lint issue fixed
1 parent 7033683 commit fc50314

4 files changed

Lines changed: 483 additions & 122 deletions

File tree

docs/table-toolbar-batch-overflow-update.md

Whitespace-only changes.

packages/react/src/components/Table/TableToolbar/TableToolbar.jsx

Lines changed: 206 additions & 122 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import { tableTranslateWithId } from '../../../utils/componentUtilityFunctions';
3232
import { settings } from '../../../constants/Settings';
3333
import { RuleGroupPropType } from '../../RuleBuilder/RuleBuilderPropTypes';
3434
import useDynamicOverflowMenuItems from '../../../hooks/useDynamicOverflowMenuItems';
35+
import useResponsiveInlineCount from '../../../hooks/useResponsiveInlineCount';
3536
import { renderTableOverflowItemText } from '../tableUtilities';
3637

3738
import 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

Comments
 (0)