diff --git a/changelog.md b/changelog.md index 7113fe93..d893b72e 100644 --- a/changelog.md +++ b/changelog.md @@ -2,6 +2,12 @@ ## [Unreleased] - TBD +### Added + +- `DiamondDSIntegrations.mrtLoadingState(loading, data)` returns MRT's `showSkeletons`/`showProgressBars` (skeletons on first load, progress bar on refetch). +- `DiamondDSIntegrations.mrtRowIntent(intent)` returns row `sx` that tints an MRT row with the intent's subtle container colour. +- `DiamondDSIntegrations.skipWhileSkeleton(callback)` wraps an MRT row/cell/checkbox props callback so it is skipped while skeleton rows show. + ### Changed - _Dialog_ now defaults to elevation 16 (`surface.strong`'s tone) instead of MUI's 24, and temporary _Drawer_ to elevation 4 instead of 16, so floating surfaces order Drawer < Menu < Dialog and dialog text keeps more contrast. diff --git a/dev/src/App.tsx b/dev/src/App.tsx index 6d1d252b..62490668 100644 --- a/dev/src/App.tsx +++ b/dev/src/App.tsx @@ -38,7 +38,11 @@ import { MaterialReactTable, useMaterialReactTable, } from "material-react-table"; -import type { MRT_ColumnDef } from "material-react-table"; +import type { + MRT_ColumnDef, + MRT_Row, + MRT_TableInstance, +} from "material-react-table"; import { ThemeProvider, @@ -100,7 +104,33 @@ export const SimpleMuiTableExample = () => { {data.map((row) => ( - + + (theme.vars || theme).palette.warning.container, + }, + "&.Mui-selected td": { + backgroundImage: + "linear-gradient(rgb(var(--ds-primary-channel) / 0.08), rgb(var(--ds-primary-channel) / 0.08))", + }, + "&:hover td": { + backgroundImage: + "linear-gradient(var(--ds-overlay-hover), var(--ds-overlay-hover))", + }, + "&.Mui-selected:hover td": { + backgroundImage: + "linear-gradient(rgb(var(--ds-primary-channel) / 0.12), rgb(var(--ds-primary-channel) / 0.12))", + }, + } + : undefined + } + > { /* MATERIAL REACT TABLE - mid-level (per-table) overrides */ export const MidLevelMrtExample = () => { + const mrtOptions = DiamondDSIntegrations.mrtOptions(); const table = useMaterialReactTable({ columns: mrtColumns, data, @@ -144,7 +175,23 @@ export const MidLevelMrtExample = () => { getRowId: (row) => row.id, initialState: { rowSelection: { "EXP-1003": true } }, mrtTheme: DiamondDSIntegrations.mrtTheme, - ...DiamondDSIntegrations.mrtOptions(), + ...mrtOptions, + // Tinted rows use the subtle container role so they follow dark mode. + muiTableBodyRowProps: DiamondDSIntegrations.skipWhileSkeleton( + ({ + row, + }: { + row: MRT_Row; + table: MRT_TableInstance; + }) => ({ + ...mrtOptions.muiTableBodyRowProps, + sx: { + ...mrtOptions.muiTableBodyRowProps.sx, + ...(row.original.status === "Paused" && + DiamondDSIntegrations.mrtRowIntent("warning")), + }, + }), + ), }); return ; @@ -196,7 +243,7 @@ const data: Experiment[] = [ id: "EXP-1002", scientist: "Alan Turing", beamline: "I03", - status: "Queued", + status: "Paused", energy: 9.8, }, { @@ -217,7 +264,7 @@ const data: Experiment[] = [ id: "EXP-1005", scientist: "Katherine Johnson", beamline: "DIAD", - status: "Paused", + status: "Queued", energy: 15.7, }, ]; diff --git a/src/components/MUI/DataDisplay/Table-1.TableGuidance.mdx b/src/components/MUI/DataDisplay/Table-1.TableGuidance.mdx index ede6e74c..d4082c50 100644 --- a/src/components/MUI/DataDisplay/Table-1.TableGuidance.mdx +++ b/src/components/MUI/DataDisplay/Table-1.TableGuidance.mdx @@ -146,6 +146,47 @@ import * as TableStories from "./Table.stories"; +## Row emphasis + +

+ To tint a row by intent (for example a warning), use the intent's subtle{" "} + container colour, never the solid one, so the row stays + readable and follows dark mode. Do not hardcode amber or red values. + Use it sparingly, for warning or error rows only, and always pair it with a + status chip or icon: a tint must never be the only signal. +

