@squiz/generic-browser-lib 1.39.1-alpha.1 → 1.39.1-alpha.11

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.
@@ -1,17 +1,17 @@
1
1
  import { DependencyList } from 'react';
2
2
  export type UseAsyncProps<TReturnType, TDefaultValueType> = {
3
- /** The async callback to call for fetching data. */
4
- callback: () => TReturnType | Promise<TReturnType>;
3
+ /** The async callback or an array of async callbacks to call for fetching data. */
4
+ callback: (() => TReturnType | Promise<TReturnType>) | Array<() => TReturnType | Promise<TReturnType>>;
5
5
  /** The default value to populate the data as when initially mounted or reloading data. */
6
6
  defaultValue: TReturnType | TDefaultValueType;
7
7
  };
8
8
  /**
9
- * Hook for invoking a piece of async code and keeping track of its state.
9
+ * Hook for invoking async code and keeping track of its state.
10
10
  *
11
11
  * Data is loaded in 3 different ways:
12
12
  * 1. On initial mount.
13
13
  * 2. When any of the `deps` change.
14
- * 3. When the `relaod` function is called.
14
+ * 3. When the `reload` function is called.
15
15
  */
16
16
  export declare const useAsync: <TReturnType, TDefaultValueType>({ callback, defaultValue }: UseAsyncProps<TReturnType, TDefaultValueType>, deps: DependencyList) => {
17
17
  data: TReturnType | TDefaultValueType;
@@ -3,12 +3,12 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.useAsync = void 0;
4
4
  const react_1 = require("react");
5
5
  /**
6
- * Hook for invoking a piece of async code and keeping track of its state.
6
+ * Hook for invoking async code and keeping track of its state.
7
7
  *
8
8
  * Data is loaded in 3 different ways:
9
9
  * 1. On initial mount.
10
10
  * 2. When any of the `deps` change.
11
- * 3. When the `relaod` function is called.
11
+ * 3. When the `reload` function is called.
12
12
  */
13
13
  const useAsync = ({ callback, defaultValue }, deps) => {
14
14
  const [data, setData] = (0, react_1.useState)(defaultValue);
@@ -19,32 +19,30 @@ const useAsync = ({ callback, defaultValue }, deps) => {
19
19
  setError(null);
20
20
  setData(defaultValue);
21
21
  try {
22
- const result = callback();
23
- if (result instanceof Promise) {
24
- // if the callback returned a promise wait for it to either resolve or reject.
25
- result
26
- .then((resolved) => {
27
- setData(resolved);
28
- setIsLoading(false);
29
- })
30
- .catch((e) => {
31
- setError(e instanceof Error ? e : new Error(String(e)));
32
- setIsLoading(false);
33
- });
34
- }
35
- else {
36
- // if the callback returned something other than a promise assume it is the data we want.
22
+ const isArrayOfCallbacks = Array.isArray(callback);
23
+ const promises = isArrayOfCallbacks ? callback.map((cb) => cb()) : [callback()];
24
+ if (!(promises[0] instanceof Promise)) {
25
+ const result = promises[0];
37
26
  setData(result);
38
27
  setIsLoading(false);
28
+ return;
39
29
  }
30
+ Promise.all(promises)
31
+ .then((resolved) => {
32
+ setData(isArrayOfCallbacks ? resolved : resolved[0]);
33
+ setIsLoading(false);
34
+ })
35
+ .catch((e) => {
36
+ setError(e instanceof Error ? e : new Error(String(e)));
37
+ setIsLoading(false);
38
+ });
40
39
  }
41
40
  catch (e) {
42
- // callback threw outside of the scope of the promise.
43
41
  setError(e instanceof Error ? e : new Error(String(e)));
44
42
  setIsLoading(false);
45
43
  }
46
44
  }, deps);
47
- // reload data on dependency change (and initial mount)
45
+ // Reload data on dependency change (and initial mount)
48
46
  (0, react_1.useEffect)(() => {
49
47
  reload();
50
48
  }, deps);
@@ -25,6 +25,7 @@ export declare const iconSources: {
25
25
  video_file: typeof import("./MatrixResources").Video;
26
26
  word_doc: typeof import("./MatrixResources").Word;
27
27
  };
28
+ component: never[];
28
29
  };
29
30
  export type ResourceSources = keyof typeof iconSources;
30
31
  export type IconOptions = string;
@@ -43,8 +44,9 @@ export type IconOptions = string;
43
44
  * @example
44
45
  * <Icon resourceSource="matrix" icon="page" className="custom-class" />
45
46
  */
46
- export declare function Icon({ resourceSource, icon, isDecorative, ...props }: {
47
+ export declare function Icon({ resourceSource, icon, componentIcon, isDecorative, ...props }: {
47
48
  icon?: IconOptions;
48
49
  resourceSource?: ResourceSources;
49
50
  isDecorative?: boolean;
51
+ componentIcon?: React.ReactNode | undefined;
50
52
  } & React.HTMLAttributes<HTMLElement> & React.SVGAttributes<SVGElement>): JSX.Element;
package/lib/Icons/Icon.js CHANGED
@@ -10,6 +10,7 @@ const MatrixResourceMap_1 = __importDefault(require("./MatrixResources/MatrixRes
10
10
  exports.iconSources = {
11
11
  generic: GenericIconMap_1.default,
12
12
  matrix: MatrixResourceMap_1.default,
13
+ component: [],
13
14
  };
14
15
  /**
15
16
  * Renders an icon based on the resource source and the icon name
@@ -26,7 +27,11 @@ exports.iconSources = {
26
27
  * @example
27
28
  * <Icon resourceSource="matrix" icon="page" className="custom-class" />
28
29
  */
29
- function Icon({ resourceSource = 'generic', icon, isDecorative = true, ...props }) {
30
+ function Icon({ resourceSource = 'generic', icon, componentIcon, isDecorative = true, ...props }) {
31
+ if (resourceSource === 'component' && componentIcon !== undefined) {
32
+ const className = props?.className || '';
33
+ return react_1.default.createElement("div", { className: className }, componentIcon);
34
+ }
30
35
  const icons = exports.iconSources[resourceSource] || null;
31
36
  // If the resource source is the current source and the icon is in the current source map, render the icon
32
37
  if (icons && icon && icon in icons) {
@@ -33,7 +33,7 @@ const react_aria_1 = require("react-aria");
33
33
  function ModalContent({ children, ...props }) {
34
34
  const ref = (0, react_1.useRef)(null);
35
35
  const { dialogProps, titleProps } = (0, react_aria_1.useDialog)(props, ref);
36
- return (react_1.default.createElement("div", { ...dialogProps, ref: ref, className: "z-50 relative bg-white rounded-lg h-screen lg:h-[calc(100vh-3.5rem)] w-screen max-w-screen-lg" }, children(titleProps)));
36
+ return (react_1.default.createElement("div", { ...dialogProps, ref: ref, className: "z-50 relative bg-white rounded-lg h-screen lg:h-[calc(100vh-3.5rem)] w-screen max-w-screen-lg outline-0" }, children(titleProps)));
37
37
  }
38
38
  function Modal({ isDismissable, state, overlayProps, children, ...props }) {
39
39
  const ref = (0, react_1.useRef)(null);
@@ -1,7 +1,9 @@
1
1
  import React from 'react';
2
2
  import { DOMAttributes, FocusableElement } from '@react-types/shared';
3
- export declare function ModalTrigger({ label, showLabel, icon, isDisabled, children, ...props }: {
3
+ export declare function ModalTrigger({ label, labelClasses, showLabel, containerClasses, icon, isDisabled, children, ...props }: {
4
4
  label: string;
5
+ labelClasses?: string;
6
+ containerClasses?: string;
5
7
  showLabel?: boolean;
6
8
  icon?: React.ReactNode;
7
9
  isDisabled?: boolean;
@@ -10,17 +10,18 @@ const react_stately_1 = require("react-stately");
10
10
  const clsx_1 = __importDefault(require("clsx"));
11
11
  const Modal_1 = require("./Modal");
12
12
  const ModalOpeningButton_1 = require("./ModalOpeningButton");
13
- function ModalTrigger({ label, showLabel, icon, isDisabled, children, ...props }) {
13
+ function ModalTrigger({ label, labelClasses, showLabel, containerClasses, icon, isDisabled, children, ...props }) {
14
14
  const state = (0, react_stately_1.useOverlayTriggerState)(props);
15
15
  const { triggerProps, overlayProps } = (0, react_aria_1.useOverlayTrigger)({ type: 'dialog' }, state);
16
16
  let ariaAttr = {};
17
17
  if (!showLabel) {
18
18
  ariaAttr = { ...ariaAttr, 'aria-label': label };
19
19
  }
20
- return (react_1.default.createElement(react_1.default.Fragment, null,
21
- react_1.default.createElement(ModalOpeningButton_1.ModalOpeningButton, { type: "button", ...triggerProps, ...ariaAttr, isDisabled: isDisabled, className: (0, clsx_1.default)('flex p-1 px-1.5 rounded mr-auto text-blue-300 hover:bg-blue-100 focus:bg-blue-100 focus:outline-none', isDisabled && 'hover:bg-transparent cursor-not-allowed text-gray-600') },
20
+ return (react_1.default.createElement("div", { className: "squiz-gb-scope" },
21
+ react_1.default.createElement(ModalOpeningButton_1.ModalOpeningButton, { type: "button", ...triggerProps, ...ariaAttr, isDisabled: isDisabled, className: (0, clsx_1.default)(`${containerClasses ||
22
+ 'flex p-1 px-1.5 rounded mr-auto text-blue-300 hover:bg-blue-100 focus:bg-blue-100 focus:outline-none'}`, isDisabled && 'hover:bg-transparent cursor-not-allowed text-gray-600') },
22
23
  icon,
23
- showLabel && react_1.default.createElement("span", { className: "ml-1 text-sm font-semibold leading-4" }, label)),
24
+ showLabel && react_1.default.createElement("span", { className: `${labelClasses || 'ml-1 text-sm font-semibold leading-4'}` }, label)),
24
25
  state.isOpen && (react_1.default.createElement(Modal_1.Modal, { isDismissable: true, state: state, overlayProps: overlayProps }, (titleProps) => children(state.close, titleProps)))));
25
26
  }
26
27
  exports.ModalTrigger = ModalTrigger;
@@ -9,5 +9,6 @@ export interface PreviewPanelProps {
9
9
  onSelect: (resource: any) => void;
10
10
  onClose: () => void;
11
11
  ResourceComponent?: React.ElementType;
12
+ selectionCallback?: (param: any) => void;
12
13
  }
13
- export declare const PreviewPanel: ({ resource, previewModalOverlayProps, modalState, onSelect, onClose, ResourceComponent, }: PreviewPanelProps) => JSX.Element;
14
+ export declare const PreviewPanel: ({ resource, previewModalOverlayProps, modalState, onSelect, onClose, ResourceComponent, selectionCallback, }: PreviewPanelProps) => JSX.Element;
@@ -31,7 +31,7 @@ const react_1 = __importStar(require("react"));
31
31
  const react_responsive_1 = require("react-responsive");
32
32
  const Icon_1 = require("../Icons/Icon");
33
33
  const PreviewModal_1 = __importDefault(require("./PreviewModal"));
34
- const PreviewPanel = function ({ resource, previewModalOverlayProps, modalState, onSelect, onClose, ResourceComponent, }) {
34
+ const PreviewPanel = function ({ resource, previewModalOverlayProps, modalState, onSelect, onClose, ResourceComponent, selectionCallback, }) {
35
35
  // Watch the media size to see if we are on mobile size
36
36
  const isMobile = (0, react_responsive_1.useMediaQuery)({ query: '(max-width: 640px)' });
37
37
  // If we are on mobile and the selected resource changes show the preview panel modal.
@@ -41,10 +41,10 @@ const PreviewPanel = function ({ resource, previewModalOverlayProps, modalState,
41
41
  }
42
42
  }, [resource, isMobile]);
43
43
  const previewPanel = resource && (react_1.default.createElement(react_1.default.Fragment, null,
44
- react_1.default.createElement("div", { className: "flex flex-col grow" }, ResourceComponent && react_1.default.createElement(ResourceComponent, { resource: resource })),
44
+ react_1.default.createElement("div", { className: "flex flex-col grow" }, ResourceComponent && react_1.default.createElement(ResourceComponent, { resource: resource, selectionCallback: selectionCallback })),
45
45
  react_1.default.createElement("div", { className: "flex justify-end border-t border-gray-300" },
46
46
  react_1.default.createElement("button", { type: "button", onClick: () => onSelect(resource), className: "rounded text-sm text-white bg-blue-300 py-2 px-2.5 m-5" }, "Select"))));
47
- return (react_1.default.createElement(react_1.default.Fragment, null,
47
+ return (react_1.default.createElement("div", { className: "squiz-gb-scope h-full" },
48
48
  !isMobile && react_1.default.createElement("h3", { className: "sr-only" }, "Resource Details"),
49
49
  resource === null && (react_1.default.createElement("div", { className: "max-sm:hidden flex flex-col h-full" },
50
50
  react_1.default.createElement("div", { className: "flex flex-col grow items-center mt-20 mx-20" },
@@ -1,3 +1,4 @@
1
+ import { ReactElement } from 'react';
1
2
  import { DOMAttributes, FocusableElement } from '@react-types/shared';
2
3
  import { OverlayTriggerState } from 'react-stately';
3
4
  import { ResourceSources } from '../Icons/Icon';
@@ -12,8 +13,9 @@ interface ResourceItem<T> {
12
13
  onDrillDown?: (node: T) => void;
13
14
  className: string;
14
15
  allowedTypes?: string[] | undefined;
16
+ componentIcon?: ReactElement | undefined;
15
17
  iconSource?: ResourceSources;
16
18
  showChevron?: boolean;
17
19
  }
18
- declare const ResourceItem: <T>({ item, selected, label, type, childCount, previewModalState, onSelect, onDrillDown, className, allowedTypes, iconSource, showChevron, }: ResourceItem<T>) => JSX.Element;
20
+ declare const ResourceItem: <T>({ item, selected, label, type, childCount, previewModalState, onSelect, onDrillDown, className, allowedTypes, componentIcon, iconSource, showChevron, }: ResourceItem<T>) => JSX.Element;
19
21
  export { ResourceItem };
@@ -8,15 +8,16 @@ const react_1 = __importDefault(require("react"));
8
8
  const react_aria_1 = require("react-aria");
9
9
  const ModalOpeningButton_1 = require("../Modal/ModalOpeningButton");
10
10
  const Icon_1 = require("../Icons/Icon");
11
- const ResourceItem = ({ item, selected, label, type, childCount, previewModalState, onSelect, onDrillDown, className, allowedTypes, iconSource = 'matrix', showChevron = false, }) => {
11
+ const uuid_1 = require("../Utils/uuid");
12
+ const ResourceItem = ({ item, selected, label, type, childCount, previewModalState, onSelect, onDrillDown, className, allowedTypes, componentIcon, iconSource = 'matrix', showChevron = false, }) => {
12
13
  const { triggerProps, overlayProps } = (0, react_aria_1.useOverlayTrigger)({ type: 'dialog' }, previewModalState);
13
14
  const isDisabled = allowedTypes !== undefined && !allowedTypes.includes(type);
14
15
  const title = isDisabled ? "You can't select this item" : label;
15
- return (react_1.default.createElement("li", { className: `flex items-stretch p-1 bg-white border-1 border-grey-200 min-h-[64px] ${className}` },
16
+ return (react_1.default.createElement("li", { className: `squiz-gb-scope flex items-stretch p-1 bg-white border-1 border-grey-200 min-h-[64px] ${className}`, key: (0, uuid_1.uuid)() },
16
17
  react_1.default.createElement(ModalOpeningButton_1.ModalOpeningButton, { type: "button", ...triggerProps, isDisabled: isDisabled, onPress: () => onSelect(item, overlayProps), "aria-label": childCount === undefined ? `Drill down to ${label} children` : '', className: `
17
18
  relative grow flex items-center px-4 py-2 rounded outline-0 ${selected ? 'bg-blue-100 text-blue-400' : ''} ${childCount !== undefined && childCount > 0 ? 'mr-2' : ''} ${isDisabled ? 'font-normal text-gray-600 cursor-not-allowed' : 'hover:bg-gray-50 focus:bg-gray-50'}
18
19
  `, title: title },
19
- react_1.default.createElement(Icon_1.Icon, { icon: type, resourceSource: iconSource, "aria-label": type, className: `mr-4 shrink-0 ${isDisabled && 'opacity-40'}` }),
20
+ react_1.default.createElement(Icon_1.Icon, { icon: type, resourceSource: iconSource, "aria-label": type, className: `mr-4 shrink-0 ${isDisabled && 'opacity-40'}`, componentIcon: componentIcon }),
20
21
  react_1.default.createElement("span", { className: `relative flex items-center ${selected ? 'mr-8' : ''}` },
21
22
  react_1.default.createElement("span", { className: "line-clamp-2 text-left break-word" }, label),
22
23
  selected && react_1.default.createElement(Icon_1.Icon, { icon: 'selected', "aria-label": "selected", className: "absolute -right-8" })),
@@ -1,7 +1,8 @@
1
1
  interface ResourceState {
2
2
  state: 'error' | 'empty';
3
3
  message?: string;
4
- handleReload: () => void;
4
+ handleReload?: () => void;
5
+ showButton?: boolean;
5
6
  }
6
- declare const ResourceState: ({ state, message, handleReload }: ResourceState) => JSX.Element;
7
+ declare const ResourceState: ({ state, message, handleReload, showButton }: ResourceState) => JSX.Element;
7
8
  export { ResourceState };
@@ -6,12 +6,13 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.ResourceState = void 0;
7
7
  const react_1 = __importDefault(require("react"));
8
8
  const Icon_1 = require("../Icons/Icon");
9
- const ResourceState = function ({ state, message, handleReload }) {
10
- return (react_1.default.createElement("div", { className: "flex flex-col items-center rounded-lg py-8 bg-white h-204 gap-3" },
9
+ const ResourceState = function ({ state, message, handleReload, showButton = true }) {
10
+ const RetryButton = (react_1.default.createElement("button", { type: "button", onClick: handleReload, className: "flex flex-row items-center justify-center gap-3 bg-black bg-opacity-10 h-9 mt-3 rounded text-md font-bold text-gray-700 py-2 px-3" },
11
+ react_1.default.createElement(Icon_1.Icon, { icon: state === 'empty' ? 'back' : 'retry', "aria-hidden": true }),
12
+ state === 'empty' ? 'Back to source list' : 'Try again'));
13
+ return (react_1.default.createElement("div", { className: "squiz-gb-scope flex flex-col items-center rounded-lg py-8 bg-white h-204 gap-3" },
11
14
  react_1.default.createElement(Icon_1.Icon, { icon: state, "aria-hidden": true }),
12
15
  react_1.default.createElement("span", { className: "text-md text-gray-800 font-semibold leading-5" }, state === 'empty' ? 'There are no items to display' : message),
13
- react_1.default.createElement("button", { type: "button", onClick: handleReload, className: "flex flex-row items-center justify-center gap-3 bg-black bg-opacity-10 h-9 mt-3 rounded text-md font-bold text-gray-700 py-2 px-3" },
14
- react_1.default.createElement(Icon_1.Icon, { icon: state === 'empty' ? 'back' : 'retry', "aria-hidden": true }),
15
- state === 'empty' ? 'Back to source list' : 'Try again')));
16
+ showButton && RetryButton));
16
17
  };
17
18
  exports.ResourceState = ResourceState;
@@ -6,8 +6,9 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.SkeletonList = void 0;
7
7
  const react_1 = __importDefault(require("react"));
8
8
  const SkeletonListItem_1 = require("../ListItem/SkeletonListItem");
9
+ const uuid_1 = require("../../Utils/uuid");
9
10
  const clsx_1 = __importDefault(require("clsx"));
10
- const SkeletonList = ({ itemCount = 8, className, ariaLabel }) => (react_1.default.createElement("ul", { className: (0, clsx_1.default)(`flex flex-col px-7 my-4 focus-visible:outline-0`, className), "aria-label": `${ariaLabel || 'Skeleton loader list'}` }, [...Array(itemCount)].map((_item, index) => {
11
- return react_1.default.createElement(SkeletonListItem_1.SkeletonListItem, { key: index });
11
+ const SkeletonList = ({ itemCount = 8, className, ariaLabel }) => (react_1.default.createElement("ul", { className: (0, clsx_1.default)(`flex flex-col px-7 my-4 focus-visible:outline-0`, className), "aria-label": `${ariaLabel || 'Skeleton loader list'}` }, [...Array(itemCount)].map((_item) => {
12
+ return react_1.default.createElement(SkeletonListItem_1.SkeletonListItem, { key: (0, uuid_1.uuid)() });
12
13
  })));
13
14
  exports.SkeletonList = SkeletonList;
@@ -7,7 +7,7 @@ exports.Spinner = void 0;
7
7
  const react_1 = __importDefault(require("react"));
8
8
  const clsx_1 = __importDefault(require("clsx"));
9
9
  const Spinner = ({ size = 'sm', className, label = 'Loading' }) => {
10
- return (react_1.default.createElement("div", { className: "spinner__wrapper text-gray-600", "aria-label": label },
10
+ return (react_1.default.createElement("div", { className: "squiz-gb-scope spinner__wrapper text-gray-600", "aria-label": label },
11
11
  react_1.default.createElement("div", { className: (0, clsx_1.default)('spinner', size && `spinner--${size}`, className), role: "status" })));
12
12
  };
13
13
  exports.Spinner = Spinner;
@@ -0,0 +1,2 @@
1
+ declare function uuid(): string;
2
+ export { uuid };
@@ -0,0 +1,7 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.uuid = void 0;
4
+ function uuid() {
5
+ return '10000000-1000-4000-8000-100000000000'.replace(/[018]/g, (c) => (Number(c) ^ (window.crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (Number(c) / 4)))).toString(16));
6
+ }
7
+ exports.uuid = uuid;
package/lib/index.d.ts CHANGED
@@ -10,3 +10,4 @@ export * from './PreviewPanel/PreviewPanel';
10
10
  export * from './PreviewPanel/PreviewPanelHOC';
11
11
  export * from './Hooks/useAsync';
12
12
  export * from './ResetButton/ResetButton';
13
+ export * from './Utils/uuid';
package/lib/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  "use strict";
2
+ /* istanbul ignore file */
2
3
  var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
4
  if (k2 === undefined) k2 = k;
4
5
  var desc = Object.getOwnPropertyDescriptor(m, k);
@@ -26,3 +27,4 @@ __exportStar(require("./PreviewPanel/PreviewPanel"), exports);
26
27
  __exportStar(require("./PreviewPanel/PreviewPanelHOC"), exports);
27
28
  __exportStar(require("./Hooks/useAsync"), exports);
28
29
  __exportStar(require("./ResetButton/ResetButton"), exports);
30
+ __exportStar(require("./Utils/uuid"), exports);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@squiz/generic-browser-lib",
3
- "version": "1.39.1-alpha.1",
3
+ "version": "1.39.1-alpha.11",
4
4
  "description": "Package with reusable components used by resource/component browsers",
5
5
  "main": "lib/index.js",
6
6
  "types": "lib/index.d.ts",
@@ -72,5 +72,5 @@
72
72
  "volta": {
73
73
  "node": "18.15.0"
74
74
  },
75
- "gitHead": "16b9f02b751a923a71f96684bd46c781fc97dcbc"
75
+ "gitHead": "788f8adc16c48ce3121dca04ca370ebc2c537758"
76
76
  }
package/postcss.config.js CHANGED
@@ -6,6 +6,12 @@ module.exports = {
6
6
  require('postcss-prefix-selector')({
7
7
  prefix: '.squiz-gb-scope',
8
8
  includeFiles: ['./src/index.scss'],
9
+ transform(prefix, selector, prefixedSelector, filePath, rule) {
10
+ if (selector.match(/(squiz-gb-scope)/)) {
11
+ return selector;
12
+ }
13
+ return prefixedSelector;
14
+ },
9
15
  }),
10
16
  ],
11
17
  };
@@ -3,7 +3,7 @@ import { renderHook, waitFor } from '@testing-library/react';
3
3
  import { useAsync } from './useAsync';
4
4
 
5
5
  describe('useAsync', () => {
6
- const renderAsyncHook = (callback: () => any, deps: DependencyList) => {
6
+ const renderAsyncHook = (callback: () => any | [], deps: DependencyList) => {
7
7
  return renderHook(
8
8
  ({ deps }: { deps: DependencyList }) => useAsync({ callback, defaultValue: 'Initial state' }, deps),
9
9
  { initialProps: { deps } },
@@ -103,4 +103,33 @@ describe('useAsync', () => {
103
103
  expect(result.current.data).toBe('Initial state');
104
104
  },
105
105
  );
106
+
107
+ test('useAsync handles multiple asynchronous callbacks', async () => {
108
+ const asyncCallbackOne = jest.fn(() => Promise.resolve('First Async Data'));
109
+ const asyncCallbackTwo = jest.fn(() => Promise.resolve('Second Async Data'));
110
+
111
+ const { result } = renderHook(() =>
112
+ useAsync(
113
+ {
114
+ callback: [asyncCallbackOne, asyncCallbackTwo],
115
+ defaultValue: 'Default Value',
116
+ },
117
+ [],
118
+ ),
119
+ );
120
+
121
+ // Initial state
122
+ expect(result.current.data).toBe('Default Value');
123
+ expect(result.current.isLoading).toBe(true);
124
+ expect(result.current.error).toBe(null);
125
+
126
+ await waitFor(() => {
127
+ expect(result.current.data).toStrictEqual(['First Async Data', 'Second Async Data']);
128
+ expect(result.current.isLoading).toBe(false);
129
+ expect(result.current.error).toBe(null);
130
+ });
131
+
132
+ expect(asyncCallbackOne).toHaveBeenCalled();
133
+ expect(asyncCallbackTwo).toHaveBeenCalled();
134
+ });
106
135
  });
@@ -1,19 +1,19 @@
1
1
  import { DependencyList, useState, useCallback, useEffect } from 'react';
2
2
 
3
3
  export type UseAsyncProps<TReturnType, TDefaultValueType> = {
4
- /** The async callback to call for fetching data. */
5
- callback: () => TReturnType | Promise<TReturnType>;
4
+ /** The async callback or an array of async callbacks to call for fetching data. */
5
+ callback: (() => TReturnType | Promise<TReturnType>) | Array<() => TReturnType | Promise<TReturnType>>;
6
6
  /** The default value to populate the data as when initially mounted or reloading data. */
7
7
  defaultValue: TReturnType | TDefaultValueType;
8
8
  };
9
9
 
10
10
  /**
11
- * Hook for invoking a piece of async code and keeping track of its state.
11
+ * Hook for invoking async code and keeping track of its state.
12
12
  *
13
13
  * Data is loaded in 3 different ways:
14
14
  * 1. On initial mount.
15
15
  * 2. When any of the `deps` change.
16
- * 3. When the `relaod` function is called.
16
+ * 3. When the `reload` function is called.
17
17
  */
18
18
  export const useAsync = <TReturnType, TDefaultValueType>(
19
19
  { callback, defaultValue }: UseAsyncProps<TReturnType, TDefaultValueType>,
@@ -22,38 +22,39 @@ export const useAsync = <TReturnType, TDefaultValueType>(
22
22
  const [data, setData] = useState(defaultValue);
23
23
  const [isLoading, setIsLoading] = useState(false);
24
24
  const [error, setError] = useState<Error | null>(null);
25
+
25
26
  const reload = useCallback(() => {
26
27
  setIsLoading(true);
27
28
  setError(null);
28
29
  setData(defaultValue);
29
30
 
30
31
  try {
31
- const result = callback();
32
-
33
- if (result instanceof Promise) {
34
- // if the callback returned a promise wait for it to either resolve or reject.
35
- result
36
- .then((resolved: TReturnType) => {
37
- setData(resolved);
38
- setIsLoading(false);
39
- })
40
- .catch((e: unknown) => {
41
- setError(e instanceof Error ? e : new Error(String(e)));
42
- setIsLoading(false);
43
- });
44
- } else {
45
- // if the callback returned something other than a promise assume it is the data we want.
32
+ const isArrayOfCallbacks = Array.isArray(callback);
33
+ const promises = isArrayOfCallbacks ? callback.map((cb) => cb()) : [callback()];
34
+
35
+ if (!(promises[0] instanceof Promise)) {
36
+ const result = promises[0];
46
37
  setData(result);
47
38
  setIsLoading(false);
39
+ return;
48
40
  }
41
+
42
+ Promise.all(promises)
43
+ .then((resolved: TReturnType[]) => {
44
+ setData(isArrayOfCallbacks ? (resolved as TReturnType) : resolved[0]);
45
+ setIsLoading(false);
46
+ })
47
+ .catch((e: unknown) => {
48
+ setError(e instanceof Error ? e : new Error(String(e)));
49
+ setIsLoading(false);
50
+ });
49
51
  } catch (e: unknown) {
50
- // callback threw outside of the scope of the promise.
51
52
  setError(e instanceof Error ? e : new Error(String(e)));
52
53
  setIsLoading(false);
53
54
  }
54
55
  }, deps);
55
56
 
56
- // reload data on dependency change (and initial mount)
57
+ // Reload data on dependency change (and initial mount)
57
58
  useEffect(() => {
58
59
  reload();
59
60
  }, deps);
@@ -59,4 +59,12 @@ describe('Icon', () => {
59
59
  }
60
60
  }
61
61
  });
62
+
63
+ it('should render passed componentIcon and resourceSource is set to component', async () => {
64
+ const Component = () => <div data-testid="test-icon" />;
65
+ const { getByTestId } = render(
66
+ <Icon resourceSource="component" componentIcon={(<Component />) as React.ReactNode} />,
67
+ );
68
+ expect(getByTestId('test-icon')).toBeInTheDocument();
69
+ });
62
70
  });
@@ -5,6 +5,7 @@ import MatrixResourceMap from './MatrixResources/MatrixResourceMap';
5
5
  export const iconSources = {
6
6
  generic: GenericIconMap,
7
7
  matrix: MatrixResourceMap,
8
+ component: [],
8
9
  };
9
10
 
10
11
  // The resource sources options
@@ -29,14 +30,21 @@ export type IconOptions = string;
29
30
  export function Icon({
30
31
  resourceSource = 'generic',
31
32
  icon,
33
+ componentIcon,
32
34
  isDecorative = true,
33
35
  ...props
34
36
  }: {
35
37
  icon?: IconOptions;
36
38
  resourceSource?: ResourceSources;
37
39
  isDecorative?: boolean;
40
+ componentIcon?: React.ReactNode | undefined;
38
41
  } & React.HTMLAttributes<HTMLElement> &
39
42
  React.SVGAttributes<SVGElement>) {
43
+ if (resourceSource === 'component' && componentIcon !== undefined) {
44
+ const className = props?.className || '';
45
+ return <div className={className}>{componentIcon}</div>;
46
+ }
47
+
40
48
  const icons = (iconSources[resourceSource] as any) || null;
41
49
 
42
50
  // If the resource source is the current source and the icon is in the current source map, render the icon
@@ -22,6 +22,26 @@ describe('Modal', () => {
22
22
  </div>,
23
23
  );
24
24
  expect(screen.queryByTestId('modal')).toBeFalsy();
25
+ expect(screen.queryAllByLabelText('Open testing modal')).toHaveLength(0);
26
+ });
27
+
28
+ it('Modal should render label', async () => {
29
+ render(
30
+ <div>
31
+ <ModalTrigger label={'Open testing modal'} showLabel={false}>
32
+ {(onClose, titleProps) => (
33
+ <div data-testingid="modal">
34
+ <div {...titleProps}>Testing</div>
35
+ <button type="button" onClick={onClose}>
36
+ Close
37
+ </button>
38
+ </div>
39
+ )}
40
+ </ModalTrigger>
41
+ <div style={{ height: '150vh' }} />
42
+ </div>,
43
+ );
44
+ expect(screen.getByLabelText('Open testing modal')).toBeTruthy();
25
45
  });
26
46
 
27
47
  it('Modal opens when triggered', async () => {
@@ -21,7 +21,7 @@ function ModalContent({
21
21
  <div
22
22
  {...dialogProps}
23
23
  ref={ref}
24
- className="z-50 relative bg-white rounded-lg h-screen lg:h-[calc(100vh-3.5rem)] w-screen max-w-screen-lg"
24
+ className="z-50 relative bg-white rounded-lg h-screen lg:h-[calc(100vh-3.5rem)] w-screen max-w-screen-lg outline-0"
25
25
  >
26
26
  {children(titleProps)}
27
27
  </div>
@@ -9,13 +9,17 @@ import { ModalOpeningButton } from './ModalOpeningButton';
9
9
 
10
10
  export function ModalTrigger({
11
11
  label,
12
+ labelClasses,
12
13
  showLabel,
14
+ containerClasses,
13
15
  icon,
14
16
  isDisabled,
15
17
  children,
16
18
  ...props
17
19
  }: {
18
20
  label: string;
21
+ labelClasses?: string;
22
+ containerClasses?: string;
19
23
  showLabel?: boolean;
20
24
  icon?: React.ReactNode;
21
25
  isDisabled?: boolean;
@@ -30,25 +34,28 @@ export function ModalTrigger({
30
34
  }
31
35
 
32
36
  return (
33
- <>
37
+ <div className="squiz-gb-scope">
34
38
  <ModalOpeningButton
35
39
  type="button"
36
40
  {...triggerProps}
37
41
  {...ariaAttr}
38
42
  isDisabled={isDisabled}
39
43
  className={clsx(
40
- 'flex p-1 px-1.5 rounded mr-auto text-blue-300 hover:bg-blue-100 focus:bg-blue-100 focus:outline-none',
44
+ `${
45
+ containerClasses ||
46
+ 'flex p-1 px-1.5 rounded mr-auto text-blue-300 hover:bg-blue-100 focus:bg-blue-100 focus:outline-none'
47
+ }`,
41
48
  isDisabled && 'hover:bg-transparent cursor-not-allowed text-gray-600',
42
49
  )}
43
50
  >
44
51
  {icon}
45
- {showLabel && <span className="ml-1 text-sm font-semibold leading-4">{label}</span>}
52
+ {showLabel && <span className={`${labelClasses || 'ml-1 text-sm font-semibold leading-4'}`}>{label}</span>}
46
53
  </ModalOpeningButton>
47
54
  {state.isOpen && (
48
55
  <Modal isDismissable state={state} overlayProps={overlayProps}>
49
56
  {(titleProps) => children(state.close, titleProps)}
50
57
  </Modal>
51
58
  )}
52
- </>
59
+ </div>
53
60
  );
54
61
  }
@@ -14,6 +14,7 @@ export interface PreviewPanelProps {
14
14
  onSelect: (resource: any) => void;
15
15
  onClose: () => void;
16
16
  ResourceComponent?: React.ElementType;
17
+ selectionCallback?: (param: any) => void;
17
18
  }
18
19
 
19
20
  export const PreviewPanel = function ({
@@ -23,6 +24,7 @@ export const PreviewPanel = function ({
23
24
  onSelect,
24
25
  onClose,
25
26
  ResourceComponent,
27
+ selectionCallback,
26
28
  }: PreviewPanelProps) {
27
29
  // Watch the media size to see if we are on mobile size
28
30
  const isMobile = useMediaQuery({ query: '(max-width: 640px)' });
@@ -36,7 +38,9 @@ export const PreviewPanel = function ({
36
38
 
37
39
  const previewPanel = resource && (
38
40
  <>
39
- <div className="flex flex-col grow">{ResourceComponent && <ResourceComponent resource={resource} />}</div>
41
+ <div className="flex flex-col grow">
42
+ {ResourceComponent && <ResourceComponent resource={resource} selectionCallback={selectionCallback} />}
43
+ </div>
40
44
  <div className="flex justify-end border-t border-gray-300">
41
45
  <button
42
46
  type="button"
@@ -50,7 +54,7 @@ export const PreviewPanel = function ({
50
54
  );
51
55
 
52
56
  return (
53
- <>
57
+ <div className="squiz-gb-scope h-full">
54
58
  {/* Dialog has its own title */}
55
59
  {!isMobile && <h3 className="sr-only">Resource Details</h3>}
56
60
 
@@ -78,6 +82,6 @@ export const PreviewPanel = function ({
78
82
 
79
83
  {/* If not mobile, just print the details out */}
80
84
  {resource && !isMobile && <div className="flex flex-col h-full">{previewPanel}</div>}
81
- </>
85
+ </div>
82
86
  );
83
87
  };
@@ -1,10 +1,11 @@
1
- import React from 'react';
1
+ import React, { ReactElement } from 'react';
2
2
  import { DOMAttributes, FocusableElement } from '@react-types/shared';
3
3
  import { useOverlayTrigger } from 'react-aria';
4
4
  import { OverlayTriggerState } from 'react-stately';
5
5
 
6
6
  import { ModalOpeningButton } from '../Modal/ModalOpeningButton';
7
7
  import { Icon, IconOptions, ResourceSources } from '../Icons/Icon';
8
+ import { uuid } from '../Utils/uuid';
8
9
 
9
10
  interface ResourceItem<T> {
10
11
  item: T;
@@ -17,6 +18,7 @@ interface ResourceItem<T> {
17
18
  onDrillDown?: (node: T) => void;
18
19
  className: string;
19
20
  allowedTypes?: string[] | undefined;
21
+ componentIcon?: ReactElement | undefined;
20
22
  iconSource?: ResourceSources;
21
23
  showChevron?: boolean;
22
24
  }
@@ -32,6 +34,7 @@ const ResourceItem = <T,>({
32
34
  onDrillDown,
33
35
  className,
34
36
  allowedTypes,
37
+ componentIcon,
35
38
  iconSource = 'matrix',
36
39
  showChevron = false,
37
40
  }: ResourceItem<T>) => {
@@ -40,7 +43,10 @@ const ResourceItem = <T,>({
40
43
  const title = isDisabled ? "You can't select this item" : label;
41
44
 
42
45
  return (
43
- <li className={`flex items-stretch p-1 bg-white border-1 border-grey-200 min-h-[64px] ${className}`}>
46
+ <li
47
+ className={`squiz-gb-scope flex items-stretch p-1 bg-white border-1 border-grey-200 min-h-[64px] ${className}`}
48
+ key={uuid()}
49
+ >
44
50
  <ModalOpeningButton
45
51
  type="button"
46
52
  {...triggerProps}
@@ -59,6 +65,7 @@ const ResourceItem = <T,>({
59
65
  resourceSource={iconSource}
60
66
  aria-label={type}
61
67
  className={`mr-4 shrink-0 ${isDisabled && 'opacity-40'}`}
68
+ componentIcon={componentIcon}
62
69
  />
63
70
  <span className={`relative flex items-center ${selected ? 'mr-8' : ''}`}>
64
71
  <span className="line-clamp-2 text-left break-word">{label}</span>
@@ -4,26 +4,31 @@ import { Icon, IconOptions } from '../Icons/Icon';
4
4
  interface ResourceState {
5
5
  state: 'error' | 'empty';
6
6
  message?: string;
7
- handleReload: () => void;
7
+ handleReload?: () => void;
8
+ showButton?: boolean;
8
9
  }
9
10
 
10
- const ResourceState = function ({ state, message, handleReload }: ResourceState) {
11
+ const ResourceState = function ({ state, message, handleReload, showButton = true }: ResourceState) {
12
+ const RetryButton = (
13
+ <button
14
+ type="button"
15
+ onClick={handleReload}
16
+ className="flex flex-row items-center justify-center gap-3 bg-black bg-opacity-10 h-9 mt-3 rounded text-md font-bold text-gray-700 py-2 px-3"
17
+ >
18
+ <Icon icon={state === 'empty' ? 'back' : 'retry'} aria-hidden />
19
+ {state === 'empty' ? 'Back to source list' : 'Try again'}
20
+ </button>
21
+ );
22
+
11
23
  return (
12
- <div className="flex flex-col items-center rounded-lg py-8 bg-white h-204 gap-3">
24
+ <div className="squiz-gb-scope flex flex-col items-center rounded-lg py-8 bg-white h-204 gap-3">
13
25
  <Icon icon={state as IconOptions} aria-hidden />
14
26
  {/* Message */}
15
27
  <span className="text-md text-gray-800 font-semibold leading-5">
16
28
  {state === 'empty' ? 'There are no items to display' : message}
17
29
  </span>
18
30
  {/* Retry button */}
19
- <button
20
- type="button"
21
- onClick={handleReload}
22
- className="flex flex-row items-center justify-center gap-3 bg-black bg-opacity-10 h-9 mt-3 rounded text-md font-bold text-gray-700 py-2 px-3"
23
- >
24
- <Icon icon={state === 'empty' ? 'back' : 'retry'} aria-hidden />
25
- {state === 'empty' ? 'Back to source list' : 'Try again'}
26
- </button>
31
+ {showButton && RetryButton}
27
32
  </div>
28
33
  );
29
34
  };
@@ -1,5 +1,6 @@
1
1
  import React from 'react';
2
2
  import { SkeletonListItem } from '../ListItem/SkeletonListItem';
3
+ import { uuid } from '../../Utils/uuid';
3
4
  import clsx from 'clsx';
4
5
 
5
6
  export type SkeletonListProps = {
@@ -13,8 +14,8 @@ export const SkeletonList = ({ itemCount = 8, className, ariaLabel }: SkeletonLi
13
14
  className={clsx(`flex flex-col px-7 my-4 focus-visible:outline-0`, className)}
14
15
  aria-label={`${ariaLabel || 'Skeleton loader list'}`}
15
16
  >
16
- {[...Array(itemCount)].map((_item, index: number) => {
17
- return <SkeletonListItem key={index} />;
17
+ {[...Array(itemCount)].map((_item) => {
18
+ return <SkeletonListItem key={uuid()} />;
18
19
  })}
19
20
  </ul>
20
21
  );
@@ -9,7 +9,7 @@ export type SpinnerProps = {
9
9
 
10
10
  export const Spinner = ({ size = 'sm', className, label = 'Loading' }: SpinnerProps): ReactElement => {
11
11
  return (
12
- <div className="spinner__wrapper text-gray-600" aria-label={label}>
12
+ <div className="squiz-gb-scope spinner__wrapper text-gray-600" aria-label={label}>
13
13
  <div className={clsx('spinner', size && `spinner--${size}`, className)} role="status" />
14
14
  </div>
15
15
  );
@@ -0,0 +1,7 @@
1
+ function uuid(): string {
2
+ return '10000000-1000-4000-8000-100000000000'.replace(/[018]/g, (c) =>
3
+ (Number(c) ^ (window.crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (Number(c) / 4)))).toString(16),
4
+ );
5
+ }
6
+
7
+ export { uuid };
package/src/index.ts CHANGED
@@ -1,3 +1,5 @@
1
+ /* istanbul ignore file */
2
+
1
3
  export * from './Icons/Icon';
2
4
 
3
5
  export * from './Spinner/Spinner';
@@ -18,3 +20,5 @@ export * from './PreviewPanel/PreviewPanelHOC';
18
20
  export * from './Hooks/useAsync';
19
21
 
20
22
  export * from './ResetButton/ResetButton';
23
+
24
+ export * from './Utils/uuid';