diff --git a/src/__testing__/BulkActionToolbar.test.tsx b/src/__testing__/BulkActionToolbar.test.tsx new file mode 100644 index 000000000..713b60e58 --- /dev/null +++ b/src/__testing__/BulkActionToolbar.test.tsx @@ -0,0 +1,66 @@ +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); + }); + + 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 new file mode 100644 index 000000000..e8cdee35e --- /dev/null +++ b/src/custom/BulkActionToolbar/BulkActionToolbar.tsx @@ -0,0 +1,72 @@ +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; + deselectAllLabel?: string; + selectedLabel?: string; +} + +export const BulkActionToolbar: React.FC = ({ + selectedCount, + onDeselectAll, + children, + style = {}, + 'data-testid': testId = 'bulk-action-toolbar', + deselectAllLabel = 'Deselect ALL', + selectedLabel = 'selected' +}) => { + const theme = useTheme(); + + if (selectedCount <= 0) { + return null; + } + + const iconFill = theme.palette.icon.default; + + return ( + + + {onDeselectAll && ( + + + + + + )} + + {selectedCount} {selectedLabel} + + + {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 ba6916b0b..b6ed97af8 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'; @@ -87,6 +88,7 @@ export { ActionButton, BBChart, BookmarkNotification, + BulkActionToolbar, Carousel, CatalogCardDesignLogo, CatalogFilter, @@ -154,6 +156,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 d7042f628..5bcbbacbc 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -23,6 +23,8 @@ 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'; // Same nested-barrel dts-drop quirk as FeedbackButton above: without this // explicit re-export the DangerConfirmationModal declarations (and its exported // props types) are dropped from the bundled d.ts, breaking