@frontify/guideline-blocks-settings 0.32.1 → 0.33.0

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 (100) hide show
  1. package/CHANGELOG.md +147 -0
  2. package/dist/components/Attachments/AttachmentItem.es.js +128 -109
  3. package/dist/components/Attachments/AttachmentItem.es.js.map +1 -1
  4. package/dist/components/Attachments/Attachments.es.js +9 -1
  5. package/dist/components/Attachments/Attachments.es.js.map +1 -1
  6. package/dist/components/Attachments/AttachmentsButtonTrigger.es.js +17 -9
  7. package/dist/components/Attachments/AttachmentsButtonTrigger.es.js.map +1 -1
  8. package/dist/components/BlockItemWrapper/BlockItemWrapper.es.js +38 -44
  9. package/dist/components/BlockItemWrapper/BlockItemWrapper.es.js.map +1 -1
  10. package/dist/components/BlockItemWrapper/Toolbar/AttachmentsToolbarButton/AttachmentsToolbarButton.es.js +33 -0
  11. package/dist/components/BlockItemWrapper/Toolbar/AttachmentsToolbarButton/AttachmentsToolbarButton.es.js.map +1 -0
  12. package/dist/components/BlockItemWrapper/Toolbar/AttachmentsToolbarButton/AttachmentsToolbarButtonTrigger.es.js +26 -0
  13. package/dist/components/BlockItemWrapper/Toolbar/AttachmentsToolbarButton/AttachmentsToolbarButtonTrigger.es.js.map +1 -0
  14. package/dist/components/BlockItemWrapper/Toolbar/BaseToolbarButton.es.js +28 -0
  15. package/dist/components/BlockItemWrapper/Toolbar/BaseToolbarButton.es.js.map +1 -0
  16. package/dist/components/BlockItemWrapper/Toolbar/DragHandleToolbarButton/DragHandleToolbarButton.es.js +35 -0
  17. package/dist/components/BlockItemWrapper/Toolbar/DragHandleToolbarButton/DragHandleToolbarButton.es.js.map +1 -0
  18. package/dist/components/BlockItemWrapper/Toolbar/FlyoutToolbarButton/FlyoutToolbarButton.es.js +44 -0
  19. package/dist/components/BlockItemWrapper/Toolbar/FlyoutToolbarButton/FlyoutToolbarButton.es.js.map +1 -0
  20. package/dist/components/BlockItemWrapper/Toolbar/MenuToolbarButton/MenuToolbarButton.es.js +25 -0
  21. package/dist/components/BlockItemWrapper/Toolbar/MenuToolbarButton/MenuToolbarButton.es.js.map +1 -0
  22. package/dist/components/BlockItemWrapper/Toolbar/MenuToolbarButton/ToolbarFlyoutMenu.es.js +29 -0
  23. package/dist/components/BlockItemWrapper/Toolbar/MenuToolbarButton/ToolbarFlyoutMenu.es.js.map +1 -0
  24. package/dist/components/BlockItemWrapper/Toolbar/Toolbar.es.js +11 -112
  25. package/dist/components/BlockItemWrapper/Toolbar/Toolbar.es.js.map +1 -1
  26. package/dist/components/BlockItemWrapper/Toolbar/ToolbarButton/ToolbarButton.es.js +12 -0
  27. package/dist/components/BlockItemWrapper/Toolbar/ToolbarButton/ToolbarButton.es.js.map +1 -0
  28. package/dist/components/BlockItemWrapper/Toolbar/ToolbarButtonTooltip.es.js +20 -0
  29. package/dist/components/BlockItemWrapper/Toolbar/ToolbarButtonTooltip.es.js.map +1 -0
  30. package/dist/components/BlockItemWrapper/Toolbar/context/DragPreviewContext.es.js +11 -0
  31. package/dist/components/BlockItemWrapper/Toolbar/context/DragPreviewContext.es.js.map +1 -0
  32. package/dist/components/BlockItemWrapper/Toolbar/context/MultiFlyoutContext.es.js +18 -0
  33. package/dist/components/BlockItemWrapper/Toolbar/context/MultiFlyoutContext.es.js.map +1 -0
  34. package/dist/components/BlockItemWrapper/Toolbar/helpers.es.js +1 -1
  35. package/dist/components/BlockItemWrapper/Toolbar/helpers.es.js.map +1 -1
  36. package/dist/components/BlockItemWrapper/Toolbar/hooks/useMultiFlyoutState.es.js +18 -0
  37. package/dist/components/BlockItemWrapper/Toolbar/hooks/useMultiFlyoutState.es.js.map +1 -0
  38. package/dist/helpers/mapColorPalettes.es.js +20 -8
  39. package/dist/helpers/mapColorPalettes.es.js.map +1 -1
  40. package/dist/index.cjs.js +3 -3
  41. package/dist/index.cjs.js.map +1 -1
  42. package/dist/index.d.ts +92 -27
  43. package/dist/index.es.js +205 -187
  44. package/dist/index.es.js.map +1 -1
  45. package/dist/index.umd.js +3 -3
  46. package/dist/index.umd.js.map +1 -1
  47. package/dist/styles.css +1 -1
  48. package/package.json +4 -2
  49. package/setupTests.ts +7 -2
  50. package/src/components/Attachments/AttachmentItem.tsx +21 -0
  51. package/src/components/Attachments/Attachments.spec.ct.tsx +20 -1
  52. package/src/components/Attachments/Attachments.tsx +8 -4
  53. package/src/components/Attachments/AttachmentsButtonTrigger.tsx +11 -3
  54. package/src/components/Attachments/types.ts +4 -2
  55. package/src/components/BlockItemWrapper/BlockItemWrapper.spec.ct.tsx +37 -38
  56. package/src/components/BlockItemWrapper/BlockItemWrapper.tsx +44 -48
  57. package/src/components/BlockItemWrapper/Toolbar/AttachmentsToolbarButton/AttachmentsToolbarButton.spec.tsx +96 -0
  58. package/src/components/BlockItemWrapper/Toolbar/AttachmentsToolbarButton/AttachmentsToolbarButton.tsx +42 -0
  59. package/src/components/BlockItemWrapper/Toolbar/AttachmentsToolbarButton/AttachmentsToolbarButtonTrigger.spec.tsx +44 -0
  60. package/src/components/BlockItemWrapper/Toolbar/AttachmentsToolbarButton/AttachmentsToolbarButtonTrigger.tsx +24 -0
  61. package/src/components/BlockItemWrapper/Toolbar/AttachmentsToolbarButton/index.ts +3 -0
  62. package/src/components/BlockItemWrapper/Toolbar/BaseToolbarButton.spec.tsx +40 -0
  63. package/src/components/BlockItemWrapper/Toolbar/BaseToolbarButton.tsx +37 -0
  64. package/src/components/BlockItemWrapper/Toolbar/DragHandleToolbarButton/DragHandleToolbarButton.spec.tsx +89 -0
  65. package/src/components/BlockItemWrapper/Toolbar/DragHandleToolbarButton/DragHandleToolbarButton.tsx +40 -0
  66. package/src/components/BlockItemWrapper/Toolbar/DragHandleToolbarButton/index.ts +3 -0
  67. package/src/components/BlockItemWrapper/Toolbar/FlyoutToolbarButton/FlyoutToolbarButton.spec.tsx +140 -0
  68. package/src/components/BlockItemWrapper/Toolbar/FlyoutToolbarButton/FlyoutToolbarButton.tsx +61 -0
  69. package/src/components/BlockItemWrapper/Toolbar/FlyoutToolbarButton/index.ts +3 -0
  70. package/src/components/BlockItemWrapper/Toolbar/MenuToolbarButton/MenuToolbarButton.spec.tsx +77 -0
  71. package/src/components/BlockItemWrapper/Toolbar/MenuToolbarButton/MenuToolbarButton.tsx +30 -0
  72. package/src/components/BlockItemWrapper/Toolbar/MenuToolbarButton/ToolbarFlyoutMenu.spec.tsx +63 -0
  73. package/src/components/BlockItemWrapper/Toolbar/MenuToolbarButton/ToolbarFlyoutMenu.tsx +40 -0
  74. package/src/components/BlockItemWrapper/Toolbar/MenuToolbarButton/index.ts +3 -0
  75. package/src/components/BlockItemWrapper/Toolbar/Toolbar.spec.tsx +189 -57
  76. package/src/components/BlockItemWrapper/Toolbar/Toolbar.tsx +29 -126
  77. package/src/components/BlockItemWrapper/Toolbar/ToolbarButton/ToolbarButton.spec.tsx +70 -0
  78. package/src/components/BlockItemWrapper/Toolbar/ToolbarButton/ToolbarButton.tsx +19 -0
  79. package/src/components/BlockItemWrapper/Toolbar/ToolbarButton/index.ts +3 -0
  80. package/src/components/BlockItemWrapper/Toolbar/ToolbarButtonTooltip.tsx +25 -0
  81. package/src/components/BlockItemWrapper/Toolbar/context/DragPreviewContext.tsx +15 -0
  82. package/src/components/BlockItemWrapper/Toolbar/context/MultiFlyoutContext.tsx +25 -0
  83. package/src/components/BlockItemWrapper/Toolbar/context/index.ts +4 -0
  84. package/src/components/BlockItemWrapper/Toolbar/helpers.ts +1 -1
  85. package/src/components/BlockItemWrapper/Toolbar/hooks/index.ts +3 -0
  86. package/src/components/BlockItemWrapper/Toolbar/hooks/useMultiFlyoutState.spec.tsx +59 -0
  87. package/src/components/BlockItemWrapper/Toolbar/hooks/useMultiFlyoutState.ts +24 -0
  88. package/src/components/BlockItemWrapper/Toolbar/index.ts +6 -0
  89. package/src/components/BlockItemWrapper/Toolbar/types.ts +10 -30
  90. package/src/components/BlockItemWrapper/types.ts +1 -2
  91. package/src/helpers/mapColorPalettes.spec.ts +14 -113
  92. package/src/helpers/mapColorPalettes.ts +51 -8
  93. package/src/hooks/useAttachments.spec.tsx +1 -0
  94. package/tsconfig.json +1 -1
  95. package/dist/components/BlockItemWrapper/Toolbar/ToolbarAttachments.es.js +0 -27
  96. package/dist/components/BlockItemWrapper/Toolbar/ToolbarAttachments.es.js.map +0 -1
  97. package/dist/components/BlockItemWrapper/Toolbar/ToolbarAttachmentsTrigger.es.js +0 -12
  98. package/dist/components/BlockItemWrapper/Toolbar/ToolbarAttachmentsTrigger.es.js.map +0 -1
  99. package/src/components/BlockItemWrapper/Toolbar/ToolbarAttachments.tsx +0 -29
  100. package/src/components/BlockItemWrapper/Toolbar/ToolbarAttachmentsTrigger.tsx +0 -14
