@squiz/resource-browser 3.3.8 → 3.3.9

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/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # Change Log
2
2
 
3
+ ## 3.3.9
4
+
5
+ ### Patch Changes
6
+
7
+ - 53de26d: add ref support and modal state change callback
8
+ - Updated dependencies [53de26d]
9
+ - @squiz/resource-browser-ui-lib@1.2.5
10
+
3
11
  ## 3.3.8
4
12
 
5
13
  ### Patch Changes
@@ -14,5 +14,6 @@ export type PluginRenderType = ResourceBrowserInputProps & {
14
14
  inline: boolean;
15
15
  inlineType?: InlineType;
16
16
  onRetry: () => void;
17
+ ref?: React.Ref<HTMLButtonElement>;
17
18
  };
18
- export declare const PluginRender: ({ render, inline, inlineType, ...props }: PluginRenderType) => React.JSX.Element;
19
+ export declare const PluginRender: React.ForwardRefExoticComponent<Omit<PluginRenderType, "ref"> & React.RefAttributes<HTMLButtonElement>>;
@@ -1,18 +1,37 @@
1
1
  "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || function (mod) {
19
+ if (mod && mod.__esModule) return mod;
20
+ var result = {};
21
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
22
+ __setModuleDefault(result, mod);
23
+ return result;
4
24
  };
5
25
  Object.defineProperty(exports, "__esModule", { value: true });
6
26
  exports.PluginRender = void 0;
7
- const react_1 = __importDefault(require("react"));
27
+ const react_1 = __importStar(require("react"));
8
28
  const ResourceBrowserInput_1 = require("../ResourceBrowserInput/ResourceBrowserInput");
9
29
  const ResourceBrowserInlineButton_1 = require("../ResourceBrowserInlineButton/ResourceBrowserInlineButton");
10
30
  const AuthProvider_1 = require("../ResourceBrowserContext/AuthProvider");
11
- const PluginRender = ({ render, inline, inlineType, ...props }) => {
31
+ exports.PluginRender = (0, react_1.forwardRef)(({ render, inline, inlineType, ...props }, forwardRef) => {
12
32
  if (!render)
13
33
  return react_1.default.createElement(react_1.default.Fragment, null);
14
34
  const requiresAuth = Boolean(props.source?.configuration?.authUrl);
15
- const content = inline && inlineType ? react_1.default.createElement(ResourceBrowserInlineButton_1.ResourceBrowserInlineButton, { inlineType: inlineType, ...props }) : react_1.default.createElement(ResourceBrowserInput_1.ResourceBrowserInput, { ...props });
35
+ const content = inline && inlineType ? (react_1.default.createElement(ResourceBrowserInlineButton_1.ResourceBrowserInlineButton, { ref: forwardRef, inlineType: inlineType, ...props })) : (react_1.default.createElement(ResourceBrowserInput_1.ResourceBrowserInput, { ...props }));
16
36
  return requiresAuth ? react_1.default.createElement(AuthProvider_1.AuthProvider, { authConfig: props.source }, content) : content;
17
- };
18
- exports.PluginRender = PluginRender;
37
+ });
@@ -5,4 +5,7 @@ export type ResourceBrowserInlineButtonProps = ResourceBrowserInputProps & {
5
5
  inlineType: InlineType;
6
6
  onRetry: () => void;
7
7
  };
