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

🔨 feat(Table): add responsive behavior #1484

Closed
wants to merge 1 commit into from
Closed
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
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ The following table has a default style. But in future, there will be several ex

You may consider using `table-layout: fixed;`. You can use the modifier class in doing so: `.dnb-table--fixed`

## Responsive Tables

## Data-grid driven tables

You may have a look at React [Table](https://github.com/TanStack/table) (former `react-table`) including this [CodeSandbox example](https://codesandbox.io/embed/eufemia-react-table-x4cwc).
Expand Down
230 changes: 230 additions & 0 deletions packages/dnb-eufemia/src/components/table/ResponsiveTableInternals.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
import React from 'react'
import { warn } from '../../shared/helpers'
import {
getPreviousSibling,
InteractionInvalidation,
} from '../../shared/component-helper'

const interactiveElements: Array<string> = [
'button',
'input',
'a',
'select',
]

export function getInteractiveElements(element: HTMLElement) {
return interactiveElements.filter((tagName) =>
getPreviousSibling('tagName:' + tagName, element)
)
}

export function useHandleInvisibleTds({
trRef,
trIsOpen,
responsive,
hasAccordionContent,
}) {
React.useEffect(() => {
if (responsive && !hasAccordionContent) {
if (trRef.current.parentElement.tagName === 'THEAD') {
return // stop here
}

let timeout: ReturnType<typeof setTimeout>
const listener = () => {
clearTimeout(timeout)
timeout = setTimeout(() => {
hideInvisibleTds(trRef.current)
}, 100)
}

window.addEventListener('resize', listener)

return () => {
clearTimeout(timeout)
window.removeEventListener('resize', listener)
}
}
}, [trRef, responsive, hasAccordionContent])

React.useEffect(() => {
if (responsive && !hasAccordionContent) {
if (trRef.current.parentElement.tagName === 'THEAD') {
return // stop here
}

hideInvisibleTds(trRef.current, trIsOpen)
}
}, [trRef, trIsOpen, responsive, hasAccordionContent])
}

/**
* Helper function to hive td elements which are not "visible"
* due to the fixed height.
* We hide them so the user can not tab invisible focus elements, like a button.
*
* @param trElement {HTMLTableRowElement}
*/
function hideInvisibleTds(
trElement: HTMLTableRowElement,
showAll?: boolean
) {
if (!isSmallScreen()) {
if (trElement.style.getPropertyValue('--tr-height--open')) {
trElement.style.removeProperty('--tr-height--open')
trElement.style.removeProperty('--tr-height--closed')

const listOfTds = getListOfTds(trElement)
listOfTds.forEach((tdElement) => {
if (tdElement?.__iiInstance) {
tdElement.__iiInstance.revert()
}
})
}

return // stop here
}

const { trHeightOpen, trHeightClosed } = getTrHeights({
trElement,
showAll,
})

try {
if (trHeightOpen > 0) {
trElement.style.setProperty(
'--tr-height--open',
String(trHeightOpen / 16) + 'rem'
)
}

if (trHeightClosed > 0) {
trElement.style.setProperty(
'--tr-height--closed',
String(trHeightClosed / 16) + 'rem'
)
}
} catch (e) {
warn(e)
}
}

function isSmallScreen() {
return Math.ceil(document.documentElement.offsetWidth / 16) <= 40
}

function getTrHeights({
trElement,
showAll,
}: {
trElement: HTMLTableRowElement
showAll: boolean
}) {
const mainElementHeight = getMainCellHeight(trElement)

const { openHeights, closedHeights } = calculateHeightsFromTr({
trElement,
mainElementHeight,
showAll,
})

const additionalOpenHeight = Object.values(openHeights).reduce(
(acc, cur) => {
acc += cur
return acc
},
0
)
const additionalClosedHeight = Object.values(closedHeights).sort(
(a, b) => {
if (a < b) {
return -1
} else if (a > b) {
return 1
}
return 0
}
)[0]

const trHeightOpen = mainElementHeight + additionalOpenHeight
const trHeightClosed =
additionalClosedHeight > 0
? mainElementHeight + additionalClosedHeight
: 0

return { trHeightOpen, trHeightClosed }
}

function getMainCellHeight(trElement: HTMLTableRowElement) {
const mainElement = trElement.querySelector(
'.dnb-table__main_cell'
) as HTMLElement | null

return mainElement?.offsetHeight || 0
}

type tdWithIi = {
__iiInstance?: InteractionInvalidation
} & HTMLTableCellElement

function calculateHeightsFromTr({
trElement,
mainElementHeight,
showAll,
}: {
trElement: HTMLTableRowElement
mainElementHeight: number
showAll: boolean
}) {
const openHeights: Record<number, number> = {}
const closedHeights: Record<number, number> = {}
const listOfTds = getListOfTds(trElement)
// console.log('listOfTds', listOfTds.length)

/**
* Find every <td> that is wrapped on a newline
* The <tr> needs a CSS style of: `position: relative`
*/
listOfTds.forEach((tdElement) => {
const offsetTop = tdElement?.offsetTop

if (typeof offsetTop === 'undefined') {
return // stop here because of invalid element
}

// Collect heights, based on top position
openHeights[offsetTop] = tdElement.offsetHeight

const elements = tdElement.querySelectorAll(
interactiveElements.map((selector) => `* ${selector}`).join(',')
)

if (offsetTop > mainElementHeight && !showAll) {
if (elements.length > 0) {
if (!tdElement.__iiInstance) {
tdElement.__iiInstance = new InteractionInvalidation({
ariaHidden: false, // Do not hide it from screen reader, because the table is still readable.
})
}

tdElement.__iiInstance.activate(tdElement)
}

closedHeights[offsetTop] = tdElement.offsetHeight
} else if (offsetTop <= mainElementHeight || showAll) {
if (tdElement?.__iiInstance) {
if (elements.length > 0) {
tdElement.__iiInstance.revert()
}
}
}
})

return { openHeights, closedHeights }
}

function getListOfTds(trElement: HTMLTableRowElement): Array<tdWithIi> {
return Array.from(
trElement.querySelectorAll('td:not(.dnb-table__main_cell)')
)
}
73 changes: 51 additions & 22 deletions packages/dnb-eufemia/src/components/table/Table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,11 @@ import { createSkeletonClass } from '../skeleton/SkeletonHelper'
import { SkeletonShow } from '../skeleton/Skeleton'
import { ISpacingProps } from '../../shared/interfaces'
import {
validateDOMAttributes,
extendPropsWithContext,
validateDOMAttributes,
} from '../../shared/component-helper'
import TableContext from './TableContext'
import AccordionContent from './TableAccordionContent'

// Internal
import {
Expand All @@ -20,8 +22,9 @@ import {
StickyTableHeaderProps,
} from './TableStickyHeader'

export type TableSizes = 'medium' | 'large'
export type TableVariants = 'basis' | 'not-defined-yet'
export type TableSizes = 'large' | 'medium'
export type TableVariants = 'table'
export type TableRespnsiveVariants = 'cards' | 'lines'

export { StickyHelper }

Expand All @@ -36,6 +39,17 @@ export interface TableProps extends StickyTableHeaderProps {
*/
className?: string

/**
* Male the Table responsive to narrow/smaller screens
*/
responsive?: boolean

/**
* The variant when responsive is true.
* Default: lines.
*/
responsiveVariant?: TableRespnsiveVariants

/**
* Skeleton should be applied when loading content
*/
Expand All @@ -49,14 +63,15 @@ export interface TableProps extends StickyTableHeaderProps {

/**
* The variant of the component.
* Default: basis.
* Default: table.
*/
variant?: TableVariants
}

export const defaultProps = {
size: 'large',
variant: 'basis',
variant: 'table',
responsiveVariant: 'table',
}

const Table = (
Expand All @@ -81,8 +96,9 @@ const Table = (
size,
skeleton,
variant,

sticky, // eslint-disable-line
responsiveVariant,
responsive,
sticky,
stickyOffset, // eslint-disable-line
...props
} = allProps
Expand All @@ -92,29 +108,42 @@ const Table = (

const { elementRef } = useStickyHeader(allProps)

// Create this ref in order to "auto" set even/odd class in tr elements
const trTmpRef = React.useRef({ count: 0 })
React.useLayoutEffect(() => {
trTmpRef.current.count = 0
})

validateDOMAttributes(allProps, props)

return (
<Provider skeleton={Boolean(skeleton)}>
<table
className={classnames(
'dnb-table',
`dnb-table__variant--${variant || 'basis'}`,
`dnb-table__size--${size || 'large'}`,
sticky && `dnb-table--sticky`,
spacingClasses,
skeletonClasses,
className
)}
ref={elementRef}
{...props}
>
{children}
</table>
<TableContext.Provider value={{ trTmpRef, responsive }}>
<table
className={classnames(
'dnb-table',
variant && `dnb-table__variant--${variant}`,
responsive && 'dnb-table--responsive',
responsive &&
responsiveVariant &&
`dnb-table__responsive-variant--${responsiveVariant}`,
size && `dnb-table__size--${size}`,
sticky && `dnb-table--sticky`,
spacingClasses,
skeletonClasses,
className
)}
ref={elementRef}
{...props}
>
{children}
</table>
</TableContext.Provider>
</Provider>
)
}

export default Table

Table.StickyHelper = StickyHelper
Table.AccordionContent = AccordionContent
Loading