From a4013c0b83347197633a008b2b56006c8da12a46 Mon Sep 17 00:00:00 2001
From: Brian Ingles
Date: Tue, 24 Oct 2023 09:18:15 -0500
Subject: [PATCH] feat: Theming - Spectrum Provider (#1582)
In this PR:
- First pass of mapping Spectrum dark theme variables to DH theme
variables
- Added Spectrum components that are being used by DH UI to the
styleguide
- Added some navigation to styleguide
- Added a Playwright config for Firefox to fix an issue where no
pointers were detected in `'(any-pointer: fine)'` media queries
resolves #1543
---
.gitignore | 1 +
__mocks__/css/mock-theme-dark-components.js | 1 +
__mocks__/css/mock-theme-dark-palette.js | 1 +
.../css/mock-theme-dark-semantic-editor.js | 1 +
.../css/mock-theme-dark-semantic-grid.js | 1 +
__mocks__/css/mock-theme-dark-semantic.js | 1 +
__mocks__/css/mock-theme-light-palette.js | 1 +
.../css/mock-theme-spectrum-alias.module.js | 3 +
.../mock-theme-spectrum-overrides.module.js | 3 +
.../css/mock-theme-spectrum-palette.module.js | 3 +
__mocks__/spectrumThemeDarkMock.js | 3 -
__mocks__/spectrumThemeLightMock.js | 3 -
jest.config.base.cjs | 4 +-
package-lock.json | 2 +-
package.json | 2 +-
.../src/components/AppBootstrap.test.tsx | 26 +-
.../src/styleguide/GotoTopButton.css | 19 ++
.../src/styleguide/GotoTopButton.tsx | 48 ++++
packages/code-studio/src/styleguide/Icons.tsx | 5 +-
.../src/styleguide/SamplesMenu.tsx | 125 ++++++++++
.../src/styleguide/SpectrumComponents.tsx | 235 ++++++++++++++++++
.../src/styleguide/StyleGuide.scss | 5 -
.../src/styleguide/StyleGuide.test.tsx | 8 +-
.../code-studio/src/styleguide/StyleGuide.tsx | 50 ++--
.../src/styleguide/ThemeColors.tsx | 11 +-
.../code-studio/src/styleguide/Typography.tsx | 22 +-
.../code-studio/src/styleguide/constants.ts | 3 +
.../src/SpectrumThemeDark.module.scss | 12 -
.../src/SpectrumThemeLight.module.scss | 12 -
packages/components/src/SpectrumUtils.test.ts | 17 +-
packages/components/src/SpectrumUtils.ts | 7 +-
.../__snapshots__/SpectrumUtils.test.ts.snap | 33 +++
packages/components/src/declaration.d.ts | 4 +
.../src/theme/SpectrumThemeProvider.tsx | 37 +++
.../src/theme/ThemeProvider.test.tsx | 64 +++--
.../components/src/theme/ThemeProvider.tsx | 44 +++-
.../components/src/theme/ThemeUtils.test.ts | 11 +-
.../__snapshots__/ThemeProvider.test.tsx.snap | 117 ++++++---
.../components/src/theme/theme-dark/index.ts | 26 ++
.../theme-dark/theme-dark-components.css | 6 +
.../theme/theme-dark/theme-dark-semantic.css | 63 ++++-
.../src/theme/theme-spectrum/index.ts | 22 ++
.../theme-spectrum-alias.module.css | 206 +++++++++++++++
.../theme-spectrum-overrides.module.css | 5 +
.../theme-spectrum-palette.module.css | 222 +++++++++++++++++
45 files changed, 1330 insertions(+), 165 deletions(-)
create mode 100644 __mocks__/css/mock-theme-dark-components.js
create mode 100644 __mocks__/css/mock-theme-dark-palette.js
create mode 100644 __mocks__/css/mock-theme-dark-semantic-editor.js
create mode 100644 __mocks__/css/mock-theme-dark-semantic-grid.js
create mode 100644 __mocks__/css/mock-theme-dark-semantic.js
create mode 100644 __mocks__/css/mock-theme-light-palette.js
create mode 100644 __mocks__/css/mock-theme-spectrum-alias.module.js
create mode 100644 __mocks__/css/mock-theme-spectrum-overrides.module.js
create mode 100644 __mocks__/css/mock-theme-spectrum-palette.module.js
delete mode 100644 __mocks__/spectrumThemeDarkMock.js
delete mode 100644 __mocks__/spectrumThemeLightMock.js
create mode 100644 packages/code-studio/src/styleguide/GotoTopButton.css
create mode 100644 packages/code-studio/src/styleguide/GotoTopButton.tsx
create mode 100644 packages/code-studio/src/styleguide/SamplesMenu.tsx
create mode 100644 packages/code-studio/src/styleguide/SpectrumComponents.tsx
create mode 100644 packages/code-studio/src/styleguide/constants.ts
delete mode 100644 packages/components/src/SpectrumThemeDark.module.scss
delete mode 100644 packages/components/src/SpectrumThemeLight.module.scss
create mode 100644 packages/components/src/__snapshots__/SpectrumUtils.test.ts.snap
create mode 100644 packages/components/src/theme/SpectrumThemeProvider.tsx
create mode 100644 packages/components/src/theme/theme-dark/theme-dark-components.css
create mode 100644 packages/components/src/theme/theme-spectrum/index.ts
create mode 100644 packages/components/src/theme/theme-spectrum/theme-spectrum-alias.module.css
create mode 100644 packages/components/src/theme/theme-spectrum/theme-spectrum-overrides.module.css
create mode 100644 packages/components/src/theme/theme-spectrum/theme-spectrum-palette.module.css
diff --git a/.gitignore b/.gitignore
index 59c717a451..216ba60082 100644
--- a/.gitignore
+++ b/.gitignore
@@ -32,6 +32,7 @@ yarn-debug.log*
yarn-error.log*
css
+!__mocks__/css
tsconfig.tsbuildinfo
packages/*/package-lock.json
diff --git a/__mocks__/css/mock-theme-dark-components.js b/__mocks__/css/mock-theme-dark-components.js
new file mode 100644
index 0000000000..c5560b1294
--- /dev/null
+++ b/__mocks__/css/mock-theme-dark-components.js
@@ -0,0 +1 @@
+module.exports = 'mock-theme-dark-components';
diff --git a/__mocks__/css/mock-theme-dark-palette.js b/__mocks__/css/mock-theme-dark-palette.js
new file mode 100644
index 0000000000..21854c8c09
--- /dev/null
+++ b/__mocks__/css/mock-theme-dark-palette.js
@@ -0,0 +1 @@
+module.exports = 'mock-theme-dark-palette';
diff --git a/__mocks__/css/mock-theme-dark-semantic-editor.js b/__mocks__/css/mock-theme-dark-semantic-editor.js
new file mode 100644
index 0000000000..48ec449051
--- /dev/null
+++ b/__mocks__/css/mock-theme-dark-semantic-editor.js
@@ -0,0 +1 @@
+module.exports = 'mock-theme-dark-semantic-editor';
diff --git a/__mocks__/css/mock-theme-dark-semantic-grid.js b/__mocks__/css/mock-theme-dark-semantic-grid.js
new file mode 100644
index 0000000000..a0cf67fa6d
--- /dev/null
+++ b/__mocks__/css/mock-theme-dark-semantic-grid.js
@@ -0,0 +1 @@
+module.exports = 'mock-theme-dark-semantic-grid';
diff --git a/__mocks__/css/mock-theme-dark-semantic.js b/__mocks__/css/mock-theme-dark-semantic.js
new file mode 100644
index 0000000000..1d252429d9
--- /dev/null
+++ b/__mocks__/css/mock-theme-dark-semantic.js
@@ -0,0 +1 @@
+module.exports = 'mock-theme-dark-semantic';
diff --git a/__mocks__/css/mock-theme-light-palette.js b/__mocks__/css/mock-theme-light-palette.js
new file mode 100644
index 0000000000..e664d94101
--- /dev/null
+++ b/__mocks__/css/mock-theme-light-palette.js
@@ -0,0 +1 @@
+module.exports = 'mock-theme-light-palette';
diff --git a/__mocks__/css/mock-theme-spectrum-alias.module.js b/__mocks__/css/mock-theme-spectrum-alias.module.js
new file mode 100644
index 0000000000..e1b3cb449c
--- /dev/null
+++ b/__mocks__/css/mock-theme-spectrum-alias.module.js
@@ -0,0 +1,3 @@
+module.exports = {
+ 'dh-spectrum-alias': 'mock-dh-spectrum-alias',
+};
diff --git a/__mocks__/css/mock-theme-spectrum-overrides.module.js b/__mocks__/css/mock-theme-spectrum-overrides.module.js
new file mode 100644
index 0000000000..4788e1aad0
--- /dev/null
+++ b/__mocks__/css/mock-theme-spectrum-overrides.module.js
@@ -0,0 +1,3 @@
+module.exports = {
+ 'dh-spectrum-overrides': 'mock-dh-spectrum-overrides',
+};
diff --git a/__mocks__/css/mock-theme-spectrum-palette.module.js b/__mocks__/css/mock-theme-spectrum-palette.module.js
new file mode 100644
index 0000000000..0ad7ec6ec2
--- /dev/null
+++ b/__mocks__/css/mock-theme-spectrum-palette.module.js
@@ -0,0 +1,3 @@
+module.exports = {
+ 'dh-spectrum-palette': 'mock-dh-spectrum-palette',
+};
diff --git a/__mocks__/spectrumThemeDarkMock.js b/__mocks__/spectrumThemeDarkMock.js
deleted file mode 100644
index ae129ec995..0000000000
--- a/__mocks__/spectrumThemeDarkMock.js
+++ /dev/null
@@ -1,3 +0,0 @@
-module.exports = {
- 'dh-spectrum-theme--dark': 'mock.dark',
-};
diff --git a/__mocks__/spectrumThemeLightMock.js b/__mocks__/spectrumThemeLightMock.js
deleted file mode 100644
index 30b205f5a3..0000000000
--- a/__mocks__/spectrumThemeLightMock.js
+++ /dev/null
@@ -1,3 +0,0 @@
-module.exports = {
- 'dh-spectrum-theme--light': 'mock.light',
-};
diff --git a/jest.config.base.cjs b/jest.config.base.cjs
index 373bf89b78..05be728cb4 100644
--- a/jest.config.base.cjs
+++ b/jest.config.base.cjs
@@ -9,9 +9,9 @@ module.exports = {
'node_modules/(?!(monaco-editor|d3-interpolate|d3-color)/)',
],
moduleNameMapper: {
- 'SpectrumTheme([^.]+)\\.module\\.scss$': path.join(
+ 'theme-([^/]+?)\\.css(\\?inline)?$': path.join(
__dirname,
- './__mocks__/spectrumTheme$1Mock.js'
+ './__mocks__/css/mock-theme-$1.js'
),
'\\.(css|less|scss|sass)$': 'identity-obj-proxy',
'\\.(css|less|scss|sass)\\?inline$': path.join(
diff --git a/package-lock.json b/package-lock.json
index fac28af5cd..8770635b06 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -59,7 +59,7 @@
"@deephaven/tsconfig": "file:../tsconfig",
"@deephaven/utils": "file:../utils",
"@fortawesome/fontawesome-common-types": "^6.1.1",
- "@playwright/test": "^1.37.1",
+ "@playwright/test": "1.37.1",
"@testing-library/jest-dom": "^5.16.4",
"@testing-library/react": "^12.1.3",
"@testing-library/react-hooks": "^8.0.1",
diff --git a/package.json b/package.json
index edf80f7449..859019a992 100644
--- a/package.json
+++ b/package.json
@@ -70,7 +70,7 @@
"@deephaven/tsconfig": "file:../tsconfig",
"@deephaven/utils": "file:../utils",
"@fortawesome/fontawesome-common-types": "^6.1.1",
- "@playwright/test": "^1.37.1",
+ "@playwright/test": "1.37.1",
"@testing-library/jest-dom": "^5.16.4",
"@testing-library/react": "^12.1.3",
"@testing-library/react-hooks": "^8.0.1",
diff --git a/packages/app-utils/src/components/AppBootstrap.test.tsx b/packages/app-utils/src/components/AppBootstrap.test.tsx
index 84a474a42d..dc838d24b7 100644
--- a/packages/app-utils/src/components/AppBootstrap.test.tsx
+++ b/packages/app-utils/src/components/AppBootstrap.test.tsx
@@ -1,4 +1,4 @@
-import React from 'react';
+import React, { useContext } from 'react';
import { AUTH_HANDLER_TYPE_ANONYMOUS } from '@deephaven/auth-plugins';
import { ApiContext } from '@deephaven/jsapi-bootstrap';
import { BROADCAST_LOGIN_MESSAGE } from '@deephaven/jsapi-utils';
@@ -10,6 +10,18 @@ import type {
import { TestUtils } from '@deephaven/utils';
import { act, render, screen } from '@testing-library/react';
import AppBootstrap from './AppBootstrap';
+import { PluginsContext } from './PluginsBootstrap';
+import { PluginModuleMap } from '../plugins';
+
+const { asMock } = TestUtils;
+
+jest.mock('react', () => {
+ const actual = jest.requireActual('react');
+ return {
+ ...actual,
+ useContext: jest.fn(actual.useContext),
+ };
+});
const API_URL = 'http://mockserver.net:8111';
const PLUGINS_URL = 'http://mockserver.net:8111/plugins';
@@ -33,6 +45,7 @@ jest.mock('@deephaven/jsapi-components', () => ({
const mockChildText = 'Mock Child';
const mockChild = {mockChildText}
;
+const mockPluginModuleMapEmpty: PluginModuleMap = new Map();
function expectMockChild() {
return expect(screen.queryByText(mockChildText));
@@ -60,6 +73,17 @@ beforeEach(() => {
it('should throw if api has not been bootstrapped', () => {
TestUtils.disableConsoleOutput();
+ asMock(useContext).mockImplementation(context => {
+ // ThemeBootstrap doesn't render children until plugins are loaded. We need
+ // a non-null PluginsContext value to render the children and get the expected
+ // missing api error.
+ if (context === PluginsContext) {
+ return mockPluginModuleMapEmpty;
+ }
+
+ return jest.requireActual('react').useContext(context);
+ });
+
expect(() =>
render(
diff --git a/packages/code-studio/src/styleguide/GotoTopButton.css b/packages/code-studio/src/styleguide/GotoTopButton.css
new file mode 100644
index 0000000000..681a8f8871
--- /dev/null
+++ b/packages/code-studio/src/styleguide/GotoTopButton.css
@@ -0,0 +1,19 @@
+/*
+ * GotoTopButton is only visible if user has scrolled down. Visibility attribute
+ * can't really make use of CSS transitions, so we use opacity instead. Including
+ * visibility for accessibility reasons.
+ */
+.goto-top-button {
+ visibility: visible;
+ opacity: 1;
+ transition:
+ opacity 300ms,
+ visibility 0s linear 0s;
+}
+html:not([data-scroll='true']) .goto-top-button {
+ visibility: hidden;
+ opacity: 0;
+ transition:
+ opacity 300ms,
+ visibility 0s linear 300ms;
+}
diff --git a/packages/code-studio/src/styleguide/GotoTopButton.tsx b/packages/code-studio/src/styleguide/GotoTopButton.tsx
new file mode 100644
index 0000000000..f51039c4f7
--- /dev/null
+++ b/packages/code-studio/src/styleguide/GotoTopButton.tsx
@@ -0,0 +1,48 @@
+import React, { useCallback, useEffect } from 'react';
+import { Button, Icon } from '@adobe/react-spectrum';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { vsChevronUp } from '@deephaven/icons';
+import './GotoTopButton.css';
+
+/**
+ * Button that scrolls to top of styleguide and clears location hash.
+ */
+export function GotoTopButton(): JSX.Element {
+ const gotoTop = useCallback(() => {
+ window.scrollTo({
+ top: 0,
+ behavior: 'smooth',
+ });
+
+ // Small delay to give scrolling a chance to move smoothly to top
+ setTimeout(() => {
+ window.location.hash = '';
+ }, 500);
+ }, []);
+
+ // Set data-scroll="true" on the html element when the user scrolls down below
+ // 120px. CSS uses this to only show the button when the user has scrolled.
+ useEffect(() => {
+ function onScroll() {
+ document.documentElement.dataset.scroll = String(window.scrollY > 120);
+ }
+ document.addEventListener('scroll', onScroll, { passive: true });
+ return () => {
+ document.removeEventListener('scroll', onScroll);
+ };
+ }, []);
+
+ return (
+
+ );
+}
+
+export default GotoTopButton;
diff --git a/packages/code-studio/src/styleguide/Icons.tsx b/packages/code-studio/src/styleguide/Icons.tsx
index efd25ac9cf..ce2e3f0b32 100644
--- a/packages/code-studio/src/styleguide/Icons.tsx
+++ b/packages/code-studio/src/styleguide/Icons.tsx
@@ -6,6 +6,7 @@ import {
dhSquareFilled,
dhAddSmall,
} from '@deephaven/icons';
+import { Icon } from '@adobe/react-spectrum';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Button } from '@deephaven/components';
import PropTypes from 'prop-types';
@@ -106,7 +107,9 @@ function Icons(): React.ReactElement {
});
}}
>
-
+
+
+
diff --git a/packages/code-studio/src/styleguide/SamplesMenu.tsx b/packages/code-studio/src/styleguide/SamplesMenu.tsx
new file mode 100644
index 0000000000..81e7a935ca
--- /dev/null
+++ b/packages/code-studio/src/styleguide/SamplesMenu.tsx
@@ -0,0 +1,125 @@
+import React, { Key, useCallback, useEffect, useState } from 'react';
+import {
+ ActionButton,
+ Icon,
+ Item,
+ Menu,
+ MenuTrigger,
+ Section,
+} from '@adobe/react-spectrum';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { vsMenu } from '@deephaven/icons';
+import {
+ MENU_CATEGORY_DATA_ATTRIBUTE,
+ NO_MENU_DATA_ATTRIBUTE,
+ SPECTRUM_COMPONENT_SAMPLES_ID,
+} from './constants';
+
+interface Link {
+ id: string;
+ label: string;
+}
+type LinkCategory = { category: string; items: Link[] };
+
+/**
+ * Metadata only div that provides a MENU_CATEGORY_DATA_ATTRIBUTE defining a
+ * menu category. These will be queried by the SamplesMenu component to build
+ * up the menu sections.
+ */
+export function SampleMenuCategory({
+ 'data-menu-category': dataMenuCategory,
+}: Record): JSX.Element {
+ return ;
+}
+
+/**
+ * Creates a menu from h2, h3 elements on the page and assigns them each an id
+ * for hash navigation purposes. If the current hash matches one of the ids, it
+ * will scroll to that element. This handles the initial page load scenario.
+ * Menu sections are identified by divs with MENU_CATEGORY_DATA_ATTRIBUTE
+ * attributes.
+ */
+export function SamplesMenu(): JSX.Element {
+ const [links, setLinks] = useState([]);
+
+ useEffect(() => {
+ let currentCategory: LinkCategory = {
+ category: '',
+ items: [],
+ };
+ const categories: LinkCategory[] = [currentCategory];
+
+ const spectrumComponentsSamples = document.querySelector(
+ `#${SPECTRUM_COMPONENT_SAMPLES_ID}`
+ );
+
+ document
+ .querySelectorAll(`h2,h3,[${MENU_CATEGORY_DATA_ATTRIBUTE}]`)
+ .forEach(el => {
+ if (el.textContent == null || el.hasAttribute(NO_MENU_DATA_ATTRIBUTE)) {
+ return;
+ }
+
+ // Create a new category section
+ if (el.hasAttribute(MENU_CATEGORY_DATA_ATTRIBUTE)) {
+ currentCategory = {
+ category: el.getAttribute(MENU_CATEGORY_DATA_ATTRIBUTE) ?? '',
+ items: [],
+ };
+ categories.push(currentCategory);
+
+ return;
+ }
+
+ const id = `${
+ spectrumComponentsSamples?.contains(el) === true ? 'spectrum-' : ''
+ }${el.textContent}`
+ .toLowerCase()
+ .replace(/\s/g, '-');
+
+ // eslint-disable-next-line no-param-reassign
+ el.id = id;
+
+ currentCategory.items.push({ id, label: el.textContent });
+
+ if (`#${id}` === window.location.hash) {
+ el.scrollIntoView();
+ }
+ });
+
+ setLinks(categories);
+ }, []);
+
+ const onAction = useCallback((key: Key) => {
+ const id = String(key);
+ const el = document.getElementById(id);
+ el?.scrollIntoView({
+ behavior: 'smooth',
+ });
+
+ // Keep hash in sync for page reloads, but give some delay to allow smooth
+ // scrolling above
+ setTimeout(() => {
+ window.location.hash = id;
+ }, 500);
+ }, []);
+
+ return (
+
+
+
+
+
+
+
+
+ );
+}
+
+export default SamplesMenu;
diff --git a/packages/code-studio/src/styleguide/SpectrumComponents.tsx b/packages/code-studio/src/styleguide/SpectrumComponents.tsx
new file mode 100644
index 0000000000..697fb87f9c
--- /dev/null
+++ b/packages/code-studio/src/styleguide/SpectrumComponents.tsx
@@ -0,0 +1,235 @@
+/* eslint-disable react/style-prop-object */
+import React from 'react';
+import {
+ ActionButton,
+ Button,
+ Cell,
+ Checkbox,
+ Content,
+ ContextualHelp,
+ Column,
+ ComboBox,
+ Form,
+ Heading,
+ Grid,
+ Icon,
+ IllustratedMessage,
+ Item,
+ minmax,
+ repeat,
+ Row,
+ Slider,
+ Switch,
+ TableBody,
+ TableHeader,
+ TableView,
+ Text,
+ TextField,
+ ToggleButton,
+ View,
+ Well,
+} from '@adobe/react-spectrum';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { dh } from '@deephaven/icons';
+import { SPECTRUM_COMPONENT_SAMPLES_ID } from './constants';
+
+export function SpectrumComponents(): JSX.Element {
+ return (
+
+
+ Spectrum Components
+
+
+
+ Buttons
+
+
+
+ Collections
+
+
+
+ Content
+
+
+
+ Forms
+
+
+
+ Overlays
+
+
+
+ Wells
+ This is a well.
+
+
+
+ );
+}
+
+export default SpectrumComponents;
+
+function ButtonsSample(): JSX.Element {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Normal
+
+ Quiet
+
+ Static Black
+ Static White
+ Disabled
+
+
+ Normal
+ Quiet
+
+ Emphasized
+
+ Static Black
+ Static White
+ Disabled
+
+ );
+}
+
+function ContextualHelpSample(): JSX.Element {
+ return (
+ <>
+ Contextual Help
+
+ Need help?
+
+
+ This is a helpful description of the thing you need help with.
+
+
+
+ >
+ );
+}
+
+function FormsSample(): JSX.Element {
+ return (
+
+ );
+}
+
+function IllustratedMessageSample(): JSX.Element {
+ return (
+
+
+
+
+ Illustrated Message
+ This is the content of the message.
+
+ );
+}
+
+function TableViewSample(): JSX.Element {
+ return (
+ <>
+
+
+
+
+ Name
+ Age
+
+
+ City
+ State
+
+
+
+
+ John |
+ 42 |
+ San Francisco |
+ CA |
+
+
+ Jane |
+ 38 |
+ San Francisco |
+ CA |
+
+
+ Becky |
+ 12 |
+ San Francisco |
+ CA |
+
+
+
+ >
+ );
+}
diff --git a/packages/code-studio/src/styleguide/StyleGuide.scss b/packages/code-studio/src/styleguide/StyleGuide.scss
index 58d270d8e2..3f77914e82 100644
--- a/packages/code-studio/src/styleguide/StyleGuide.scss
+++ b/packages/code-studio/src/styleguide/StyleGuide.scss
@@ -125,11 +125,6 @@ h5.sub-title {
}
}
- .icon,
- .icon .svg-inline--fa {
- font-size: 36px;
- }
-
.card label {
max-width: 100%;
font-size: 0.8rem;
diff --git a/packages/code-studio/src/styleguide/StyleGuide.test.tsx b/packages/code-studio/src/styleguide/StyleGuide.test.tsx
index 69e84a2bd2..d6d2d324d4 100644
--- a/packages/code-studio/src/styleguide/StyleGuide.test.tsx
+++ b/packages/code-studio/src/styleguide/StyleGuide.test.tsx
@@ -1,15 +1,21 @@
import React from 'react';
import { render } from '@testing-library/react';
+import { ThemeData, ThemeProvider } from '@deephaven/components';
import { dh } from '@deephaven/jsapi-shim';
import { ApiContext } from '@deephaven/jsapi-bootstrap';
import StyleGuide from './StyleGuide';
describe(' mounts', () => {
test('h1 text of StyleGuide renders', () => {
+ // Provide a non-null array to ThemeProvider to tell it to initialize
+ const customThemes: ThemeData[] = [];
+
expect(() =>
render(
-
+
+
+
)
).not.toThrow();
diff --git a/packages/code-studio/src/styleguide/StyleGuide.tsx b/packages/code-studio/src/styleguide/StyleGuide.tsx
index a71843a61c..6bc7911999 100644
--- a/packages/code-studio/src/styleguide/StyleGuide.tsx
+++ b/packages/code-studio/src/styleguide/StyleGuide.tsx
@@ -1,4 +1,6 @@
+/* eslint-disable react/jsx-props-no-spreading */
import React from 'react';
+import { Flex } from '@adobe/react-spectrum';
import { ContextMenuRoot } from '@deephaven/components';
import Alerts from './Alerts';
@@ -22,55 +24,63 @@ import './StyleGuide.scss';
import DraggableLists from './DraggableLists';
import Navigations from './Navigations';
import ThemeColors from './ThemeColors';
+import SpectrumComponents from './SpectrumComponents';
+import SamplesMenu, { SampleMenuCategory } from './SamplesMenu';
+import GotoTopButton from './GotoTopButton';
+
+const stickyProps = {
+ position: 'sticky',
+ justifyContent: 'end',
+ zIndex: 1,
+} as const;
function StyleGuide(): React.ReactElement {
return (
-
+
Deephaven UI Components
-
+
+
+
+
+
+
+
+
+
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
);
}
diff --git a/packages/code-studio/src/styleguide/ThemeColors.tsx b/packages/code-studio/src/styleguide/ThemeColors.tsx
index 07e2ca95ad..dc7ba764cf 100644
--- a/packages/code-studio/src/styleguide/ThemeColors.tsx
+++ b/packages/code-studio/src/styleguide/ThemeColors.tsx
@@ -5,6 +5,7 @@ import palette from '@deephaven/components/src/theme/theme-dark/theme-dark-palet
import semantic from '@deephaven/components/src/theme/theme-dark/theme-dark-semantic.css?inline';
import semanticEditor from '@deephaven/components/src/theme/theme-dark/theme-dark-semantic-editor.css?inline';
import semanticGrid from '@deephaven/components/src/theme/theme-dark/theme-dark-semantic-grid.css?inline';
+import components from '@deephaven/components/src/theme/theme-dark/theme-dark-components.css?inline';
import styles from './ThemeColors.module.scss';
// Group names are extracted from var names via a regex capture group. Most of
@@ -45,15 +46,23 @@ const renameGroups = {
focus: 'state',
},
grid: { data: 'Data Bars', context: 'Context Menu' },
+ semantic: {
+ positive: 'status',
+ negative: 'status',
+ notice: 'status',
+ info: 'status',
+ well: 'wells',
+ },
};
export function ThemeColors(): JSX.Element {
const swatchDataGroups = useMemo(
() => ({
'Theme Color Palette': buildColorGroups(palette, 1),
- 'Semantic Colors': buildColorGroups(semantic, 1),
+ 'Semantic Colors': buildColorGroups(semantic, 1, renameGroups.semantic),
'Editor Colors': buildColorGroups(semanticEditor, 2, renameGroups.editor),
'Grid Colors': buildColorGroups(semanticGrid, 2, renameGroups.grid),
+ 'Component Colors': buildColorGroups(components, 1),
}),
[]
);
diff --git a/packages/code-studio/src/styleguide/Typography.tsx b/packages/code-studio/src/styleguide/Typography.tsx
index 747e2ec79e..a9255d2f55 100644
--- a/packages/code-studio/src/styleguide/Typography.tsx
+++ b/packages/code-studio/src/styleguide/Typography.tsx
@@ -3,15 +3,23 @@ import React from 'react';
function Typography(): React.ReactElement {
return (
-
Typograpy
+
Typography
-
h1. Unused
- h2. Unused
- h3. Unused
- h4. Standard Heading
- h5. Small Heading
- h6. Unused
+
+ h1. Unused
+
+
+ h2. Unused
+
+
+ h3. Unused
+
+ h4. Standard Heading
+ h5. Small Heading
+
+ h6. Unused
+
diff --git a/packages/code-studio/src/styleguide/constants.ts b/packages/code-studio/src/styleguide/constants.ts
new file mode 100644
index 0000000000..af5b342f11
--- /dev/null
+++ b/packages/code-studio/src/styleguide/constants.ts
@@ -0,0 +1,3 @@
+export const MENU_CATEGORY_DATA_ATTRIBUTE = 'data-menu-category';
+export const NO_MENU_DATA_ATTRIBUTE = 'data-no-menu';
+export const SPECTRUM_COMPONENT_SAMPLES_ID = 'spectrum-component-samples';
diff --git a/packages/components/src/SpectrumThemeDark.module.scss b/packages/components/src/SpectrumThemeDark.module.scss
deleted file mode 100644
index 2def9236d9..0000000000
--- a/packages/components/src/SpectrumThemeDark.module.scss
+++ /dev/null
@@ -1,12 +0,0 @@
-/**
- This module contains overrides of React Spectrum css variables for the default
- `dark` Deephaven theme.
-*/
-@use '../scss/bootstrap_overrides' as bootstrap;
-@use '../scss/util' as *;
-
-// Doubling specificity to ensure this takes precedence over default Spectrum
-// styles.
-#{multiply-specificity-n('.dh-spectrum-theme--dark', 2)} {
- --spectrum-alias-background-color-default: #{bootstrap.$interfaceblack};
-}
diff --git a/packages/components/src/SpectrumThemeLight.module.scss b/packages/components/src/SpectrumThemeLight.module.scss
deleted file mode 100644
index 037ec68732..0000000000
--- a/packages/components/src/SpectrumThemeLight.module.scss
+++ /dev/null
@@ -1,12 +0,0 @@
-/**
- This module contains overrides of React Spectrum css variables for the default
- `light` Deephaven theme.
-*/
-@use '../scss/bootstrap_overrides' as bootstrap;
-@use '../scss/util' as *;
-
-// Doubling specificity to ensure this takes precedence over default Spectrum
-// styles.
-#{multiply-specificity-n('.dh-spectrum-theme--light', 2)} {
- --spectrum-alias-background-color-default: #{bootstrap.$interfacewhite};
-}
diff --git a/packages/components/src/SpectrumUtils.test.ts b/packages/components/src/SpectrumUtils.test.ts
index c1e40405db..7f5ce71337 100644
--- a/packages/components/src/SpectrumUtils.test.ts
+++ b/packages/components/src/SpectrumUtils.test.ts
@@ -1,22 +1,7 @@
-import { theme } from '@react-spectrum/theme-default';
import { themeDHDefault } from './SpectrumUtils';
describe('themeDHDefault', () => {
it('should merge Spectrum default with DH custom styles', () => {
- const { global, light, dark, medium, large } = theme;
-
- expect(themeDHDefault).toEqual({
- global,
- light: {
- ...light,
- 'dh-spectrum-theme--light': 'mock.light',
- },
- dark: {
- ...dark,
- 'dh-spectrum-theme--dark': 'mock.dark',
- },
- medium,
- large,
- });
+ expect(themeDHDefault).toMatchSnapshot();
});
});
diff --git a/packages/components/src/SpectrumUtils.ts b/packages/components/src/SpectrumUtils.ts
index 70f676a363..95e6f7e995 100644
--- a/packages/components/src/SpectrumUtils.ts
+++ b/packages/components/src/SpectrumUtils.ts
@@ -1,6 +1,5 @@
import { theme } from '@react-spectrum/theme-default';
-import darkDH from './SpectrumThemeDark.module.scss';
-import lightDH from './SpectrumThemeLight.module.scss';
+import { themeSpectrumClassesCommon } from './theme/theme-spectrum';
const { global, light, dark, medium, large } = theme;
@@ -42,11 +41,11 @@ export const themeDHDefault = {
global,
light: {
...light,
- ...lightDH,
+ ...themeSpectrumClassesCommon,
},
dark: {
...dark,
- ...darkDH,
+ ...themeSpectrumClassesCommon,
},
// scales
medium,
diff --git a/packages/components/src/__snapshots__/SpectrumUtils.test.ts.snap b/packages/components/src/__snapshots__/SpectrumUtils.test.ts.snap
new file mode 100644
index 0000000000..f5973cdacc
--- /dev/null
+++ b/packages/components/src/__snapshots__/SpectrumUtils.test.ts.snap
@@ -0,0 +1,33 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`themeDHDefault should merge Spectrum default with DH custom styles 1`] = `
+{
+ "dark": {
+ "dh-spectrum-alias": "mock-dh-spectrum-alias",
+ "dh-spectrum-overrides": "mock-dh-spectrum-overrides",
+ "dh-spectrum-palette": "mock-dh-spectrum-palette",
+ "spectrum--darkest": "spectrum--darkest_256eeb",
+ },
+ "global": {
+ "spectrum": "spectrum_9e130c",
+ "spectrum--dark": "spectrum--dark_9e130c",
+ "spectrum--darkest": "spectrum--darkest_9e130c",
+ "spectrum--large": "spectrum--large_9e130c",
+ "spectrum--light": "spectrum--light_9e130c",
+ "spectrum--lightest": "spectrum--lightest_9e130c",
+ "spectrum--medium": "spectrum--medium_9e130c",
+ },
+ "large": {
+ "spectrum--large": "spectrum--large_c40598",
+ },
+ "light": {
+ "dh-spectrum-alias": "mock-dh-spectrum-alias",
+ "dh-spectrum-overrides": "mock-dh-spectrum-overrides",
+ "dh-spectrum-palette": "mock-dh-spectrum-palette",
+ "spectrum--light": "spectrum--light_a40724",
+ },
+ "medium": {
+ "spectrum--medium": "spectrum--medium_4b172c",
+ },
+}
+`;
diff --git a/packages/components/src/declaration.d.ts b/packages/components/src/declaration.d.ts
index fde32658a1..f1577432ee 100644
--- a/packages/components/src/declaration.d.ts
+++ b/packages/components/src/declaration.d.ts
@@ -1,3 +1,7 @@
+declare module '*.module.css' {
+ const content: Record
;
+ export default content;
+}
declare module '*.module.scss' {
const content: Record;
export default content;
diff --git a/packages/components/src/theme/SpectrumThemeProvider.tsx b/packages/components/src/theme/SpectrumThemeProvider.tsx
new file mode 100644
index 0000000000..6b190e1c32
--- /dev/null
+++ b/packages/components/src/theme/SpectrumThemeProvider.tsx
@@ -0,0 +1,37 @@
+import { ReactNode, useState } from 'react';
+import { Provider } from '@adobe/react-spectrum';
+import type { Theme } from '@react-types/provider';
+import shortid from 'shortid';
+import { themeDHDefault } from '../SpectrumUtils';
+
+export interface SpectrumThemeProviderProps {
+ children: ReactNode;
+ isPortal?: boolean;
+ theme?: Theme;
+ colorScheme?: 'light' | 'dark';
+}
+
+/**
+ * Wrapper around React Spectrum's theme Provider that provides DH mappings of
+ * Spectrum's theme variables to DH's theme variables. Also exposes an optional
+ * `isPortal` prop that if provided, adds a unique `data-unique-id` attribute to
+ * the Provider. This is needed to force the Provider to render the theme wrapper
+ * inside of portals.
+ */
+export function SpectrumThemeProvider({
+ children,
+ isPortal = false,
+ theme = themeDHDefault,
+ colorScheme,
+}: SpectrumThemeProviderProps): JSX.Element {
+ // a unique ID is used per provider to force it to render the theme wrapper element inside portals
+ // based on https://github.com/adobe/react-spectrum/issues/1697#issuecomment-999827266
+ // won't be needed if https://github.com/adobe/react-spectrum/pull/2669 is merged
+ const [id] = useState(isPortal ? shortid() : null);
+
+ return (
+
+ {children}
+
+ );
+}
diff --git a/packages/components/src/theme/ThemeProvider.test.tsx b/packages/components/src/theme/ThemeProvider.test.tsx
index 11856899fe..8fb489461a 100644
--- a/packages/components/src/theme/ThemeProvider.test.tsx
+++ b/packages/components/src/theme/ThemeProvider.test.tsx
@@ -75,18 +75,20 @@ describe('ThemeProvider', () => {
assertNotNull(themeContextValueRef.current);
- expect(themeContextValueRef.current.activeThemes).toEqual(
- themes == null
- ? null
- : getActiveThemes(preloadData?.themeKey ?? DEFAULT_DARK_THEME_KEY, {
- base: getDefaultBaseThemes(),
- custom: themes,
- })
- );
+ if (themes == null) {
+ expect(themeContextValueRef.current.activeThemes).toBeNull();
+ } else {
+ expect(themeContextValueRef.current.activeThemes).toEqual(
+ getActiveThemes(preloadData?.themeKey ?? DEFAULT_DARK_THEME_KEY, {
+ base: getDefaultBaseThemes(),
+ custom: themes,
+ })
+ );
- expect(themeContextValueRef.current.selectedThemeKey).toEqual(
- preloadData?.themeKey ?? DEFAULT_DARK_THEME_KEY
- );
+ expect(themeContextValueRef.current.selectedThemeKey).toEqual(
+ preloadData?.themeKey ?? DEFAULT_DARK_THEME_KEY
+ );
+ }
expect(component.baseElement).toMatchSnapshot();
}
@@ -108,10 +110,14 @@ describe('ThemeProvider', () => {
);
- expect(setThemePreloadData).toHaveBeenCalledWith({
- themeKey: preloadData?.themeKey ?? DEFAULT_DARK_THEME_KEY,
- preloadStyleContent: calculatePreloadStyleContent(),
- });
+ if (themes == null) {
+ expect(setThemePreloadData).not.toHaveBeenCalled();
+ } else {
+ expect(setThemePreloadData).toHaveBeenCalledWith({
+ themeKey: preloadData?.themeKey ?? DEFAULT_DARK_THEME_KEY,
+ preloadStyleContent: calculatePreloadStyleContent(),
+ });
+ }
}
);
@@ -127,20 +133,24 @@ describe('ThemeProvider', () => {
assertNotNull(themeContextValueRef.current);
- act(() => {
- themeContextValueRef.current!.setSelectedThemeKey(themeKey);
- });
+ if (themes == null) {
+ expect(themeContextValueRef.current.activeThemes).toBeNull();
+ } else {
+ act(() => {
+ themeContextValueRef.current!.setSelectedThemeKey(themeKey);
+ });
- expect(themeContextValueRef.current.activeThemes).toEqual(
- themes == null
- ? null
- : getActiveThemes(themeKey, {
- base: getDefaultBaseThemes(),
- custom: themes,
- })
- );
+ expect(themeContextValueRef.current.activeThemes).toEqual(
+ getActiveThemes(themeKey, {
+ base: getDefaultBaseThemes(),
+ custom: themes,
+ })
+ );
- expect(themeContextValueRef.current.selectedThemeKey).toEqual(themeKey);
+ expect(themeContextValueRef.current.selectedThemeKey).toEqual(
+ themeKey
+ );
+ }
expect(component.baseElement).toMatchSnapshot();
}
diff --git a/packages/components/src/theme/ThemeProvider.tsx b/packages/components/src/theme/ThemeProvider.tsx
index d3ff14d0ec..abf61a9800 100644
--- a/packages/components/src/theme/ThemeProvider.tsx
+++ b/packages/components/src/theme/ThemeProvider.tsx
@@ -8,6 +8,7 @@ import {
getThemePreloadData,
setThemePreloadData,
} from './ThemeUtils';
+import { SpectrumThemeProvider } from './SpectrumThemeProvider';
export interface ThemeContextValue {
activeThemes: ThemeData[] | null;
@@ -20,6 +21,11 @@ const log = Log.module('ThemeProvider');
export const ThemeContext = createContext(null);
export interface ThemeProviderProps {
+ /*
+ * Additional themes to load in addition to the base themes. If no additional
+ * themes are to be loaded, this must be set to an empty array in order to
+ * tell the provider to activate the base themes.
+ */
themes: ThemeData[] | null;
children: ReactNode;
}
@@ -34,11 +40,9 @@ export function ThemeProvider({
() => getThemePreloadData()?.themeKey ?? DEFAULT_DARK_THEME_KEY
);
+ // Calculate active themes once a non-null themes array is provided.
const activeThemes = useMemo(
() =>
- // Themes remain inactive until a non-null themes array is provided. This
- // avoids the default base theme overriding the preload if we are waiting
- // on additional themes to be available.
themes == null
? null
: getActiveThemes(selectedThemeKey, {
@@ -50,14 +54,26 @@ export function ThemeProvider({
useEffect(
function updateThemePreloadData() {
- log.debug('Active themes:', activeThemes?.map(theme => theme.themeKey));
+ // Don't update preload data until themes have been loaded and activated
+ if (activeThemes == null || themes == null) {
+ return;
+ }
+
+ const preloadStyleContent = calculatePreloadStyleContent();
+
+ log.debug2('updateThemePreloadData:', {
+ active: activeThemes.map(theme => theme.themeKey),
+ all: themes.map(theme => theme.themeKey),
+ preloadStyleContent,
+ selectedThemeKey,
+ });
setThemePreloadData({
themeKey: selectedThemeKey,
- preloadStyleContent: calculatePreloadStyleContent(),
+ preloadStyleContent,
});
},
- [activeThemes, selectedThemeKey]
+ [activeThemes, selectedThemeKey, themes]
);
const value = useMemo(
@@ -71,12 +87,16 @@ export function ThemeProvider({
return (
- {activeThemes?.map(theme => (
-
- ))}
- {children}
+ {activeThemes == null ? null : (
+ <>
+ {activeThemes.map(theme => (
+
+ ))}
+ >
+ )}
+ {children}
);
}
diff --git a/packages/components/src/theme/ThemeUtils.test.ts b/packages/components/src/theme/ThemeUtils.test.ts
index ba34a9b329..bf53d632d8 100644
--- a/packages/components/src/theme/ThemeUtils.test.ts
+++ b/packages/components/src/theme/ThemeUtils.test.ts
@@ -191,13 +191,18 @@ describe('getDefaultBaseThemes', () => {
{
name: 'Default Dark',
themeKey: 'default-dark',
- styleContent:
- 'test-file-stub\ntest-file-stub\ntest-file-stub\ntest-file-stub',
+ styleContent: [
+ 'mock-theme-dark-palette',
+ 'mock-theme-dark-semantic',
+ 'mock-theme-dark-semantic-editor',
+ 'mock-theme-dark-semantic-grid',
+ 'mock-theme-dark-components',
+ ].join('\n'),
},
{
name: 'Default Light',
themeKey: 'default-light',
- styleContent: 'test-file-stub',
+ styleContent: 'mock-theme-light-palette',
},
]);
});
diff --git a/packages/components/src/theme/__snapshots__/ThemeProvider.test.tsx.snap b/packages/components/src/theme/__snapshots__/ThemeProvider.test.tsx.snap
index 33d1ae08e2..30808714c6 100644
--- a/packages/components/src/theme/__snapshots__/ThemeProvider.test.tsx.snap
+++ b/packages/components/src/theme/__snapshots__/ThemeProvider.test.tsx.snap
@@ -6,10 +6,17 @@ exports[`ThemeProvider setSelectedThemeKey: [ [Object] ] should change selected
-