8
- export declare const ResourceBrowserInlineButton: ({ inlineType, modalTitle, allowedTypes, onChange, onRetry, value, useResource, isDisabled, plugin, pluginMode, searchEnabled, source, sources, isLoading, error, setSource, isModalOpen, onModalStateChange, }: ResourceBrowserInlineButtonProps) => React.JSX.Element;
8
+ export declare const ResourceBrowserInlineButton: React.ForwardRefExoticComponent<ResourceBrowserInputProps & {
9
+ inlineType: InlineType;
10
+ onRetry: () => void;
11
+ } & React.RefAttributes<HTMLButtonElement>>;
@@ -34,7 +34,8 @@ const InlineLoadingErrorState_1 = require("./InlineLoadingErrorState/InlineLoadi
34
34
  const ImageInline_1 = require("../Icons/ImageInline");
35
35
  const LinkInline_1 = require("../Icons/LinkInline");
36
36
  const AdsClickIcon_1 = require("../Icons/AdsClickIcon");
37
- const ResourceBrowserInlineButton = ({ inlineType, modalTitle, allowedTypes, onChange, onRetry, value, useResource, isDisabled, plugin, pluginMode, searchEnabled, source, sources, isLoading, error, setSource, isModalOpen, onModalStateChange, }) => {
37
+ exports.ResourceBrowserInlineButton = (0, react_1.forwardRef)((props, forwardRef) => {
38
+ const { inlineType, modalTitle, allowedTypes, onChange, onRetry, value, useResource, isDisabled, plugin, pluginMode, searchEnabled, source, sources, isLoading, error, setSource, isModalOpen, onModalStateChange, } = props;
38
39
  // If an error happens loading the resource the inline browser just opens without it as if no preselectedResource is provided
39
40
  const { data: resource, isLoading: isResourceLoading } = useResource(value || null, source);
40
41
  const inlineTypePickerLabels = (0, react_1.useMemo)(() => ({
@@ -48,12 +49,11 @@ const ResourceBrowserInlineButton = ({ inlineType, modalTitle, allowedTypes, onC
48
49
  resource: react_1.default.createElement(AdsClickIcon_1.AdsClickIcon, { "aria-label": "change resource" }),
49
50
  }), [inlineType]);
50
51
  return (react_1.default.createElement("div", { className: "inline-launch-button" },
51
- react_1.default.createElement(resource_browser_ui_lib_1.ModalTrigger, { overlayTriggerState: {
52
+ react_1.default.createElement(resource_browser_ui_lib_1.ModalTrigger, { ref: forwardRef, overlayTriggerState: {
52
53
  isOpen: isModalOpen,
53
54
  onOpenChange: onModalStateChange,
54
55
  }, showLabel: false, label: "", icon: inlineTypePickerIcons[inlineType], isDisabled: isDisabled, scope: "squiz-rb-scope", containerClasses: "inline-launch-button__button" }, (onClose, titleProps) => (react_1.default.createElement(react_1.default.Fragment, null,
55
56
  (isLoading || isResourceLoading || error) && (react_1.default.createElement(InlineLoadingErrorState_1.InlineLoadingErrorState, { title: modalTitle, titleAriaProps: titleProps, onClose: onClose, onRetry: onRetry, isLoading: isLoading || isResourceLoading, error: error })),
56
57
  !(isLoading || isResourceLoading) && !error && (react_1.default.createElement(MainContainer_1.default, { selectedSource: source, sources: sources, preselectedResource: resource, plugin: plugin, pluginMode: pluginMode, searchEnabled: searchEnabled, title: modalTitle, titleAriaProps: titleProps, allowedTypes: allowedTypes, onSourceSelect: setSource, onClose: onClose, onChange: onChange }))))),
57
58
  react_1.default.createElement("div", { className: "inline-launch-button__label" }, inlineTypePickerLabels[inlineType])));
58
- };
59
- exports.ResourceBrowserInlineButton = ResourceBrowserInlineButton;
59
+ });
package/lib/index.d.ts CHANGED
@@ -16,6 +16,7 @@ export type ResourceBrowserProps = {
16
16
  inline?: boolean;
17
17
  inlineType?: InlineType;
18
18
  onChange(resource: ResourceBrowserResource | null): void;
19
+ onModalStateChange?(isOpen: boolean): void;
19
20
  onClear?(): void;
20
21
  };
21
- export declare const ResourceBrowser: (props: ResourceBrowserProps) => React.JSX.Element;
22
+ export declare const ResourceBrowser: React.ForwardRefExoticComponent<ResourceBrowserProps & React.RefAttributes<HTMLButtonElement>>;
package/lib/index.js CHANGED
@@ -47,8 +47,8 @@ exports.SourceDropdown = SourceDropdown_1.default;
47
47
  const SourceDropdownContainer_1 = __importDefault(require("./SourceDropdownContainer/SourceDropdownContainer"));
48
48
  exports.SourceDropdownContainer = SourceDropdownContainer_1.default;
49
49
  __exportStar(require("./types"), exports);
50
- const ResourceBrowser = (props) => {
51
- const { value, inline, inlineType, allowedPlugins } = props;
50
+ exports.ResourceBrowser = react_1.default.forwardRef((props, forwardRef) => {
51
+ const { value, inline, inlineType, allowedPlugins, onModalStateChange: onModalStateChangeExternalNotification } = props;
52
52
  const [error, setError] = (0, react_1.useState)(null);
53
53
  const { onRequestSources, searchEnabled, plugins: allPlugins } = (0, react_1.useContext)(ResourceBrowserContext_1.ResourceBrowserContext);
54
54
  const [isModalOpen, setIsModalOpen] = (0, react_1.useState)(false);
@@ -96,7 +96,8 @@ const ResourceBrowser = (props) => {
96
96
  // The modal has some control over it own open/closed state (for WCAG reasons) so keep this in sync with our state
97
97
  const handleModalStateChange = (0, react_1.useCallback)((isOpen) => {
98
98
  setIsModalOpen(isOpen);
99
- }, [setIsModalOpen]);
99
+ onModalStateChangeExternalNotification?.(isOpen);
100
+ }, [setIsModalOpen, onModalStateChangeExternalNotification]);
100
101
  // If the modal closes and we dont have a value clear the source state so it goes back to the launcher on re-open
101
102
  (0, react_1.useEffect)(() => {
102
103
  // If modal is closed and we dont have a value
@@ -120,7 +121,7 @@ const ResourceBrowser = (props) => {
120
121
  }, [reloadSources]);
121
122
  // Render a default "plugin" and one for each item in the plugins array. They are conditionally rendered based on what is selected
122
123
  return (react_1.default.createElement("div", { className: "squiz-rb-scope" },
123
- react_1.default.createElement(Plugin_1.PluginRender, { key: "default", type: null, render: plugin === null, inline: !!inline, inlineType: inlineType, ...props, source: source, sources: sources, setSource: handleSourceSelect, isLoading: isLoading, isOtherSourceValue: false, error: sourcesError || error, plugin: plugin, pluginMode: mode, searchEnabled: searchEnabled, useResource: () => {
124
+ react_1.default.createElement(Plugin_1.PluginRender, { key: "default", ref: forwardRef, type: null, render: plugin === null, inline: !!inline, inlineType: inlineType, ...props, source: source, sources: sources, setSource: handleSourceSelect, isLoading: isLoading, isOtherSourceValue: false, error: sourcesError || error, plugin: plugin, pluginMode: mode, searchEnabled: searchEnabled, useResource: () => {
124
125
  return {
125
126
  data: null,
126
127
  error: null,
@@ -128,7 +129,6 @@ const ResourceBrowser = (props) => {
128
129
  };
129
130
  }, isModalOpen: isModalOpen, onModalStateChange: handleModalStateChange, onRetry: handleReset }),
130
131
  plugins.map((thisPlugin) => {
131
- return (react_1.default.createElement(Plugin_1.PluginRender, { key: thisPlugin.type, type: thisPlugin.type, render: thisPlugin.type === plugin?.type, inline: !!inline, inlineType: inlineType, ...props, value: value && source ? (value.sourceId === source.id ? value : null) : null, isOtherSourceValue: value && source ? (value.sourceId !== source.id ? true : false) : false, source: source, sources: sources, setSource: handleSourceSelect, isLoading: isLoading, error: sourcesError || error, plugin: plugin, pluginMode: mode, searchEnabled: searchEnabled, useResource: thisPlugin.useResolveResource, isModalOpen: isModalOpen, onModalStateChange: handleModalStateChange, onRetry: handleReset }));
132
+ return (react_1.default.createElement(Plugin_1.PluginRender, { key: thisPlugin.type, ref: forwardRef, type: thisPlugin.type, render: thisPlugin.type === plugin?.type, inline: !!inline, inlineType: inlineType, ...props, value: value && source ? (value.sourceId === source.id ? value : null) : null, isOtherSourceValue: value && source ? (value.sourceId !== source.id ? true : false) : false, source: source, sources: sources, setSource: handleSourceSelect, isLoading: isLoading, error: sourcesError || error, plugin: plugin, pluginMode: mode, searchEnabled: searchEnabled, useResource: thisPlugin.useResolveResource, isModalOpen: isModalOpen, onModalStateChange: handleModalStateChange, onRetry: handleReset }));
132
133
  })));
133
- };
134
- exports.ResourceBrowser = ResourceBrowser;
134
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@squiz/resource-browser",
3
- "version": "3.3.8",
3
+ "version": "3.3.9",
4
4
  "main": "lib/index.js",
5
5
  "types": "lib/index.d.ts",
6
6
  "private": false,
@@ -28,7 +28,7 @@
28
28
  "@react-types/shared": "^3.23.1",
29
29
  "@squiz/dx-json-schema-lib": "^1.67.0",
30
30
  "@squiz/generic-browser-lib": "^1.67.5",
31
- "@squiz/resource-browser-ui-lib": "^1.2.4",
31
+ "@squiz/resource-browser-ui-lib": "^1.2.5",
32
32
  "clsx": "^2.1.0",
33
33
  "expiry-map": "^2.0.0",
34
34
  "p-memoize": "^4.0.4",
@@ -1,4 +1,4 @@
1
- import React from 'react';
1
+ import React, { forwardRef } from 'react';
2
2
  import { render, waitFor } from '@testing-library/react';
3
3
 
4
4
  import { PluginRender } from './Plugin';
@@ -8,7 +8,9 @@ import * as RBInlineButton from '../ResourceBrowserInlineButton/ResourceBrowserI
8
8
  import * as AuthContext from '../ResourceBrowserContext/AuthProvider';
9
9
 
10
10
  jest.spyOn(RBI, 'ResourceBrowserInput');
11
- jest.spyOn(RBInlineButton, 'ResourceBrowserInlineButton');
11
+ jest.mock('../ResourceBrowserInlineButton/ResourceBrowserInlineButton', () => ({
12
+ ResourceBrowserInlineButton: jest.fn(() => <div data-testid="mocked-inline-button" />),
13
+ }));
12
14
  jest.spyOn(AuthContext, 'AuthProvider');
13
15
 
14
16
  const baseProps = {
@@ -1,4 +1,4 @@
1
- import React from 'react';
1
+ import React, { forwardRef } from 'react';
2
2
  import { ResourceBrowserInput, ResourceBrowserInputProps } from '../ResourceBrowserInput/ResourceBrowserInput';
3
3
  import { ResourceBrowserInlineButton } from '../ResourceBrowserInlineButton/ResourceBrowserInlineButton';
4
4
  import { AuthProvider } from '../ResourceBrowserContext/AuthProvider';
@@ -17,14 +17,19 @@ export type PluginRenderType = ResourceBrowserInputProps & {
17
17
  inline: boolean;
18
18
  inlineType?: InlineType;
19
19
  onRetry: () => void;
20
+ ref?: React.Ref<HTMLButtonElement>;
20
21
  };
21
- export const PluginRender = ({ render, inline, inlineType, ...props }: PluginRenderType) => {
22
+ export const PluginRender = forwardRef<HTMLButtonElement, PluginRenderType>(({ render, inline, inlineType, ...props }, forwardRef) => {
22
23
  if (!render) return <></>;
23
24
 
24
25
  const requiresAuth = Boolean((props.source as ResourceBrowserSourceWithConfig)?.configuration?.authUrl);
25
26
 
26
27
  const content =
27
- inline && inlineType ? <ResourceBrowserInlineButton inlineType={inlineType} {...props} /> : <ResourceBrowserInput {...props} />;
28
+ inline && inlineType ? (
29
+ <ResourceBrowserInlineButton ref={forwardRef} inlineType={inlineType} {...props} />
30
+ ) : (
31
+ <ResourceBrowserInput {...props} />
32
+ );
28
33
 
29
34
  return requiresAuth ? <AuthProvider authConfig={props.source}>{content}</AuthProvider> : content;
30
- };
35
+ });
@@ -1,4 +1,4 @@
1
- import React, { useMemo } from 'react';
1
+ import React, { forwardRef, useMemo } from 'react';
2
2
  import MainContainer from '../MainContainer/MainContainer';
3
3
  import { ResourceBrowserInputProps } from '../ResourceBrowserInput/ResourceBrowserInput';
4
4
  import { ModalTrigger } from '@squiz/resource-browser-ui-lib';
@@ -13,26 +13,28 @@ export type ResourceBrowserInlineButtonProps = ResourceBrowserInputProps & {
13
13
  onRetry: () => void;
14
14
  };
15
15
 
16
- export const ResourceBrowserInlineButton = ({
17
- inlineType,
18
- modalTitle,
19
- allowedTypes,
20
- onChange,
21
- onRetry,
22
- value,
23
- useResource,
24
- isDisabled,
25
- plugin,
26
- pluginMode,
27
- searchEnabled,
28
- source,
29
- sources,
30
- isLoading,
31
- error,
32
- setSource,
33
- isModalOpen,
34
- onModalStateChange,
35
- }: ResourceBrowserInlineButtonProps) => {
16
+ export const ResourceBrowserInlineButton = forwardRef<HTMLButtonElement, ResourceBrowserInlineButtonProps>((props, forwardRef) => {
17
+ const {
18
+ inlineType,
19
+ modalTitle,
20
+ allowedTypes,
21
+ onChange,
22
+ onRetry,
23
+ value,
24
+ useResource,
25
+ isDisabled,
26
+ plugin,
27
+ pluginMode,
28
+ searchEnabled,
29
+ source,
30
+ sources,
31
+ isLoading,
32
+ error,
33
+ setSource,
34
+ isModalOpen,
35
+ onModalStateChange,
36
+ } = props;
37
+
36
38
  // If an error happens loading the resource the inline browser just opens without it as if no preselectedResource is provided
37
39
  const { data: resource, isLoading: isResourceLoading } = useResource(value || null, source);
38
40
 
@@ -56,6 +58,7 @@ export const ResourceBrowserInlineButton = ({
56
58
  return (
57
59
  <div className="inline-launch-button">
58
60
  <ModalTrigger
61
+ ref={forwardRef}
59
62
  overlayTriggerState={{
60
63
  isOpen: isModalOpen,
61
64
  onOpenChange: onModalStateChange,
@@ -101,4 +104,4 @@ export const ResourceBrowserInlineButton = ({
101
104
  <div className="inline-launch-button__label">{inlineTypePickerLabels[inlineType]}</div>
102
105
  </div>
103
106
  );
104
- };
107
+ });
@@ -568,12 +568,47 @@ describe('Resource browser input', () => {
568
568
  });
569
569
  });
570
570
 
571
+ it('onModalStateChange calls onModalStateChangeExternalNotification when provided', async () => {
572
+ const mockOnModalStateChange = jest.fn();
573
+ const sourcesInput = [mockSource({ type: 'dam' })];
574
+ mockRequestSources.mockResolvedValueOnce(sourcesInput);
575
+
576
+ renderComponent({ onModalStateChange: mockOnModalStateChange });
577
+ await waitFor(() => {
578
+ expect(RBI.ResourceBrowserInput).toHaveBeenCalled();
579
+ });
580
+
581
+ const { onModalStateChange } = (RBI.ResourceBrowserInput as unknown as jest.SpyInstance).mock.calls[0][0];
582
+
583
+ // Test opening modal
584
+ act(() => {
585
+ onModalStateChange(true);
586
+ });
587
+
588
+ await waitFor(() => {
589
+ expect(mockOnModalStateChange).toHaveBeenCalledWith(true);
590
+ });
591
+
592
+ // Test closing modal
593
+ act(() => {
594
+ onModalStateChange(false);
595
+ });
596
+
597
+ await waitFor(() => {
598
+ expect(mockOnModalStateChange).toHaveBeenCalledWith(false);
599
+ });
600
+
601
+ expect(mockOnModalStateChange).toHaveBeenCalledTimes(2);
602
+ });
603
+
571
604
  describe('Resource browser plugin', () => {
605
+ let PluginRenderSpy: jest.SpyInstance;
572
606
  beforeEach(() => {
573
- jest.spyOn(Plugin, 'PluginRender');
607
+ //@ts-ignore
608
+ PluginRenderSpy = jest.spyOn(Plugin.PluginRender, 'render');
574
609
  });
575
610
  afterEach(() => {
576
- (Plugin.PluginRender as jest.Mock).mockRestore();
611
+ PluginRenderSpy.mockRestore();
577
612
  });
578
613
 
579
614
  it('Will default to a non plugin based render for initial load and selection of first source', async () => {
@@ -583,29 +618,29 @@ describe('Resource browser input', () => {
583
618
 
584
619
  // Will render a default with no selected source
585
620
  await waitFor(() => {
586
- expect(Plugin.PluginRender).toHaveBeenCalledWith(
621
+ expect(PluginRenderSpy).toHaveBeenCalledWith(
587
622
  expect.objectContaining({
588
623
  render: true,
589
624
  type: null,
590
625
  plugin: null,
591
626
  }),
592
- {},
627
+ null,
593
628
  );
594
629
 
595
- expect(Plugin.PluginRender).toHaveBeenCalledWith(
630
+ expect(PluginRenderSpy).toHaveBeenCalledWith(
596
631
  expect.objectContaining({
597
632
  render: false,
598
633
  type: 'dam',
599
634
  }),
600
- {},
635
+ null,
601
636
  );
602
637
 
603
- expect(Plugin.PluginRender).toHaveBeenCalledWith(
638
+ expect(PluginRenderSpy).toHaveBeenCalledWith(
604
639
  expect.objectContaining({
605
640
  render: false,
606
641
  type: 'matrix',
607
642
  }),
608
- {},
643
+ null,
609
644
  );
610
645
  });
611
646
  });
@@ -619,28 +654,28 @@ describe('Resource browser input', () => {
619
654
 
620
655
  // Will render a default with no selected source
621
656
  await waitFor(() => {
622
- expect(Plugin.PluginRender).toHaveBeenCalledWith(
657
+ expect(PluginRenderSpy).toHaveBeenCalledWith(
623
658
  expect.objectContaining({
624
659
  render: false,
625
660
  type: null,
626
661
  }),
627
- {},
662
+ null,
628
663
  );
629
664
 
630
- expect(Plugin.PluginRender).toHaveBeenCalledWith(
665
+ expect(PluginRenderSpy).toHaveBeenCalledWith(
631
666
  expect.objectContaining({
632
667
  render: true,
633
668
  type: 'dam',
634
669
  }),
635
- {},
670
+ null,
636
671
  );
637
672
 
638
- expect(Plugin.PluginRender).toHaveBeenCalledWith(
673
+ expect(PluginRenderSpy).toHaveBeenCalledWith(
639
674
  expect.objectContaining({
640
675
  render: false,
641
676
  type: 'matrix',
642
677
  }),
643
- {},
678
+ null,
644
679
  );
645
680
  });
646
681
 
@@ -672,28 +707,28 @@ describe('Resource browser input', () => {
672
707
 
673
708
  // Will render a default with no selected source
674
709
  await waitFor(() => {
675
- expect(Plugin.PluginRender).toHaveBeenCalledWith(
710
+ expect(PluginRenderSpy).toHaveBeenCalledWith(
676
711
  expect.objectContaining({
677
712
  render: true,
678
713
  type: null,
679
714
  }),
680
- {},
715
+ null,
681
716
  );
682
717
 
683
- expect(Plugin.PluginRender).toHaveBeenCalledWith(
718
+ expect(PluginRenderSpy).toHaveBeenCalledWith(
684
719
  expect.objectContaining({
685
720
  render: true,
686
721
  type: 'dam',
687
722
  }),
688
- {},
723
+ null,
689
724
  );
690
725
 
691
- expect(Plugin.PluginRender).toHaveBeenCalledWith(
726
+ expect(PluginRenderSpy).toHaveBeenCalledWith(
692
727
  expect.objectContaining({
693
728
  render: false,
694
729
  type: 'matrix',
695
730
  }),
696
- {},
731
+ null,
697
732
  );
698
733
  });
699
734
  });
@@ -724,20 +759,20 @@ describe('Resource browser input', () => {
724
759
  );
725
760
  });
726
761
 
727
- (Plugin.PluginRender as jest.Mock).mockClear();
762
+ (PluginRenderSpy as unknown as jest.Mock).mockClear();
728
763
 
729
764
  act(() => {
730
765
  setSource({ ...matrixSource, plugin: pluginB });
731
766
  });
732
767
 
733
768
  await waitFor(() => {
734
- expect(Plugin.PluginRender).toHaveBeenCalledWith(
769
+ expect(PluginRenderSpy).toHaveBeenCalledWith(
735
770
  expect.objectContaining({
736
771
  plugin: pluginB,
737
772
  }),
738
773
  expect.any(Object),
739
774
  );
740
- expect(Plugin.PluginRender).not.toHaveBeenCalledWith(
775
+ expect(PluginRenderSpy).not.toHaveBeenCalledWith(
741
776
  expect.objectContaining({
742
777
  plugin: pluginA,
743
778
  }),
package/src/index.tsx CHANGED
@@ -38,11 +38,12 @@ export type ResourceBrowserProps = {
38
38
  inline?: boolean; // Will render open button only, no input / showing of existing selection
39
39
  inlineType?: InlineType; // Type of inline button to show
40
40
  onChange(resource: ResourceBrowserResource | null): void;
41
+ onModalStateChange?(isOpen: boolean): void;
41
42
  onClear?(): void;
42
43
  };
43
44
 
44
- export const ResourceBrowser = (props: ResourceBrowserProps) => {
45
- const { value, inline, inlineType, allowedPlugins } = props;
45
+ export const ResourceBrowser = React.forwardRef<HTMLButtonElement, ResourceBrowserProps>((props, forwardRef) => {
46
+ const { value, inline, inlineType, allowedPlugins, onModalStateChange: onModalStateChangeExternalNotification } = props;
46
47
  const [error, setError] = useState<Error | null>(null);
47
48
  const { onRequestSources, searchEnabled, plugins: allPlugins } = useContext(ResourceBrowserContext);
48
49
 
@@ -98,8 +99,9 @@ export const ResourceBrowser = (props: ResourceBrowserProps) => {
98
99
  const handleModalStateChange = useCallback(
99
100
  (isOpen: boolean) => {
100
101
  setIsModalOpen(isOpen);
102
+ onModalStateChangeExternalNotification?.(isOpen);
101
103
  },
102
- [setIsModalOpen],
104
+ [setIsModalOpen, onModalStateChangeExternalNotification],
103
105
  );
104
106
 
105
107
  // If the modal closes and we dont have a value clear the source state so it goes back to the launcher on re-open
@@ -130,6 +132,7 @@ export const ResourceBrowser = (props: ResourceBrowserProps) => {
130
132
  <div className="squiz-rb-scope">
131
133
  <PluginRender
132
134
  key="default"
135
+ ref={forwardRef} // This is passed to every plugin but only actually render on one at a time
133
136
  type={null}
134
137
  render={plugin === null}
135
138
  inline={!!inline}
@@ -159,6 +162,7 @@ export const ResourceBrowser = (props: ResourceBrowserProps) => {
159
162
  return (
160
163
  <PluginRender
161
164
  key={thisPlugin.type}
165
+ ref={forwardRef} // This is passed to every plugin but only actually render on one at a time
162
166
  type={thisPlugin.type}
163
167
  render={thisPlugin.type === plugin?.type}
164
168
  inline={!!inline}
@@ -183,4 +187,4 @@ export const ResourceBrowser = (props: ResourceBrowserProps) => {
183
187
  })}
184
188
  </div>
185
189
  );
186
- };
190
+ });