+ +
+  {`
+        (theme.vars || theme).palette.warning.container,
+    },
+    // Cell backgrounds hide the row's hover and selected states: layer them back.
+    "&.Mui-selected td, &.Mui-selected th": {
+      backgroundImage:
+        "linear-gradient(rgb(var(--ds-primary-channel) / 0.08), rgb(var(--ds-primary-channel) / 0.08))",
+    },
+    "&:hover td, &:hover th": {
+      backgroundImage:
+        "linear-gradient(var(--ds-overlay-hover), var(--ds-overlay-hover))",
+    },
+    "&.Mui-selected:hover td, &.Mui-selected:hover th": {
+      backgroundImage:
+        "linear-gradient(rgb(var(--ds-primary-channel) / 0.12), rgb(var(--ds-primary-channel) / 0.12))",
+    },
+  }}
+/>`}
+
+ + + +

+ In Material React Table, use mrtRowIntent() instead; see the + Material React Table guidance. +

+ ## Selection guidance

diff --git a/src/components/MUI/DataDisplay/Table-2.MaterialReactTableGuidance.mdx b/src/components/MUI/DataDisplay/Table-2.MaterialReactTableGuidance.mdx index 8d187b7b..0a906b5a 100644 --- a/src/components/MUI/DataDisplay/Table-2.MaterialReactTableGuidance.mdx +++ b/src/components/MUI/DataDisplay/Table-2.MaterialReactTableGuidance.mdx @@ -348,6 +348,62 @@ enableHiding: true`} });`} +## Loading + +

+ Show one loading indicator at a time. Skeleton rows on first load, when there + is no data yet. A progress bar when refetching, so existing rows stay visible. + Do not show both, and do not use MRT's spinner overlay. +

+ +
+  {`const table = useMaterialReactTable({
+  columns,
+  data,
+  // Use these instead of isLoading, which adds a spinner overlay on top of the skeletons.
+  state: DiamondDSIntegrations.mrtLoadingState(loading, data),
+});`}
+
+ +

+ Row and checkbox style callbacks run on skeleton placeholder data. Wrap them + in DiamondDSIntegrations.skipWhileSkeleton() so they are + skipped while skeletons show. +

+ +## Row emphasis + +

+ To tint a row by intent (for example a warning), use{" "} + DiamondDSIntegrations.mrtRowIntent(). It applies the intent's + subtle container colour, never the solid one, so rows stay readable and follow + dark mode. Use it sparingly, for warning or error rows only, and always pair + it with a status chip or icon: a tint must never be the only signal. Spread it + after mrtOptions() row props, so it + replaces MRT's own hover and selected overlays. Wrap the callback in{" "} + skipWhileSkeleton() so skeleton rows are not tinted. +

+ +
+  {`const mrtOptions = DiamondDSIntegrations.mrtOptions();
+
+const table = useMaterialReactTable({
+  columns,
+  data,
+  mrtTheme: DiamondDSIntegrations.mrtTheme,
+  ...mrtOptions,
+  // Spread mrtOptions' row props so hover and selected styles are kept.
+  muiTableBodyRowProps: DiamondDSIntegrations.skipWhileSkeleton(({ row }) => ({
+    ...mrtOptions.muiTableBodyRowProps,
+    sx: {
+      ...mrtOptions.muiTableBodyRowProps.sx,
+      ...(row.original.status === "Paused" &&
+        DiamondDSIntegrations.mrtRowIntent("warning")),
+    },
+  })),
+});`}
+
+ ## Example configuration
diff --git a/src/components/MUI/DataDisplay/Table.stories.tsx b/src/components/MUI/DataDisplay/Table.stories.tsx
index 7a9555df..d562ecc1 100644
--- a/src/components/MUI/DataDisplay/Table.stories.tsx
+++ b/src/components/MUI/DataDisplay/Table.stories.tsx
@@ -277,6 +277,73 @@ export const HoverRows: Story = {
   ),
 };
 
