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.
Files changed (58) hide show
  1. package/dist/explorer.js +1 -1
  2. package/dist/preview.js +1 -1
  3. package/dist/sidebar.js +1 -1
  4. package/es/components/flyout/OverlayHeader.js +6 -3
  5. package/es/components/flyout/OverlayHeader.js.map +1 -1
  6. package/es/elements/common/nav-router/NavRouter.js +7 -3
  7. package/es/elements/common/nav-router/NavRouter.js.flow +10 -1
  8. package/es/elements/common/nav-router/NavRouter.js.map +1 -1
  9. package/es/elements/common/nav-router/types.js.map +1 -1
  10. package/es/elements/common/nav-router/withNavRouter.js +10 -1
  11. package/es/elements/common/nav-router/withNavRouter.js.flow +5 -0
  12. package/es/elements/common/nav-router/withNavRouter.js.map +1 -1
  13. package/es/elements/common/routing/withRouterAndRef.js +17 -3
  14. package/es/elements/common/routing/withRouterAndRef.js.flow +11 -3
  15. package/es/elements/common/routing/withRouterAndRef.js.map +1 -1
  16. package/es/elements/content-explorer/stories/tests/ContentExplorer-visual.stories.js +22 -13
  17. package/es/elements/content-explorer/stories/tests/ContentExplorer-visual.stories.js.flow +30 -17
  18. package/es/elements/content-explorer/stories/tests/ContentExplorer-visual.stories.js.map +1 -1
  19. package/es/elements/content-sidebar/ContentSidebar.js +3 -1
  20. package/es/elements/content-sidebar/ContentSidebar.js.flow +2 -1
  21. package/es/elements/content-sidebar/ContentSidebar.js.map +1 -1
  22. package/es/elements/content-sidebar/SidebarNavButton.js +49 -1
  23. package/es/elements/content-sidebar/SidebarNavButton.js.flow +65 -3
  24. package/es/elements/content-sidebar/SidebarNavButton.js.map +1 -1
  25. package/es/elements/content-sidebar/SidebarToggle.js +27 -9
  26. package/es/elements/content-sidebar/SidebarToggle.js.flow +29 -6
  27. package/es/elements/content-sidebar/SidebarToggle.js.map +1 -1
  28. package/es/elements/content-sidebar/stories/tests/MetadataSidebarRedesign-visual.stories.js +12 -1
  29. package/es/elements/content-sidebar/stories/tests/MetadataSidebarRedesign-visual.stories.js.map +1 -1
  30. package/es/features/header-flyout/HeaderFlyout.js +6 -3
  31. package/es/features/header-flyout/HeaderFlyout.js.flow +15 -2
  32. package/es/features/header-flyout/HeaderFlyout.js.map +1 -1
  33. package/es/features/header-flyout/styles/HeaderFlyout.scss +2 -0
  34. package/es/src/components/flyout/OverlayHeader.d.ts +3 -1
  35. package/es/src/elements/common/nav-router/NavRouter.d.ts +3 -1
  36. package/es/src/elements/common/nav-router/types.d.ts +2 -0
  37. package/package.json +1 -1
  38. package/src/components/flyout/OverlayHeader.tsx +7 -3
  39. package/src/components/flyout/__tests__/OverlayHeader.test.js +25 -0
  40. package/src/elements/common/nav-router/NavRouter.js.flow +10 -1
  41. package/src/elements/common/nav-router/NavRouter.tsx +9 -3
  42. package/src/elements/common/nav-router/__tests__/withNavRouter.test.tsx +34 -20
  43. package/src/elements/common/nav-router/types.ts +2 -0
  44. package/src/elements/common/nav-router/withNavRouter.js.flow +5 -0
  45. package/src/elements/common/nav-router/withNavRouter.tsx +9 -1
  46. package/src/elements/common/routing/__tests__/withRouterAndRef.test.js +64 -12
  47. package/src/elements/common/routing/withRouterAndRef.js +11 -3
  48. package/src/elements/content-explorer/stories/tests/ContentExplorer-visual.stories.js +30 -17
  49. package/src/elements/content-sidebar/ContentSidebar.js +2 -1
  50. package/src/elements/content-sidebar/SidebarNavButton.js +65 -3
  51. package/src/elements/content-sidebar/SidebarToggle.js +29 -6
  52. package/src/elements/content-sidebar/__tests__/SidebarNavButton.test.js +155 -3
  53. package/src/elements/content-sidebar/__tests__/SidebarToggle.test.js +74 -10
  54. package/src/elements/content-sidebar/stories/tests/MetadataSidebarRedesign-visual.stories.tsx +14 -1
  55. package/src/features/header-flyout/HeaderFlyout.js +15 -2
  56. package/src/features/header-flyout/__tests__/__snapshots__/HeaderFlyout.test.js.snap +9 -3
  57. package/src/features/header-flyout/styles/HeaderFlyout.scss +2 -0
  58. 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 { shallow } from 'enzyme';
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 getWrapper = (props?: Props & WithNavRouterProps) => shallow(<WrappedComponent {...props} />);
18
+ const renderComponent = (props?: Props & WithNavRouterProps) =>
19
+ render(<WrappedComponent {...props} />);
17
20
 
18
21
  test('should wrap component with NavRouter', () => {
19
- const wrapper = getWrapper();
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
- expect(wrapper.find(NavRouter)).toBeTruthy();
22
- expect(wrapper.find(TestComponent)).toBeTruthy();
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
- test('should provide the appropriate props to NavRouter and the wrapped component', () => {
26
- const history = createMemoryHistory();
27
- const initialEntries = ['foo'];
28
- const value = 'foo';
29
- const wrapper = getWrapper({
30
- history,
31
- initialEntries,
32
- value,
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
- const navRouter = wrapper.find(NavRouter);
36
- expect(navRouter.prop('history')).toEqual(history);
37
- expect(navRouter.prop('initialEntries')).toEqual(initialEntries);
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
- expect(wrapper.find(TestComponent).prop('value')).toEqual(value);
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
  });
@@ -1,6 +1,8 @@
1
1
  import { History } from 'history';
2
+ import { FeatureConfig } from '../feature-checking';
2
3
 
3
4
  export type WithNavRouterProps = {
5
+ features?: FeatureConfig;
4
6
  history?: History;
5
7
  initialEntries?: History.LocationDescriptor[];
6
8
  };
@@ -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 { mount } from 'enzyme';
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) => <div ref={ref}>{value}</div>);
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
- test('should pass ref down to wrapped component', () => {
16
- const ref = React.createRef();
17
- const wrapper = mount(
18
- <MemoryRouter>
19
- <WithRouterComponent ref={ref} value="foo" />
20
- </MemoryRouter>,
21
- );
22
- const referenced = wrapper.find('div').getDOMNode();
23
- expect(ref.current).toEqual(referenced);
24
- expect(referenced.innerHTML).toEqual('foo');
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
- <Route>{routeProps => <Wrapped ref={ref} {...routeProps} {...props} />}</Route>
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: string) => void,
24
- sidebarView: string,
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: RouterHistory,
14
+ history?: RouterHistory,
15
+ internalSidebarNavigation?: InternalSidebarNavigation,
16
+ internalSidebarNavigationHandler?: InternalSidebarNavigationHandler,
14
17
  isOpen?: boolean,
18
+ routerDisabled?: boolean,
15
19
  };
16
20
 
17
- const SidebarToggle = ({ history, isOpen }: Props) => {
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={event => {
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, { button: 0 });
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, { button: 0 });
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
+ });