From 4ccd53feed377ae2a48cf25fdaf9ad5f0c77d235 Mon Sep 17 00:00:00 2001 From: Parth Gartan Date: Wed, 1 Jul 2026 06:50:26 +0000 Subject: [PATCH 1/2] feat(bulk-action): create reusable BulkActionToolbar component Signed-off-by: Parth Gartan --- src/__testing__/BulkActionToolbar.test.tsx | 53 +++++++++++++++ .../BulkActionToolbar/BulkActionToolbar.tsx | 68 +++++++++++++++++++ src/custom/BulkActionToolbar/index.ts | 2 + src/custom/index.tsx | 3 + src/index.tsx | 2 + 5 files changed, 128 insertions(+) create mode 100644 src/__testing__/BulkActionToolbar.test.tsx create mode 100644 src/custom/BulkActionToolbar/BulkActionToolbar.tsx create mode 100644 src/custom/BulkActionToolbar/index.ts diff --git a/src/__testing__/BulkActionToolbar.test.tsx b/src/__testing__/BulkActionToolbar.test.tsx new file mode 100644 index 000000000..a23701bfb --- /dev/null +++ b/src/__testing__/BulkActionToolbar.test.tsx @@ -0,0 +1,53 @@ +import { fireEvent, render, screen } from '@testing-library/react'; +import React from 'react'; + +jest.mock('react-markdown', () => ({ + __esModule: true, + default: ({ children }: { children: React.ReactNode }) =>
{children}
+})); + +jest.mock('remark-gfm', () => ({ + __esModule: true, + default: () => {} +})); + +jest.mock('rehype-raw', () => ({ + __esModule: true, + default: () => {} +})); + +import { BulkActionToolbar } from '../custom/BulkActionToolbar'; +import { SistentThemeProviderWithoutBaseLine } from '../theme'; + +function renderWithTheme(ui: React.ReactElement) { + return render({ui}); +} + +describe('BulkActionToolbar', () => { + it('renders null when selectedCount is 0', () => { + const { container } = renderWithTheme(); + expect(container.firstChild).toBeNull(); + }); + + it('renders selected count and children when selectedCount > 0', () => { + renderWithTheme( + + + + ); + + expect(screen.getByText('3 selected')).toBeTruthy(); + expect(screen.getByTestId('custom-action')).toBeTruthy(); + }); + + it('renders deselect button and handles callback', () => { + const onDeselectAll = jest.fn(); + renderWithTheme(); + + const deselectButton = screen.getByTestId('deselect-all-button'); + expect(deselectButton).toBeTruthy(); + + fireEvent.click(deselectButton); + expect(onDeselectAll).toHaveBeenCalledTimes(1); + }); +}); diff --git a/src/custom/BulkActionToolbar/BulkActionToolbar.tsx b/src/custom/BulkActionToolbar/BulkActionToolbar.tsx new file mode 100644 index 000000000..930c8a456 --- /dev/null +++ b/src/custom/BulkActionToolbar/BulkActionToolbar.tsx @@ -0,0 +1,68 @@ +import { styled } from '@mui/material/styles'; +import React from 'react'; +import { Box, IconButton, Toolbar, Typography } from '../../base'; +import { IndeterminateCheckBoxIcon } from '../../icons'; +import { useTheme } from '../../theme'; +import { CustomTooltip } from '../CustomTooltip'; + +const StyledToolbar = styled(Toolbar)(({ theme }) => ({ + backgroundColor: + theme.palette.mode === 'dark' + ? theme.palette.background.card + : theme.palette.background.secondary, + paddingLeft: theme.spacing(3), + paddingRight: theme.spacing(3), + display: 'flex', + justifyContent: 'space-between', + alignItems: 'center', + width: '100%', + minHeight: '64px' +})); + +export interface BulkActionToolbarProps { + selectedCount: number; + onDeselectAll?: () => void; + children?: React.ReactNode; + style?: React.CSSProperties; + 'data-testid'?: string; +} + +export const BulkActionToolbar: React.FC = ({ + selectedCount, + onDeselectAll, + children, + style = {}, + 'data-testid': testId = 'bulk-action-toolbar' +}) => { + const theme = useTheme(); + + if (selectedCount <= 0) { + return null; + } + + const iconFill = theme.palette.icon.default; + + return ( + + + {onDeselectAll && ( + + + + + + )} + + {selectedCount} selected + + + {children} + + ); +}; + +export default BulkActionToolbar; diff --git a/src/custom/BulkActionToolbar/index.ts b/src/custom/BulkActionToolbar/index.ts new file mode 100644 index 000000000..621e880ed --- /dev/null +++ b/src/custom/BulkActionToolbar/index.ts @@ -0,0 +1,2 @@ +export * from './BulkActionToolbar'; +export { default as BulkActionToolbar } from './BulkActionToolbar'; diff --git a/src/custom/index.tsx b/src/custom/index.tsx index 51a104ce5..ca989d8e3 100644 --- a/src/custom/index.tsx +++ b/src/custom/index.tsx @@ -1,6 +1,7 @@ import { ActionButton } from './ActionButton'; import { BBChart } from './BBChart'; import { BookmarkNotification } from './BookmarkNotification'; +import BulkActionToolbar, { BulkActionToolbarProps } from './BulkActionToolbar'; import { Carousel } from './Carousel'; import CatalogFilter, { CatalogFilterProps } from './CatalogFilter/CatalogFilter'; import { ChapterCard } from './ChapterCard'; @@ -82,6 +83,7 @@ export { ActionButton, BBChart, BookmarkNotification, + BulkActionToolbar, Carousel, CatalogCardDesignLogo, CatalogFilter, @@ -149,6 +151,7 @@ export { BasicMarkdown, RenderMarkdown }; export { CustomizedStepper, useStepper } from './Stepper'; export type { + BulkActionToolbarProps, CatalogFilterProps, ColView, CustomColumn, diff --git a/src/index.tsx b/src/index.tsx index c5a4561e8..8c61deeef 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -23,3 +23,5 @@ export { FeedbackButton, type FeedbackComponentProps } from './custom/Feedback'; // `@sistent/mui-datatables` and would crash the dts build) precisely so this // explicit re-export can force them into the published declaration bundle. export { getCopyDeepLinkAction, type TableAction } from './custom/TableActions'; + +export { BulkActionToolbar, type BulkActionToolbarProps } from './custom/BulkActionToolbar'; From b5199f0415b312981d56b994a0990eef5b1e693c Mon Sep 17 00:00:00 2001 From: Parth Gartan Date: Wed, 1 Jul 2026 08:35:41 +0000 Subject: [PATCH 2/2] feat(bulk-action): add i18n support for UI strings in BulkActionToolbar Signed-off-by: Parth Gartan --- src/__testing__/BulkActionToolbar.test.tsx | 13 +++++++++++++ src/custom/BulkActionToolbar/BulkActionToolbar.tsx | 10 +++++++--- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/src/__testing__/BulkActionToolbar.test.tsx b/src/__testing__/BulkActionToolbar.test.tsx index a23701bfb..713b60e58 100644 --- a/src/__testing__/BulkActionToolbar.test.tsx +++ b/src/__testing__/BulkActionToolbar.test.tsx @@ -50,4 +50,17 @@ describe('BulkActionToolbar', () => { fireEvent.click(deselectButton); expect(onDeselectAll).toHaveBeenCalledTimes(1); }); + + it('renders custom labels when provided', () => { + renderWithTheme( + + ); + + expect(screen.getByText('3 items chosen')).toBeTruthy(); + }); }); diff --git a/src/custom/BulkActionToolbar/BulkActionToolbar.tsx b/src/custom/BulkActionToolbar/BulkActionToolbar.tsx index 930c8a456..e8cdee35e 100644 --- a/src/custom/BulkActionToolbar/BulkActionToolbar.tsx +++ b/src/custom/BulkActionToolbar/BulkActionToolbar.tsx @@ -25,6 +25,8 @@ export interface BulkActionToolbarProps { children?: React.ReactNode; style?: React.CSSProperties; 'data-testid'?: string; + deselectAllLabel?: string; + selectedLabel?: string; } export const BulkActionToolbar: React.FC = ({ @@ -32,7 +34,9 @@ export const BulkActionToolbar: React.FC = ({ onDeselectAll, children, style = {}, - 'data-testid': testId = 'bulk-action-toolbar' + 'data-testid': testId = 'bulk-action-toolbar', + deselectAllLabel = 'Deselect ALL', + selectedLabel = 'selected' }) => { const theme = useTheme(); @@ -46,7 +50,7 @@ export const BulkActionToolbar: React.FC = ({ {onDeselectAll && ( - + = ({ )} - {selectedCount} selected + {selectedCount} {selectedLabel} {children}