Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Theming - Spectrum Provider #1582

Merged
merged 17 commits into from
Oct 24, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why 120px? Name the constant to give it some more context.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's arbitrary. It just "felt" right. I'll name it in next PR

}
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