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

DOP-5259: interactive method selector for offline docs #1331

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
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
21 changes: 19 additions & 2 deletions src/components/MethodSelector/MethodOptionContent.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,27 @@ import React from 'react';
import { css, cx } from '@leafygreen-ui/emotion';
import ComponentFactory from '../ComponentFactory';
import { theme } from '../../theme/docsTheme';
import { isOfflineDocsBuild } from '../../utils/is-offline-docs-build';
import { OFFLINE_CONTENT_CLASSNAME } from '../../utils/head-scripts/offline-ui/method-selector';
import MethodDescription from './MethodDescription';

const METHOD_DESCRIPTION_NAME = 'method-description';

const displayStyle = (isSelectedOption) => css`
${!isSelectedOption && 'display: none;'}
${!isSelectedOption && !isOfflineDocsBuild && 'display: none;'}
`;

export const getTestId = (optionId) => `method-option-content-${optionId}`;

const containerStyle = css`
margin-top: ${theme.size.default};

${isOfflineDocsBuild &&
`
&[aria-expanded=false] {
display: none;
}
`}
`;

const MethodOptionContent = ({
Expand All @@ -27,7 +36,15 @@ const MethodOptionContent = ({
const methodDescription = children.find(({ name }) => name === METHOD_DESCRIPTION_NAME);

return (
<div className={cx(containerStyle, displayStyle(isSelectedOption))} data-testid={getTestId(id)}>
<div
aria-expanded={isSelectedOption}
className={cx(
containerStyle,
displayStyle(isSelectedOption),
isOfflineDocsBuild ? OFFLINE_CONTENT_CLASSNAME : ''
)}
data-testid={getTestId(id)}
>
{methodDescription && <MethodDescription nodeData={methodDescription} />}
{children.map((node, index) => {
if (node.name === METHOD_DESCRIPTION_NAME) return null;
Expand Down
38 changes: 32 additions & 6 deletions src/components/MethodSelector/MethodSelector.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import { theme } from '../../theme/docsTheme';
import { getLocalValue, setLocalValue } from '../../utils/browser-storage';
import { reportAnalytics } from '../../utils/report-analytics';
import { ContentsContext } from '../Contents/contents-context';
import { isOfflineDocsBuild } from '../../utils/is-offline-docs-build';
import { OFFLINE_CLASSNAME } from '../../utils/head-scripts/offline-ui/method-selector';
import MethodOptionContent from './MethodOptionContent';

const STORAGE_KEY = 'methodSelectorId';
Expand Down Expand Up @@ -50,8 +52,22 @@ const radioBoxStyle = css`
padding: 14px 12px;
background-color: inherit;
color: inherit;

${isOfflineDocsBuild &&
`
border-color: ${palette.gray.base};
box-shadow: none;
`}
}

${isOfflineDocsBuild &&
`
&[aria-selected=true] div {
border-color: transparent;
box-shadow: 0 0 0 3px ${palette.green.dark1};
}
`}

:not(:last-of-type) {
margin: 0;
}
Expand Down Expand Up @@ -129,7 +145,7 @@ const MethodSelector = ({ nodeData: { children } }) => {

return (
<>
<div className={optionsContainer}>
<div className={cx(optionsContainer, isOfflineDocsBuild ? OFFLINE_CLASSNAME : '')}>
<RadioBoxGroup
className={cx(radioBoxGroupStyle(children.length))}
size={'full'}
Expand All @@ -146,17 +162,27 @@ const MethodSelector = ({ nodeData: { children } }) => {
>
{children.map(({ options: { title, id } }, index) => {
return (
<RadioBox key={id} className={cx(radioBoxStyle)} value={`${id}-${index}`} checked={selectedMethod === id}>
<RadioBox
id={id}
key={id}
className={cx(radioBoxStyle)}
value={`${id}-${index}`}
checked={selectedMethod === id}
aria-selected={isOfflineDocsBuild ? selectedMethod === id : null}
>
{title}
</RadioBox>
);
})}
</RadioBoxGroup>
{/* Keep separate div for triangle to allow for relative positioning */}
<div className={cx(lineStyle)}>
<div className={cx(triangleStyle(optionCount, selectedIdx))} />
<hr className={cx(hrStyle)} />
</div>
{/* Offline docs will not have this triangle indicator */}
{!isOfflineDocsBuild && (
<div className={cx(lineStyle)}>
<div className={cx(triangleStyle(optionCount, selectedIdx))} />
<hr className={cx(hrStyle)} />
</div>
)}
</div>
{children.map((child, index) => {
if (child.name !== 'method-option') return null;
Expand Down
12 changes: 9 additions & 3 deletions src/utils/head-scripts/offline-ui/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import bindTabUI from './tabs';
import bindCollapsibleUI from './collapsible';
import updateSidenavHeight from './sidenav';
import bindTabsSelectorsUI from './tabs-selectors';
import bindMethodSelectorUI from './method-selector';

const OFFLINE_UI_CLASSNAME = 'snooty-offline-ui';

const getScript = ({ key, fn }) => (
Expand All @@ -13,6 +15,10 @@ const getScript = ({ key, fn }) => (
/>
);

export const OFFLINE_HEAD_SCRIPTS = [bindTabUI, updateSidenavHeight, bindTabsSelectorsUI, bindCollapsibleUI].map(
(fn, idx) => getScript({ key: `offline-docs-ui-${idx}`, fn })
);
export const OFFLINE_HEAD_SCRIPTS = [
bindTabUI,
updateSidenavHeight,
bindTabsSelectorsUI,
bindCollapsibleUI,
bindMethodSelectorUI,
].map((fn, idx) => getScript({ key: `offline-docs-ui-${idx}`, fn }));
51 changes: 51 additions & 0 deletions src/utils/head-scripts/offline-ui/method-selector.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
function bindMethodSelectorUI() {
const onContentLoaded = () => {
try {
// find all method selectors
const methodSelectorComponents = document.querySelectorAll('.offline-method-selector');
for (const methodSelectorComponent of methodSelectorComponents) {
// find the button group role=group
const buttonGroup = methodSelectorComponent.querySelector('[role=group]');

// find the radio box inputs within button group and bind action
const buttons = buttonGroup?.querySelectorAll('input') ?? [];

// find all the content within method selectors
const contentDivs =
methodSelectorComponent.parentElement?.querySelectorAll('.offline-method-selector-content') ?? [];

// find all the labels to style for selected
const labels = methodSelectorComponent.querySelectorAll('label');

// for each input, find value `{name}-{index}` ie. `driver-0`
// find content div with data-testid=[method-option-content-{name}] and show that content div
for (let idx = 0; idx < buttons.length; idx++) {
const button = buttons[idx];
button.addEventListener('click', (e) => {
const id = (e.currentTarget.getAttribute('value') || '').split('-')[0];
const targetTestId = 'method-option-content-' + id;
const parentLabel = button.parentElement;
for (const contentDiv of contentDivs) {
contentDiv.setAttribute('aria-expanded', targetTestId === contentDiv.getAttribute('data-testid'));
}
for (const label of labels) {
label.setAttribute('aria-selected', label.isSameNode(parentLabel));
}
});
}

// on load, set first label as active
labels[0]?.setAttribute('aria-selected', true);
}
} catch (e) {
console.error(e);
}
};

document.addEventListener('DOMContentLoaded', onContentLoaded, false);
}

export default bindMethodSelectorUI;

export const OFFLINE_CLASSNAME = `offline-method-selector`;
export const OFFLINE_CONTENT_CLASSNAME = `offline-method-selector-content`;
Loading