From 1f1c7a86c39e6aa950519e44fc5000f93a525ebd Mon Sep 17 00:00:00 2001 From: Kasper Welbers Date: Tue, 15 Aug 2023 12:40:06 +0200 Subject: [PATCH] table tooltips for clipped cells --- package-lock.json | 120 +++++++++ src/framework/styles.css | 8 + .../react/ui/elements/figure.tsx | 228 +----------------- .../ui/elements/figures/recharts_graph.tsx | 177 ++++++++++++++ .../react/ui/elements/figures/wordcloud.tsx | 38 +++ .../visualisation/react/ui/elements/table.tsx | 202 ++++++++++++---- .../react/ui/elements/table_container.tsx | 2 +- 7 files changed, 515 insertions(+), 260 deletions(-) create mode 100644 src/framework/visualisation/react/ui/elements/figures/recharts_graph.tsx create mode 100644 src/framework/visualisation/react/ui/elements/figures/wordcloud.tsx diff --git a/package-lock.json b/package-lock.json index b4d899a1..5a37eb32 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,9 +8,11 @@ "name": "port", "version": "0.1.0", "dependencies": { + "@floating-ui/react": "^0.25.1", "@testing-library/jest-dom": "^5.16.5", "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", + "@tippyjs/react": "^4.2.6", "@types/jest": "^27.5.2", "@types/node": "^16.11.59", "@types/react": "^18.0.21", @@ -2442,6 +2444,54 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@floating-ui/core": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.4.1.tgz", + "integrity": "sha512-jk3WqquEJRlcyu7997NtR5PibI+y5bi+LS3hPmguVClypenMsCY3CBa3LAQnozRCtCrYWSEtAdiskpamuJRFOQ==", + "dependencies": { + "@floating-ui/utils": "^0.1.1" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.5.1.tgz", + "integrity": "sha512-KwvVcPSXg6mQygvA1TjbN/gh///36kKtllIF8SUm0qpFj8+rvYrpvlYdL1JoA71SHpDqgSSdGOSoQ0Mp3uY5aw==", + "dependencies": { + "@floating-ui/core": "^1.4.1", + "@floating-ui/utils": "^0.1.1" + } + }, + "node_modules/@floating-ui/react": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.25.1.tgz", + "integrity": "sha512-lxuWxfSgDJwOeZK07PIDjTSlH0CY6LRDKo6eI0H7TnctP+5IAn0n8+npNveM0L2wNIVdAr0S8RvvoHfhzPbBAQ==", + "dependencies": { + "@floating-ui/react-dom": "^2.0.1", + "@floating-ui/utils": "^0.1.1", + "tabbable": "^6.0.1" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.0.1.tgz", + "integrity": "sha512-rZtAmSht4Lry6gdhAJDrCp/6rKN7++JnL1/Anbr/DdeyYXQPxvg/ivrbYvJulbRf4vL8b212suwMM2lxbv+RQA==", + "dependencies": { + "@floating-ui/dom": "^1.3.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.1.1.tgz", + "integrity": "sha512-m0G6wlnhm/AX0H12IOWtK8gASEMffnX08RtKkCgTdHb9JpHKGloI7icFfLg9ZmQeavcvR0PKmzxClyuFPSjKWw==" + }, "node_modules/@humanwhocodes/config-array": { "version": "0.10.7", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.10.7.tgz", @@ -3477,6 +3527,18 @@ "@testing-library/dom": ">=7.21.4" } }, + "node_modules/@tippyjs/react": { + "version": "4.2.6", + "resolved": "https://registry.npmjs.org/@tippyjs/react/-/react-4.2.6.tgz", + "integrity": "sha512-91RicDR+H7oDSyPycI13q3b7o4O60wa2oRbjlz2fyRLmHImc4vyDwuUP8NtZaN0VARJY5hybvDYrFzhY9+Lbyw==", + "dependencies": { + "tippy.js": "^6.3.1" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, "node_modules/@tootallnate/once": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", @@ -16122,6 +16184,11 @@ "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", "dev": true }, + "node_modules/tabbable": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz", + "integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==" + }, "node_modules/table": { "version": "6.8.0", "resolved": "https://registry.npmjs.org/table/-/table-6.8.0.tgz", @@ -20027,6 +20094,46 @@ } } }, + "@floating-ui/core": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.4.1.tgz", + "integrity": "sha512-jk3WqquEJRlcyu7997NtR5PibI+y5bi+LS3hPmguVClypenMsCY3CBa3LAQnozRCtCrYWSEtAdiskpamuJRFOQ==", + "requires": { + "@floating-ui/utils": "^0.1.1" + } + }, + "@floating-ui/dom": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.5.1.tgz", + "integrity": "sha512-KwvVcPSXg6mQygvA1TjbN/gh///36kKtllIF8SUm0qpFj8+rvYrpvlYdL1JoA71SHpDqgSSdGOSoQ0Mp3uY5aw==", + "requires": { + "@floating-ui/core": "^1.4.1", + "@floating-ui/utils": "^0.1.1" + } + }, + "@floating-ui/react": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.25.1.tgz", + "integrity": "sha512-lxuWxfSgDJwOeZK07PIDjTSlH0CY6LRDKo6eI0H7TnctP+5IAn0n8+npNveM0L2wNIVdAr0S8RvvoHfhzPbBAQ==", + "requires": { + "@floating-ui/react-dom": "^2.0.1", + "@floating-ui/utils": "^0.1.1", + "tabbable": "^6.0.1" + } + }, + "@floating-ui/react-dom": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.0.1.tgz", + "integrity": "sha512-rZtAmSht4Lry6gdhAJDrCp/6rKN7++JnL1/Anbr/DdeyYXQPxvg/ivrbYvJulbRf4vL8b212suwMM2lxbv+RQA==", + "requires": { + "@floating-ui/dom": "^1.3.0" + } + }, + "@floating-ui/utils": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.1.1.tgz", + "integrity": "sha512-m0G6wlnhm/AX0H12IOWtK8gASEMffnX08RtKkCgTdHb9JpHKGloI7icFfLg9ZmQeavcvR0PKmzxClyuFPSjKWw==" + }, "@humanwhocodes/config-array": { "version": "0.10.7", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.10.7.tgz", @@ -20764,6 +20871,14 @@ "@babel/runtime": "^7.12.5" } }, + "@tippyjs/react": { + "version": "4.2.6", + "resolved": "https://registry.npmjs.org/@tippyjs/react/-/react-4.2.6.tgz", + "integrity": "sha512-91RicDR+H7oDSyPycI13q3b7o4O60wa2oRbjlz2fyRLmHImc4vyDwuUP8NtZaN0VARJY5hybvDYrFzhY9+Lbyw==", + "requires": { + "tippy.js": "^6.3.1" + } + }, "@tootallnate/once": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", @@ -30186,6 +30301,11 @@ "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", "dev": true }, + "tabbable": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz", + "integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==" + }, "table": { "version": "6.8.0", "resolved": "https://registry.npmjs.org/table/-/table-6.8.0.tgz", diff --git a/src/framework/styles.css b/src/framework/styles.css index 2cff40cd..88caf7f1 100644 --- a/src/framework/styles.css +++ b/src/framework/styles.css @@ -23,3 +23,11 @@ ::-webkit-scrollbar-thumb:hover { background: theme('colors.grey2'); } + +.tippy-box[data-theme~='wordcloud'] { + background-color: #333b; + backdrop-filter: blur(5px); + color: white; + padding: 0.5rem; + border-radius: 0.5rem; +} diff --git a/src/framework/visualisation/react/ui/elements/figure.tsx b/src/framework/visualisation/react/ui/elements/figure.tsx index 9bc7d05a..ba83e922 100644 --- a/src/framework/visualisation/react/ui/elements/figure.tsx +++ b/src/framework/visualisation/react/ui/elements/figure.tsx @@ -2,36 +2,19 @@ import { TableWithContext } from '../../../../types/elements' import TextBundle from '../../../../text_bundle' import { Translator } from '../../../../translator' -import { - VisualizationType, - VisualizationData, - AxisSettings, - TickerFormat -} from '../../../../types/visualizations' +import { VisualizationType, VisualizationData } from '../../../../types/visualizations' -import React, { useMemo, useState } from 'react' +import { useMemo } from 'react' import { ReactFactoryContext } from '../../factory' -import { - ResponsiveContainer, - LineChart, - Line, - XAxis, - YAxis, - Tooltip, - Legend, - BarChart, - Bar, - AreaChart, - Area -} from 'recharts' import useVisualizationData from '../hooks/useVisualizationData' import { Title6 } from './text' import Lottie from 'lottie-react' import spinnerDark from '../../../../../assets/lottie/spinner-dark.json' -import ReactWordcloud, { Options } from 'react-wordcloud' +import RechartsGraph from './figures/recharts_graph' +import Wordcloud from './figures/wordcloud' type Props = VisualizationProps & ReactFactoryContext @@ -71,13 +54,15 @@ export const Figure = ({ const minHeight = visualization.height ? visualization.height + 'px' : `20rem` - console.log(visualizationData) return ( -
+
-
+
{visualizationData?.data.length === 0 ? ( -
{noDataMsg}
+
{noDataMsg}
) : ( )} @@ -94,168 +79,13 @@ const RenderVisualization = ({ if (!visualizationData) return null if (['line', 'bar', 'area'].includes(visualizationData.type)) - return + return if (visualizationData.type === 'wordcloud') - return + return return null } -interface RenderVisualizationProps { - visualizationData: VisualizationData -} - -const RenderRechartsGraph = ({ - visualizationData -}: RenderVisualizationProps): JSX.Element | null => { - function tooltip() { - return ( - - ) - } - - function axes(minTickGap: number) { - if (!visualizationData) return null - const secondary = - Object.values(visualizationData.yKeys).findIndex((yKey: AxisSettings) => yKey.secondAxis) !== - -1 - const { tickFormatter, tickFormatter2 } = getTickFormatters( - Object.values(visualizationData.yKeys) - ) - - return ( - <> - - - {secondary && } - - ) - } - - function legend() { - return ( - - ) - } - - let chart: JSX.Element | null = null - - if (visualizationData.type === 'line') { - chart = ( - - {axes(20)} - {tooltip()} - {legend()} - {Object.values(visualizationData.yKeys).map((yKey: AxisSettings, i: number) => { - const { color, dash } = getLineStyle(i) - return ( - - ) - })} - - ) - } - - if (visualizationData.type === 'bar') { - chart = ( - - {axes(0)} - {tooltip()} - {legend()} - {Object.values(visualizationData.yKeys).map((yKey: AxisSettings, i: number) => { - const { color, dash } = getLineStyle(i) - return ( - - ) - })} - - ) - } - - if (visualizationData.type === 'area') { - chart = ( - - {axes(20)} - {tooltip()} - {legend()} - {Object.values(visualizationData.yKeys).map((yKey: AxisSettings, i: number) => { - const { color, dash } = getLineStyle(i) - return ( - - ) - })} - - ) - } - - if (!chart) return null - return ( - - {chart} - - ) -} - -function RenderWordcloud({ visualizationData }: RenderVisualizationProps): JSX.Element | null { - const [options] = useState({ - rotations: 2, - rotationAngles: [0], - scale: 'sqrt', - fontSizes: [10, 50], - enableTooltip: true - }) - - const words = useMemo(() => { - const words = [] - for (let row of visualizationData.data) { - const text = row[visualizationData?.xKey.label] as string - let value = 0 - for (let yKey of Object.values(visualizationData.yKeys)) { - value += Number(row[yKey.label]) || 0 - } - words.push({ text, value }) - } - - return words.sort((a, b) => b.value - a.value).slice(0, 50) - }, [visualizationData]) - - return -} - function prepareCopy(locale: string): Record { return { errorMsg: Translator.translate(errorMsg, locale), @@ -268,37 +98,3 @@ const noDataMsg = new TextBundle().add('en', 'No data').add('nl', 'Geen data') const errorMsg = new TextBundle() .add('en', 'Could not create visualization') .add('nl', 'Kon visualisatie niet maken') - -function getLineStyle(index: number): { color: string; dash: string } { - const COLORS = ['#4272EF', '#FF5E5E', '#FFCF60', '#1E3FCC', '#CC3F3F', '#CC9F3F'] - const DASHES = ['1', '5 5', '10 10', '5 5 10 10'] - - const cell = index % (COLORS.length * DASHES.length) - const row = index % COLORS.length - const column = Math.floor(cell / COLORS.length) - - return { color: COLORS[row], dash: DASHES[column] } -} - -function getTickFormatters(yKeys: AxisSettings[]) { - let tickerFormat: TickerFormat | undefined = undefined - let tickerFormat2: TickerFormat | undefined = undefined - for (let yKey of yKeys) { - if (!yKey.secondAxis) { - if (!tickerFormat) tickerFormat = yKey.tickerFormat - if (tickerFormat !== yKey.tickerFormat) tickerFormat = 'default' - } else { - if (!tickerFormat2) tickerFormat2 = yKey.tickerFormat - if (tickerFormat2 !== yKey.tickerFormat) tickerFormat2 = 'default' - } - } - - const tickFormatter = getTickFormatter(tickerFormat || 'default') - const tickFormatter2 = getTickFormatter(tickerFormat2 || 'default') - return { tickFormatter, tickFormatter2 } -} - -function getTickFormatter(format: TickerFormat) { - if (format === 'percent') return (value: number) => `${value}%` - return undefined -} diff --git a/src/framework/visualisation/react/ui/elements/figures/recharts_graph.tsx b/src/framework/visualisation/react/ui/elements/figures/recharts_graph.tsx new file mode 100644 index 00000000..ad23dd3f --- /dev/null +++ b/src/framework/visualisation/react/ui/elements/figures/recharts_graph.tsx @@ -0,0 +1,177 @@ +import TextBundle from '../../../../../text_bundle' +import { VisualizationData, AxisSettings, TickerFormat } from '../../../../../types/visualizations' + +import { + ResponsiveContainer, + LineChart, + Line, + XAxis, + YAxis, + Tooltip, + Legend, + BarChart, + Bar, + AreaChart, + Area +} from 'recharts' + +interface Props { + visualizationData: VisualizationData +} + +export default function RechartsGraph({ visualizationData }: Props): JSX.Element | null { + function tooltip() { + return ( + + ) + } + + function axes(minTickGap: number) { + if (!visualizationData) return null + const secondary = + Object.values(visualizationData.yKeys).findIndex((yKey: AxisSettings) => yKey.secondAxis) !== + -1 + const { tickFormatter, tickFormatter2 } = getTickFormatters( + Object.values(visualizationData.yKeys) + ) + + return ( + <> + + + {secondary && } + + ) + } + + function legend() { + return ( + + ) + } + + let chart: JSX.Element | null = null + + if (visualizationData.type === 'line') { + chart = ( + + {axes(20)} + {tooltip()} + {legend()} + {Object.values(visualizationData.yKeys).map((yKey: AxisSettings, i: number) => { + const { color, dash } = getLineStyle(i) + return ( + + ) + })} + + ) + } + + if (visualizationData.type === 'bar') { + chart = ( + + {axes(0)} + {tooltip()} + {legend()} + {Object.values(visualizationData.yKeys).map((yKey: AxisSettings, i: number) => { + const { color, dash } = getLineStyle(i) + return ( + + ) + })} + + ) + } + + if (visualizationData.type === 'area') { + chart = ( + + {axes(20)} + {tooltip()} + {legend()} + {Object.values(visualizationData.yKeys).map((yKey: AxisSettings, i: number) => { + const { color, dash } = getLineStyle(i) + return ( + + ) + })} + + ) + } + + if (!chart) return null + return ( + + {chart} + + ) +} + +function getLineStyle(index: number): { color: string; dash: string } { + const COLORS = ['#4272EF', '#FF5E5E', '#FFCF60', '#1E3FCC', '#CC3F3F', '#CC9F3F'] + const DASHES = ['1', '5 5', '10 10', '5 5 10 10'] + + const cell = index % (COLORS.length * DASHES.length) + const row = index % COLORS.length + const column = Math.floor(cell / COLORS.length) + + return { color: COLORS[row], dash: DASHES[column] } +} + +function getTickFormatters(yKeys: AxisSettings[]) { + let tickerFormat: TickerFormat | undefined = undefined + let tickerFormat2: TickerFormat | undefined = undefined + for (let yKey of yKeys) { + if (!yKey.secondAxis) { + if (!tickerFormat) tickerFormat = yKey.tickerFormat + if (tickerFormat !== yKey.tickerFormat) tickerFormat = 'default' + } else { + if (!tickerFormat2) tickerFormat2 = yKey.tickerFormat + if (tickerFormat2 !== yKey.tickerFormat) tickerFormat2 = 'default' + } + } + + const tickFormatter = getTickFormatter(tickerFormat || 'default') + const tickFormatter2 = getTickFormatter(tickerFormat2 || 'default') + return { tickFormatter, tickFormatter2 } +} + +function getTickFormatter(format: TickerFormat) { + if (format === 'percent') return (value: number) => `${value}%` + return undefined +} diff --git a/src/framework/visualisation/react/ui/elements/figures/wordcloud.tsx b/src/framework/visualisation/react/ui/elements/figures/wordcloud.tsx new file mode 100644 index 00000000..5e5a0f63 --- /dev/null +++ b/src/framework/visualisation/react/ui/elements/figures/wordcloud.tsx @@ -0,0 +1,38 @@ +import ReactWordcloud from 'react-wordcloud' +import { VisualizationData } from '../../../../../types/visualizations' +import { useMemo, useState } from 'react' + +interface Props { + visualizationData: VisualizationData +} + +export default function Wordcloud({ visualizationData }: Props): JSX.Element | null { + const [options] = useState({ + rotations: 2, + rotationAngles: [0], + scale: 'sqrt', + fontSizes: [10, 50], + enableTooltip: true, + deterministic: true, + tooltipOptions: { + theme: 'wordcloud' + }, + colors: ['#4272EF', '#FFCF60', '#1E3FCC', '#CC9F3F'] + }) + + const words = useMemo(() => { + const words = [] + for (let row of visualizationData.data) { + const text = row[visualizationData?.xKey.label] as string + let value = 0 + for (let yKey of Object.values(visualizationData.yKeys)) { + value += Number(row[yKey.label]) || 0 + } + words.push({ text, value }) + } + + return words.sort((a, b) => b.value - a.value).slice(0, 50) + }, [visualizationData]) + + return +} diff --git a/src/framework/visualisation/react/ui/elements/table.tsx b/src/framework/visualisation/react/ui/elements/table.tsx index d1a35763..94e420ef 100644 --- a/src/framework/visualisation/react/ui/elements/table.tsx +++ b/src/framework/visualisation/react/ui/elements/table.tsx @@ -1,13 +1,23 @@ -import { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react' +import { + useEffect, + useLayoutEffect, + useMemo, + useRef, + useState, + ReactNode, + Dispatch, + SetStateAction +} from 'react' import Highlighter from 'react-highlight-words' import { PropsUITableCell, TableWithContext, PropsUITableRow } from '../../../../types/elements' import { CheckBox } from './check_box' -import { LabelButton } from './button' + import UndoSvg from '../../../../../assets/images/undo.svg' import DeleteSvg from '../../../../../assets/images/delete.svg' import { Pagination } from './pagination' import TextBundle from '../../../../text_bundle' import { Translator } from '../../../../translator' +import { useFloating, useFocus, useHover, useInteractions } from '@floating-ui/react' export interface Props { table: TableWithContext @@ -19,6 +29,13 @@ export interface Props { pageSize?: number } +interface Tooltip { + show: boolean + content: ReactNode + x: number + y: number +} + export const Table = ({ table, show, @@ -36,14 +53,29 @@ export const Table = ({ const selectedLabel = selected.size.toLocaleString(locale, { useGrouping: true }) const text = useMemo(() => getTranslations(locale), [locale]) - const cellClass = `min-w-[8rem] h-[3rem] px-3 flex items-center` - const valueClass = `line-clamp-2` + const [tooltip, setTooltip] = useState({ + show: false, + content: null, + x: 0, + y: 0 + }) + + const cellClass = ` h-[3rem] px-3 flex items-center font-table-row` useEffect(() => { setSelected(new Set()) setPage((page) => Math.max(0, Math.min(page, nPages - 1))) }, [table, nPages]) + useEffect(() => { + // rm tooltip on scroll + function rmTooltip() { + setTooltip((tooltip: Tooltip) => (tooltip.show ? { ...tooltip, show: false } : tooltip)) + } + window.addEventListener('scroll', rmTooltip) + return () => window.removeEventListener('scroll', rmTooltip) + }) + useLayoutEffect(() => { // set exact height of grid row for height transition if (!ref.current) return @@ -81,20 +113,32 @@ export const Table = ({ ) } - function renderCell(cell: PropsUITableCell, i: number) { + function renderRow(item: PropsUITableRow | null, i: number) { + if (!item) + return ( + + +
+ + + ) return ( - -
-
- -
-
- + + + toggleSelected(item.id)} + /> + + + {item.cells.map((cell, j) => ( + + + + ))} + ) } @@ -121,7 +165,7 @@ export const Table = ({ className={`grid grid-cols-1 transition-[grid,color] duration-500 relative overflow-hidden `} >
-
+
@@ -138,31 +182,7 @@ export const Table = ({ {columnNames.map(renderHeaderCell)} - - {items.map((item, i) => { - if (!item) - return ( - - - - ) - return ( - - - {item.cells.map(renderCell)} - - ) - })} - + {items.map(renderRow)}
-
-
- toggleSelected(item.id)} - /> -
@@ -186,10 +206,106 @@ export const Table = ({
+
+ {tooltip.content} +
+
+ ) +} + +function Cell({ + cell, + search, + cellClass, + setTooltip +}: { + cell: PropsUITableCell + search: string + cellClass: string + setTooltip: Dispatch> +}) { + const textRef = useRef(null) + const [overflows, setOverflows] = useState(false) + + useEffect(() => { + if (!textRef.current) return + setOverflows(textRef.current.scrollWidth > textRef.current.clientWidth) + }, [textRef]) + + function onSetTooltip() { + if (!textRef.current) return + if (!overflows) return + + const rect = textRef.current.getBoundingClientRect() + + const content = ( + + ) + + setTooltip({ + show: true, + content, + x: rect.x, + y: rect.y + }) + } + function onRmTooltip() { + setTooltip((tooltip: Tooltip) => (tooltip.show ? { ...tooltip, show: false } : tooltip)) + } + + return ( +
+
+ +
+ {overflows && }
) } +function TooltipIcon() { + return ( + + ) +} + function IconButton(props: { icon: string label: string diff --git a/src/framework/visualisation/react/ui/elements/table_container.tsx b/src/framework/visualisation/react/ui/elements/table_container.tsx index 07e63cdd..fcbffaf4 100644 --- a/src/framework/visualisation/react/ui/elements/table_container.tsx +++ b/src/framework/visualisation/react/ui/elements/table_container.tsx @@ -123,7 +123,7 @@ export const TableContainer = ({ return (