Skip to content

Commit 2b136e4

Browse files
feat(preview-optimizations): modernize Versions sidebar (#4381)
* feat(preview-optimizations): modernize Versions sidebar * feat(preview-optimizations): move toggle styles into BUIE --------- Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
1 parent 5db66df commit 2b136e4

File tree

9 files changed

+209
-300
lines changed

9 files changed

+209
-300
lines changed

src/elements/content-sidebar/versions/VersionsItem.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ const VersionsItem = ({
130130
onClick={handleAction(onPreview)}
131131
>
132132
<div className="bcs-VersionsItem-badge">
133-
<VersionsItemBadge versionNumber={versionNumber} />
133+
<VersionsItemBadge isCurrent={isCurrent} versionNumber={versionNumber} />
134134
</div>
135135

136136
<div className="bcs-VersionsItem-details">

src/elements/content-sidebar/versions/VersionsItem.scss

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,12 @@
2828
box-shadow: $bdl-btn-primary-box-shadow;
2929
}
3030
}
31+
32+
.bcs-VersionsItemActions-toggle--modernized {
33+
position: absolute;
34+
top: var(--space-2);
35+
right: var(--space-2);
36+
}
3137
}
3238

3339
.bcs-VersionsItem-badge {

src/elements/content-sidebar/versions/VersionsItemActions.js

Lines changed: 33 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,10 @@
55
*/
66

77
import * as React from 'react';
8-
import { FormattedMessage } from 'react-intl';
8+
import { FormattedMessage, useIntl } from 'react-intl';
9+
import { IconButton } from '@box/blueprint-web';
10+
import { Ellipsis } from '@box/blueprint-web-assets/icons/Fill';
11+
import { useFeatureConfig } from '../../common/feature-checking/hooks';
912
import DropdownMenu from '../../../components/dropdown-menu';
1013
import IconClockPast from '../../../icons/general/IconClockPast';
1114
import IconDownload from '../../../icons/general/IconDownload';
@@ -59,6 +62,9 @@ const VersionsItemActions = ({
5962
showPromote = false,
6063
showRestore = false,
6164
}: Props) => {
65+
const { enabled: isPreviewModernizationEnabled } = useFeatureConfig('previewModernization');
66+
const { formatMessage } = useIntl();
67+
6268
if (!showDelete && !showDownload && !showPreview && !showPromote && !showRestore) {
6369
return null;
6470
}
@@ -71,19 +77,32 @@ const VersionsItemActions = ({
7177
isRightAligned
7278
onMenuClose={handleMenuClose}
7379
>
74-
<PlainButton
75-
className="bcs-VersionsItemActions-toggle"
76-
data-resin-iscurrent={isCurrent}
77-
data-resin-itemid={fileId}
78-
data-resin-target="overflow"
79-
onClick={handleToggleClick}
80-
type="button"
81-
>
82-
<IconEllipsis height={4} width={14} />
83-
<FormattedMessage {...messages.versionActionToggle}>
84-
{(text: string) => <span className="accessibility-hidden">{text}</span>}
85-
</FormattedMessage>
86-
</PlainButton>
80+
{isPreviewModernizationEnabled ? (
81+
<IconButton
82+
aria-label={formatMessage(messages.versionActionToggle)}
83+
className="bcs-VersionsItemActions-toggle--modernized"
84+
data-resin-iscurrent={isCurrent}
85+
data-resin-itemid={fileId}
86+
data-resin-target="overflow"
87+
icon={Ellipsis}
88+
size="small"
89+
onClick={handleToggleClick}
90+
/>
91+
) : (
92+
<PlainButton
93+
className="bcs-VersionsItemActions-toggle"
94+
data-resin-iscurrent={isCurrent}
95+
data-resin-itemid={fileId}
96+
data-resin-target="overflow"
97+
onClick={handleToggleClick}
98+
type="button"
99+
>
100+
<IconEllipsis height={4} width={14} />
101+
<FormattedMessage {...messages.versionActionToggle}>
102+
{(text: string) => <span className="accessibility-hidden">{text}</span>}
103+
</FormattedMessage>
104+
</PlainButton>
105+
)}
87106

88107
<Menu
89108
className="bcs-VersionsItemActions-menu"

src/elements/content-sidebar/versions/VersionsItemBadge.js

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,26 @@
55
*/
66
import * as React from 'react';
77
import { FormattedMessage, injectIntl } from 'react-intl';
8+
import classNames from 'classnames';
89
import messages from './messages';
910
import './VersionsItemBadge.scss';
1011

1112
type Props = {
1213
intl: any,
13-
versionNumber: string,
14+
isCurrent?: boolean,
15+
versionNumber?: string,
1416
};
1517

16-
const VersionsItemBadge = ({ intl, versionNumber }: Props) => {
18+
const VersionsItemBadge = ({ intl, isCurrent, versionNumber }: Props) => {
1719
const intlValues = { versionNumber };
1820

1921
return (
20-
<div aria-label={intl.formatMessage(messages.versionNumberLabel, intlValues)} className="bcs-VersionsItemBadge">
22+
<div
23+
aria-label={intl.formatMessage(messages.versionNumberLabel, intlValues)}
24+
className={classNames('bcs-VersionsItemBadge', {
25+
'bcs-VersionsItemBadge--current': isCurrent,
26+
})}
27+
>
2128
<FormattedMessage {...messages.versionNumberBadge} values={intlValues} />
2229
</div>
2330
);

src/elements/content-sidebar/versions/VersionsList.js

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@
66

77
import * as React from 'react';
88
import { Route } from 'react-router-dom';
9+
import classNames from 'classnames';
910
import VersionsItem from './VersionsItem';
11+
import { useFeatureConfig } from '../../common/feature-checking/hooks';
1012
import type { BoxItemVersion } from '../../../common/types/core';
1113
import type { InternalSidebarNavigation } from '../../common/types/SidebarNavigation';
1214
import './VersionsList.scss';
@@ -22,6 +24,8 @@ type Props = {
2224
};
2325

2426
const VersionsList = ({ currentId, internalSidebarNavigation, routerDisabled = false, versions, ...rest }: Props) => {
27+
const { enabled: isPreviewModernizationEnabled } = useFeatureConfig('previewModernization');
28+
2529
const renderVersionItemWithoutRouter = (version: BoxItemVersion) => (
2630
<VersionsItem
2731
isCurrent={currentId === version.id}
@@ -45,7 +49,11 @@ const VersionsList = ({ currentId, internalSidebarNavigation, routerDisabled = f
4549
);
4650

4751
return (
48-
<ul className="bcs-VersionsList">
52+
<ul
53+
className={classNames('bcs-VersionsList', {
54+
'bcs-VersionsList--modernized': isPreviewModernizationEnabled,
55+
})}
56+
>
4957
{versions.map(version => (
5058
<li className="bcs-VersionsList-item" key={version.id}>
5159
{routerDisabled ? renderVersionItemWithoutRouter(version) : renderVersionItemWithRouter(version)}
Lines changed: 118 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,134 @@
11
import * as React from 'react';
2-
import { shallow } from 'enzyme/build';
3-
import IconClockPast from '../../../../icons/general/IconClockPast';
4-
import IconDownload from '../../../../icons/general/IconDownload';
5-
import IconEllipsis from '../../../../icons/general/IconEllipsis';
6-
import IconOpenWith from '../../../../icons/general/IconOpenWith';
7-
import IconTrash from '../../../../icons/general/IconTrash';
8-
import IconUpload from '../../../../icons/general/IconUpload';
9-
import Tooltip from '../../../../components/tooltip/Tooltip';
2+
import { userEvent } from '@testing-library/user-event';
3+
import { screen, render } from '../../../../test-utils/testing-library';
104
import VersionsItemActions from '../VersionsItemActions';
115

126
describe('elements/content-sidebar/versions/VersionsItemActions', () => {
13-
const getWrapper = (props = {}) => shallow(<VersionsItemActions isDownloadable isPreviewable {...props} />);
7+
const defaultProps = {
8+
fileId: '12345',
9+
};
10+
11+
const renderComponent = (props = {}, features = {}) =>
12+
render(<VersionsItemActions {...defaultProps} {...props} />, {
13+
wrapperProps: { features },
14+
});
1415

1516
describe('render', () => {
16-
test.each([true, false])('should return the correct menu items based on options', option => {
17-
const wrapper = getWrapper({
18-
showDelete: option,
19-
showDownload: option,
20-
showPreview: option,
21-
showPromote: option,
22-
showRestore: option,
17+
test('should return null when no actions are shown', () => {
18+
const { container } = renderComponent({
19+
showDelete: false,
20+
showDownload: false,
21+
showPreview: false,
22+
showPromote: false,
23+
showRestore: false,
24+
});
25+
26+
expect(container).toBeEmptyDOMElement();
27+
});
28+
29+
test('should render actions toggle button when at least one action is shown', () => {
30+
renderComponent({ showDownload: true });
31+
32+
expect(screen.getByRole('button', { name: 'Toggle Actions Menu' })).toBeInTheDocument();
33+
});
34+
35+
test.each([
36+
{ actionProp: 'showPreview', label: 'Preview' },
37+
{ actionProp: 'showDownload', label: 'Download' },
38+
{ actionProp: 'showPromote', label: 'Make Current' },
39+
{ actionProp: 'showRestore', label: 'Restore' },
40+
{ actionProp: 'showDelete', label: 'Delete' },
41+
])('should render $label action when $actionProp is true', async ({ actionProp, label }) => {
42+
renderComponent({ [actionProp]: true });
43+
44+
const toggleButton = screen.getByRole('button', { name: 'Toggle Actions Menu' });
45+
await userEvent.click(toggleButton);
46+
47+
expect(screen.getByRole('menuitem', { name: new RegExp(label) })).toBeInTheDocument();
48+
});
49+
50+
test('should render all actions when all show props are true', async () => {
51+
renderComponent({
52+
showDelete: true,
53+
showDownload: true,
54+
showPreview: true,
55+
showPromote: true,
56+
showRestore: true,
2357
});
2458

25-
expect(wrapper.find(IconEllipsis).exists()).toBe(option); // Versions show actions if any permission is true
26-
expect(wrapper.find(IconClockPast).exists()).toBe(option);
27-
expect(wrapper.find(IconDownload).exists()).toBe(option);
28-
expect(wrapper.find(IconOpenWith).exists()).toBe(option);
29-
expect(wrapper.find(IconTrash).exists()).toBe(option);
30-
expect(wrapper.find(IconUpload).exists()).toBe(option);
31-
expect(wrapper).toMatchSnapshot();
59+
const toggleButton = screen.getByRole('button', { name: 'Toggle Actions Menu' });
60+
await userEvent.click(toggleButton);
61+
62+
expect(screen.getByRole('menuitem', { name: /Preview/ })).toBeInTheDocument();
63+
expect(screen.getByRole('menuitem', { name: /Download/ })).toBeInTheDocument();
64+
expect(screen.getByRole('menuitem', { name: /Make Current/ })).toBeInTheDocument();
65+
expect(screen.getByRole('menuitem', { name: /Restore/ })).toBeInTheDocument();
66+
expect(screen.getByRole('menuitem', { name: /Delete/ })).toBeInTheDocument();
3267
});
3368

34-
test.each([true, false])('should enable/disable actions and tooltips if isRetained is %s', option => {
35-
const wrapper = getWrapper({
36-
isRetained: option,
69+
test('should disable delete action when isRetained is true', async () => {
70+
renderComponent({
71+
isRetained: true,
3772
showDelete: true,
3873
});
3974

40-
expect(wrapper.find(Tooltip).prop('isDisabled')).toBe(!option);
41-
expect(wrapper).toMatchSnapshot();
75+
const toggleButton = screen.getByRole('button', { name: 'Toggle Actions Menu' });
76+
await userEvent.click(toggleButton);
77+
78+
const deleteButton = screen.getByRole('menuitem', { name: /Delete/ });
79+
expect(deleteButton).toHaveAttribute('aria-disabled', 'true');
80+
});
81+
82+
test('should not disable delete action when isRetained is false', async () => {
83+
renderComponent({
84+
isRetained: false,
85+
showDelete: true,
86+
});
87+
88+
const toggleButton = screen.getByRole('button', { name: 'Toggle Actions Menu' });
89+
await userEvent.click(toggleButton);
90+
91+
const deleteButton = screen.getByRole('menuitem', { name: /Delete/ });
92+
expect(deleteButton).not.toHaveAttribute('aria-disabled', 'true');
93+
});
94+
95+
describe('with previewModernization feature enabled', () => {
96+
const previewModernizationFeatures = {
97+
previewModernization: { enabled: true },
98+
};
99+
100+
test('should render modernized toggle button', () => {
101+
renderComponent({ showDownload: true }, previewModernizationFeatures);
102+
103+
const toggleButton = screen.getByRole('button', { name: 'Toggle Actions Menu' });
104+
expect(toggleButton).toHaveClass('bcs-VersionsItemActions-toggle--modernized');
105+
});
106+
107+
test('should render actions in dropdown menu', async () => {
108+
renderComponent(
109+
{
110+
showDownload: true,
111+
showPreview: true,
112+
},
113+
previewModernizationFeatures,
114+
);
115+
116+
const toggleButton = screen.getByRole('button', { name: 'Toggle Actions Menu' });
117+
await userEvent.click(toggleButton);
118+
119+
expect(screen.getByRole('menuitem', { name: /Download/ })).toBeInTheDocument();
120+
expect(screen.getByRole('menuitem', { name: /Preview/ })).toBeInTheDocument();
121+
});
122+
});
123+
124+
describe('with previewModernization feature disabled', () => {
125+
test('should render legacy toggle button', () => {
126+
renderComponent({ showDownload: true });
127+
128+
const toggleButton = screen.getByRole('button', { name: 'Toggle Actions Menu' });
129+
expect(toggleButton).toHaveClass('bcs-VersionsItemActions-toggle');
130+
expect(toggleButton).not.toHaveClass('bcs-VersionsItemActions-toggle--modernized');
131+
});
42132
});
43133
});
44134
});
Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,41 @@
11
import * as React from 'react';
2-
import { render } from 'enzyme/build';
2+
import { screen, render } from '../../../../test-utils/testing-library';
33
import VersionsItemBadge from '../VersionsItemBadge';
44

55
describe('elements/content-sidebar/versions/VersionsItemBadge', () => {
6-
const getWrapper = (props = {}) => render(<VersionsItemBadge {...props} />);
6+
const renderComponent = (props = {}) => render(<VersionsItemBadge {...props} />);
77

88
describe('render', () => {
9-
test('should match its snapshot', () => {
10-
const wrapper = getWrapper({ versionNumber: '1' });
11-
expect(wrapper).toMatchSnapshot();
9+
test('should render version number badge', () => {
10+
renderComponent({ versionNumber: '1' });
11+
12+
expect(screen.getByText('V1')).toBeInTheDocument();
13+
});
14+
15+
test('should have correct aria-label', () => {
16+
renderComponent({ versionNumber: '5' });
17+
18+
expect(screen.getByLabelText('Version number 5')).toBeInTheDocument();
1219
});
20+
21+
test.each`
22+
isCurrent | shouldHaveCurrentClass
23+
${true} | ${true}
24+
${false} | ${false}
25+
${undefined} | ${false}
26+
`(
27+
'should apply current class correctly when isCurrent is $isCurrent',
28+
({ isCurrent, shouldHaveCurrentClass }) => {
29+
renderComponent({ versionNumber: '1', isCurrent });
30+
31+
const badge = screen.getByText('V1');
32+
expect(badge).toHaveClass('bcs-VersionsItemBadge');
33+
if (shouldHaveCurrentClass) {
34+
expect(badge).toHaveClass('bcs-VersionsItemBadge--current');
35+
} else {
36+
expect(badge).not.toHaveClass('bcs-VersionsItemBadge--current');
37+
}
38+
},
39+
);
1340
});
1441
});

0 commit comments

Comments
 (0)