Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
121 changes: 121 additions & 0 deletions components/ui/Table/AccordionController.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import React, { useState, useContext, useMemo } from 'react';

import { useModes } from '@livepreso/content-react';

import { AccordionToggleAll } from './AccordionToggleAll';

export const AccordionControlContext = React.createContext({
registerRow: () => {},
unregisterRow: () => {},
isOpen: () => false,
toggleRow: () => {},
hasRows: false,
expandAll: () => {},
collapseAll: () => {},
allRowsOpen: false,
hasController: false,
});

export const useAccordionControls = () => {
const context = useContext(AccordionControlContext);

if (!context.hasController) {
throw new Error(`
useAccordionControls must be used within an AccordionController.
Please wrap your table with AccordionController to use this hook.
`);
}

return context;
};

export function AccordionController({ children }) {
const { isPdfScreenshot } = useModes();
const [registry, setRegistry] = useState(new Set());
const [expandedRows, setExpandedRows] = useState(new Set());
const [expandByDefault, setExpandByDefault] = useState(isPdfScreenshot);

const isOpen = (uid) => expandedRows.has(uid);

const hasRows = registry.size > 0;
const allRowsOpen = hasRows && expandedRows.size === registry.size;

const registerRow = (uid) => {
setRegistry((prev) => new Set(prev).add(uid));

if (expandByDefault) {
setExpandedRows((prev) => new Set(prev).add(uid));
}
};

const unregisterRow = (uid) => {
setRegistry((prev) => {
const newSet = new Set(prev);
newSet.delete(uid);
return newSet;
});
setExpandedRows((prev) => {
const newSet = new Set(prev);
newSet.delete(uid);
return newSet;
});
};

const toggleRow = (uid) => {
setExpandedRows((prev) => {
const newSet = new Set(prev);
if (newSet.has(uid)) {
newSet.delete(uid);
} else {
newSet.add(uid);
}
return newSet;
});
};

const expandAll = () => {
setExpandedRows(new Set(registry));
setExpandByDefault(true);
};

const collapseAll = () => {
setExpandedRows(new Set());
setExpandByDefault(false);
};

const value = useMemo(
() => ({
registerRow,
unregisterRow,
isOpen,
toggleRow,
hasRows,
expandAll,
collapseAll,
allRowsOpen,
hasController: true,
}),
[
registerRow,
unregisterRow,
isOpen,
toggleRow,
hasRows,
expandAll,
collapseAll,
allRowsOpen,
],
);

return (
<AccordionControlContext.Provider value={value}>
{hasRows && (
<div style={{ position: 'relative' }}>
<AccordionToggleAll />
</div>
)}

{children}
</AccordionControlContext.Provider>
);
}
32 changes: 32 additions & 0 deletions components/ui/Table/AccordionToggleAll.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import React from 'react';

import classNames from 'classnames';

import { useAccordionControls } from './AccordionController';

import style from './AccordionToggleAll.module.scss';

