Skip to content

Commit 6fe4251

Browse files
authored
Control columns visibility (#427)
* disable telemetry in storybook (config file does not seem to work) * BREAKING remove localStorage for columns visibility * [refactor] rename ColumnVisibilityStatesContext to ColumnsVisibilityContext * create tests * [refactor] reduce the number of props, depend on ColumnParametersContext * add tests * refactor test * add columnsVisibility prop to HighTable
1 parent 360ec0b commit 6fe4251

15 files changed

+679
-193
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ interface TableProps {
105105
numRowsPerPage?: number // number of rows per page for keyboard navigation (default 20)
106106
selection?: Selection // selection state (if defined, the component selection is controlled by the parent)
107107
styled?: boolean // use styled component? (default true)
108-
onColumnsVisibilityChange?: (columnVisibilityStates: Record<string, MaybeHiddenColumn>) => void // columns visibility change handler
108+
onColumnsVisibilityChange?: (columnsVisibility: Record<string, { hidden: true } | undefined>) => void // columns visibility change handler
109109
onDoubleClickCell?: (event: MouseEvent, col: number, row: number) => void // double-click handler
110110
onError?: (error: Error) => void // error handler
111111
onKeyDownCell?: (event: KeyboardEvent, col: number, row: number) => void // key down handler. For accessibility, it should be passed if onDoubleClickCell is passed.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@
4343
"lint": "eslint",
4444
"lint:fix": "eslint --fix",
4545
"prepublishOnly": "npm run build",
46-
"storybook": "storybook dev -p 6006",
46+
"storybook": "storybook dev -p 6006 --disable-telemetry",
4747
"test": "vitest run",
4848
"typecheck": "tsc --noEmit"
4949
},

src/components/ColumnHeader/ColumnHeader.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { useCallback, useContext, useEffect, useMemo, useRef } from 'react'
33
import { flushSync } from 'react-dom'
44

55
import type { ColumnParameters } from '../../contexts/ColumnParametersContext.js'
6-
import { ColumnVisibilityStatesContext } from '../../contexts/ColumnVisibilityStatesContext.js'
6+
import { ColumnsVisibilityContext } from '../../contexts/ColumnsVisibilityContext.js'
77
import { ColumnWidthsContext } from '../../contexts/ColumnWidthsContext.js'
88
import type { Direction } from '../../helpers/sort.js'
99
import { getOffsetWidth } from '../../helpers/width.js'
@@ -35,7 +35,7 @@ export default function ColumnHeader({ columnIndex, columnName, columnConfig, ca
3535
const { tabIndex, navigateToCell, focusIfNeeded } = useCellFocus({ ariaColIndex, ariaRowIndex })
3636
const { sortable } = columnConfig
3737
const { isOpen, position, menuId, close, handleMenuClick } = useColumnMenu(ref, navigateToCell)
38-
const { getHideColumn, showAllColumns } = useContext(ColumnVisibilityStatesContext)
38+
const { getHideColumn, showAllColumns } = useContext(ColumnsVisibilityContext)
3939

4040
// Focus the cell if needed. We use an effect, as it acts on the DOM element after render.
4141
useEffect(() => {

src/components/HighTable/HighTable.stories.tsx

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import type { Fetch, ResolvedValue } from '../../helpers/dataframe/types.js'
1010
import type { Selection } from '../../helpers/selection.js'
1111
import type { OrderBy } from '../../helpers/sort.js'
1212
import { createEventTarget } from '../../helpers/typedEventTarget.js'
13+
import type { ColumnsVisibility } from '../../providers/ColumnsVisibilityProvider.js'
1314
import type { CellPosition } from '../../types.js'
1415
import type { CellContentProps } from '../Cell/Cell.js'
1516
import HighTable from './HighTable.js'
@@ -681,3 +682,53 @@ export const JumpToCell: Story = {
681682
data: createLargeData(1_000_000_000),
682683
},
683684
}
685+
export const ColumnsVisibilityControlled: Story = {
686+
render: ({ data }) => {
687+
const [columnsVisibility, setColumnsVisibility] = useState<ColumnsVisibility>({
688+
Value1: { hidden: true },
689+
Value3: { hidden: true },
690+
})
691+
return (
692+
<div style={{ display: 'flex', flexDirection: 'column', width: '100%' }}>
693+
694+
<div style={{ display: 'flex', gap: '16px', flexWrap: 'wrap', padding: '12px' }}>
695+
{
696+
data.columnDescriptors.map(({ name }) => {
697+
const isHidden = columnsVisibility[name]?.hidden === true
698+
return (
699+
<button
700+
key={name}
701+
type="button"
702+
onClick={() => {
703+
setColumnsVisibility({
704+
...columnsVisibility,
705+
[name]: isHidden ? undefined : { hidden: true },
706+
})
707+
}}
708+
>
709+
{isHidden ? `Show ${name}` : `Hide ${name}`}
710+
</button>
711+
)
712+
})
713+
}
714+
</div>
715+
<div style={{ display: 'flex', gap: '16px', flexWrap: 'wrap', padding: '12px', alignItems: 'center' }}>
716+
<button type="button" onClick={() => { setColumnsVisibility({}) }}>
717+
Show all columns
718+
</button>
719+
<div>
720+
{`Hidden columns: ${Object.entries(columnsVisibility).filter(([, visibility]) => visibility?.hidden).map(([name]) => name).join(', ') || 'None'}`}
721+
</div>
722+
</div>
723+
<HighTable
724+
data={data}
725+
columnsVisibility={columnsVisibility}
726+
onColumnsVisibilityChange={setColumnsVisibility}
727+
/>
728+
</div>
729+
)
730+
},
731+
args: {
732+
data: createUnsortableData(),
733+
},
734+
}

src/components/HighTable/HighTable.tsx

Lines changed: 7 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,13 @@ import type { CSSProperties } from 'react'
22
import { useMemo, useState } from 'react'
33

44
import { PortalContainerContext } from '../../contexts/PortalContainerContext.js'
5-
import { columnVisibilityStatesSuffix, columnWidthsSuffix, rowHeight } from '../../helpers/constants.js'
5+
import { columnWidthsSuffix, rowHeight } from '../../helpers/constants.js'
66
import styles from '../../HighTable.module.css'
77
import { useData } from '../../hooks/useData.js'
88
import { useHTMLElement } from '../../hooks/useHTMLElement.js'
99
import { CellNavigationProvider } from '../../providers/CellNavigationProvider.js'
1010
import { ColumnParametersProvider } from '../../providers/ColumnParametersProvider.js'
11-
import { ColumnVisibilityStatesProvider } from '../../providers/ColumnVisibilityStatesProvider.js'
11+
import { ColumnsVisibilityProvider } from '../../providers/ColumnsVisibilityProvider.js'
1212
import { ColumnWidthsProvider } from '../../providers/ColumnWidthsProvider.js'
1313
import { OrderByProvider } from '../../providers/OrderByProvider.js'
1414
import { ScrollProvider } from '../../providers/ScrollProvider.js'
@@ -22,6 +22,7 @@ export default function HighTable({
2222
cacheKey,
2323
cellPosition,
2424
className = '',
25+
columnsVisibility,
2526
data,
2627
focus,
2728
maxRowNumber,
@@ -41,20 +42,6 @@ export default function HighTable({
4142
const [tableCornerSize, setTableCornerSize] = useState<{ width: number, height: number } | undefined>(undefined)
4243
const { dataId, numRows, version } = useData({ data })
4344

44-
const columnNames = useMemo(() => data.columnDescriptors.map(d => d.name), [data.columnDescriptors])
45-
46-
const initialVisibilityStates = useMemo(() => {
47-
if (!columnConfiguration) return undefined
48-
const states: Record<string, { hidden: true } | undefined> = {}
49-
for (const descriptor of data.columnDescriptors) {
50-
const config = columnConfiguration[descriptor.name]
51-
if (config?.initiallyHidden) {
52-
states[descriptor.name] = { hidden: true as const }
53-
}
54-
}
55-
return states
56-
}, [columnConfiguration, data.columnDescriptors])
57-
5845
const headerHeight = useMemo(() => {
5946
return tableCornerSize?.height ?? rowHeight
6047
}, [tableCornerSize])
@@ -96,16 +83,12 @@ export default function HighTable({
9683
viewportWidth={viewportWidth}
9784
tableCornerWidth={tableCornerSize?.width}
9885
>
99-
<ColumnVisibilityStatesProvider
86+
<ColumnsVisibilityProvider
10087
/**
10188
* Recreate a context if a new data frame is passed (but not if only the number of rows changed)
102-
* The user can also pass a cacheKey to force a new set of visibility states, or keep the current ones.
10389
*/
104-
key={cacheKey ?? dataId}
105-
// TODO(SL): pass cacheKey, memoize
106-
localStorageKey={cacheKey ? `${cacheKey}${columnVisibilityStatesSuffix}` : undefined}
107-
columnNames={columnNames}
108-
initialVisibilityStates={initialVisibilityStates}
90+
key={dataId}
91+
columnsVisibility={columnsVisibility}
10992
onColumnsVisibilityChange={onColumnsVisibilityChange}
11093
>
11194
<OrderByProvider
@@ -162,7 +145,7 @@ export default function HighTable({
162145

163146
</SelectionProvider>
164147
</OrderByProvider>
165-
</ColumnVisibilityStatesProvider>
148+
</ColumnsVisibilityProvider>
166149
</ColumnWidthsProvider>
167150
</ColumnParametersProvider>
168151
</PortalContainerContext.Provider>

src/components/HighTable/Slice.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import type { KeyboardEvent } from 'react'
22
import { useCallback, useContext, useMemo } from 'react'
33

44
import { CellNavigationContext } from '../../contexts/CellNavigationContext.js'
5-
import { ColumnVisibilityStatesContext } from '../../contexts/ColumnVisibilityStatesContext.js'
5+
import { ColumnsVisibilityContext } from '../../contexts/ColumnsVisibilityContext.js'
66
import { OrderByContext } from '../../contexts/OrderByContext.js'
77
import { ScrollContext } from '../../contexts/ScrollContext.js'
88
import { SelectionContext } from '../../contexts/SelectionContext.js'
@@ -41,7 +41,7 @@ export default function Slice({
4141
const { moveCell } = useContext(CellNavigationContext)
4242
const { orderBy, setOrderBy } = useContext(OrderByContext)
4343
const { selectable, toggleAllRows, pendingSelectionGesture, onTableKeyDown: onSelectionTableKeyDown, allRowsSelected, isRowSelected, toggleRowNumber, toggleRangeToRowNumber } = useContext(SelectionContext)
44-
const { visibleColumnsParameters: columnsParameters } = useContext(ColumnVisibilityStatesContext)
44+
const { visibleColumnsParameters: columnsParameters } = useContext(ColumnsVisibilityContext)
4545
const { renderedRowsStart, renderedRowsEnd } = useContext(ScrollContext)
4646

4747
// Fetch the required cells if needed (visible + overscan)

src/contexts/ColumnVisibilityStatesContext.ts renamed to src/contexts/ColumnsVisibilityContext.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { createContext } from 'react'
22

33
import type { ColumnParameters } from './ColumnParametersContext'
44

5-
interface ColumnVisibilityStatesContextType {
5+
interface ColumnsVisibilityContextType {
66
/** Number of visible columns */
77
numberOfVisibleColumns: number
88
/** Visible columns parameters */
@@ -29,8 +29,8 @@ interface ColumnVisibilityStatesContextType {
2929
isHiddenColumn?: (columnName: string) => boolean // returns true if the column is hidden
3030
}
3131

32-
export const defaultColumnVisibilityStatesContext: ColumnVisibilityStatesContextType = {
32+
export const defaultColumnsVisibilityContext: ColumnsVisibilityContextType = {
3333
numberOfVisibleColumns: 0,
3434
}
3535

36-
export const ColumnVisibilityStatesContext = createContext<ColumnVisibilityStatesContextType>(defaultColumnVisibilityStatesContext)
36+
export const ColumnsVisibilityContext = createContext<ColumnsVisibilityContextType>(defaultColumnsVisibilityContext)

src/helpers/constants.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,6 @@ export const defaultNumRowsPerPage = 20 // number of rows per page for keyboard
66

77
const columnWidthsFormatVersion = '2' // increase in case of breaking changes in the column widths format
88
export const columnWidthsSuffix = `:${columnWidthsFormatVersion}:column:widths` // suffix used to store the column widths in local storage
9-
const columnVisibilityStatesFormatVersion = '2' // increase in case of breaking changes in the column visibility format (changed from array by index to record by name)
10-
export const columnVisibilityStatesSuffix = `:${columnVisibilityStatesFormatVersion}:column:visibility` // suffix used to store the columns visibility in local storage
119

1210
export const ariaOffset = 2 // 1-based index, +1 for the header
1311

src/hooks/useFetchCells.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { useContext, useEffect, useEffectEvent, useMemo } from 'react'
22

3-
import { ColumnVisibilityStatesContext } from '../contexts/ColumnVisibilityStatesContext.js'
3+
import { ColumnsVisibilityContext } from '../contexts/ColumnsVisibilityContext.js'
44
import { OrderByContext } from '../contexts/OrderByContext.js'
55
import { ScrollContext } from '../contexts/ScrollContext.js'
66
import { defaultOverscan } from '../helpers/constants.js'
@@ -16,7 +16,7 @@ type Props = Pick<HighTableProps, 'data' | 'onError' | 'overscan'> & {
1616
*/
1717
export function useFetchCells({ data, numRows, overscan = defaultOverscan, onError }: Props) {
1818
const { visibleRowsStart, visibleRowsEnd } = useContext(ScrollContext)
19-
const { visibleColumnsParameters } = useContext(ColumnVisibilityStatesContext)
19+
const { visibleColumnsParameters } = useContext(ColumnsVisibilityContext)
2020
const { orderBy } = useContext(OrderByContext)
2121

2222
const fetchedRowsStart = useMemo(() => {

src/providers/CellNavigationProvider.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { useCallback, useContext, useEffect, useMemo, useReducer } from 'react'
33

44
import type { FocusAction, FocusState, MoveCellAction } from '../contexts/CellNavigationContext.js'
55
import { CellNavigationContext } from '../contexts/CellNavigationContext.js'
6-
import { ColumnVisibilityStatesContext } from '../contexts/ColumnVisibilityStatesContext.js'
6+
import { ColumnsVisibilityContext } from '../contexts/ColumnsVisibilityContext.js'
77
import { defaultNumRowsPerPage } from '../helpers/constants.js'
88
import { useInputState } from '../hooks/useInputState.js'
99
import type { HighTableProps } from '../types.js'
@@ -75,7 +75,7 @@ export function CellNavigationProvider({
7575
const rowCount = useMemo(() => numDataRows + 1, [numDataRows])
7676

7777
// number of columns in the table, including the row header column
78-
const { numberOfVisibleColumns: numDataColumns } = useContext(ColumnVisibilityStatesContext)
78+
const { numberOfVisibleColumns: numDataColumns } = useContext(ColumnsVisibilityContext)
7979
const colCount = useMemo(() => numDataColumns + 1, [numDataColumns])
8080

8181
const goToCurrentCell = useCallback(() => {

0 commit comments

Comments
 (0)