Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement sticky columns in GenericTable #1023

Merged
merged 9 commits into from
Jan 16, 2025
Merged
3 changes: 3 additions & 0 deletions src/components/elements/CommonMenuButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,9 @@ const CommonMenuButton = ({
vertical: 'top',
horizontal: 'right',
}}
// Bug: Opening the CommonMenu applies padding to the body, which can look weird on mobile.
martha marked this conversation as resolved.
Show resolved Hide resolved
// It's sort of fixable with disableScrollLock, but that seems to introduce other scroll problems.
// disableScrollLock={true}
{...MenuProps}
>
{items.map(
Expand Down
2 changes: 2 additions & 0 deletions src/components/elements/SemanticIcons.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import AccessTimeIcon from '@mui/icons-material/AccessTime';
import AddIcon from '@mui/icons-material/Add';
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
import ArrowCircleRight from '@mui/icons-material/ArrowCircleRight';
import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown';
import ArrowDropDownRoundedIcon from '@mui/icons-material/ArrowDropDownRounded';
import ArrowDropUpRoundedIcon from '@mui/icons-material/ArrowDropUpRounded';
Expand Down Expand Up @@ -62,6 +63,7 @@ export {
CloseIcon as CloseIcon,
MoreVertRoundedIcon as MoreMenuIcon,
MyLocationIcon as MyLocationIcon,
ArrowCircleRight as GoIcon,

// Icons for Form Builder
CheckBoxIcon as FormBooleanIcon,
Expand Down
78 changes: 64 additions & 14 deletions src/components/elements/table/GenericTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,50 @@ const clickableRowStyles = {
cursor: 'pointer',
};

export const getStickyCellStyles = (
sticky?: 'left' | 'right' | undefined
): SxProps<Theme> => {
if (!sticky) return {} as SxProps<Theme>;

const base = {
backgroundColor: 'background.paper', // Otherwise it's transparent and other cell content appears beneath it
position: 'sticky',
zIndex: 1,
maxWidth: '200px', // Mitigates the risk that the column may be so wide as to obscure any scrollable columns
overflow: 'clip',
};

// Pseudo-element to achieve a border on sticky cells. `position: sticky` doesn't work with regular border
const pseudo = {
content: '""',
position: 'absolute',
top: 0,
bottom: 0,
width: '1px',
backgroundColor: 'borders.light',
pointerEvents: 'none', // Don't interfere with interactions
};

if (sticky === 'right')
return {
...base,
right: 0,
'&::before': {
...pseudo,
left: 0,
},
} as SxProps<Theme>;
gigxz marked this conversation as resolved.
Show resolved Hide resolved

return {
...base,
left: 0,
'&::after': {
...pseudo,
right: 0,
},
} as SxProps<Theme>;
};

const HeaderCell = ({
children,
sx,
Expand Down Expand Up @@ -237,11 +281,14 @@ const GenericTable = <T extends { id: string }>({
{columns.map((def, i) => (
<HeaderCell
key={key(def) || i}
sx={{
...(headerCellSx ? headerCellSx(def) : undefined),
textAlign: def.textAlign,
width: def.width,
}}
sx={
{
...getStickyCellStyles(def.sticky),
...(headerCellSx ? headerCellSx(def) : {}),
textAlign: def.textAlign,
width: def.width,
} as SxProps<Theme>
}
>
{def.header ? (
<strong>{def.header}</strong>
Expand Down Expand Up @@ -411,15 +458,18 @@ const GenericTable = <T extends { id: string }>({
<TableCell
key={key(def) || index}
{...tableCellProps}
sx={{
width,
minWidth,
...(isLinked ? { p: 0 } : undefined),
textAlign,
whiteSpace: 'initial',
...onClickLinkTreatment,
...tableCellProps?.sx,
}}
sx={
{
...getStickyCellStyles(def.sticky),
width,
minWidth,
...(isLinked ? { p: 0 } : undefined),
textAlign,
whiteSpace: 'initial',
...onClickLinkTreatment,
...tableCellProps?.sx,
} as SxProps<Theme>
}
>
{isLinked ? (
<RouterLink
Expand Down
89 changes: 60 additions & 29 deletions src/components/elements/table/TableRowActions.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import { Button, Stack } from '@mui/material';
import { Button, ButtonProps, Stack } from '@mui/material';
import { ReactNode } from 'react';
import ButtonLink from '../ButtonLink';
import CommonMenuButton, { CommonMenuItem } from '../CommonMenuButton';
import ButtonTooltipContainer from '@/components/elements/ButtonTooltipContainer';
import { GoIcon } from '@/components/elements/SemanticIcons';
import { useIsMobile } from '@/hooks/useIsMobile';
import IconButtonContainer from '@/modules/enrollment/components/IconButtonContainer';

interface TableRowActionsProps<T> {
record: T;
Expand All @@ -21,35 +25,62 @@ const TableRowActions = <T extends { id: string }>({
primaryActionConfig,
secondaryActionConfigs,
}: TableRowActionsProps<T>) => {
const isTiny = useIsMobile('sm');

const primaryAria =
primaryActionConfig?.ariaLabel ||
`${primaryActionConfig?.title}, ${recordName}`;

const buttonProps: Pick<ButtonProps, 'size' | 'variant' | 'aria-label'> = {
size: 'small',
variant: 'outlined',
'aria-label': primaryAria,
};

return (
<Stack direction='row' alignItems='center' justifyContent='end' gap={0.5}>
{!!primaryActionConfig && primaryActionConfig.to && (
<ButtonLink
to={primaryActionConfig.to || ''}
size='small'
variant='outlined'
aria-label={
primaryActionConfig.ariaLabel ||
`${primaryActionConfig.title}, ${recordName}`
}
state={primaryActionConfig.linkState}
>
{primaryActionConfig.title}
</ButtonLink>
)}
{!!primaryActionConfig && primaryActionConfig.onClick && (
<Button
onClick={primaryActionConfig.onClick}
size='small'
variant='outlined'
aria-label={
primaryActionConfig.ariaLabel ||
`${primaryActionConfig.title}, ${recordName}`
}
>
{primaryActionConfig.title}
</Button>
)}
<Stack
direction='row'
alignItems='center'
justifyContent='end'
gap={{ xs: 0, sm: 0.5 }}
>
{!!primaryActionConfig &&
primaryActionConfig.to &&
(isTiny ? (
<ButtonTooltipContainer title={primaryAria}>
<ButtonLink
to={primaryActionConfig.to || ''}
state={primaryActionConfig.linkState}
{...buttonProps}
variant='text'
sx={{ maxWidth: '20px', minWidth: 0 }}
>
<GoIcon />
</ButtonLink>
</ButtonTooltipContainer>
) : (
<ButtonLink
to={primaryActionConfig.to || ''}
state={primaryActionConfig.linkState}
{...buttonProps}
>
{primaryActionConfig.title}
</ButtonLink>
))}
{!!primaryActionConfig &&
primaryActionConfig.onClick &&
(isTiny ? (
<IconButtonContainer
Icon={GoIcon}
onClick={primaryActionConfig.onClick}
tooltip={primaryAria}
IconButtonProps={{ size: 'medium' }}
/>
) : (
<Button onClick={primaryActionConfig.onClick} {...buttonProps}>
{primaryActionConfig.title}
</Button>
))}
{primaryAction}
{!!secondaryActionConfigs && secondaryActionConfigs.length > 0 && (
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Noting that I think there was agreement around including the primary action config inside the secondary menu, so I'd say we can go ahead with that. Maybe it would be as simple as passing items={[primaryActionConfig , ...secondaryActionConfigs]} below.

<CommonMenuButton
Expand Down
4 changes: 3 additions & 1 deletion src/components/elements/table/tableRowActionUtil.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,10 @@ import { generateSafePath } from '@/utils/pathEncoding';

export const BASE_ACTION_COLUMN_DEF: ColumnDef<any> = {
key: 'Actions',
tableCellProps: { sx: { py: 0 } },
tableCellProps: { sx: { py: 0, px: { xs: 1, sm: 2 } } },
render: '', // gets overridden when used
sticky: 'right',
width: '0px',
};

export const getViewClientMenuItem = (client: ClientNameFragment) => {
Expand Down
1 change: 1 addition & 0 deletions src/components/elements/table/types.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ type BaseColumnDef<T> = {
tableCellProps?: TableCellProps;
optional?: boolean;
defaultHidden?: boolean;
sticky?: 'left' | 'right';
};

export type ColumnDef<T> =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ const ClientAssessmentsPage = () => {

const columns: ColumnDef<ClientAssessmentType>[] = useMemo(
() => [
ASSESSMENT_COLUMNS.date,
{ ...ASSESSMENT_COLUMNS.date, sticky: 'left' },
{
header: 'Project Name',
render: (row: ClientAssessmentType) => row.enrollment.projectName,
Expand Down
1 change: 1 addition & 0 deletions src/modules/assessments/util.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -99,5 +99,6 @@ export const ASSESSMENT_CLIENT_NAME_COL: ColumnDef<
ProjectAssessmentType | HhmAssessmentType
> = {
header: 'Client Name',
sticky: 'left',
render: (a) => clientBriefName(a.enrollment.client),
};
2 changes: 1 addition & 1 deletion src/modules/bulkServices/components/BulkServicesTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ const BulkServicesTable: React.FC<Props> = ({
</NotCollectedText>
);
return [
CLIENT_COLUMNS.name,
{ ...CLIENT_COLUMNS.name, sticky: 'left' },
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Known issue: Checkbox is not sticky. I think my current approach would need a bit more work to accomplish more than 1 sticky column per side. It's not ideal, but I am proposing to descope this and open another ticket, unless you see an easy way around it.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok with me @martha. Not sure how much it would simplify things, but one option is to make the checkbox always be sticky. That seems reasonable to me.

Copy link
Contributor Author

@martha martha Jan 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, Gig, good point! With this suggestion it wasn't so bad, see 273b059

...(canViewDob ? [CLIENT_COLUMNS.dobAge] : []),
{
header: 'Entry Date',
Expand Down
8 changes: 6 additions & 2 deletions src/modules/caseNotes/components/EnrollmentCaseNotes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { useViewEditRecordDialogs } from '../../form/hooks/useViewEditRecordDial
import RelativeDateDisplay from '@/components/elements/RelativeDateDisplay';
import TableRowActions from '@/components/elements/table/TableRowActions';
import { BASE_ACTION_COLUMN_DEF } from '@/components/elements/table/tableRowActionUtil';
import { ColumnDef } from '@/components/elements/table/types';
import TitleCard from '@/components/elements/TitleCard';
import NotFound from '@/components/pages/NotFound';
import GenericTableWithData from '@/modules/dataFetching/components/GenericTableWithData';
Expand All @@ -26,12 +27,16 @@ import {
RecordFormRole,
} from '@/types/gqlTypes';

export const CASE_NOTE_COLUMNS = {
export const CASE_NOTE_COLUMNS: Record<
string,
ColumnDef<CustomCaseNoteFieldsFragment>
> = {
InformationDate: {
header: 'Information Date',
width: '150px',
render: ({ informationDate }: CustomCaseNoteFieldsFragment) =>
parseAndFormatDate(informationDate),
sticky: 'left',
},
NoteContent: {
header: 'Note Content',
Expand All @@ -52,7 +57,6 @@ export const CASE_NOTE_COLUMNS = {
NoteContentPreview: {
key: 'content-preview',
header: 'Note Content Preview',
maxWidth: '450px',
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed since this wasn't respected (maxWidth is not a valid property on ColumnDef and GenericTable doesn't do anything with it). There is a maxWidth applied to the Box below.

render: ({ content }: CustomCaseNoteFieldsFragment) => (
<Box
sx={{
Expand Down
1 change: 1 addition & 0 deletions src/modules/clientFiles/components/ClientFilesPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ const ClientFilesPage = () => {
render: (file) => (
<Typography variant='inherit'>{file.name}</Typography>
),
sticky: 'left',
},
{
header: 'File Tags',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
BASE_ACTION_COLUMN_DEF,
getViewAssessmentMenuItem,
} from '@/components/elements/table/tableRowActionUtil';
import { ColumnDef } from '@/components/elements/table/types';
import { ASSESSMENT_COLUMNS } from '@/modules/assessments/util';
import GenericTableWithData from '@/modules/dataFetching/components/GenericTableWithData';
import { useFilters } from '@/modules/hmis/filterUtil';
Expand All @@ -27,9 +28,9 @@ const EnrollmentAssessmentsTable: React.FC<Props> = ({
enrollmentId,
projectId,
}) => {
const columns = useMemo(
const columns: ColumnDef<AssessmentFieldsFragment>[] = useMemo(
() => [
ASSESSMENT_COLUMNS.date,
{ ...ASSESSMENT_COLUMNS.date, sticky: 'left' },
ASSESSMENT_COLUMNS.type,
ASSESSMENT_COLUMNS.lastUpdated,
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ const CLIENT_ENROLLMENT_COLUMNS: {
projectName: {
header: 'Project Name',
render: 'projectName',
sticky: 'left',
},
organizationName: {
header: 'Organization Name',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ const EnrollmentCeAssessmentsPage = () => {
header: 'Assessment Date',
render: (a: CeAssessmentFieldsFragment) =>
parseAndFormatDate(a.assessmentDate),
sticky: 'left',
},
{
header: 'Assessment Level',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { useCallback, useMemo } from 'react';

import TableRowActions from '@/components/elements/table/TableRowActions';
import { BASE_ACTION_COLUMN_DEF } from '@/components/elements/table/tableRowActionUtil';
import { ColumnDef } from '@/components/elements/table/types';
import TitleCard from '@/components/elements/TitleCard';
import NotFound from '@/components/pages/NotFound';
import GenericTableWithData from '@/modules/dataFetching/components/GenericTableWithData';
Expand Down Expand Up @@ -66,13 +67,14 @@ const EnrollmentCeEventsPage = () => {
DataCollectionFeatureRole.CeEvent
);

const columns = useMemo(
const columns: ColumnDef<EventFieldsFragment>[] = useMemo(
() => [
{
header: 'Event Type',
render: (e: EventFieldsFragment) => (
<HmisEnum value={e.event} enumMap={HmisEnums.EventType} />
),
sticky: 'left',
},
{
header: 'Event Date',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Button } from '@mui/material';
import { useCallback, useMemo } from 'react';
import TableRowActions from '@/components/elements/table/TableRowActions';
import { BASE_ACTION_COLUMN_DEF } from '@/components/elements/table/tableRowActionUtil';
import { ColumnDef } from '@/components/elements/table/types';
import TitleCard from '@/components/elements/TitleCard';
import NotFound from '@/components/pages/NotFound';
import GenericTableWithData from '@/modules/dataFetching/components/GenericTableWithData';
Expand Down Expand Up @@ -93,10 +94,12 @@ const EnrollmentCurrentLivingSituationsPage = () => {
});

const getColumnDefs = useCallback(
(rows: CurrentLivingSituationFieldsFragment[]) => {
(
rows: CurrentLivingSituationFieldsFragment[]
): ColumnDef<CurrentLivingSituationFieldsFragment>[] => {
const customColumns = getCustomDataElementColumns(rows);
return [
CLS_COLUMNS.informationDate,
{ ...CLS_COLUMNS.informationDate, sticky: 'left' },
CLS_COLUMNS.livingSituation,
CLS_COLUMNS.locationDetails,
...customColumns,
Expand Down
Loading