export function AccordionToggleAll() {
const { expandAll, collapseAll, hasRows, allRowsOpen } =
useAccordionControls();

function handleClick() {
if (allRowsOpen) {
collapseAll();
} else {
expandAll();
}
}
Comment thread
Peter-Brick marked this conversation as resolved.

if (!hasRows) {
return null;
}

return (
<div
className={classNames(style.toggle, { [style.open]: allRowsOpen })}
onClick={handleClick}
title={allRowsOpen ? 'Collapse all' : 'Expand all'}
></div>
);
Comment thread
Peter-Brick marked this conversation as resolved.
}
55 changes: 55 additions & 0 deletions components/ui/Table/AccordionToggleAll.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
.toggle {
height: 45px;
width: 30px;
position: absolute;
left: -30px;
top: 0;

display: flex;
flex-direction: column;
align-items: center;
justify-content: center;

cursor: pointer;

.screenshot-full & {
display: none;
}

&:hover::before {
margin-bottom: 8px;
}

&::before {
content: '';
width: 0;
height: 0;
border: 8px solid transparent;
border-bottom-color: var(--color-text);
margin-bottom: 4px;
transition: margin-bottom 150ms ease-out;
}
&::after {
content: '';
width: 0;
height: 0;
border: 8px solid transparent;
border-top-color: var(--color-text);
}

&.open {
&::before {
border-bottom-color: transparent;
border-top-color: var(--color-text);
margin-bottom: -10px;
}
&::after {
border-top-color: transparent;
border-bottom-color: var(--color-text);
}

&:hover::before {
margin-bottom: -14px;
}
}
}
87 changes: 46 additions & 41 deletions components/ui/Table/Table.js
Original file line number Diff line number Diff line change
@@ -1,50 +1,55 @@
import React from 'react';
import PropTypes from 'prop-types';
import { ROW_TYPES } from './table-constants';
import { TableBase } from './TableBase';
import { OrderableTable } from './OrderableTable';

export function Table(props) {
const { onReorder } = props;
/**
* @typedef {Object} TableRow
* @property {string} uid - Unique identifier for the row.
* @property {('header'|'body'|'footer')} [type] - The type of row based on ROW_TYPES.
* @property {Function} [component] - Optional component override for the row.
* @property {Object[]} [cells] - Array of cell data objects.
* @property {Object[]} [rows] - Nested rows for expandable table structures.
* @property {string} [className] - CSS class for the row.
*/

/**
* @param {Object} props
* @param {boolean} [props.hasBorder=false] - Whether the table has a border.
* @param {TableRow[]} [props.rows=[]] - Array of row objects to render.
* @param {(number|string)[]} [props.columnWidths=[]] - Widths for each column.
* @param {React.ReactNode} [props.children=[]] - Child elements.
* @param {'none'|'row'|'column'|'both'} [props.sticky='none'] - Provides scrolling with a 'sticky' header or column.
* @param {boolean} [props.isPresoManagerInteractive=false] - Allows interaction in PresoManager. Mouse events are otherwise ignored.
* @param {string} [props.tbodyClassName=''] - CSS class for the tbody element.
* @param {string} [props.className=''] - CSS class for the table element.
*/
export function Table({
hasBorder = false,
rows = [],
columnWidths = [],
children = [],
sticky = 'none',
isPresoManagerInteractive = false,
tbodyClassName = '',
className = '',
...otherProps
}) {
const { onReorder, ...rest } = otherProps;

const props = {
hasBorder,
rows,
columnWidths,
children,
sticky,
isPresoManagerInteractive,
tbodyClassName,
className,
};

if (typeof onReorder === 'function') {
return <OrderableTable {...props} />;
return <OrderableTable {...props} {...otherProps} />;
}

return <TableBase {...props} />;
return <TableBase {...props} {...rest} />;
}

Table.propTypes = {
hasBorder: PropTypes.bool,
rows: PropTypes.arrayOf(
PropTypes.shape({
uid: PropTypes.string.isRequired,
type: PropTypes.oneOf(Object.values(ROW_TYPES)),
component: PropTypes.func,
// We're letting the components further down check the cell types
// rather than trying to check at the top level due to complexity of the propTypes
cells: PropTypes.arrayOf(PropTypes.shape({})),
rows: PropTypes.arrayOf(PropTypes.shape({})),
className: PropTypes.string,
}),
),
columnWidths: PropTypes.arrayOf(
PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
),
children: PropTypes.node,
sticky: PropTypes.oneOf(['none', 'row', 'column', 'both']),
isPresoManagerInteractive: PropTypes.bool,
tbodyClassName: PropTypes.string,
className: PropTypes.string,
};

Table.defaultProps = {
hasBorder: false,
rows: [],
columnWidths: [],
children: [],
sticky: 'none',
isPresoManagerInteractive: false,
tbodyClassName: '',
className: '',
};
Loading