box-ui-elements 23.4.0-beta.21 → 23.4.0-beta.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/explorer.js +1 -1
- package/dist/preview.js +1 -1
- package/dist/sidebar.js +1 -1
- package/es/components/flyout/OverlayHeader.js +6 -3
- package/es/components/flyout/OverlayHeader.js.map +1 -1
- package/es/elements/common/nav-router/NavRouter.js +7 -3
- package/es/elements/common/nav-router/NavRouter.js.flow +10 -1
- package/es/elements/common/nav-router/NavRouter.js.map +1 -1
- package/es/elements/common/nav-router/types.js.map +1 -1
- package/es/elements/common/nav-router/withNavRouter.js +10 -1
- package/es/elements/common/nav-router/withNavRouter.js.flow +5 -0
- package/es/elements/common/nav-router/withNavRouter.js.map +1 -1
- package/es/elements/common/routing/withRouterAndRef.js +17 -3
- package/es/elements/common/routing/withRouterAndRef.js.flow +11 -3
- package/es/elements/common/routing/withRouterAndRef.js.map +1 -1
- package/es/elements/content-explorer/stories/tests/ContentExplorer-visual.stories.js +22 -13
- package/es/elements/content-explorer/stories/tests/ContentExplorer-visual.stories.js.flow +30 -17
- package/es/elements/content-explorer/stories/tests/ContentExplorer-visual.stories.js.map +1 -1
- package/es/elements/content-sidebar/ContentSidebar.js +3 -1
- package/es/elements/content-sidebar/ContentSidebar.js.flow +2 -1
- package/es/elements/content-sidebar/ContentSidebar.js.map +1 -1
- package/es/elements/content-sidebar/SidebarNavButton.js +49 -1
- package/es/elements/content-sidebar/SidebarNavButton.js.flow +65 -3
- package/es/elements/content-sidebar/SidebarNavButton.js.map +1 -1
- package/es/elements/content-sidebar/SidebarToggle.js +27 -9
- package/es/elements/content-sidebar/SidebarToggle.js.flow +29 -6
- package/es/elements/content-sidebar/SidebarToggle.js.map +1 -1
- package/es/elements/content-sidebar/stories/tests/MetadataSidebarRedesign-visual.stories.js +12 -1
- package/es/elements/content-sidebar/stories/tests/MetadataSidebarRedesign-visual.stories.js.map +1 -1
- package/es/features/header-flyout/HeaderFlyout.js +6 -3
- package/es/features/header-flyout/HeaderFlyout.js.flow +15 -2
- package/es/features/header-flyout/HeaderFlyout.js.map +1 -1
- package/es/features/header-flyout/styles/HeaderFlyout.scss +2 -0
- package/es/src/components/flyout/OverlayHeader.d.ts +3 -1
- package/es/src/elements/common/nav-router/NavRouter.d.ts +3 -1
- package/es/src/elements/common/nav-router/types.d.ts +2 -0
- package/package.json +1 -1
- package/src/components/flyout/OverlayHeader.tsx +7 -3
- package/src/components/flyout/__tests__/OverlayHeader.test.js +25 -0
- package/src/elements/common/nav-router/NavRouter.js.flow +10 -1
- package/src/elements/common/nav-router/NavRouter.tsx +9 -3
- package/src/elements/common/nav-router/__tests__/withNavRouter.test.tsx +34 -20
- package/src/elements/common/nav-router/types.ts +2 -0
- package/src/elements/common/nav-router/withNavRouter.js.flow +5 -0
- package/src/elements/common/nav-router/withNavRouter.tsx +9 -1
- package/src/elements/common/routing/__tests__/withRouterAndRef.test.js +64 -12
- package/src/elements/common/routing/withRouterAndRef.js +11 -3
- package/src/elements/content-explorer/stories/tests/ContentExplorer-visual.stories.js +30 -17
- package/src/elements/content-sidebar/ContentSidebar.js +2 -1
- package/src/elements/content-sidebar/SidebarNavButton.js +65 -3
- package/src/elements/content-sidebar/SidebarToggle.js +29 -6
- package/src/elements/content-sidebar/__tests__/SidebarNavButton.test.js +155 -3
- package/src/elements/content-sidebar/__tests__/SidebarToggle.test.js +74 -10
- package/src/elements/content-sidebar/stories/tests/MetadataSidebarRedesign-visual.stories.tsx +14 -1
- package/src/features/header-flyout/HeaderFlyout.js +15 -2
- package/src/features/header-flyout/__tests__/__snapshots__/HeaderFlyout.test.js.snap +9 -3
- package/src/features/header-flyout/styles/HeaderFlyout.scss +2 -0
- package/src/elements/content-sidebar/__tests__/__snapshots__/SidebarToggle.test.js.snap +0 -19
|
@@ -1,41 +1,55 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
|
-
import {
|
|
3
|
-
import { createMemoryHistory } from 'history';
|
|
4
|
-
import NavRouter from '../NavRouter';
|
|
2
|
+
import { render } from '../../../../test-utils/testing-library';
|
|
5
3
|
import withNavRouter from '../withNavRouter';
|
|
6
4
|
import { WithNavRouterProps } from '../types';
|
|
7
5
|
|
|
6
|
+
jest.mock('../NavRouter', () => ({ children }: { children: React.ReactNode }) => (
|
|
7
|
+
<div data-testid="nav-router-wrapper">{children}</div>
|
|
8
|
+
));
|
|
9
|
+
|
|
8
10
|
type Props = {
|
|
9
11
|
value?: string;
|
|
10
12
|
};
|
|
11
13
|
|
|
12
14
|
describe('src/eleemnts/common/nav-router/withNavRouter', () => {
|
|
13
|
-
const TestComponent = ({ value }: Props) => <div>{`Test ${value}`}</div>;
|
|
15
|
+
const TestComponent = ({ value }: Props) => <div data-testid="test-component">{`Test ${value}`}</div>;
|
|
14
16
|
const WrappedComponent = withNavRouter(TestComponent);
|
|
15
17
|
|
|
16
|
-
const
|
|
18
|
+
const renderComponent = (props?: Props & WithNavRouterProps) =>
|
|
19
|
+
render(<WrappedComponent {...props} />);
|
|
17
20
|
|
|
18
21
|
test('should wrap component with NavRouter', () => {
|
|
19
|
-
const
|
|
22
|
+
const { getByTestId } = renderComponent();
|
|
23
|
+
|
|
24
|
+
expect(getByTestId('test-component')).toBeInTheDocument();
|
|
25
|
+
expect(getByTestId('test-component')).toHaveTextContent('Test undefined');
|
|
26
|
+
expect(getByTestId('nav-router-wrapper')).toBeInTheDocument();
|
|
27
|
+
});
|
|
20
28
|
|
|
21
|
-
|
|
22
|
-
|
|
29
|
+
test('should pass props to wrapped component', () => {
|
|
30
|
+
const { getByTestId } = renderComponent({ value: 'test-value' });
|
|
31
|
+
|
|
32
|
+
expect(getByTestId('test-component')).toBeInTheDocument();
|
|
33
|
+
expect(getByTestId('test-component')).toHaveTextContent('Test test-value');
|
|
23
34
|
});
|
|
24
35
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
36
|
+
describe('when routerDisabled feature flag is provided', () => {
|
|
37
|
+
test('should return unwrapped component when feature flag is true', () => {
|
|
38
|
+
const features = { routerDisabled: { value: true } };
|
|
39
|
+
const { getByTestId, queryByTestId } = renderComponent({ features });
|
|
40
|
+
|
|
41
|
+
expect(getByTestId('test-component')).toBeInTheDocument();
|
|
42
|
+
expect(getByTestId('test-component')).toHaveTextContent('Test undefined');
|
|
43
|
+
expect(queryByTestId('nav-router-wrapper')).not.toBeInTheDocument();
|
|
33
44
|
});
|
|
34
45
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
46
|
+
test('should wrap component with NavRouter when feature flag is false', () => {
|
|
47
|
+
const features = { routerDisabled: { value: false } };
|
|
48
|
+
const { getByTestId } = renderComponent({ features });
|
|
38
49
|
|
|
39
|
-
|
|
50
|
+
expect(getByTestId('test-component')).toBeInTheDocument();
|
|
51
|
+
expect(getByTestId('test-component')).toHaveTextContent('Test undefined');
|
|
52
|
+
expect(getByTestId('nav-router-wrapper')).toBeInTheDocument();
|
|
53
|
+
});
|
|
40
54
|
});
|
|
41
55
|
});
|
|
@@ -6,9 +6,14 @@
|
|
|
6
6
|
|
|
7
7
|
import React from "react";
|
|
8
8
|
import { History } from "history";
|
|
9
|
+
import { type FeatureConfig } from '../feature-checking';
|
|
9
10
|
import NavRouter from "./NavRouter";
|
|
11
|
+
|
|
10
12
|
export type WithNavRouterProps = {
|
|
13
|
+
features?: FeatureConfig,
|
|
11
14
|
history?: History,
|
|
15
|
+
initialEntries?: Array<any>,
|
|
12
16
|
...
|
|
13
17
|
};
|
|
18
|
+
|
|
14
19
|
declare export var withNavRouter: any; // /* NO PRINT IMPLEMENTED: ArrowFunction */ any
|
|
@@ -1,11 +1,19 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
2
|
import NavRouter from './NavRouter';
|
|
3
3
|
import { WithNavRouterProps } from './types';
|
|
4
|
+
import { isFeatureEnabled } from '../feature-checking';
|
|
4
5
|
|
|
5
6
|
const withNavRouter = <P extends object>(Component: React.ComponentType<P>): React.FC<P & WithNavRouterProps> => {
|
|
6
7
|
function WithNavRouter({ history, initialEntries, ...rest }: P & WithNavRouterProps) {
|
|
8
|
+
const { features } = rest;
|
|
9
|
+
const isRouterDisabled = isFeatureEnabled(features, 'routerDisabled.value');
|
|
10
|
+
|
|
11
|
+
if (isRouterDisabled) {
|
|
12
|
+
return <Component {...(rest as P)} />;
|
|
13
|
+
}
|
|
14
|
+
|
|
7
15
|
return (
|
|
8
|
-
<NavRouter history={history} initialEntries={initialEntries}>
|
|
16
|
+
<NavRouter history={history} initialEntries={initialEntries} features={features}>
|
|
9
17
|
<Component {...(rest as P)} />
|
|
10
18
|
</NavRouter>
|
|
11
19
|
);
|
|
@@ -1,26 +1,78 @@
|
|
|
1
1
|
// @flow
|
|
2
2
|
import * as React from 'react';
|
|
3
3
|
import { MemoryRouter } from 'react-router-dom';
|
|
4
|
-
import {
|
|
4
|
+
import { render } from '../../../../test-utils/testing-library';
|
|
5
5
|
import withRouterAndRef from '../withRouterAndRef';
|
|
6
6
|
|
|
7
7
|
describe('elements/common/routing/withRouterAndRef', () => {
|
|
8
8
|
type Props = {
|
|
9
9
|
value: string,
|
|
10
|
+
routerDisabled?: boolean,
|
|
10
11
|
};
|
|
11
12
|
|
|
12
|
-
const TestComponent = React.forwardRef(({ value }: Props, ref) =>
|
|
13
|
+
const TestComponent = React.forwardRef(({ value, staticContext, routerDisabled, ...props }: Props, ref) => (
|
|
14
|
+
<div ref={ref} data-testid="test-component" data-router-disabled={routerDisabled} {...props}>
|
|
15
|
+
{value}
|
|
16
|
+
</div>
|
|
17
|
+
));
|
|
18
|
+
TestComponent.displayName = 'TestComponent';
|
|
19
|
+
|
|
13
20
|
const WithRouterComponent = withRouterAndRef(TestComponent);
|
|
14
21
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
<
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
22
|
+
describe('router enabled (default)', () => {
|
|
23
|
+
test('should pass ref and router props to wrapped component', () => {
|
|
24
|
+
const ref = React.createRef();
|
|
25
|
+
const { getByTestId } = render(
|
|
26
|
+
<MemoryRouter initialEntries={['/test']}>
|
|
27
|
+
<WithRouterComponent ref={ref} value="test" />
|
|
28
|
+
</MemoryRouter>,
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
const element = getByTestId('test-component');
|
|
32
|
+
expect(ref.current).toBe(element);
|
|
33
|
+
expect(element).toHaveTextContent('test');
|
|
34
|
+
expect(element).not.toHaveAttribute('data-router-disabled');
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
describe('router disabled', () => {
|
|
39
|
+
test('should pass ref down to wrapped component without router', () => {
|
|
40
|
+
const ref = React.createRef();
|
|
41
|
+
const { getByTestId } = render(
|
|
42
|
+
<WithRouterComponent ref={ref} value="foo" routerDisabled />
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
const element = getByTestId('test-component');
|
|
46
|
+
expect(ref.current).toBe(element);
|
|
47
|
+
expect(element).toHaveTextContent('foo');
|
|
48
|
+
expect(element).toHaveAttribute('data-router-disabled', 'true');
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test('should render component directly without Route wrapper', () => {
|
|
52
|
+
const { getByTestId } = render(
|
|
53
|
+
<WithRouterComponent value="direct" routerDisabled />
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
const element = getByTestId('test-component');
|
|
57
|
+
expect(element).toHaveTextContent('direct');
|
|
58
|
+
expect(element).toHaveAttribute('data-router-disabled', 'true');
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test('should pass through all props including routerDisabled', () => {
|
|
62
|
+
const { getByTestId } = render(
|
|
63
|
+
<WithRouterComponent
|
|
64
|
+
value="test"
|
|
65
|
+
routerDisabled
|
|
66
|
+
data-custom="custom-value"
|
|
67
|
+
className="test-class"
|
|
68
|
+
/>
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
const element = getByTestId('test-component');
|
|
72
|
+
expect(element).toHaveTextContent('test');
|
|
73
|
+
expect(element).toHaveAttribute('data-custom', 'custom-value');
|
|
74
|
+
expect(element).toHaveClass('test-class');
|
|
75
|
+
expect(element).toHaveAttribute('data-router-disabled', 'true');
|
|
76
|
+
});
|
|
25
77
|
});
|
|
26
78
|
});
|
|
@@ -5,9 +5,17 @@ import { Route } from 'react-router-dom';
|
|
|
5
5
|
// Basically a workaround for the fact that react-router's withRouter cannot forward ref's through
|
|
6
6
|
// functional components. Use this instead to gain the benefits of withRouter but also ref forwarding
|
|
7
7
|
export default function withRouterAndRef(Wrapped: React.ComponentType<any>) {
|
|
8
|
-
const WithRouterAndRef = React.forwardRef<Object, React.Ref<any>>((props, ref) =>
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
const WithRouterAndRef = React.forwardRef<Object, React.Ref<any>>((props, ref) => {
|
|
9
|
+
const { routerDisabled } = props;
|
|
10
|
+
|
|
11
|
+
// If router is disabled, return component directly without Route wrapper
|
|
12
|
+
if (routerDisabled) {
|
|
13
|
+
return <Wrapped ref={ref} {...props} />;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Default behavior: wrap with Route to get router props
|
|
17
|
+
return <Route>{routeProps => <Wrapped ref={ref} {...routeProps} {...props} />}</Route>;
|
|
18
|
+
});
|
|
11
19
|
const name = Wrapped.displayName || Wrapped.name || 'Component';
|
|
12
20
|
WithRouterAndRef.displayName = `withRouterAndRef(${name})`;
|
|
13
21
|
return WithRouterAndRef;
|
|
@@ -5,6 +5,7 @@ import ContentExplorer from '../../ContentExplorer';
|
|
|
5
5
|
import { mockEmptyRootFolder, mockRootFolder } from '../../../common/__mocks__/mockRootFolder';
|
|
6
6
|
import mockSubfolder from '../../../common/__mocks__/mockSubfolder';
|
|
7
7
|
import mockRecentItems from '../../../common/__mocks__/mockRecentItems';
|
|
8
|
+
import { mockUserRequest } from '../../../common/__mocks__/mockRequests';
|
|
8
9
|
|
|
9
10
|
import { DEFAULT_HOSTNAME_API } from '../../../../constants';
|
|
10
11
|
|
|
@@ -227,10 +228,38 @@ export const closeCreateFolderDialog = {
|
|
|
227
228
|
// },
|
|
228
229
|
// };
|
|
229
230
|
|
|
231
|
+
const defaultHandlers = [
|
|
232
|
+
http.get(`${DEFAULT_HOSTNAME_API}/2.0/folders/69083462919`, () => {
|
|
233
|
+
return HttpResponse.json(mockRootFolder);
|
|
234
|
+
}),
|
|
235
|
+
http.get(`${DEFAULT_HOSTNAME_API}/2.0/folders/73426618530`, () => {
|
|
236
|
+
return HttpResponse.json(mockSubfolder);
|
|
237
|
+
}),
|
|
238
|
+
http.get(`${DEFAULT_HOSTNAME_API}/2.0/folders/74729718131`, () => {
|
|
239
|
+
return HttpResponse.json(mockEmptyRootFolder);
|
|
240
|
+
}),
|
|
241
|
+
http.get(`${DEFAULT_HOSTNAME_API}/2.0/folders/191354690948`, () => {
|
|
242
|
+
return new HttpResponse('Internal Server Error', { status: 500 });
|
|
243
|
+
}),
|
|
244
|
+
http.get(`${DEFAULT_HOSTNAME_API}/2.0/recent_items`, () => {
|
|
245
|
+
return HttpResponse.json(mockRecentItems);
|
|
246
|
+
}),
|
|
247
|
+
];
|
|
248
|
+
|
|
230
249
|
export const emptyState = {
|
|
231
250
|
args: {
|
|
232
251
|
rootFolderId: '74729718131',
|
|
233
252
|
},
|
|
253
|
+
parameters: {
|
|
254
|
+
msw: {
|
|
255
|
+
handlers: [
|
|
256
|
+
...defaultHandlers,
|
|
257
|
+
http.get(mockUserRequest.url, () => {
|
|
258
|
+
return HttpResponse.json(mockUserRequest.response);
|
|
259
|
+
}),
|
|
260
|
+
],
|
|
261
|
+
},
|
|
262
|
+
},
|
|
234
263
|
play: async ({ canvasElement }) => {
|
|
235
264
|
const canvas = within(canvasElement);
|
|
236
265
|
await waitFor(() => {
|
|
@@ -271,23 +300,7 @@ export default {
|
|
|
271
300
|
},
|
|
272
301
|
parameters: {
|
|
273
302
|
msw: {
|
|
274
|
-
handlers:
|
|
275
|
-
http.get(`${DEFAULT_HOSTNAME_API}/2.0/folders/69083462919`, () => {
|
|
276
|
-
return HttpResponse.json(mockRootFolder);
|
|
277
|
-
}),
|
|
278
|
-
http.get(`${DEFAULT_HOSTNAME_API}/2.0/folders/73426618530`, () => {
|
|
279
|
-
return HttpResponse.json(mockSubfolder);
|
|
280
|
-
}),
|
|
281
|
-
http.get(`${DEFAULT_HOSTNAME_API}/2.0/folders/74729718131`, () => {
|
|
282
|
-
return HttpResponse.json(mockEmptyRootFolder);
|
|
283
|
-
}),
|
|
284
|
-
http.get(`${DEFAULT_HOSTNAME_API}/2.0/folders/191354690948`, () => {
|
|
285
|
-
return new HttpResponse('Internal Server Error', { status: 500 });
|
|
286
|
-
}),
|
|
287
|
-
http.get(`${DEFAULT_HOSTNAME_API}/2.0/recent_items`, () => {
|
|
288
|
-
return HttpResponse.json(mockRecentItems);
|
|
289
|
-
}),
|
|
290
|
-
],
|
|
303
|
+
handlers: defaultHandlers,
|
|
291
304
|
},
|
|
292
305
|
},
|
|
293
306
|
};
|
|
@@ -349,6 +349,7 @@ class ContentSidebar extends React.Component<Props, State> {
|
|
|
349
349
|
defaultView,
|
|
350
350
|
detailsSidebarProps,
|
|
351
351
|
docGenSidebarProps,
|
|
352
|
+
features,
|
|
352
353
|
fileId,
|
|
353
354
|
getPreview,
|
|
354
355
|
getViewer,
|
|
@@ -382,7 +383,7 @@ class ContentSidebar extends React.Component<Props, State> {
|
|
|
382
383
|
return (
|
|
383
384
|
<Internationalize language={language} messages={messages}>
|
|
384
385
|
<APIContext.Provider value={(this.api: any)}>
|
|
385
|
-
<NavRouter history={history} initialEntries={[initialPath]}>
|
|
386
|
+
<NavRouter history={history} initialEntries={[initialPath]} features={features}>
|
|
386
387
|
<TooltipProvider>
|
|
387
388
|
<Sidebar
|
|
388
389
|
activitySidebarProps={activitySidebarProps}
|
|
@@ -11,6 +11,7 @@ import classNames from 'classnames';
|
|
|
11
11
|
import { Button } from '@box/blueprint-web';
|
|
12
12
|
import Tooltip from '../../components/tooltip/Tooltip';
|
|
13
13
|
import { isLeftClick } from '../../utils/dom';
|
|
14
|
+
import type { InternalSidebarNavigation, InternalSidebarNavigationHandler, ViewTypeValues } from '../common/types/SidebarNavigation';
|
|
14
15
|
import './SidebarNavButton.scss';
|
|
15
16
|
|
|
16
17
|
type Props = {
|
|
@@ -18,10 +19,13 @@ type Props = {
|
|
|
18
19
|
'data-testid'?: string,
|
|
19
20
|
children: React.Node,
|
|
20
21
|
elementId?: string,
|
|
22
|
+
internalSidebarNavigation?: InternalSidebarNavigation,
|
|
23
|
+
internalSidebarNavigationHandler?: InternalSidebarNavigationHandler,
|
|
21
24
|
isDisabled?: boolean,
|
|
22
25
|
isOpen?: boolean,
|
|
23
|
-
onClick?: (sidebarView:
|
|
24
|
-
|
|
26
|
+
onClick?: (sidebarView: ViewTypeValues) => void,
|
|
27
|
+
routerDisabled?: boolean,
|
|
28
|
+
sidebarView: ViewTypeValues,
|
|
25
29
|
tooltip: React.Node,
|
|
26
30
|
};
|
|
27
31
|
|
|
@@ -31,13 +35,72 @@ const SidebarNavButton = React.forwardRef<Props, React.Ref<any>>((props: Props,
|
|
|
31
35
|
'data-testid': dataTestId,
|
|
32
36
|
children,
|
|
33
37
|
elementId = '',
|
|
38
|
+
internalSidebarNavigation,
|
|
39
|
+
internalSidebarNavigationHandler,
|
|
34
40
|
isDisabled,
|
|
35
41
|
isOpen,
|
|
36
42
|
onClick = noop,
|
|
43
|
+
routerDisabled = false,
|
|
37
44
|
sidebarView,
|
|
38
45
|
tooltip,
|
|
39
46
|
} = props;
|
|
40
47
|
const sidebarPath = `/${sidebarView}`;
|
|
48
|
+
const id = `${elementId}${elementId === '' ? '' : '_'}${sidebarView}`;
|
|
49
|
+
|
|
50
|
+
if (routerDisabled) {
|
|
51
|
+
// Mimic router behavior using internalSidebarNavigation
|
|
52
|
+
const isMatch = !!internalSidebarNavigation && internalSidebarNavigation.sidebar === sidebarView;
|
|
53
|
+
const isActiveValue = isMatch && !!isOpen;
|
|
54
|
+
|
|
55
|
+
// Mimic isExactMatch: true when no extra navigation parameters are present
|
|
56
|
+
const hasExtraParams = internalSidebarNavigation && (
|
|
57
|
+
internalSidebarNavigation.versionId ||
|
|
58
|
+
internalSidebarNavigation.activeFeedEntryType ||
|
|
59
|
+
internalSidebarNavigation.activeFeedEntryId ||
|
|
60
|
+
internalSidebarNavigation.fileVersionId
|
|
61
|
+
);
|
|
62
|
+
const isExactMatch = isMatch && !hasExtraParams;
|
|
63
|
+
|
|
64
|
+
const handleNavButtonClick = event => {
|
|
65
|
+
onClick(sidebarView);
|
|
66
|
+
|
|
67
|
+
// Mimic router navigation behavior
|
|
68
|
+
if (internalSidebarNavigationHandler && !event.defaultPrevented && isLeftClick(event)) {
|
|
69
|
+
const replace = isExactMatch;
|
|
70
|
+
internalSidebarNavigationHandler({
|
|
71
|
+
sidebar: sidebarView,
|
|
72
|
+
open: true,
|
|
73
|
+
}, replace);
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
return (
|
|
78
|
+
<Tooltip position="middle-left" text={tooltip} isTabbable={false}>
|
|
79
|
+
<Button
|
|
80
|
+
accessibleWhenDisabled={true}
|
|
81
|
+
aria-controls={`${id}-content`}
|
|
82
|
+
aria-label={tooltip}
|
|
83
|
+
aria-selected={isActiveValue}
|
|
84
|
+
className={classNames('bcs-NavButton', {
|
|
85
|
+
'bcs-is-selected': isActiveValue,
|
|
86
|
+
'bdl-is-disabled': isDisabled,
|
|
87
|
+
})}
|
|
88
|
+
data-resin-target={dataResinTarget}
|
|
89
|
+
data-testid={dataTestId}
|
|
90
|
+
ref={ref}
|
|
91
|
+
id={id}
|
|
92
|
+
disabled={isDisabled}
|
|
93
|
+
onClick={handleNavButtonClick}
|
|
94
|
+
role="tab"
|
|
95
|
+
tabIndex={isActiveValue ? '0' : '-1'}
|
|
96
|
+
type="button"
|
|
97
|
+
variant="tertiary"
|
|
98
|
+
>
|
|
99
|
+
{children}
|
|
100
|
+
</Button>
|
|
101
|
+
</Tooltip>
|
|
102
|
+
);
|
|
103
|
+
}
|
|
41
104
|
|
|
42
105
|
return (
|
|
43
106
|
<Route path={sidebarPath}>
|
|
@@ -45,7 +108,6 @@ const SidebarNavButton = React.forwardRef<Props, React.Ref<any>>((props: Props,
|
|
|
45
108
|
const isMatch = !!match;
|
|
46
109
|
const isActiveValue = isMatch && !!isOpen;
|
|
47
110
|
const isExactMatch = isMatch && match.isExact;
|
|
48
|
-
const id = `${elementId}${elementId === '' ? '' : '_'}${sidebarView}`;
|
|
49
111
|
|
|
50
112
|
const handleNavButtonClick = event => {
|
|
51
113
|
onClick(sidebarView);
|
|
@@ -8,23 +8,46 @@ import * as React from 'react';
|
|
|
8
8
|
import { withRouter, type RouterHistory } from 'react-router-dom';
|
|
9
9
|
import SidebarToggleButton from '../../components/sidebar-toggle-button/SidebarToggleButton';
|
|
10
10
|
import { SIDEBAR_NAV_TARGETS } from '../common/interactionTargets';
|
|
11
|
+
import type { InternalSidebarNavigation, InternalSidebarNavigationHandler } from '../common/types/SidebarNavigation';
|
|
11
12
|
|
|
12
13
|
type Props = {
|
|
13
|
-
history
|
|
14
|
+
history?: RouterHistory,
|
|
15
|
+
internalSidebarNavigation?: InternalSidebarNavigation,
|
|
16
|
+
internalSidebarNavigationHandler?: InternalSidebarNavigationHandler,
|
|
14
17
|
isOpen?: boolean,
|
|
18
|
+
routerDisabled?: boolean,
|
|
15
19
|
};
|
|
16
20
|
|
|
17
|
-
const SidebarToggle = ({
|
|
21
|
+
const SidebarToggle = ({
|
|
22
|
+
history,
|
|
23
|
+
internalSidebarNavigation,
|
|
24
|
+
internalSidebarNavigationHandler,
|
|
25
|
+
isOpen,
|
|
26
|
+
routerDisabled = false,
|
|
27
|
+
}: Props) => {
|
|
28
|
+
const handleToggleClick = event => {
|
|
29
|
+
event.preventDefault();
|
|
30
|
+
|
|
31
|
+
if (routerDisabled) {
|
|
32
|
+
// Use internal navigation handler when router is disabled
|
|
33
|
+
if (internalSidebarNavigationHandler && internalSidebarNavigation) {
|
|
34
|
+
internalSidebarNavigationHandler({
|
|
35
|
+
...internalSidebarNavigation,
|
|
36
|
+
open: !isOpen,
|
|
37
|
+
}, true); // Always use replace for toggle
|
|
38
|
+
}
|
|
39
|
+
} else if (history) {
|
|
40
|
+
history.replace({ state: { open: !isOpen } });
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
|
|
18
44
|
return (
|
|
19
45
|
<SidebarToggleButton
|
|
20
46
|
data-resin-target={SIDEBAR_NAV_TARGETS.TOGGLE}
|
|
21
47
|
data-testid="sidebartoggle"
|
|
22
48
|
// $FlowFixMe
|
|
23
49
|
isOpen={isOpen}
|
|
24
|
-
onClick={
|
|
25
|
-
event.preventDefault();
|
|
26
|
-
history.replace({ state: { open: !isOpen } });
|
|
27
|
-
}}
|
|
50
|
+
onClick={handleToggleClick}
|
|
28
51
|
/>
|
|
29
52
|
);
|
|
30
53
|
};
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
2
|
import { MemoryRouter, Router } from 'react-router-dom';
|
|
3
|
+
// Using fireEvent for all click interactions instead of userEvent because
|
|
4
|
+
// userEvent.pointer with right-click doesn't reliably trigger onClick handlers
|
|
3
5
|
import { render, screen, fireEvent } from '../../../test-utils/testing-library';
|
|
4
6
|
import SidebarNavButton from '../SidebarNavButton';
|
|
5
7
|
|
|
@@ -197,7 +199,7 @@ describe('elements/content-sidebar/SidebarNavButton', () => {
|
|
|
197
199
|
renderWithRouter({ onClick: mockOnClick }, mockHistoryWithDifferentPath);
|
|
198
200
|
|
|
199
201
|
const button = screen.getByText('Activity');
|
|
200
|
-
fireEvent.click(button
|
|
202
|
+
fireEvent.click(button);
|
|
201
203
|
|
|
202
204
|
expect(mockOnClick).toBeCalledWith('activity');
|
|
203
205
|
expect(mockHistoryPush).toBeCalledWith({
|
|
@@ -213,7 +215,7 @@ describe('elements/content-sidebar/SidebarNavButton', () => {
|
|
|
213
215
|
renderWithRouter({ onClick: mockOnClick });
|
|
214
216
|
|
|
215
217
|
const button = screen.getByText('Activity');
|
|
216
|
-
fireEvent.click(button
|
|
218
|
+
fireEvent.click(button);
|
|
217
219
|
|
|
218
220
|
expect(mockOnClick).toBeCalledWith('activity');
|
|
219
221
|
expect(mockHistoryReplace).toBeCalledWith({
|
|
@@ -243,7 +245,6 @@ describe('elements/content-sidebar/SidebarNavButton', () => {
|
|
|
243
245
|
|
|
244
246
|
const button = screen.getByText('Activity');
|
|
245
247
|
|
|
246
|
-
// Prevent default on the button click
|
|
247
248
|
button.addEventListener('click', e => e.preventDefault());
|
|
248
249
|
fireEvent.click(button, { button: 0 });
|
|
249
250
|
|
|
@@ -253,3 +254,154 @@ describe('elements/content-sidebar/SidebarNavButton', () => {
|
|
|
253
254
|
});
|
|
254
255
|
});
|
|
255
256
|
});
|
|
257
|
+
|
|
258
|
+
describe('elements/content-sidebar/SidebarNavButton - Router Disabled', () => {
|
|
259
|
+
beforeEach(() => {
|
|
260
|
+
jest.clearAllMocks();
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
const defaultProps = {
|
|
264
|
+
routerDisabled: true,
|
|
265
|
+
tooltip: 'foo',
|
|
266
|
+
sidebarView: 'activity',
|
|
267
|
+
internalSidebarNavigation: { sidebar: 'skills' },
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
const renderWithoutRouter = ({ children = 'test button', ref, ...props }) =>
|
|
271
|
+
render(
|
|
272
|
+
<SidebarNavButton ref={ref} {...defaultProps} {...props}>
|
|
273
|
+
{children}
|
|
274
|
+
</SidebarNavButton>,
|
|
275
|
+
);
|
|
276
|
+
|
|
277
|
+
test('should render nav button properly', () => {
|
|
278
|
+
renderWithoutRouter({});
|
|
279
|
+
const button = screen.getByRole('tab');
|
|
280
|
+
|
|
281
|
+
expect(button).toHaveAttribute('aria-label', 'foo');
|
|
282
|
+
expect(button).toHaveAttribute('aria-selected', 'false');
|
|
283
|
+
expect(button).toHaveAttribute('aria-controls', 'activity-content');
|
|
284
|
+
expect(button).toHaveAttribute('role', 'tab');
|
|
285
|
+
expect(button).toHaveAttribute('tabindex', '-1');
|
|
286
|
+
expect(button).toHaveAttribute('type', 'button');
|
|
287
|
+
expect(button).toHaveAttribute('id', 'activity');
|
|
288
|
+
expect(button).toHaveClass('bcs-NavButton');
|
|
289
|
+
expect(button).not.toHaveClass('bcs-is-selected');
|
|
290
|
+
expect(button).toHaveTextContent('test button');
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
test.each`
|
|
294
|
+
internalSidebarNavigation | expected
|
|
295
|
+
${null} | ${false}
|
|
296
|
+
${undefined} | ${false}
|
|
297
|
+
${{ sidebar: 'skills' }} | ${false}
|
|
298
|
+
${{ sidebar: 'activity' }} | ${true}
|
|
299
|
+
${{ sidebar: 'activity', versionId: '123' }} | ${true}
|
|
300
|
+
`('should reflect active state ($expected) correctly based on internal navigation', ({ expected, internalSidebarNavigation }) => {
|
|
301
|
+
renderWithoutRouter({
|
|
302
|
+
internalSidebarNavigation,
|
|
303
|
+
isOpen: true,
|
|
304
|
+
});
|
|
305
|
+
const button = screen.getByRole('tab');
|
|
306
|
+
|
|
307
|
+
if (expected) {
|
|
308
|
+
expect(button).toHaveClass('bcs-is-selected');
|
|
309
|
+
expect(button).toHaveAttribute('aria-selected', 'true');
|
|
310
|
+
expect(button).toHaveAttribute('tabindex', '0');
|
|
311
|
+
} else {
|
|
312
|
+
expect(button).not.toHaveClass('bcs-is-selected');
|
|
313
|
+
expect(button).toHaveAttribute('aria-selected', 'false');
|
|
314
|
+
expect(button).toHaveAttribute('tabindex', '-1');
|
|
315
|
+
}
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
test('should call onClick with sidebarView when clicked', () => {
|
|
319
|
+
const mockOnClick = jest.fn();
|
|
320
|
+
const mockSidebarView = 'activity';
|
|
321
|
+
|
|
322
|
+
renderWithoutRouter({
|
|
323
|
+
onClick: mockOnClick,
|
|
324
|
+
sidebarView: mockSidebarView,
|
|
325
|
+
});
|
|
326
|
+
const button = screen.getByRole('tab');
|
|
327
|
+
|
|
328
|
+
fireEvent.click(button);
|
|
329
|
+
expect(mockOnClick).toBeCalledWith(mockSidebarView);
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
describe('navigation on click', () => {
|
|
333
|
+
const mockInternalSidebarNavigationHandler = jest.fn();
|
|
334
|
+
|
|
335
|
+
test('calls onClick handler and internalSidebarNavigationHandler with replace=false when not exact match', () => {
|
|
336
|
+
const mockOnClick = jest.fn();
|
|
337
|
+
|
|
338
|
+
renderWithoutRouter({
|
|
339
|
+
onClick: mockOnClick,
|
|
340
|
+
internalSidebarNavigation: { sidebar: 'activity', versionId: '123' },
|
|
341
|
+
internalSidebarNavigationHandler: mockInternalSidebarNavigationHandler,
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
const button = screen.getByRole('tab');
|
|
345
|
+
fireEvent.click(button);
|
|
346
|
+
|
|
347
|
+
expect(mockOnClick).toBeCalledWith('activity');
|
|
348
|
+
expect(mockInternalSidebarNavigationHandler).toBeCalledWith({
|
|
349
|
+
sidebar: 'activity',
|
|
350
|
+
open: true,
|
|
351
|
+
}, false);
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
test('calls internalSidebarNavigationHandler with replace=true when exact match', () => {
|
|
355
|
+
const mockOnClick = jest.fn();
|
|
356
|
+
|
|
357
|
+
renderWithoutRouter({
|
|
358
|
+
onClick: mockOnClick,
|
|
359
|
+
internalSidebarNavigation: { sidebar: 'activity' },
|
|
360
|
+
internalSidebarNavigationHandler: mockInternalSidebarNavigationHandler,
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
const button = screen.getByRole('tab');
|
|
364
|
+
fireEvent.click(button);
|
|
365
|
+
|
|
366
|
+
expect(mockOnClick).toBeCalledWith('activity');
|
|
367
|
+
expect(mockInternalSidebarNavigationHandler).toBeCalledWith({
|
|
368
|
+
sidebar: 'activity',
|
|
369
|
+
open: true,
|
|
370
|
+
}, true);
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
test('does not call internalSidebarNavigationHandler on right click', () => {
|
|
374
|
+
const mockOnClick = jest.fn();
|
|
375
|
+
|
|
376
|
+
renderWithoutRouter({
|
|
377
|
+
onClick: mockOnClick,
|
|
378
|
+
internalSidebarNavigation: { sidebar: 'activity' },
|
|
379
|
+
internalSidebarNavigationHandler: mockInternalSidebarNavigationHandler,
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
const button = screen.getByRole('tab');
|
|
383
|
+
fireEvent.click(button, { button: 1 });
|
|
384
|
+
|
|
385
|
+
expect(mockOnClick).toBeCalledWith('activity');
|
|
386
|
+
expect(mockInternalSidebarNavigationHandler).not.toBeCalled();
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
test('does not call internalSidebarNavigationHandler on prevented event', () => {
|
|
390
|
+
const mockOnClick = jest.fn();
|
|
391
|
+
|
|
392
|
+
renderWithoutRouter({
|
|
393
|
+
onClick: mockOnClick,
|
|
394
|
+
internalSidebarNavigation: { sidebar: 'activity' },
|
|
395
|
+
internalSidebarNavigationHandler: mockInternalSidebarNavigationHandler,
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
const button = screen.getByRole('tab');
|
|
399
|
+
|
|
400
|
+
button.addEventListener('click', e => e.preventDefault());
|
|
401
|
+
fireEvent.click(button, { button: 0 });
|
|
402
|
+
|
|
403
|
+
expect(mockOnClick).toBeCalledWith('activity');
|
|
404
|
+
expect(mockInternalSidebarNavigationHandler).not.toBeCalled();
|
|
405
|
+
});
|
|
406
|
+
});
|
|
407
|
+
});
|