Skip to content

Commit

Permalink
feat: Theming - Spectrum Provider (#1582)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
bmingles authored Oct 24, 2023
1 parent c736708 commit a4013c0
Show file tree
Hide file tree
Showing 45 changed files with 1,330 additions and 165 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ yarn-debug.log*
yarn-error.log*

css
!__mocks__/css

tsconfig.tsbuildinfo
packages/*/package-lock.json
Expand Down
1 change: 1 addition & 0 deletions __mocks__/css/mock-theme-dark-components.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = 'mock-theme-dark-components';
1 change: 1 addition & 0 deletions __mocks__/css/mock-theme-dark-palette.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = 'mock-theme-dark-palette';
1 change: 1 addition & 0 deletions __mocks__/css/mock-theme-dark-semantic-editor.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = 'mock-theme-dark-semantic-editor';
1 change: 1 addition & 0 deletions __mocks__/css/mock-theme-dark-semantic-grid.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = 'mock-theme-dark-semantic-grid';
1 change: 1 addition & 0 deletions __mocks__/css/mock-theme-dark-semantic.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = 'mock-theme-dark-semantic';
1 change: 1 addition & 0 deletions __mocks__/css/mock-theme-light-palette.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = 'mock-theme-light-palette';
3 changes: 3 additions & 0 deletions __mocks__/css/mock-theme-spectrum-alias.module.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module.exports = {
'dh-spectrum-alias': 'mock-dh-spectrum-alias',
};
3 changes: 3 additions & 0 deletions __mocks__/css/mock-theme-spectrum-overrides.module.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module.exports = {
'dh-spectrum-overrides': 'mock-dh-spectrum-overrides',
};
3 changes: 3 additions & 0 deletions __mocks__/css/mock-theme-spectrum-palette.module.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module.exports = {
'dh-spectrum-palette': 'mock-dh-spectrum-palette',
};
3 changes: 0 additions & 3 deletions __mocks__/spectrumThemeDarkMock.js

This file was deleted.

3 changes: 0 additions & 3 deletions __mocks__/spectrumThemeLightMock.js

This file was deleted.

4 changes: 2 additions & 2 deletions jest.config.base.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
26 changes: 25 additions & 1 deletion packages/app-utils/src/components/AppBootstrap.test.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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';
Expand All @@ -33,6 +45,7 @@ jest.mock('@deephaven/jsapi-components', () => ({

const mockChildText = 'Mock Child';
const mockChild = <div>{mockChildText}</div>;
const mockPluginModuleMapEmpty: PluginModuleMap = new Map();

function expectMockChild() {
return expect(screen.queryByText(mockChildText));
Expand Down Expand Up @@ -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(
<AppBootstrap serverUrl={API_URL} pluginsUrl={PLUGINS_URL}>
Expand Down
19 changes: 19 additions & 0 deletions packages/code-studio/src/styleguide/GotoTopButton.css
Original file line number Diff line number Diff line change
@@ -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;
}
48 changes: 48 additions & 0 deletions packages/code-studio/src/styleguide/GotoTopButton.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<Button
UNSAFE_className="goto-top-button"
variant="accent"
onPress={gotoTop}
>
<Icon>
<FontAwesomeIcon icon={vsChevronUp} />
</Icon>
</Button>
);
}

export default GotoTopButton;
5 changes: 4 additions & 1 deletion packages/code-studio/src/styleguide/Icons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -106,7 +107,9 @@ function Icons(): React.ReactElement {
});
}}
>
<FontAwesomeIcon icon={icon} className="icon" />
<Icon size="L">
<FontAwesomeIcon icon={icon} />
</Icon>

<label title={prefixedName}>{prefixedName}</label>
</Button>
Expand Down
125 changes: 125 additions & 0 deletions packages/code-studio/src/styleguide/SamplesMenu.tsx
Original file line number Diff line number Diff line change
@@ -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<typeof MENU_CATEGORY_DATA_ATTRIBUTE, string>): JSX.Element {
return <div data-menu-category={dataMenuCategory} />;
}

/**
* 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<LinkCategory[]>([]);

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 (
<MenuTrigger>
<ActionButton>
<Icon>
<FontAwesomeIcon icon={vsMenu} />
</Icon>
</ActionButton>
<Menu items={links} onAction={onAction}>
{({ category, items }) => (
<Section key={category} items={items} title={category}>
{({ id, label }) => <Item key={id}>{label}</Item>}
</Section>
)}
</Menu>
</MenuTrigger>
);
}

export default SamplesMenu;
Loading

0 comments on commit a4013c0

Please sign in to comment.