Skip to content

Commit c2dc462

Browse files
authored
feat: add ability to render table rows as links (#727)
1 parent a996413 commit c2dc462

File tree

6 files changed

+167
-80
lines changed

6 files changed

+167
-80
lines changed

src/components/Code.tsx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -395,9 +395,8 @@ function CodeContent({
395395
<Mermaid
396396
ref={mermaidRef}
397397
setError={setMermaidError}
398-
>
399-
{codeString}
400-
</Mermaid>
398+
diagram={codeString}
399+
/>
401400
) : (
402401
<Highlight
403402
key={codeString}

src/components/Mermaid.tsx

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,12 @@ export type MermaidRefHandle = {
2323

2424
export function Mermaid({
2525
ref,
26-
children,
26+
diagram,
2727
setError: setErrorProp,
2828
...props
2929
}: Omit<ComponentPropsWithoutRef<'div'>, 'children'> & {
30-
children: string
31-
ref: Ref<MermaidRefHandle>
30+
diagram: string
31+
ref?: Ref<MermaidRefHandle>
3232
setError?: (error: Nullable<Error>) => void
3333
}) {
3434
const [svgStr, setSvgStr] = useState<Nullable<string>>()
@@ -46,7 +46,7 @@ export function Mermaid({
4646
useImperativeHandle(ref, () => ({ svgStr }))
4747

4848
useLayoutEffect(() => {
49-
const id = getMermaidId(children)
49+
const id = getMermaidId(diagram)
5050
const cached = cachedRenders[id]
5151
if (cached) {
5252
setIsLoading(false)
@@ -61,7 +61,7 @@ export function Mermaid({
6161
try {
6262
setIsLoading(true)
6363
setError(null)
64-
setSvgStr(await renderMermaid(children))
64+
setSvgStr(await renderMermaid(diagram))
6565
setIsLoading(false)
6666
} catch (caughtErr) {
6767
let err = caughtErr
@@ -81,15 +81,15 @@ export function Mermaid({
8181
checkAndRender()
8282

8383
return () => clearTimeout(pollTimeout)
84-
}, [children, setError, svgStr])
84+
}, [diagram, setError, svgStr])
8585

8686
if (error)
8787
return (
8888
<Highlight
89-
key={children}
89+
key={diagram}
9090
language="mermaid"
9191
>
92-
{children}
92+
{diagram}
9393
</Highlight>
9494
)
9595

src/components/table/Table.tsx

Lines changed: 98 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,12 @@ import {
1919
} from 'react'
2020
import styled, { useTheme } from 'styled-components'
2121

22-
import { InfoOutlineIcon, Tooltip, WrapWithIf } from '../../index'
22+
import {
23+
InfoOutlineIcon,
24+
Tooltip,
25+
useResizeObserver,
26+
WrapWithIf,
27+
} from '../../index'
2328
import Button from '../Button'
2429
import EmptyState from '../EmptyState'
2530
import CaretUpIcon from '../icons/CaretUpIcon'
@@ -44,15 +49,17 @@ import {
4449
TableProps,
4550
} from './tableUtils'
4651
import { Tbody } from './Tbody'
47-
import { Td, TdBasic, TdExpand, TdLoading } from './Td'
52+
import { Td, TdBasic, TdExpand, TdGhostLink, TdLoading } from './Td'
4853
import { Th } from './Th'
4954
import { Thead } from './Thead'
5055
import { Tr } from './Tr'
5156

57+
const GHOST_LINK_ID = 'ghost-link'
58+
5259
function Table({
5360
ref: forwardedRef,
5461
data,
55-
columns,
62+
columns: columnsProp,
5663
loading = false,
5764
loadingSkeletonRows = 10,
5865
hideHeader = false,
@@ -74,6 +81,7 @@ function Table({
7481
reactTableOptions,
7582
highlightedRowId,
7683
onRowClick,
84+
getRowLink,
7785
emptyStateProps,
7886
hasNextPage,
7987
isFetchingNextPage,
@@ -84,10 +92,18 @@ function Table({
8492
}: TableProps) {
8593
const theme = useTheme()
8694
const tableContainerRef = useRef<HTMLDivElement>(undefined)
95+
96+
const [tableWidth, setTableWidth] = useState(0)
97+
useResizeObserver(tableContainerRef, (entry) => setTableWidth(entry.width))
98+
8799
const [hover, setHover] = useState(false)
88100
const [scrollTop, setScrollTop] = useState(0)
89101
const [expanded, setExpanded] = useState({})
90102

103+
const columns = useMemo(() => {
104+
return [...columnsProp, ...(!!getRowLink ? [{ id: GHOST_LINK_ID }] : [])]
105+
}, [columnsProp, getRowLink])
106+
91107
const table = useReactTable({
92108
data,
93109
columns,
@@ -245,50 +261,54 @@ function Table({
245261
key={headerGroup.id}
246262
$fillLevel={fillLevel}
247263
>
248-
{headerGroup.headers.map((header) => (
249-
<Th
250-
key={header.id}
251-
$fillLevel={fillLevel}
252-
$hideHeader={hideHeader}
253-
$stickyColumn={stickyColumn}
254-
$highlight={header.column.columnDef?.meta?.highlight}
255-
{...(header.column.getCanSort()
256-
? {
257-
$cursor:
258-
header.column.getIsSorted() === 'asc'
259-
? 's-resize'
260-
: header.column.getIsSorted() === 'desc'
261-
? 'ns-resize'
262-
: 'n-resize',
263-
onClick: header.column.getToggleSortingHandler(),
264-
}
265-
: {})}
266-
>
267-
<div className="thOuterWrap">
268-
<div className="thSortIndicatorWrap">
269-
<div>
270-
{header.isPlaceholder
271-
? null
272-
: flexRender(
273-
header.column.columnDef.header,
274-
header.getContext()
275-
)}
264+
{headerGroup.headers.map((header) =>
265+
header.column.id.includes(GHOST_LINK_ID) ? (
266+
<th key={header.id} />
267+
) : (
268+
<Th
269+
key={header.id}
270+
$fillLevel={fillLevel}
271+
$hideHeader={hideHeader}
272+
$stickyColumn={stickyColumn}
273+
$highlight={header.column.columnDef?.meta?.highlight}
274+
{...(header.column.getCanSort()
275+
? {
276+
$cursor:
277+
header.column.getIsSorted() === 'asc'
278+
? 's-resize'
279+
: header.column.getIsSorted() === 'desc'
280+
? 'ns-resize'
281+
: 'n-resize',
282+
onClick: header.column.getToggleSortingHandler(),
283+
}
284+
: {})}
285+
>
286+
<div className="thOuterWrap">
287+
<div className="thSortIndicatorWrap">
288+
<div>
289+
{header.isPlaceholder
290+
? null
291+
: flexRender(
292+
header.column.columnDef.header,
293+
header.getContext()
294+
)}
295+
</div>
296+
{header.column.columnDef.meta?.tooltip && (
297+
<Tooltip
298+
label={header.column.columnDef.meta.tooltip}
299+
{...header.column.columnDef.meta.tooltipProps}
300+
>
301+
<InfoOutlineIcon />
302+
</Tooltip>
303+
)}
304+
<SortIndicator
305+
direction={header.column.getIsSorted()}
306+
/>
276307
</div>
277-
{header.column.columnDef.meta?.tooltip && (
278-
<Tooltip
279-
label={header.column.columnDef.meta.tooltip}
280-
{...header.column.columnDef.meta.tooltipProps}
281-
>
282-
<InfoOutlineIcon />
283-
</Tooltip>
284-
)}
285-
<SortIndicator
286-
direction={header.column.getIsSorted()}
287-
/>
288308
</div>
289-
</div>
290-
</Th>
291-
))}
309+
</Th>
310+
)
311+
)}
292312
</Tr>
293313
))}
294314
</Thead>
@@ -358,7 +378,7 @@ function Table({
358378
$highlighted={tableRow?.id === highlightedRowId}
359379
$selectable={tableRow?.getCanSelect() ?? false}
360380
$selected={tableRow?.getIsSelected() ?? false}
361-
$clickable={!!onRowClick}
381+
$clickable={!!onRowClick || !!getRowLink}
362382
>
363383
{isNil(tableRow) && isLoaderRow ? (
364384
<TdLoading
@@ -376,28 +396,37 @@ function Table({
376396
<Spinner color={theme.colors['text-xlight']} />
377397
</TdLoading>
378398
) : (
379-
tableRow?.getVisibleCells().map((cell) => (
380-
<Td
381-
key={cell.id}
382-
$fillLevel={fillLevel}
383-
$firstRow={i === 0}
384-
$padCells={padCells}
385-
$loose={loose}
386-
$stickyColumn={stickyColumn}
387-
$highlight={
388-
cell.column?.columnDef?.meta?.highlight
389-
}
390-
$truncateColumn={
391-
cell.column?.columnDef?.meta?.truncate
392-
}
393-
$center={cell.column?.columnDef?.meta?.center}
394-
>
395-
{flexRender(
396-
cell.column.columnDef.cell,
397-
cell.getContext()
398-
)}
399-
</Td>
400-
))
399+
tableRow?.getVisibleCells().map((cell) =>
400+
cell.id.includes(GHOST_LINK_ID) ? (
401+
<TdGhostLink
402+
key={cell.id}
403+
width={tableWidth}
404+
href={getRowLink?.(tableRow)}
405+
/>
406+
) : (
407+
<Td
408+
key={cell.id}
409+
$fillLevel={fillLevel}
410+
$firstRow={i === 0}
411+
$padCells={padCells}
412+
$loose={loose}
413+
$stickyColumn={stickyColumn}
414+
$highlight={
415+
cell.column?.columnDef?.meta?.highlight
416+
}
417+
$truncateColumn={
418+
cell.column?.columnDef?.meta?.truncate
419+
}
420+
$center={cell.column?.columnDef?.meta?.center}
421+
$rowsHaveLinks={!!getRowLink}
422+
>
423+
{flexRender(
424+
cell.column.columnDef.cell,
425+
cell.getContext()
426+
)}
427+
</Td>
428+
)
429+
)
401430
)}
402431
</Tr>
403432
{tableRow?.getIsExpanded() && (
@@ -454,7 +483,7 @@ function Table({
454483
small
455484
floating
456485
width={140}
457-
css={{ position: 'absolute', right: 24, bottom: 24 }}
486+
css={{ position: 'absolute', right: 24, bottom: 24, zIndex: 1 }}
458487
endIcon={<CaretUpIcon />}
459488
onClick={() =>
460489
tableContainerRef?.current?.scrollTo({

src/components/table/Td.tsx

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export const Td = styled.td<{
1616
$highlight?: boolean
1717
$truncateColumn: boolean
1818
$center?: boolean
19+
$rowsHaveLinks?: boolean
1920
}>(
2021
({
2122
theme,
@@ -27,6 +28,7 @@ export const Td = styled.td<{
2728
$highlight: highlight,
2829
$truncateColumn: truncateColumn = false,
2930
$center: center,
31+
$rowsHaveLinks: rowsHaveLinks,
3032
}) => ({
3133
...theme.partials.text.body2LooseLineHeight,
3234
display: 'flex',
@@ -68,6 +70,12 @@ export const Td = styled.td<{
6870
},
6971
}
7072
: {}),
73+
...(rowsHaveLinks && {
74+
zIndex: 1,
75+
// disable pointer events for children besides interactive elements so row links can capture most clicks
76+
pointerEvents: 'none',
77+
'& button, & a, & input, & select, & textarea': { pointerEvents: 'auto' },
78+
}),
7179
})
7280
)
7381

@@ -99,3 +107,14 @@ export const TdBasic = styled.td({
99107
padding: 0,
100108
overflow: 'hidden',
101109
})
110+
111+
export function TdGhostLink({ width, href }: { width: number; href: string }) {
112+
return (
113+
<td style={{ position: 'relative', width: 0 }}>
114+
<a
115+
style={{ width, position: 'absolute', inset: '0 0 0 auto', zIndex: 0 }}
116+
href={href}
117+
/>
118+
</td>
119+
)
120+
}

src/components/table/tableUtils.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ export type TableBaseProps = {
4646
>
4747
reactTableOptions?: Partial<Omit<TableOptions<any>, 'data' | 'columns'>>
4848
onRowClick?: (e: MouseEvent<HTMLTableRowElement>, row: Row<any>) => void
49+
getRowLink?: (row: Row<unknown>) => Nullable<string>
4950
emptyStateProps?: EmptyStateProps
5051
hasNextPage?: boolean
5152
fetchNextPage?: () => void

src/stories/Table.stories.tsx

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -516,3 +516,42 @@ Selectable.args = {
516516
data: repeatedData,
517517
columns: expandingColumns,
518518
}
519+
520+
export const LinkableRows = Template.bind({})
521+
522+
LinkableRows.args = {
523+
fillLevel: 0,
524+
rowBg: 'base',
525+
width: '900px',
526+
height: '400px',
527+
data: repeatedData,
528+
columns: [
529+
...columns.slice(0, 2),
530+
columnHelper.display({
531+
id: 'actions',
532+
header: () => <span>Actions</span>,
533+
cell: ({ row }) => (
534+
<button
535+
onClick={(e) => {
536+
e.stopPropagation()
537+
alert(`Action clicked for: ${row.original.function}`)
538+
}}
539+
style={{
540+
padding: '4px 12px',
541+
cursor: 'pointer',
542+
background: '#1a1d24',
543+
border: '1px solid #3d4149',
544+
borderRadius: '4px',
545+
color: '#babbbd',
546+
}}
547+
>
548+
Action
549+
</button>
550+
),
551+
}),
552+
...columns.slice(2),
553+
],
554+
onRowClick: (e: MouseEvent, row: Row<any>) => console.log(row?.original),
555+
getRowLink: (row: Row<Method>) =>
556+
`https://example.com/function/${row.original.function}`,
557+
}

0 commit comments

Comments
 (0)