diff --git a/client/src/webview/ColumnHeader.tsx b/client/src/webview/ColumnHeader.tsx index d29aa1573..b0c298cf3 100644 --- a/client/src/webview/ColumnHeader.tsx +++ b/client/src/webview/ColumnHeader.tsx @@ -4,6 +4,8 @@ import { useRef } from "react"; import { AgColumn, GridApi } from "ag-grid-community"; +import useTheme from "./useTheme"; + const getIconForColumnType = (type: string) => { switch (type.toLocaleLowerCase()) { case "float": @@ -29,16 +31,15 @@ const ColumnHeader = ({ column, currentColumn: getCurrentColumn, columnType, - theme, displayMenuForColumn, }: { api: GridApi; column: AgColumn; currentColumn: () => AgColumn | undefined; columnType: string; - theme: string; displayMenuForColumn: (api: GridApi, column: AgColumn, rect: DOMRect) => void; }) => { + const theme = useTheme(); const ref = useRef(undefined!); const currentColumn = getCurrentColumn(); const currentSortedColumns = api.getColumnState().filter((c) => c.sort); diff --git a/client/src/webview/ColumnMenu.tsx b/client/src/webview/ColumnMenu.tsx index 366854714..27178c06f 100644 --- a/client/src/webview/ColumnMenu.tsx +++ b/client/src/webview/ColumnMenu.tsx @@ -5,6 +5,7 @@ import { AgColumn, ColumnState, GridApi } from "ag-grid-community"; import GridMenu from "./GridMenu"; import localize from "./localize"; import { storeViewProperties } from "./useDataViewer"; +import useTheme from "./useTheme"; export interface ColumnMenuProps { column: AgColumn; @@ -15,7 +16,6 @@ export interface ColumnMenuProps { removeAllSorting: () => void; removeFromSort: () => void; sortColumn: (direction: "asc" | "desc") => void; - theme: string; top: number; } @@ -27,7 +27,6 @@ const applyColumnState = (api: GridApi, state: ColumnState[]) => { export const getColumnMenu = ( api: GridApi, - theme: ColumnMenuProps["theme"], column: AgColumn, { height, top, left }: DOMRect, dismissMenu: () => void, @@ -37,7 +36,6 @@ export const getColumnMenu = ( dismissMenu, hasSort: api.getColumnState().some((c) => c.sort), left, - theme, top: top + height, sortColumn: (direction: "asc" | "desc" | null) => { const newColumnState = api.getColumnState().filter((c) => c.sort); @@ -85,9 +83,9 @@ const ColumnMenu = ({ removeAllSorting, removeFromSort, sortColumn, - theme, top, }: ColumnMenuProps) => { + const theme = useTheme(); const menuItems = [ { name: localize("Sort"), diff --git a/client/src/webview/DataViewer.tsx b/client/src/webview/DataViewer.tsx index 6acfc1f5a..579c8b8a5 100644 --- a/client/src/webview/DataViewer.tsx +++ b/client/src/webview/DataViewer.tsx @@ -1,6 +1,6 @@ // Copyright © 2023, SAS Institute Inc., Cary, NC, USA. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { useCallback, useEffect, useMemo } from "react"; +import { useCallback, useEffect } from "react"; import { createRoot } from "react-dom/client"; import { AgGridReact } from "ag-grid-react"; @@ -8,6 +8,7 @@ import { AgGridReact } from "ag-grid-react"; import "."; import ColumnMenu from "./ColumnMenu"; import useDataViewer from "./useDataViewer"; +import useTheme from "./useTheme"; import "./DataViewer.css"; import "ag-grid-community/styles/ag-grid.css"; @@ -21,22 +22,8 @@ const gridStyles = { }; const DataViewer = () => { - const theme = useMemo(() => { - const themeKind = document - .querySelector("[data-vscode-theme-kind]") - .getAttribute("data-vscode-theme-kind"); - - switch (themeKind) { - case "vscode-high-contrast-light": - case "vscode-light": - return "ag-theme-alpine"; - case "vscode-high-contrast": - case "vscode-dark": - return "ag-theme-alpine-dark"; - } - }, []); - const { columns, onGridReady, columnMenu, dismissMenu } = - useDataViewer(theme); + const theme = useTheme(); + const { columns, onGridReady, columnMenu, dismissMenu } = useDataViewer(); const handleKeydown = useCallback( (event) => { diff --git a/client/src/webview/useDataViewer.ts b/client/src/webview/useDataViewer.ts index 3f4c15fad..8536a35b5 100644 --- a/client/src/webview/useDataViewer.ts +++ b/client/src/webview/useDataViewer.ts @@ -114,7 +114,7 @@ export const storeViewProperties = (viewProperties: ViewProperties) => data: { viewProperties }, }); -const useDataViewer = (theme: string) => { +const useDataViewer = () => { const [columns, setColumns] = useState([]); const [columnMenu, setColumnMenu] = useState(); const columnMenuRef = useRef(columnMenu); @@ -168,7 +168,6 @@ const useDataViewer = (theme: string) => { setColumnMenu( getColumnMenu( api, - theme, column, rect, () => setColumnMenu(undefined), @@ -181,7 +180,7 @@ const useDataViewer = (theme: string) => { ), ); }, - [theme], + [], ); useEffect(() => { @@ -201,7 +200,6 @@ const useDataViewer = (theme: string) => { columnType: column.type, currentColumn: () => columnMenuRef.current?.column, displayMenuForColumn, - theme, }, ...getColumnState(column.name), suppressHeaderKeyboardEvent: ( @@ -247,7 +245,7 @@ const useDataViewer = (theme: string) => { setColumns(columns); }); - }, [columns.length, theme, displayMenuForColumn]); + }, [columns.length, displayMenuForColumn]); useEffect(() => { window.addEventListener("contextmenu", contextMenuHandler, true); diff --git a/client/src/webview/useTheme.ts b/client/src/webview/useTheme.ts new file mode 100644 index 000000000..cf78ce3d2 --- /dev/null +++ b/client/src/webview/useTheme.ts @@ -0,0 +1,47 @@ +// Copyright © 2025, SAS Institute Inc., Cary, NC, USA. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import { useEffect, useMemo, useState } from "react"; + +const THEME_ATTRIBUTE = "data-vscode-theme-kind"; +const SELECTOR = `[${THEME_ATTRIBUTE}]`; + +/** + * This listens for changes to vscode's theme kind and updates our internal + * theme to match. + * @returns theme:string matching the ag grid theme for the vscode theme kind + */ +const useTheme = () => { + const [themeKind, setThemeKind] = useState( + document.querySelector(SELECTOR).getAttribute(THEME_ATTRIBUTE), + ); + useEffect(() => { + const obs = new MutationObserver((record) => + setThemeKind( + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions + (record[0].target as HTMLElement).getAttribute(THEME_ATTRIBUTE), + ), + ); + obs.observe(document.querySelector(SELECTOR), { + attributes: true, + attributeFilter: [THEME_ATTRIBUTE], + }); + return () => { + obs.disconnect(); + }; + }, []); + + const theme = useMemo(() => { + switch (themeKind) { + case "vscode-high-contrast-light": + case "vscode-light": + return "ag-theme-alpine"; + case "vscode-high-contrast": + case "vscode-dark": + return "ag-theme-alpine-dark"; + } + }, [themeKind]); + + return theme; +}; + +export default useTheme;