diff --git a/.changeset/lazy-items-impress.md b/.changeset/lazy-items-impress.md new file mode 100644 index 0000000000..6652544c05 --- /dev/null +++ b/.changeset/lazy-items-impress.md @@ -0,0 +1,14 @@ +--- +"@comet/admin": minor +--- + +Add `EllipsisTooltip` component + +Used to automatically add a tooltip to text that is too long to fit in its container. +This is useful for displaying text in a table or data grid when the text might be too long to fit in the column. + +```tsx + + {textThatMightBeVeryLong} + +``` diff --git a/packages/admin/admin-stories/src/admin/helpers/EllipsisTooltip.tsx b/packages/admin/admin-stories/src/admin/helpers/EllipsisTooltip.tsx new file mode 100644 index 0000000000..12ad739b76 --- /dev/null +++ b/packages/admin/admin-stories/src/admin/helpers/EllipsisTooltip.tsx @@ -0,0 +1,166 @@ +import { EllipsisTooltip } from "@comet/admin/src"; +import { Minus, Plus } from "@comet/admin-icons"; +import { Button, Stack, Table, TableBody, TableCell, TableHead, TableRow, Typography } from "@mui/material"; +import { Box } from "@mui/system"; +import { DataGrid, GridColDef } from "@mui/x-data-grid"; +import { storiesOf } from "@storybook/react"; +import * as React from "react"; + +import { storyRouterDecorator } from "../../story-router.decorator"; + +const words = ["Cursus", "Ridiculus", "Pharetra", "Ligula", "Sem", "Nullam", "Viverra", "Vestibulum", "Vestibulum", "Vestibulum"]; + +const getRandomWord = () => { + return words[Math.floor(Math.random() * words.length)]; +}; + +const getSomeWords = (numberOfWords: number) => { + return words.slice(0, numberOfWords).join(" "); +}; + +const DEBUG_SHOW_SINGLE_ROW_TABLE = true; +const DEBUG_SHOW_TABLE = true; +const DEBUG_SHOW_DATA_GRID = true; + +export function Story() { + const [singleRowWords, setSingleRowWords] = React.useState(["Lorem", "Ipsum"]); + const [dataGridRowWords, setDataGridRowWords] = React.useState(["Lorem", "Ipsum"]); + + const gridRows = Array.from({ length: 5 }).map((_, index) => ({ + id: index, + firstName: getSomeWords(1 * (index + 1)), + lastName: index === 0 ? dataGridRowWords.join(" ") : getSomeWords(3 * (index + 1)), + })); + + return ( + + {DEBUG_SHOW_SINGLE_ROW_TABLE && ( +
+ + Table (Single Row) + + + + + + {getSomeWords(3)} + + + {singleRowWords.join(" ")} + + + {getSomeWords(10)}. + + + +
+ + Update number of words in second column: + + + +
+ )} + {DEBUG_SHOW_TABLE && ( +
+ + Table + + + + + Col 1 + Col 2 + Col 3 + + + + {Array.from({ length: 5 }).map((_, index) => ( + + + {getSomeWords(1 * (index + 1))} + + + {getSomeWords(2 * (index + 1))} + + + {getSomeWords(3 * (index + 1))} + + + ))} + +
+
+ )} + {DEBUG_SHOW_DATA_GRID && ( + + + Data-Grid + + + + Update number of words in second column of first row: + + + + + )} +
+ ); +} + +const gridColumns: GridColDef[] = [ + { + field: "firstName", + headerName: "First name", + resizable: true, + width: 250, + renderCell: ({ row }) => { + return {row.firstName}; + }, + }, + { + field: "lastName", + headerName: "Last name", + flex: 1, + renderCell: ({ row }) => { + return {row.lastName}; + }, + }, +]; + +storiesOf("@comet/admin/helpers", module) + .addDecorator(storyRouterDecorator()) + .add("Ellipsis Tooltip", () => ); diff --git a/packages/admin/admin-stories/src/docs/components/EllipsisTooltip/DataGrid.stories.tsx b/packages/admin/admin-stories/src/docs/components/EllipsisTooltip/DataGrid.stories.tsx new file mode 100644 index 0000000000..c6245d57ed --- /dev/null +++ b/packages/admin/admin-stories/src/docs/components/EllipsisTooltip/DataGrid.stories.tsx @@ -0,0 +1,40 @@ +import { EllipsisTooltip } from "@comet/admin"; +import { DataGrid, GridColDef } from "@mui/x-data-grid"; +import { storiesOf } from "@storybook/react"; +import * as React from "react"; + +storiesOf("stories/components/EllipsisTooltip", module).add("DataGrid", () => { + const words = ["Cursus", "Ridiculus", "Pharetra", "Ligula", "Sem", "Nullam", "Viverra", "Vestibulum", "Vestibulum", "Vestibulum"]; + + const getSomeWords = (numberOfWords: number) => { + return [...words, ...words, ...words].slice(0, numberOfWords).join(" "); + }; + + const gridRows = Array.from({ length: 5 }).map((_, index) => ({ + id: index, + firstName: getSomeWords(1 * (index + 2)), + lastName: getSomeWords(3 * (index + 2)), + })); + + const gridColumns: GridColDef[] = [ + { + field: "firstName", + headerName: "First name", + resizable: true, + width: 250, + renderCell: ({ row }) => { + return {row.firstName}; + }, + }, + { + field: "lastName", + headerName: "Last name", + flex: 1, + renderCell: ({ row }) => { + return {row.lastName}; + }, + }, + ]; + + return ; +}); diff --git a/packages/admin/admin-stories/src/docs/components/EllipsisTooltip/EllipsisTooltip.stories.mdx b/packages/admin/admin-stories/src/docs/components/EllipsisTooltip/EllipsisTooltip.stories.mdx new file mode 100644 index 0000000000..eea450874a --- /dev/null +++ b/packages/admin/admin-stories/src/docs/components/EllipsisTooltip/EllipsisTooltip.stories.mdx @@ -0,0 +1,44 @@ +import { Meta, Story, Canvas } from "@storybook/addon-docs"; + + + +# EllipsisTooltip + +`EllipsisTooltip` is used to automatically add a tooltip to text that is too long to fit in its container.
+This is useful for displaying text in a table or data grid when the text might be too long to fit in the column. + +## Simple Example + + + + + +## Noteworthy + +Some noteworthy things to keep in mind when using `EllipsisTooltip`. + +### Usage inside a table + +When used inside a table (standard HTML table or MuiTable), the width of the individual cells must be limited.
+This can be done by setting a fixed width on the cell or setting `table-layout: fixed` on the table. + +### Usage with text styling + +When used in combination with text styling, e.g., `MuiTypography` the `EllipsisTooltip` must be contained by the element that styles the text.
+This will make sure only the normal text is styled, and the text inside the tooltip looks the same as in any other tooltip. + + + + + +## Example with `MuiTable` + + + + + +## Example with `MuiDataGrid` + + + + diff --git a/packages/admin/admin-stories/src/docs/components/EllipsisTooltip/Simple.stories.tsx b/packages/admin/admin-stories/src/docs/components/EllipsisTooltip/Simple.stories.tsx new file mode 100644 index 0000000000..5827197b3f --- /dev/null +++ b/packages/admin/admin-stories/src/docs/components/EllipsisTooltip/Simple.stories.tsx @@ -0,0 +1,23 @@ +import { EllipsisTooltip } from "@comet/admin"; +import { Paper, Stack, Typography } from "@mui/material"; +import { storiesOf } from "@storybook/react"; +import * as React from "react"; + +storiesOf("stories/components/EllipsisTooltip", module).add("Simple", () => { + return ( + + + + Short Text + + + + + + Really long text that requires the tooltip to show the entire text that should be shown in this element. + + + + + ); +}); diff --git a/packages/admin/admin-stories/src/docs/components/EllipsisTooltip/Table.stories.tsx b/packages/admin/admin-stories/src/docs/components/EllipsisTooltip/Table.stories.tsx new file mode 100644 index 0000000000..cc92e18f19 --- /dev/null +++ b/packages/admin/admin-stories/src/docs/components/EllipsisTooltip/Table.stories.tsx @@ -0,0 +1,39 @@ +import { EllipsisTooltip } from "@comet/admin"; +import { Table, TableBody, TableCell, TableHead, TableRow } from "@mui/material"; +import { storiesOf } from "@storybook/react"; +import * as React from "react"; + +storiesOf("stories/components/EllipsisTooltip", module).add("Table", () => { + const words = ["Cursus", "Ridiculus", "Pharetra", "Ligula", "Sem", "Nullam", "Viverra", "Vestibulum", "Vestibulum", "Vestibulum"]; + + const getWordsForCell = (cellNumber: number, rowIndex: number) => { + return words.slice(0, cellNumber * (rowIndex + 1)).join(" "); + }; + + return ( + + + + Column 1 + Column 2 + Column 3 + + + + {Array.from({ length: 5 }).map((_, rowIndex) => ( + + + {getWordsForCell(1, rowIndex)} + + + {getWordsForCell(2, rowIndex)} + + + {getWordsForCell(3, rowIndex)}. + + + ))} + +
+ ); +}); diff --git a/packages/admin/admin-stories/src/docs/components/EllipsisTooltip/TextStyling.stories.tsx b/packages/admin/admin-stories/src/docs/components/EllipsisTooltip/TextStyling.stories.tsx new file mode 100644 index 0000000000..ec357abf69 --- /dev/null +++ b/packages/admin/admin-stories/src/docs/components/EllipsisTooltip/TextStyling.stories.tsx @@ -0,0 +1,49 @@ +import { EllipsisTooltip } from "@comet/admin"; +import { CheckmarkCircle, CrossCircle } from "@comet/admin-icons"; +import { Paper, Stack, Typography } from "@mui/material"; +import { storiesOf } from "@storybook/react"; +import * as React from "react"; + +storiesOf("stories/components/EllipsisTooltip", module).add("TextStyling", () => { + return ( + + + + + Correct usage + + + + Lorem ipsum integer posuere erat a ante venenatis dapibus posuere velit aliquet. + + + + + + + Breaks tooltip styling + + + + + Lorem ipsum integer posuere erat a ante venenatis dapibus posuere velit aliquet. + + + + + + + + Prevents tooltip from rendering + + + + + Lorem ipsum integer posuere erat a ante venenatis dapibus posuere velit aliquet. + + + + + + ); +}); diff --git a/packages/admin/admin/src/EllipsisTooltip.tsx b/packages/admin/admin/src/EllipsisTooltip.tsx new file mode 100644 index 0000000000..d2aba35cdd --- /dev/null +++ b/packages/admin/admin/src/EllipsisTooltip.tsx @@ -0,0 +1,108 @@ +import { ComponentsOverrides, css, styled, Theme, useThemeProps } from "@mui/material/styles"; +import React from "react"; + +import { Tooltip as CometTooltip } from "./common/Tooltip"; +import { ThemedComponentBaseProps } from "./helpers/ThemedComponentBaseProps"; + +export type EllipsisTooltipClassKey = "content" | "tooltip"; + +export interface EllipsisTooltipProps + extends ThemedComponentBaseProps<{ + content: "div"; + tooltip: typeof CometTooltip; + }> { + children?: React.ReactNode; +} + +const Content = styled("div", { + name: "CometAdminEllipsisTooltip", + slot: "content", + overridesResolver(_, styles) { + return [styles.content]; + }, +})(css` + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +`); + +const Tooltip = styled(CometTooltip, { + name: "CometAdminEllipsisTooltip", + slot: "tooltip", + overridesResolver(_, styles) { + return [styles.tooltip]; + }, +})(); + +export const EllipsisTooltip = (inProps: EllipsisTooltipProps) => { + const { children, slotProps, ...restProps } = useThemeProps({ props: inProps, name: "CometAdminEllipsisTooltip" }); + const contentRef = React.useRef(null); + const innerContentRef = React.useRef(null); + + const [renderWithTooltip, setRenderWithTooltip] = React.useState(false); + + const updateRenderWithTooltip = React.useCallback(() => { + if (contentRef.current && innerContentRef.current) { + setRenderWithTooltip(innerContentRef.current.offsetWidth > contentRef.current.clientWidth); + } + // The dependency array items must be `.current`, otherwise the callback will not be called, when the html-element is rendered. + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [contentRef.current, innerContentRef.current]); + + React.useEffect(() => { + const contentElement = contentRef.current; + + const resizeObserver = new ResizeObserver(updateRenderWithTooltip); + const mutationObserver = new MutationObserver(updateRenderWithTooltip); + + if (contentElement) { + resizeObserver.observe(contentElement); + mutationObserver.observe(contentElement, { + characterData: true, + childList: true, + subtree: true, + }); + } + + return () => { + if (contentElement) { + resizeObserver.unobserve(contentElement); + } + + mutationObserver.disconnect(); + }; + }, [contentRef, innerContentRef, updateRenderWithTooltip]); + + const content = ( + + {children} + + ); + + if (renderWithTooltip) { + return ( + + {content} + + ); + } + + return <>{content}; +}; + +declare module "@mui/material/styles" { + interface ComponentsPropsList { + CometAdminEllipsisTooltip: EllipsisTooltipProps; + } + + interface ComponentNameToClassKey { + CometAdminEllipsisTooltip: EllipsisTooltipClassKey; + } + + interface Components { + CometAdminEllipsisTooltip?: { + defaultProps?: Partial; + styleOverrides?: ComponentsOverrides["CometAdminEllipsisTooltip"]; + }; + } +} diff --git a/packages/admin/admin/src/index.ts b/packages/admin/admin/src/index.ts index adaffd7cd7..e8ed5dfc72 100644 --- a/packages/admin/admin/src/index.ts +++ b/packages/admin/admin/src/index.ts @@ -55,6 +55,7 @@ export { usePersistentColumnState } from "./dataGrid/usePersistentColumnState"; export { DeleteMutation } from "./DeleteMutation"; export { EditDialog, useEditDialog } from "./EditDialog"; export { EditDialogApiContext, IEditDialogApi, useEditDialogApi } from "./EditDialogApiContext"; +export { EllipsisTooltip, EllipsisTooltipClassKey, EllipsisTooltipProps } from "./EllipsisTooltip"; export { ErrorBoundary, ErrorBoundaryClassKey, ErrorBoundaryProps } from "./error/errorboundary/ErrorBoundary"; export { RouteWithErrorBoundary } from "./error/errorboundary/RouteWithErrorBoundary"; export { createErrorDialogApolloLink } from "./error/errordialog/createErrorDialogApolloLink";