@@ -0,0 +1,42 @@
1
+ /* (c) Copyright Frontify Ltd., all rights reserved. */
2
+
3
+ import { useMemoizedId } from '@frontify/fondue';
4
+
5
+ import { useAttachmentsContext } from '../../../../hooks';
6
+ import { Attachments } from '../../../Attachments';
7
+ import { useMultiFlyoutState } from '../hooks/useMultiFlyoutState';
8
+
9
+ import { AttachmentsToolbarButtonTrigger } from './AttachmentsToolbarButtonTrigger';
10
+ import { useDragPreviewContext } from '../context/DragPreviewContext';
11
+
12
+ export const DEFAULT_ATTACHMENTS_BUTTON_ID = 'attachments';
13
+
14
+ type AttachmentsToolbarButtonProps = { flyoutId?: string };
15
+
16
+ export const AttachmentsToolbarButton = ({
17
+ flyoutId = DEFAULT_ATTACHMENTS_BUTTON_ID,
18
+ }: AttachmentsToolbarButtonProps) => {
19
+ const id = useMemoizedId(flyoutId);
20
+
21
+ const { appBridge, attachments, onAttachmentsAdd, onAttachmentDelete, onAttachmentReplace, onAttachmentsSorted } =
22
+ useAttachmentsContext();
23
+
24
+ const { isOpen, onOpenChange } = useMultiFlyoutState(id);
25
+ const isDragPreview = useDragPreviewContext();
26
+
27
+ return (
28
+ <Attachments
29
+ onUpload={onAttachmentsAdd}
30
+ onDelete={onAttachmentDelete}
31
+ onReplaceWithBrowse={onAttachmentReplace}
32
+ onReplaceWithUpload={onAttachmentReplace}
33
+ onSorted={onAttachmentsSorted}
34
+ onBrowse={onAttachmentsAdd}
35
+ items={attachments}
36
+ appBridge={appBridge}
37
+ triggerComponent={AttachmentsToolbarButtonTrigger}
38
+ isOpen={isOpen && !isDragPreview}
39
+ onOpenChange={onOpenChange}
40
+ />
41
+ );
42
+ };
@@ -0,0 +1,44 @@
1
+ /* (c) Copyright Frontify Ltd., all rights reserved. */
2
+
3
+ import { cleanup, fireEvent, render } from '@testing-library/react';
4
+ import { afterEach, describe, expect, it, vi } from 'vitest';
5
+ import { AttachmentsToolbarButtonTrigger } from './AttachmentsToolbarButtonTrigger';
6
+ import { MutableRefObject } from 'react';
7
+
8
+ const BUTTON_ID = 'attachments-toolbar-button-trigger';
9
+
10
+ describe('AttachmentsToolbarButtonTrigger', () => {
11
+ afterEach(() => {
12
+ cleanup();
13
+ });
14
+
15
+ it('should apply active styles when flyout is open', async () => {
16
+ const { getByTestId } = render(
17
+ <AttachmentsToolbarButtonTrigger
18
+ isFlyoutOpen
19
+ triggerProps={{}}
20
+ triggerRef={{} as MutableRefObject<HTMLButtonElement>}
21
+ >
22
+ Button
23
+ </AttachmentsToolbarButtonTrigger>,
24
+ );
25
+
26
+ expect(getByTestId(BUTTON_ID)).toHaveClass('tw-text-box-neutral-inverse-pressed');
27
+ });
28
+ it('should forward trigger props to button', async () => {
29
+ const onPointerUpStub = vi.fn();
30
+
31
+ const { getByTestId } = render(
32
+ <AttachmentsToolbarButtonTrigger
33
+ isFlyoutOpen={false}
34
+ triggerProps={{ onPointerUp: onPointerUpStub }}
35
+ triggerRef={{} as MutableRefObject<HTMLButtonElement>}
36
+ >
37
+ Button
38
+ </AttachmentsToolbarButtonTrigger>,
39
+ );
40
+ await fireEvent.pointerUp(getByTestId(BUTTON_ID));
41
+
42
+ expect(onPointerUpStub).toHaveBeenCalledOnce();
43
+ });
44
+ });
@@ -0,0 +1,24 @@
1
+ /* (c) Copyright Frontify Ltd., all rights reserved. */
2
+
3
+ import { IconCaretDown12, IconPaperclip16 } from '@frontify/fondue';
4
+ import { type AttachmentsTriggerProps } from '../../../Attachments/types';
5
+
6
+ import { BaseToolbarButton } from '../BaseToolbarButton';
7
+
8
+ export const AttachmentsToolbarButtonTrigger = ({
9
+ children,
10
+ isFlyoutOpen,
11
+ triggerProps,
12
+ triggerRef,
13
+ }: AttachmentsTriggerProps) => (
14
+ <BaseToolbarButton
15
+ forceActiveStyle={isFlyoutOpen}
16
+ data-test-id="attachments-toolbar-button-trigger"
17
+ {...triggerProps}
18
+ ref={triggerRef}
19
+ >
20
+ <IconPaperclip16 />
21
+ {children}
22
+ <IconCaretDown12 />
23
+ </BaseToolbarButton>
24
+ );
@@ -0,0 +1,3 @@
1
+ /* (c) Copyright Frontify Ltd., all rights reserved. */
2
+
3
+ export * from './AttachmentsToolbarButton';
@@ -0,0 +1,40 @@
1
+ /* (c) Copyright Frontify Ltd., all rights reserved. */
2
+
3
+ import { cleanup, fireEvent, render } from '@testing-library/react';
4
+ import { afterEach, describe, expect, it, vi } from 'vitest';
5
+ import { BaseToolbarButton } from './BaseToolbarButton';
6
+
7
+ const BUTTON_ID = 'base-toolbar-button';
8
+
9
+ describe('BaseToolbarButton', () => {
10
+ afterEach(() => {
11
+ cleanup();
12
+ });
13
+ it('should call onClick', async () => {
14
+ const onClickStub = vi.fn();
15
+ const { getByTestId } = render(<BaseToolbarButton onClick={onClickStub}>Button</BaseToolbarButton>);
16
+
17
+ await fireEvent.click(getByTestId(BUTTON_ID));
18
+
19
+ expect(onClickStub).toHaveBeenCalledOnce();
20
+ });
21
+
22
+ it('should apply cursor styles', async () => {
23
+ const { getByTestId } = render(<BaseToolbarButton cursor="grab">Button</BaseToolbarButton>);
24
+
25
+ expect(getByTestId(BUTTON_ID)).toHaveClass('!tw-cursor-grab');
26
+ });
27
+
28
+ it('should apply active styles', async () => {
29
+ const { getByTestId } = render(<BaseToolbarButton forceActiveStyle>Button</BaseToolbarButton>);
30
+
31
+ expect(getByTestId(BUTTON_ID)).toHaveClass('tw-text-box-neutral-inverse-pressed');
32
+ });
33
+
34
+ it('should forward other attributes to button', async () => {
35
+ const BUTTON_TYPE = 'base';
36
+ const { getByTestId } = render(<BaseToolbarButton data-button-type={BUTTON_TYPE}>Button</BaseToolbarButton>);
37
+
38
+ expect(getByTestId(BUTTON_ID).dataset.buttonType).toEqual(BUTTON_TYPE);
39
+ });
40
+ });
@@ -0,0 +1,37 @@
1
+ /* (c) Copyright Frontify Ltd., all rights reserved. */
2
+
3
+ import { type HTMLAttributes, type ReactNode, forwardRef } from 'react';
4
+ import { getToolbarButtonClassNames } from './helpers';
5
+
6
+ type BaseToolbarButtonProps = {
7
+ children: ReactNode;
8
+ forceActiveStyle?: boolean;
9
+ cursor?: 'pointer' | 'grab';
10
+ 'data-test-id'?: string;
11
+ } & HTMLAttributes<HTMLButtonElement>;
12
+
13
+ export const BaseToolbarButton = forwardRef<HTMLButtonElement, BaseToolbarButtonProps>(
14
+ (
15
+ {
16
+ onClick,
17
+ children,
18
+ forceActiveStyle,
19
+ cursor = 'pointer',
20
+ 'data-test-id': dataTestId = 'base-toolbar-button',
21
+ ...props
22
+ },
23
+ ref,
24
+ ) => (
25
+ <button
26
+ onClick={onClick}
27
+ className={getToolbarButtonClassNames(cursor, forceActiveStyle)}
28
+ data-test-id={dataTestId}
29
+ {...props}
30
+ ref={ref}
31
+ >
32
+ {children}
33
+ </button>
34
+ ),
35
+ );
36
+
37
+ BaseToolbarButton.displayName = 'BaseToolbarButton';
@@ -0,0 +1,89 @@
1
+ /* (c) Copyright Frontify Ltd., all rights reserved. */
2
+
3
+ import { fireEvent, render, waitFor } from '@testing-library/react';
4
+ import { describe, expect, it, vi } from 'vitest';
5
+ import { DragHandleToolbarButton } from './DragHandleToolbarButton';
6
+ import { IconAdobeCreativeCloud } from '@frontify/fondue';
7
+ import { DragPreviewContextProvider } from '../context/DragPreviewContext';
8
+
9
+ const TOOLBAR_BUTTON_ID = 'block-item-wrapper-toolbar-btn';
10
+ const TOOLTIP_ID = 'toolbar-button-tooltip';
11
+
12
+ const TOOLTIP_CONTENT = 'content';
13
+
14
+ /**
15
+ * @vitest-environment happy-dom
16
+ */
17
+
18
+ describe('DragHandleToolbarButton', () => {
19
+ it('should show tooltip and activeStyles when item is in drag preview context', async () => {
20
+ const { getByTestId } = render(
21
+ <DragPreviewContextProvider isDragPreview>
22
+ <DragHandleToolbarButton
23
+ tooltip={TOOLTIP_CONTENT}
24
+ icon={<IconAdobeCreativeCloud />}
25
+ draggableProps={{}}
26
+ />
27
+ </DragPreviewContextProvider>,
28
+ );
29
+
30
+ expect(getByTestId(TOOLTIP_ID)).not.toHaveClass('tw-opacity-0');
31
+ expect(getByTestId(TOOLBAR_BUTTON_ID)).toHaveClass('tw-cursor-grabbing');
32
+ });
33
+
34
+ it('should show tooltip when item is focused', async () => {
35
+ const { getByTestId } = render(
36
+ <DragHandleToolbarButton tooltip={TOOLTIP_CONTENT} icon={<IconAdobeCreativeCloud />} draggableProps={{}} />,
37
+ );
38
+
39
+ expect(getByTestId(TOOLTIP_ID)).toHaveClass('tw-opacity-0');
40
+ expect(getByTestId(TOOLTIP_ID)).toHaveTextContent(TOOLTIP_CONTENT);
41
+
42
+ getByTestId(TOOLBAR_BUTTON_ID).focus();
43
+
44
+ await waitFor(() => {
45
+ expect(getByTestId(TOOLTIP_ID)).not.toHaveClass('tw-opacity-0');
46
+ });
47
+ });
48
+
49
+ it('should forward draggableProps', async () => {
50
+ const onDragStub = vi.fn();
51
+
52
+ const { getByTestId } = render(
53
+ <DragHandleToolbarButton
54
+ tooltip={TOOLTIP_CONTENT}
55
+ icon={<IconAdobeCreativeCloud />}
56
+ draggableProps={{ onDrag: onDragStub }}
57
+ />,
58
+ );
59
+
60
+ await fireEvent.drag(getByTestId(TOOLBAR_BUTTON_ID));
61
+
62
+ expect(onDragStub).toHaveBeenCalledOnce();
63
+ });
64
+
65
+ it('should forward setActivatorNodeRef', async () => {
66
+ const setActivatorNodeRefStub = vi.fn();
67
+
68
+ render(
69
+ <DragHandleToolbarButton
70
+ tooltip={TOOLTIP_CONTENT}
71
+ icon={<IconAdobeCreativeCloud />}
72
+ draggableProps={{}}
73
+ setActivatorNodeRef={setActivatorNodeRefStub}
74
+ />,
75
+ );
76
+
77
+ expect(setActivatorNodeRefStub).toHaveBeenCalledOnce();
78
+ });
79
+
80
+ it('should display icon', async () => {
81
+ const { getByTestId } = render(
82
+ <DragHandleToolbarButton tooltip={TOOLTIP_CONTENT} icon={<IconAdobeCreativeCloud />} draggableProps={{}} />,
83
+ );
84
+
85
+ const icons = [...getByTestId(TOOLBAR_BUTTON_ID).querySelectorAll('svg')];
86
+ expect(icons).toHaveLength(1);
87
+ expect(icons[0].outerHTML).toMatch('IconAdobeCreativeCloud');
88
+ });
89
+ });
@@ -0,0 +1,40 @@
1
+ /* (c) Copyright Frontify Ltd., all rights reserved. */
2
+
3
+ import { ReactNode } from 'react';
4
+ import { DEFAULT_DRAGGING_TOOLTIP, DEFAULT_DRAG_TOOLTIP } from '../../constants';
5
+ import { useDragPreviewContext } from '../context/DragPreviewContext';
6
+ import { BaseToolbarButton } from '../BaseToolbarButton';
7
+ import { ToolbarButtonTooltip } from '../ToolbarButtonTooltip';
8
+
9
+ export type DragHandleToolbarButtonProps = {
10
+ icon?: ReactNode;
11
+ tooltip?: string;
12
+ draggableProps: Record<string, unknown>;
13
+ setActivatorNodeRef?: (node: HTMLElement | null) => void;
14
+ };
15
+
16
+ export const DragHandleToolbarButton = ({
17
+ tooltip,
18
+ icon,
19
+ setActivatorNodeRef,
20
+ draggableProps,
21
+ }: DragHandleToolbarButtonProps) => {
22
+ const isDragPreview = useDragPreviewContext();
23
+
24
+ return (
25
+ <ToolbarButtonTooltip
26
+ open={isDragPreview}
27
+ content={<div>{isDragPreview ? DEFAULT_DRAGGING_TOOLTIP : tooltip ?? DEFAULT_DRAG_TOOLTIP}</div>}
28
+ >
29
+ <BaseToolbarButton
30
+ ref={setActivatorNodeRef}
31
+ data-test-id="block-item-wrapper-toolbar-btn"
32
+ forceActiveStyle={isDragPreview}
33
+ cursor="grab"
34
+ {...draggableProps}
35
+ >
36
+ {icon}
37
+ </BaseToolbarButton>
38
+ </ToolbarButtonTooltip>
39
+ );
40
+ };
@@ -0,0 +1,3 @@
1
+ /* (c) Copyright Frontify Ltd., all rights reserved. */
2
+
3
+ export * from './DragHandleToolbarButton';
@@ -0,0 +1,140 @@
1
+ /* (c) Copyright Frontify Ltd., all rights reserved. */
2
+
3
+ import { fireEvent, render, waitFor } from '@testing-library/react';
4
+ import { describe, expect, it, vi } from 'vitest';
5
+ import { FlyoutToolbarButton } from './FlyoutToolbarButton';
6
+ import { MultiFlyoutContextProvider } from '../context/MultiFlyoutContext';
7
+ import { IconAdobeCreativeCloud } from '@frontify/fondue';
8
+ import { DragPreviewContextProvider } from '../context/DragPreviewContext';
9
+
10
+ const BUTTON_ID = 'block-item-wrapper-toolbar-flyout';
11
+ const TOOLTIP_ID = 'toolbar-button-tooltip';
12
+
13
+ const TEST_FLYOUT_ID = 'test';
14
+ const TEST_TOOLTIP = 'tooltip';
15
+ /**
16
+ * @vitest-environment happy-dom
17
+ */
18
+
19
+ describe('FlyoutToolbarButton', () => {
20
+ it('should log error if not inside a flyout provider when opening', async () => {
21
+ vi.spyOn(console, 'error');
22
+ const { getByTestId } = render(
23
+ <FlyoutToolbarButton
24
+ flyoutId={TEST_FLYOUT_ID}
25
+ icon={<IconAdobeCreativeCloud />}
26
+ tooltip={TEST_TOOLTIP}
27
+ content="screen"
28
+ />,
29
+ );
30
+
31
+ await fireEvent.click(getByTestId(BUTTON_ID));
32
+
33
+ expect(console.error).toBeCalled();
34
+ });
35
+
36
+ it('should use flyoutId in flyout context', async () => {
37
+ const setOpenFlyoutIdsStub = vi.fn();
38
+
39
+ const { getByTestId } = render(
40
+ <MultiFlyoutContextProvider openFlyoutIds={[]} setOpenFlyoutIds={setOpenFlyoutIdsStub}>
41
+ <FlyoutToolbarButton
42
+ flyoutId={TEST_FLYOUT_ID}
43
+ icon={<IconAdobeCreativeCloud />}
44
+ tooltip={TEST_TOOLTIP}
45
+ content="children"
46
+ />
47
+ </MultiFlyoutContextProvider>,
48
+ );
49
+
50
+ await fireEvent.click(getByTestId(BUTTON_ID));
51
+
52
+ expect(setOpenFlyoutIdsStub).toHaveBeenCalled();
53
+ const dispatchedStateResult = setOpenFlyoutIdsStub.mock.lastCall[0]([]);
54
+ expect(dispatchedStateResult).toEqual([TEST_FLYOUT_ID]);
55
+ });
56
+
57
+ it('should display content', async () => {
58
+ const setOpenFlyoutIdsStub = vi.fn();
59
+
60
+ const { getByTestId } = render(
61
+ <MultiFlyoutContextProvider openFlyoutIds={[TEST_FLYOUT_ID]} setOpenFlyoutIds={setOpenFlyoutIdsStub}>
62
+ <FlyoutToolbarButton
63
+ flyoutId={TEST_FLYOUT_ID}
64
+ icon={<IconAdobeCreativeCloud />}
65
+ tooltip={TEST_TOOLTIP}
66
+ flyoutFooter={<div data-test-id="footer">Footer</div>}
67
+ flyoutHeader={<div data-test-id="header">Header</div>}
68
+ content={<div data-test-id="content">Content</div>}
69
+ />
70
+ </MultiFlyoutContextProvider>,
71
+ );
72
+
73
+ expect(getByTestId('content')).toBeVisible();
74
+ expect(getByTestId('header')).toBeVisible();
75
+ expect(getByTestId('footer')).toBeVisible();
76
+ });
77
+
78
+ it('should show tooltip content', async () => {
79
+ const setOpenFlyoutIdsStub = vi.fn();
80
+
81
+ const { getByTestId } = render(
82
+ <MultiFlyoutContextProvider openFlyoutIds={[]} setOpenFlyoutIds={setOpenFlyoutIdsStub}>
83
+ <FlyoutToolbarButton
84
+ flyoutId={TEST_FLYOUT_ID}
85
+ icon={<IconAdobeCreativeCloud />}
86
+ tooltip={TEST_TOOLTIP}
87
+ content="children"
88
+ />
89
+ </MultiFlyoutContextProvider>,
90
+ );
91
+
92
+ getByTestId(BUTTON_ID).focus();
93
+
94
+ await waitFor(() => expect(getByTestId(TOOLTIP_ID)).not.toHaveClass('tw-opacity-0'));
95
+ expect(getByTestId(TOOLTIP_ID)).toHaveTextContent(TEST_TOOLTIP);
96
+ });
97
+
98
+ it('should use supplied icon', async () => {
99
+ const setOpenFlyoutIdsStub = vi.fn();
100
+
101
+ const { getByTestId } = render(
102
+ <MultiFlyoutContextProvider openFlyoutIds={[]} setOpenFlyoutIds={setOpenFlyoutIdsStub}>
103
+ <FlyoutToolbarButton
104
+ flyoutId={TEST_FLYOUT_ID}
105
+ icon={<IconAdobeCreativeCloud />}
106
+ tooltip={TEST_TOOLTIP}
107
+ content="content"
108
+ />
109
+ </MultiFlyoutContextProvider>,
110
+ );
111
+
112
+ const icons = [...getByTestId(BUTTON_ID).querySelectorAll('svg')];
113
+ expect(icons).toHaveLength(1);
114
+ expect(icons[0].outerHTML).toMatch('IconAdobeCreativeCloud');
115
+ });
116
+
117
+ it('should disable tooltip and flyout when content is inside drag preview', async () => {
118
+ const { getByTestId, queryByTestId } = render(
119
+ <MultiFlyoutContextProvider openFlyoutIds={[TEST_FLYOUT_ID]} setOpenFlyoutIds={vi.fn()}>
120
+ <DragPreviewContextProvider isDragPreview>
121
+ <FlyoutToolbarButton
122
+ flyoutId={TEST_FLYOUT_ID}
123
+ icon={<IconAdobeCreativeCloud />}
124
+ tooltip={TEST_TOOLTIP}
125
+ content={<div data-test-id="content">Content</div>}
126
+ />
127
+ </DragPreviewContextProvider>
128
+ </MultiFlyoutContextProvider>,
129
+ );
130
+
131
+ expect(getByTestId(TOOLTIP_ID)).toHaveClass('tw-opacity-0');
132
+
133
+ getByTestId(TOOLTIP_ID).focus();
134
+
135
+ await waitFor(() => {
136
+ expect(getByTestId(TOOLTIP_ID)).toHaveClass('tw-opacity-0');
137
+ expect(queryByTestId('content')).toBeNull();
138
+ });
139
+ });
140
+ });
@@ -0,0 +1,61 @@
1
+ /* (c) Copyright Frontify Ltd., all rights reserved. */
2
+
3
+ import { type MutableRefObject, type ReactNode } from 'react';
4
+ import { useDragPreviewContext } from '../context/DragPreviewContext';
5
+ import { ToolbarButtonTooltip } from '../ToolbarButtonTooltip';
6
+ import { Flyout, FlyoutPlacement, useMemoizedId } from '@frontify/fondue';
7
+ import { BaseToolbarButton } from '../BaseToolbarButton';
8
+ import { useMultiFlyoutState } from '../hooks/useMultiFlyoutState';
9
+
10
+ export type FlyoutToolbarButtonProps = {
11
+ content: ReactNode;
12
+ icon: ReactNode;
13
+ tooltip: ReactNode;
14
+ flyoutId: string;
15
+ flyoutFooter?: ReactNode;
16
+ flyoutHeader?: ReactNode;
17
+ };
18
+
19
+ export const FlyoutToolbarButton = ({
20
+ content,
21
+ icon,
22
+ tooltip,
23
+ flyoutId,
24
+ flyoutFooter,
25
+ flyoutHeader,
26
+ }: FlyoutToolbarButtonProps) => {
27
+ const id = useMemoizedId(flyoutId);
28
+
29
+ const { isOpen, onOpenChange } = useMultiFlyoutState(id);
30
+
31
+ const isDragPreview = useDragPreviewContext();
32
+
33
+ return (
34
+ <ToolbarButtonTooltip disabled={isDragPreview || isOpen} content={tooltip}>
35
+ <div className="tw-flex tw-flex-shrink-0 tw-flex-1 tw-h-6 tw-relative">
36
+ <Flyout
37
+ isOpen={isOpen && !isDragPreview}
38
+ legacyFooter={false}
39
+ fixedFooter={flyoutFooter}
40
+ fixedHeader={flyoutHeader}
41
+ fitContent
42
+ hug={false}
43
+ placement={FlyoutPlacement.BottomRight}
44
+ onOpenChange={onOpenChange}
45
+ trigger={(triggerProps, triggerRef) => (
46
+ <BaseToolbarButton
47
+ data-test-id="block-item-wrapper-toolbar-flyout"
48
+ forceActiveStyle={isOpen && !isDragPreview}
49
+ {...triggerProps}
50
+ ref={triggerRef as MutableRefObject<HTMLButtonElement>}
51
+ >
52
+ {icon}
53
+ </BaseToolbarButton>
54
+ )}
55
+ >
56
+ {content}
57
+ </Flyout>
58
+ </div>
59
+ </ToolbarButtonTooltip>
60
+ );
61
+ };
@@ -0,0 +1,3 @@
1
+ /* (c) Copyright Frontify Ltd., all rights reserved. */
2
+
3
+ export * from './FlyoutToolbarButton';
@@ -0,0 +1,77 @@
1
+ /* (c) Copyright Frontify Ltd., all rights reserved. */
2
+
3
+ import { fireEvent, render, waitFor } from '@testing-library/react';
4
+ import { describe, expect, it, vi } from 'vitest';
5
+ import { IconAdobeCreativeCloud } from '@frontify/fondue';
6
+ import { MenuToolbarButton } from '.';
7
+ import { MultiFlyoutContextProvider } from '../context/MultiFlyoutContext';
8
+
9
+ const BUTTON_ID = 'block-item-wrapper-toolbar-flyout';
10
+ const MENU_ITEM_ID = 'menu-item';
11
+ const TOOLTIP_ID = 'toolbar-button-tooltip';
12
+
13
+ const TEST_FLYOUT_ID = 'test';
14
+ const TEST_TOOLTIP = 'tooltip';
15
+ /**
16
+ * @vitest-environment happy-dom
17
+ */
18
+
19
+ describe('MenuToolbarButton', () => {
20
+ it('should log error if not inside a flyout provider when opening', async () => {
21
+ vi.spyOn(console, 'error');
22
+ const { getByTestId } = render(<MenuToolbarButton items={[]} />);
23
+
24
+ await fireEvent.click(getByTestId(BUTTON_ID));
25
+
26
+ expect(console.error).toBeCalled();
27
+ });
28
+
29
+ it('should use flyout Id in flyout context', async () => {
30
+ const setOpenFlyoutIdsStub = vi.fn();
31
+
32
+ const { getByTestId } = render(
33
+ <MultiFlyoutContextProvider openFlyoutIds={[]} setOpenFlyoutIds={setOpenFlyoutIdsStub}>
34
+ <MenuToolbarButton items={[]} flyoutId={TEST_FLYOUT_ID} />
35
+ </MultiFlyoutContextProvider>,
36
+ );
37
+
38
+ await fireEvent.click(getByTestId(BUTTON_ID));
39
+
40
+ expect(setOpenFlyoutIdsStub).toHaveBeenCalled();
41
+ const dispatchedStateResult = setOpenFlyoutIdsStub.mock.lastCall[0]([]);
42
+ expect(dispatchedStateResult).toEqual([TEST_FLYOUT_ID]);
43
+ });
44
+
45
+ it('should display menu items', async () => {
46
+ const setOpenFlyoutIdsStub = vi.fn();
47
+
48
+ const { getAllByTestId } = render(
49
+ <MultiFlyoutContextProvider openFlyoutIds={[TEST_FLYOUT_ID]} setOpenFlyoutIds={setOpenFlyoutIdsStub}>
50
+ <MenuToolbarButton
51
+ items={[
52
+ [{ title: 'item-1', onClick: vi.fn(), icon: <IconAdobeCreativeCloud /> }],
53
+ [{ title: 'item-2', onClick: vi.fn(), icon: <IconAdobeCreativeCloud /> }],
54
+ ]}
55
+ flyoutId={TEST_FLYOUT_ID}
56
+ />
57
+ </MultiFlyoutContextProvider>,
58
+ );
59
+
60
+ expect(getAllByTestId(MENU_ITEM_ID)).toHaveLength(2);
61
+ });
62
+
63
+ it('should show tooltip content', async () => {
64
+ const setOpenFlyoutIdsStub = vi.fn();
65
+
66
+ const { getByTestId } = render(
67
+ <MultiFlyoutContextProvider openFlyoutIds={[]} setOpenFlyoutIds={setOpenFlyoutIdsStub}>
68
+ <MenuToolbarButton items={[]} tooltip={TEST_TOOLTIP} flyoutId={TEST_FLYOUT_ID} />
69
+ </MultiFlyoutContextProvider>,
70
+ );
71
+
72
+ getByTestId(BUTTON_ID).focus();
73
+
74
+ await waitFor(() => expect(getByTestId(TOOLTIP_ID)).not.toHaveClass('tw-opacity-0'));
75
+ expect(getByTestId(TOOLTIP_ID)).toHaveTextContent(TEST_TOOLTIP);
76
+ });
77
+ });
@@ -0,0 +1,30 @@
1
+ /* (c) Copyright Frontify Ltd., all rights reserved. */
2
+
3
+ import { IconDotsHorizontal16, useMemoizedId } from '@frontify/fondue';
4
+ import { ToolbarFlyoutMenu, ToolbarFlyoutMenuItem } from './ToolbarFlyoutMenu';
5
+ import { FlyoutToolbarButton } from '../FlyoutToolbarButton/FlyoutToolbarButton';
6
+
7
+ export const DEFAULT_MENU_BUTTON_ID = 'menu';
8
+
9
+ export type MenuToolbarButtonProps = {
10
+ items: ToolbarFlyoutMenuItem[][];
11
+ flyoutId?: string;
12
+ tooltip?: string;
13
+ };
14
+
15
+ export const MenuToolbarButton = ({
16
+ items,
17
+ flyoutId = DEFAULT_MENU_BUTTON_ID,
18
+ tooltip = 'Options',
19
+ }: MenuToolbarButtonProps) => {
20
+ const id = useMemoizedId(flyoutId);
21
+
22
+ return (
23
+ <FlyoutToolbarButton
24
+ icon={<IconDotsHorizontal16 />}
25
+ tooltip={tooltip}
26
+ flyoutId={id}
27
+ content={<ToolbarFlyoutMenu items={items} flyoutId={id} />}
28
+ />
29
+ );
30
+ };