Skip to content

Commit 534958c

Browse files
feat(content-sidebar): convert SidebarNav to TypeScript
1 parent 70d8876 commit 534958c

File tree

17 files changed

+1000
-1
lines changed

17 files changed

+1000
-1
lines changed
Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
import * as React from 'react';
2+
import { useIntl } from 'react-intl';
3+
import noop from 'lodash/noop';
4+
import { BoxAiLogo } from '@box/blueprint-web-assets/icons/Logo';
5+
import { Size6 } from '@box/blueprint-web-assets/tokens/tokens';
6+
import AdditionalTabs from './additional-tabs';
7+
import DocGenIcon from '../../icon/fill/DocGenIcon';
8+
import IconChatRound from '../../icons/general/IconChatRound';
9+
import IconDocInfo from '../../icons/general/IconDocInfo';
10+
import IconMagicWand from '../../icons/general/IconMagicWand';
11+
import IconMetadataThick from '../../icons/general/IconMetadataThick';
12+
import SidebarNavButton from './SidebarNavButton';
13+
import SidebarNavSign from './SidebarNavSign';
14+
import SidebarNavTablist from './SidebarNavTablist';
15+
import SidebarToggle from './SidebarToggle';
16+
import messages from '../common/messages';
17+
import { SIDEBAR_NAV_TARGETS } from '../common/interactionTargets';
18+
import {
19+
SIDEBAR_VIEW_ACTIVITY,
20+
SIDEBAR_VIEW_BOXAI,
21+
SIDEBAR_VIEW_DETAILS,
22+
SIDEBAR_VIEW_DOCGEN,
23+
SIDEBAR_VIEW_METADATA,
24+
SIDEBAR_VIEW_SKILLS,
25+
} from '../../constants';
26+
import { useFeatureConfig } from '../common/feature-checking';
27+
import type { NavigateOptions, AdditionalSidebarTab } from './flowTypes';
28+
import './SidebarNav.scss';
29+
30+
interface Props {
31+
additionalTabs?: AdditionalSidebarTab[];
32+
elementId: string;
33+
fileId: string;
34+
hasActivity: boolean;
35+
hasAdditionalTabs: boolean;
36+
hasBoxAI: boolean;
37+
hasDetails: boolean;
38+
hasDocGen?: boolean;
39+
hasMetadata: boolean;
40+
hasSkills: boolean;
41+
isOpen?: boolean;
42+
onNavigate?: (event: React.SyntheticEvent<HTMLElement>, options: NavigateOptions) => void;
43+
onPanelChange?: (name: string, isInitialState: boolean) => void;
44+
}
45+
46+
const SidebarNav = ({
47+
additionalTabs,
48+
elementId,
49+
fileId,
50+
hasActivity,
51+
hasAdditionalTabs,
52+
hasBoxAI,
53+
hasDetails,
54+
hasMetadata,
55+
hasSkills,
56+
hasDocGen = false,
57+
isOpen,
58+
onNavigate,
59+
onPanelChange = noop,
60+
}: Props): React.ReactElement => {
61+
const { formatMessage } = useIntl();
62+
const { enabled: hasBoxSign } = useFeatureConfig('boxSign');
63+
const { disabledTooltip: boxAIDisabledTooltip, showOnlyNavButton: showOnlyBoxAINavButton } =
64+
useFeatureConfig('boxai.sidebar');
65+
66+
const handleSidebarNavButtonClick = (sidebarview: string): void => {
67+
onPanelChange(sidebarview, false);
68+
};
69+
70+
return (
71+
<nav className="bcs-SidebarNav" aria-label={formatMessage(messages.sidebarNavLabel)} data-testid="sidebar-nav">
72+
<div className="bcs-SidebarNav-tabs" data-testid="sidebar-nav-tabs">
73+
<SidebarNavTablist elementId={elementId} isOpen={isOpen} onNavigate={onNavigate}>
74+
{hasBoxAI && (
75+
<SidebarNavButton
76+
data-resin-target={SIDEBAR_NAV_TARGETS.BOXAI}
77+
data-target-id="SidebarNavButton-boxAI"
78+
data-testid="sidebarboxai"
79+
isDisabled={showOnlyBoxAINavButton}
80+
onClick={handleSidebarNavButtonClick}
81+
sidebarView={SIDEBAR_VIEW_BOXAI}
82+
tooltip={
83+
showOnlyBoxAINavButton
84+
? boxAIDisabledTooltip
85+
: formatMessage(messages.sidebarBoxAITitle)
86+
}
87+
>
88+
<BoxAiLogo height={Size6} width={Size6} />
89+
</SidebarNavButton>
90+
)}
91+
{hasActivity && (
92+
<SidebarNavButton
93+
data-resin-target={SIDEBAR_NAV_TARGETS.ACTIVITY}
94+
data-target-id="SidebarNavButton-activity"
95+
data-testid="sidebaractivity"
96+
onClick={handleSidebarNavButtonClick}
97+
sidebarView={SIDEBAR_VIEW_ACTIVITY}
98+
tooltip={formatMessage(messages.sidebarActivityTitle)}
99+
>
100+
<IconChatRound className="bcs-SidebarNav-icon" />
101+
</SidebarNavButton>
102+
)}
103+
{hasDetails && (
104+
<SidebarNavButton
105+
data-resin-target={SIDEBAR_NAV_TARGETS.DETAILS}
106+
data-target-id="SidebarNavButton-details"
107+
data-testid="sidebardetails"
108+
onClick={handleSidebarNavButtonClick}
109+
sidebarView={SIDEBAR_VIEW_DETAILS}
110+
tooltip={formatMessage(messages.sidebarDetailsTitle)}
111+
>
112+
<IconDocInfo className="bcs-SidebarNav-icon" />
113+
</SidebarNavButton>
114+
)}
115+
{hasSkills && (
116+
<SidebarNavButton
117+
data-resin-target={SIDEBAR_NAV_TARGETS.SKILLS}
118+
data-target-id="SidebarNavButton-skills"
119+
data-testid="sidebarskills"
120+
onClick={handleSidebarNavButtonClick}
121+
sidebarView={SIDEBAR_VIEW_SKILLS}
122+
tooltip={formatMessage(messages.sidebarSkillsTitle)}
123+
>
124+
<IconMagicWand className="bcs-SidebarNav-icon" />
125+
</SidebarNavButton>
126+
)}
127+
{hasMetadata && (
128+
<SidebarNavButton
129+
data-resin-target={SIDEBAR_NAV_TARGETS.METADATA}
130+
data-target-id="SidebarNavButton-metadata"
131+
data-testid="sidebarmetadata"
132+
onClick={handleSidebarNavButtonClick}
133+
sidebarView={SIDEBAR_VIEW_METADATA}
134+
tooltip={formatMessage(messages.sidebarMetadataTitle)}
135+
>
136+
<IconMetadataThick className="bcs-SidebarNav-icon" />
137+
</SidebarNavButton>
138+
)}
139+
{hasDocGen && (
140+
<SidebarNavButton
141+
data-resin-target={SIDEBAR_NAV_TARGETS.DOCGEN}
142+
data-target-id="SidebarNavButton-docGen"
143+
onClick={handleSidebarNavButtonClick}
144+
sidebarView={SIDEBAR_VIEW_DOCGEN}
145+
tooltip={formatMessage(messages.sidebarDocGenTooltip)}
146+
>
147+
<DocGenIcon className="bcs-SidebarNav-icon" />
148+
</SidebarNavButton>
149+
)}
150+
</SidebarNavTablist>
151+
152+
{hasBoxSign && (
153+
<div className="bcs-SidebarNav-secondary">
154+
<SidebarNavSign />
155+
</div>
156+
)}
157+
158+
{hasAdditionalTabs && (
159+
<div className="bcs-SidebarNav-overflow">
160+
<AdditionalTabs key={fileId} tabs={additionalTabs} />
161+
</div>
162+
)}
163+
</div>
164+
<div className="bcs-SidebarNav-footer">
165+
<SidebarToggle isOpen={isOpen} />
166+
</div>
167+
</nav>
168+
);
169+
};
170+
171+
export default SidebarNav;
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import * as React from 'react';
2+
import { Route } from 'react-router-dom';
3+
import noop from 'lodash/noop';
4+
import NavButton from '../common/nav-button';
5+
import Tooltip, { TooltipPosition } from '../../components/tooltip/Tooltip';
6+
import './SidebarNavButton.scss';
7+
8+
interface Props {
9+
'data-resin-target'?: string;
10+
'data-testid'?: string;
11+
children: React.ReactNode;
12+
elementId?: string;
13+
isDisabled?: boolean;
14+
isOpen?: boolean;
15+
onClick?: (sidebarView: string) => void;
16+
sidebarView: string;
17+
tooltip: React.ReactNode;
18+
}
19+
20+
const SidebarNavButton = React.forwardRef<HTMLButtonElement, Props>((props, ref) => {
21+
const {
22+
'data-resin-target': dataResinTarget,
23+
'data-testid': dataTestId,
24+
children,
25+
elementId = '',
26+
isDisabled,
27+
isOpen,
28+
onClick = noop,
29+
sidebarView,
30+
tooltip,
31+
} = props;
32+
const sidebarPath = `/${sidebarView}`;
33+
34+
const handleNavButtonClick = () => {
35+
onClick(sidebarView);
36+
};
37+
38+
return (
39+
<Route path={sidebarPath}>
40+
{({ match }) => {
41+
const isMatch = !!match;
42+
const isActive = () => isMatch && !!isOpen;
43+
const isActiveValue = isActive();
44+
const isExactMatch = isMatch && match.isExact;
45+
const id = `${elementId}${elementId === '' ? '' : '_'}${sidebarView}`;
46+
47+
return (
48+
<Tooltip position={TooltipPosition.MIDDLE_LEFT} text={tooltip} isTabbable={false}>
49+
<NavButton
50+
activeClassName="bcs-is-selected"
51+
aria-selected={isActiveValue}
52+
aria-controls={`${id}-content`}
53+
aria-label={tooltip}
54+
className="bcs-NavButton"
55+
data-resin-target={dataResinTarget}
56+
data-testid={dataTestId}
57+
getDOMRef={ref}
58+
id={id}
59+
isActive={isActive}
60+
isDisabled={isDisabled}
61+
onClick={handleNavButtonClick}
62+
replace={isExactMatch}
63+
role="tab"
64+
tabIndex={isActiveValue ? 0 : -1}
65+
to={{
66+
pathname: sidebarPath,
67+
state: { open: true },
68+
}}
69+
type="button"
70+
>
71+
{children}
72+
</NavButton>
73+
</Tooltip>
74+
);
75+
}}
76+
</Route>
77+
);
78+
});
79+
80+
export default SidebarNavButton;
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import * as React from 'react';
2+
import { useHistory } from 'react-router-dom';
3+
import { KEYS } from '../../constants';
4+
import type { NavigateOptions } from './flowTypes';
5+
6+
interface Props {
7+
children: React.ReactNode;
8+
elementId: string;
9+
isOpen?: boolean;
10+
onNavigate?: (event: React.SyntheticEvent<HTMLElement>, options: NavigateOptions) => void;
11+
}
12+
13+
const SidebarNavTablist = ({ children, elementId, isOpen, onNavigate }: Props): React.ReactElement => {
14+
const history = useHistory();
15+
const refs: Array<HTMLElement | null> = [];
16+
const tablist = React.Children.map(children, child => {
17+
if (!child || !React.isValidElement(child)) return null;
18+
return `/${child.props.sidebarView}`;
19+
});
20+
21+
const handleKeyDown = (event: React.KeyboardEvent<HTMLDivElement>): void => {
22+
const currentIndex = tablist ? tablist.indexOf(history.location.pathname) : -1;
23+
const length = tablist ? tablist.length : 0;
24+
let nextIndex = currentIndex;
25+
26+
switch (event.key) {
27+
case KEYS.arrowUp:
28+
nextIndex = (currentIndex - 1 + length) % length;
29+
30+
if (tablist) {
31+
const path = tablist[nextIndex];
32+
if (path) {
33+
history.push({ pathname: path });
34+
}
35+
if (refs.length > 0 && refs[nextIndex]) {
36+
refs[nextIndex]?.focus();
37+
}
38+
}
39+
40+
event.stopPropagation();
41+
event.preventDefault();
42+
break;
43+
case KEYS.arrowDown:
44+
nextIndex = (currentIndex + 1) % length;
45+
46+
if (tablist) {
47+
const path = tablist[nextIndex];
48+
if (path) {
49+
history.push({ pathname: path });
50+
}
51+
if (refs.length > 0 && refs[nextIndex]) {
52+
refs[nextIndex]?.focus();
53+
}
54+
}
55+
56+
event.stopPropagation();
57+
event.preventDefault();
58+
break;
59+
default:
60+
break;
61+
}
62+
};
63+
64+
return (
65+
<div
66+
aria-orientation="vertical"
67+
className="bcs-SidebarNav-main"
68+
role="tablist"
69+
tabIndex={0}
70+
onKeyDown={handleKeyDown}
71+
>
72+
{React.Children.map(children, tab => {
73+
if (!tab) {
74+
return null;
75+
}
76+
77+
if (!React.isValidElement(tab)) return null;
78+
return React.cloneElement(tab, {
79+
elementId,
80+
isOpen,
81+
onNavigate,
82+
ref: (ref: HTMLElement | null) => {
83+
refs.push(ref);
84+
},
85+
...tab.props,
86+
});
87+
})}
88+
</div>
89+
);
90+
};
91+
92+
export default SidebarNavTablist;

0 commit comments

Comments
 (0)