+export const WarningRow: Story = {
+  parameters: {
+    docs: {
+      description: {
+        story:
+          "The warning tint reinforces the status chip. It never replaces it.",
+      },
+    },
+  },
+  render: (args) => (
+    
+      
+        
+          
+            Task
+            Step
+            Status
+          
+        
+
+        
+          {tasks.map((task) => {
+            const paused = task.id === "TASK-3";
+
+            return (
+              
+                            (theme.vars || theme).palette.warning.container,
+                        },
+                        "&:hover td": {
+                          backgroundImage:
+                            "linear-gradient(var(--ds-overlay-hover), var(--ds-overlay-hover))",
+                        },
+                      }
+                    : undefined
+                }
+              >
+                {task.id}
+                {task.name}
+
+                
+                  {paused ? (
+                    
+                  ) : (
+                    renderStatusChip(task.status)
+                  )}
+                
+              
+            );
+          })}
+        
+      
+
+ ), +}; + export const SelectedRows: Story = { render: (args) => ( diff --git a/src/themes/DiamondDSIntegrations.test.ts b/src/themes/DiamondDSIntegrations.test.ts index 2e624476..7aa54806 100644 --- a/src/themes/DiamondDSIntegrations.test.ts +++ b/src/themes/DiamondDSIntegrations.test.ts @@ -51,3 +51,62 @@ describe("DiamondDSIntegrations.mrtOptions", () => { ); }); }); + +describe("DiamondDSIntegrations.mrtLoadingState", () => { + it("shows skeletons without data and a progress bar with data", () => { + const { mrtLoadingState } = DiamondDSIntegrations; + + expect(mrtLoadingState(true, undefined)).toEqual({ + showSkeletons: true, + showProgressBars: false, + }); + expect(mrtLoadingState(true, [])).toEqual({ + showSkeletons: true, + showProgressBars: false, + }); + expect(mrtLoadingState(true, [1])).toEqual({ + showSkeletons: false, + showProgressBars: true, + }); + expect(mrtLoadingState(false, [])).toEqual({ + showSkeletons: false, + showProgressBars: false, + }); + }); +}); + +describe("DiamondDSIntegrations.skipWhileSkeleton", () => { + it("returns no props while skeletons show", () => { + const wrapped = DiamondDSIntegrations.skipWhileSkeleton(() => ({ a: 1 })); + const table = (showSkeletons: boolean) => ({ + getState: () => ({ showSkeletons }), + }); + + expect(wrapped({ table: table(true) })).toEqual({}); + expect(wrapped({ table: table(false) })).toEqual({ a: 1 }); + expect( + wrapped({ table: { getState: () => ({ isLoading: true }) } }), + ).toEqual({}); + }); +}); + +describe("DiamondDSIntegrations.mrtRowIntent", () => { + it("tints rows with the intent's container token, mapping error to danger", () => { + const { mrtRowIntent } = DiamondDSIntegrations; + + expect(mrtRowIntent("warning")["& td"].backgroundColor).toBe( + "var(--ds-warning-container)", + ); + expect( + mrtRowIntent("warning")["&.Mui-selected td"].backgroundImage, + ).toContain("--ds-primary-channel"); + const merged = { + ...DiamondDSIntegrations.mrtOptions().muiTableBodyRowProps.sx, + ...mrtRowIntent("warning"), + }; + expect(merged["&:hover td:after"]).toEqual({ display: "none" }); + expect(mrtRowIntent("error")["& td"].backgroundColor).toBe( + "var(--ds-danger-container)", + ); + }); +}); diff --git a/src/themes/DiamondDSIntegrations.ts b/src/themes/DiamondDSIntegrations.ts index 637f1880..a648e3fc 100644 --- a/src/themes/DiamondDSIntegrations.ts +++ b/src/themes/DiamondDSIntegrations.ts @@ -1,3 +1,5 @@ +const overlay = (colour: string) => `linear-gradient(${colour}, ${colour})`; + export const DiamondDSIntegrations = { /* Material React Table (MRT) */ @@ -15,6 +17,56 @@ export const DiamondDSIntegrations = { selectedRowBackgroundColor: "rgb(var(--ds-primary-channel) / 0.08)", }, + // Skeleton rows on first load, a progress bar when refetching existing rows. + // An empty array counts as no data yet. + mrtLoadingState(loading: boolean, data?: readonly unknown[]) { + const hasData = !!data?.length; + return { + showSkeletons: loading && !hasData, + showProgressBars: loading && hasData, + }; + }, + + // Wraps a row/cell/checkbox props callback so it returns nothing while MRT + // is showing skeleton rows, including via isLoading (their placeholder data + // would otherwise be styled). + skipWhileSkeleton( + callback: (args: Args) => Result, + ) { + return (args: Args): Result | Record => { + const { showSkeletons, isLoading } = args.table.getState() as { + showSkeletons?: boolean; + isLoading?: boolean; + }; + return showSkeletons || isLoading ? {} : callback(args); + }; + }, + + // Row sx that tints a row with an intent's subtle container colour (never the + // solid one), so it follows dark mode. Spread it into a row's sx after + // mrtOptions()' row sx, so it overrides MRT's own overlay keys. + mrtRowIntent( + intent: "primary" | "secondary" | "info" | "success" | "warning" | "error", + ) { + const token = intent === "error" ? "danger" : intent; + return { + "& td": { backgroundColor: `var(--ds-${token}-container)` }, + // MRT's own hover/selected overlays (td:after) would stack with the layers + // below, so hide them on tinted rows and draw the states over the tint, + // as the plain table does. + "& td:after": { display: "none" }, + "&:hover td:after": { display: "none" }, + "&.Mui-selected:hover td:after": { display: "none" }, + "&.Mui-selected td": { + backgroundImage: overlay("rgb(var(--ds-primary-channel) / 0.08)"), + }, + "&:hover td": { backgroundImage: overlay("var(--ds-overlay-hover)") }, + "&.Mui-selected:hover td": { + backgroundImage: overlay("rgb(var(--ds-primary-channel) / 0.12)"), + }, + }; + }, + // fullWidth drops the border/radius for tables with no surrounding container. mrtOptions(options?: { fullWidth?: boolean }) { const headCellBackgroundColor = "var(--ds-surface-container)";