@simplybusiness/mobius 4.1.0 → 4.1.2

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 (87) hide show
  1. package/CHANGELOG.md +15 -1
  2. package/dist/cjs/components/Accordion/Accordion.js.map +1 -1
  3. package/dist/cjs/components/Button/Button.js.map +1 -1
  4. package/dist/cjs/components/Checkbox/Checkbox.js.map +1 -1
  5. package/dist/cjs/components/Checkbox/CheckboxGroup.js.map +1 -1
  6. package/dist/cjs/components/Drawer/Content.js.map +1 -1
  7. package/dist/cjs/components/Drawer/Drawer.js.map +1 -1
  8. package/dist/cjs/components/DropdownMenu/DropdownMenu.js.map +1 -1
  9. package/dist/cjs/components/DropdownMenu/DropdownMenu.stories.d.ts +1 -1
  10. package/dist/cjs/components/Fieldset/Fieldset.js.map +1 -1
  11. package/dist/cjs/components/Grid/Grid.stories.d.ts +1 -1
  12. package/dist/cjs/components/Icon/Icon.js.map +1 -1
  13. package/dist/cjs/components/Icon/IconStyle.js.map +1 -1
  14. package/dist/cjs/components/LinkButton/LinkButton.js.map +1 -1
  15. package/dist/cjs/components/LinkButton/LinkButton.test.js.map +1 -1
  16. package/dist/cjs/components/List/List.js.map +1 -1
  17. package/dist/cjs/components/List/ListItem.js.map +1 -1
  18. package/dist/cjs/components/Modal/Content.js.map +1 -1
  19. package/dist/cjs/components/Modal/Modal.js.map +1 -1
  20. package/dist/cjs/components/NumberField/NumberField.js.map +1 -1
  21. package/dist/cjs/components/Popover/Popover.d.ts +1 -2
  22. package/dist/cjs/components/Popover/Popover.js +40 -47
  23. package/dist/cjs/components/Popover/Popover.js.map +1 -1
  24. package/dist/cjs/components/Popover/Popover.stories.d.ts +3 -10
  25. package/dist/cjs/components/Popover/Popover.stories.js +4 -16
  26. package/dist/cjs/components/Popover/Popover.stories.js.map +1 -1
  27. package/dist/cjs/components/Popover/Popover.test.js +25 -26
  28. package/dist/cjs/components/Popover/Popover.test.js.map +1 -1
  29. package/dist/cjs/components/Progress/Progress.js.map +1 -1
  30. package/dist/cjs/components/Radio/Radio.js.map +1 -1
  31. package/dist/cjs/components/Radio/RadioGroup.js.map +1 -1
  32. package/dist/cjs/components/Select/Select.js.map +1 -1
  33. package/dist/cjs/components/Slider/Slider.js.map +1 -1
  34. package/dist/cjs/components/Slider/helpers.js.map +1 -1
  35. package/dist/cjs/components/Text/Text.js.map +1 -1
  36. package/dist/cjs/hooks/useBodyScrollLock/useBodyScrollLock.js.map +1 -1
  37. package/dist/cjs/hooks/useBreakpoint/useBreakpoint.js.map +1 -1
  38. package/dist/cjs/hooks/useButton/useButton.js.map +1 -1
  39. package/dist/cjs/hooks/useLabel/useLabel.js.map +1 -1
  40. package/dist/cjs/hooks/useOnClickOutside/useOnClickOutside.js.map +1 -1
  41. package/dist/cjs/hooks/useWindowEvent/useWindowEvent.js.map +1 -1
  42. package/dist/cjs/tsconfig.tsbuildinfo +1 -1
  43. package/dist/cjs/utils/changeCSS.js.map +1 -1
  44. package/dist/cjs/utils/mergeRefs.js.map +1 -1
  45. package/dist/cjs/utils/sizeClasses.js.map +1 -1
  46. package/dist/esm/components/Accordion/Accordion.js.map +1 -1
  47. package/dist/esm/components/Button/Button.js.map +1 -1
  48. package/dist/esm/components/Checkbox/Checkbox.js.map +1 -1
  49. package/dist/esm/components/Checkbox/CheckboxGroup.js.map +1 -1
  50. package/dist/esm/components/Drawer/Content.js.map +1 -1
  51. package/dist/esm/components/Drawer/Drawer.js.map +1 -1
  52. package/dist/esm/components/DropdownMenu/DropdownMenu.js.map +1 -1
  53. package/dist/esm/components/Fieldset/Fieldset.js.map +1 -1
  54. package/dist/esm/components/Icon/Icon.js.map +1 -1
  55. package/dist/esm/components/Icon/IconStyle.js.map +1 -1
  56. package/dist/esm/components/LinkButton/LinkButton.js.map +1 -1
  57. package/dist/esm/components/List/List.js.map +1 -1
  58. package/dist/esm/components/List/ListItem.js.map +1 -1
  59. package/dist/esm/components/Modal/Content.js.map +1 -1
  60. package/dist/esm/components/Modal/Modal.js.map +1 -1
  61. package/dist/esm/components/NumberField/NumberField.js.map +1 -1
  62. package/dist/esm/components/Popover/Popover.js +41 -48
  63. package/dist/esm/components/Popover/Popover.js.map +1 -1
  64. package/dist/esm/components/Progress/Progress.js.map +1 -1
  65. package/dist/esm/components/Radio/Radio.js.map +1 -1
  66. package/dist/esm/components/Radio/RadioGroup.js.map +1 -1
  67. package/dist/esm/components/Select/Select.js.map +1 -1
  68. package/dist/esm/components/Slider/Slider.js.map +1 -1
  69. package/dist/esm/components/Slider/helpers.js.map +1 -1
  70. package/dist/esm/components/Text/Text.js.map +1 -1
  71. package/dist/esm/hooks/useBodyScrollLock/useBodyScrollLock.js.map +1 -1
  72. package/dist/esm/hooks/useBreakpoint/useBreakpoint.js.map +1 -1
  73. package/dist/esm/hooks/useButton/useButton.js.map +1 -1
  74. package/dist/esm/hooks/useLabel/useLabel.js.map +1 -1
  75. package/dist/esm/hooks/useOnClickOutside/useOnClickOutside.js.map +1 -1
  76. package/dist/esm/hooks/useWindowEvent/useWindowEvent.js.map +1 -1
  77. package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
  78. package/dist/esm/utils/changeCSS.js.map +1 -1
  79. package/dist/esm/utils/mergeRefs.js.map +1 -1
  80. package/dist/esm/utils/sizeClasses.js.map +1 -1
  81. package/dist/mobius.d.ts +0 -1
  82. package/package.json +24 -24
  83. package/src/components/Popover/Popover.mdx +19 -27
  84. package/src/components/Popover/Popover.stories.tsx +18 -28
  85. package/src/components/Popover/Popover.story.styles.css +3 -2
  86. package/src/components/Popover/Popover.test.tsx +39 -38
  87. package/src/components/Popover/Popover.tsx +66 -66
@@ -8,9 +8,9 @@ import { Popover } from "./Popover";
8
8
 
9
9
  The `Popover` component is used to display popup tooltips with additional information for users. Children of the Popover become tooltip contents and `trigger` is a render prop for the element that will present the Popover when clicked.
10
10
 
11
- When presented, the Popover will align (vertically) with the trigger element. By default it will appear below.
11
+ When presented, the Popover will align horizontally with the centre of the trigger element, and by default it will appear below.
12
12
 
13
- You can dismiss a Popover by clicking the close icon or by pressing `Enter` or `Escape` keys.
13
+ You can dismiss a Popover by clicking the close icon, clicking outside, or by pressing `Enter` or `Escape` keys.
14
14
 
15
15
  Focus always remains on the element that triggers the Popover; its content should not accept focus.
16
16
 
@@ -38,40 +38,32 @@ return (
38
38
 
39
39
  <Story of={PopoverStories.Normal} />
40
40
 
41
- ## QCP Covers
41
+ ## Custom Trigger
42
42
 
43
- This is a customised trigger as used in the Quote Comparison Page, which has a large, clickable trigger (text and icon), but a small target for aligning the Popover arrow (just the icon).
44
-
45
- To achieve this, you need to control `isOpen` state with the clickable element and only use Popover to wrap the element you want to align with.
43
+ This is a customised trigger as used in the Quote Comparison Page, which has a large, clickable link (text and icon).
46
44
 
47
45
  ```jsx
48
46
  import { Popover, Button, Flex } from "@simplybusiness/mobius";
49
47
 
50
- const [isOpen, setIsOpen] = useState<boolean>(false);
51
- const handleClick = () => setIsOpen(!isOpen);
52
-
53
- // Component passed to trigger prop must be wrapped with forwardRef()
54
- const Icon = forwardRef(
55
- (props: IconProps, ref: Ref<SVGSVGElement>) => (
56
- <svg ref={ref} {...props}>...</svg>
57
- ),
58
- );
59
-
60
48
  return (
61
- <Button variant="ghost" onClick={handleClick}>
62
- <Flex>
63
- <span>Occurrence Limit</span>
64
- <Popover isOpen={isOpen} trigger={<Icon />}>
65
- This is the maximum amount of money you&apos;ll get to cover a single
66
- incident that occurs during the policy period, regardless of when
67
- it&apos;s reported.
68
- </Popover>
69
- </Flex>
70
- </Button>
49
+ <Popover
50
+ trigger={
51
+ <Button variant="ghost">
52
+ <Flex>
53
+ <span>Occurrence Limit</span>
54
+ <QuestionIcon />
55
+ </Flex>
56
+ </Button>
57
+ }
58
+ >
59
+ This is the maximum amount of money you&apos;ll get to cover a single
60
+ incident that occurs during the policy period, regardless of when it&apos;s
61
+ reported.
62
+ </Popover>
71
63
  );
72
64
  ```
73
65
 
74
- <Canvas of={PopoverStories.QCP} />
66
+ <Canvas of={PopoverStories.CustomTrigger} />
75
67
 
76
68
  ## Props
77
69
 
@@ -1,5 +1,5 @@
1
1
  import type { Meta } from "@storybook/react";
2
- import { Ref, forwardRef, useState } from "react";
2
+ import { Ref, forwardRef } from "react";
3
3
  import { Button, Flex, Popover, PopoverProps } from "..";
4
4
  import { excludeControls } from "../../utils/excludeControls";
5
5
 
@@ -7,9 +7,6 @@ export default {
7
7
  title: "Components/Popover",
8
8
  component: Popover,
9
9
  argTypes: {
10
- isOpen: {
11
- control: { type: "boolean" },
12
- },
13
10
  ...excludeControls(
14
11
  "className",
15
12
  "children",
@@ -17,6 +14,7 @@ export default {
17
14
  "id",
18
15
  "onOpen",
19
16
  "onClose",
17
+ "anchor",
20
18
  ),
21
19
  },
22
20
  decorators: [
@@ -70,36 +68,28 @@ const QuestionIcon = forwardRef(
70
68
  export const Normal: Meta<typeof Popover> = {
71
69
  render: (args: PopoverProps) => <Popover {...args} />,
72
70
  args: {
73
- isOpen: undefined,
74
71
  children: <>No way! Now I know everything I need to 🚀.</>,
75
72
  trigger: <Button variant="primary">If only I had more information</Button>,
76
73
  },
77
74
  };
78
75
 
79
- export const QCP: Meta<typeof Popover> = {
80
- render: (args: PopoverProps) => {
81
- // eslint-disable-next-line react-hooks/rules-of-hooks
82
- const [isOpen, setIsOpen] = useState<boolean>(false);
83
- const handleClick = () => setIsOpen(!isOpen);
84
- return (
85
- <div className="popover-example">
86
- <Button variant="ghost" onClick={handleClick}>
87
- <Flex>
88
- <span>Occurrence Limit</span>
89
- <Popover {...args} isOpen={isOpen} />
90
- </Flex>
91
- </Button>
92
- </div>
93
- );
94
- },
95
- args: {
96
- trigger: <QuestionIcon />,
97
- children: (
98
- <>
76
+ export const CustomTrigger: Meta<typeof Popover> = {
77
+ render: () => (
78
+ <div className="popover-example">
79
+ <Popover
80
+ trigger={
81
+ <Button variant="ghost">
82
+ <Flex>
83
+ <span>Occurrence Limit</span>
84
+ <QuestionIcon />
85
+ </Flex>
86
+ </Button>
87
+ }
88
+ >
99
89
  This is the maximum amount of money you&apos;ll get to cover a single
100
90
  incident that occurs during the policy period, regardless of when
101
91
  it&apos;s reported.
102
- </>
103
- ),
104
- },
92
+ </Popover>
93
+ </div>
94
+ ),
105
95
  };
@@ -1,9 +1,10 @@
1
1
  .popover-example {
2
2
  max-width: 500px;
3
+ min-height: 220px;
3
4
  margin: 0 auto;
4
5
  }
5
6
 
6
- .popover-example .mobius\/Button {
7
+ .popover-example > .mobius\/Button {
7
8
  border: none;
8
9
  padding: 0;
9
10
  font-weight: var(--font-weight-normal);
@@ -19,7 +20,7 @@
19
20
  margin-bottom: 6px;
20
21
  }
21
22
 
22
- .popover-example .mobius\/PopoverToggle {
23
+ .popover-example .mobius\/PopoverToggle svg {
23
24
  width: 16px;
24
25
  margin-left: 4px;
25
26
  }
@@ -1,7 +1,6 @@
1
1
  import { fireEvent, render, screen, waitFor } from "@testing-library/react";
2
2
  import { Popover } from ".";
3
3
 
4
- const CONTAINER_ID = "react-tiny-popover-container";
5
4
  const CONTAINER_CLASS_NAME = "mobius/PopoverContainer";
6
5
  const TOGGLE_CLASS_NAME = "mobius/PopoverToggle";
7
6
  const POPOVER_CLASS_NAME = "mobius/Popover";
@@ -63,8 +62,7 @@ describe("Popover", () => {
63
62
 
64
63
  fireEvent.click(button);
65
64
 
66
- const popoverContainer = document.querySelector(`#${CONTAINER_ID}`)
67
- ?.firstChild;
65
+ const popoverContainer = button.nextSibling;
68
66
 
69
67
  await waitFor(() => {
70
68
  expect(popoverContainer).toHaveClass("mobius");
@@ -99,39 +97,38 @@ describe("Popover", () => {
99
97
 
100
98
  fireEvent.click(button);
101
99
 
102
- const popoverContainer = document.querySelector(`#${CONTAINER_ID}`);
100
+ const popoverContainer = button.nextSibling;
103
101
 
104
102
  await waitFor(() => {
105
- expect(popoverContainer?.firstChild).toHaveClass(customClassName);
103
+ expect(popoverContainer).toHaveClass(customClassName);
106
104
  });
107
105
  });
108
- });
109
106
 
110
- describe("events", () => {
111
- describe("given isOpen prop is set", () => {
112
- it("should not call onOpen or onClose when first rendered", () => {
113
- const sampleText = "Sample Text";
114
- const triggerText = "Click me";
115
- const onOpen = jest.fn();
116
- const onClose = jest.fn();
117
-
118
- render(
119
- <Popover
120
- isOpen={false}
121
- trigger={<button type="button">{triggerText}</button>}
122
- onOpen={onOpen}
123
- onClose={onClose}
124
- >
125
- {sampleText}
126
- </Popover>,
127
- );
128
-
129
- expect(onOpen).not.toHaveBeenCalled();
130
- expect(onClose).not.toHaveBeenCalled();
131
- });
107
+ it("passes trigger class names", () => {
108
+ const sampleText = "Sample Text";
109
+ const triggerText = "Click me";
110
+ const customClassName = "custom-trigger-class-name";
111
+
112
+ render(
113
+ <Popover
114
+ trigger={
115
+ <button type="button" className={customClassName}>
116
+ {triggerText}
117
+ </button>
118
+ }
119
+ >
120
+ {sampleText}
121
+ </Popover>,
122
+ );
123
+
124
+ const button = screen.getByText(triggerText);
125
+
126
+ expect(button).toHaveClass(customClassName);
132
127
  });
128
+ });
133
129
 
134
- it("calls onOpen when Popover is opened", () => {
130
+ describe("events", () => {
131
+ it("calls onOpen when Popover is opened", async () => {
135
132
  const sampleText = "Sample Text";
136
133
  const triggerText = "Click me";
137
134
  const onOpen = jest.fn();
@@ -147,15 +144,17 @@ describe("Popover", () => {
147
144
  </Popover>,
148
145
  );
149
146
 
150
- const button = screen.getByText(triggerText);
147
+ const toggle = screen.getByText(triggerText);
151
148
 
152
- fireEvent.click(button);
149
+ fireEvent.click(toggle);
153
150
 
154
- expect(onOpen).toHaveBeenCalled();
155
- expect(onClose).not.toHaveBeenCalled();
151
+ await waitFor(() => {
152
+ expect(onOpen).toHaveBeenCalled();
153
+ expect(onClose).not.toHaveBeenCalled();
154
+ });
156
155
  });
157
156
 
158
- it("calls onClose when Popover is closed", () => {
157
+ it("calls onClose when Popover is closed", async () => {
159
158
  const sampleText = "Sample Text";
160
159
  const triggerText = "Click me";
161
160
  const onOpen = jest.fn();
@@ -171,16 +170,18 @@ describe("Popover", () => {
171
170
  </Popover>,
172
171
  );
173
172
 
174
- const button = screen.getByText(triggerText);
173
+ const toggle = screen.getByText(triggerText);
175
174
 
176
- fireEvent.click(button);
175
+ fireEvent.click(toggle);
177
176
 
178
177
  const closeButton = screen.getByLabelText("Close");
179
178
 
180
179
  fireEvent.click(closeButton);
181
180
 
182
- expect(onClose).toHaveBeenCalled();
183
- expect(onOpen).toHaveBeenCalledTimes(1);
181
+ await waitFor(() => {
182
+ expect(onClose).toHaveBeenCalled();
183
+ expect(onOpen).toHaveBeenCalledTimes(1);
184
+ });
184
185
  });
185
186
  });
186
187
  });
@@ -1,19 +1,25 @@
1
+ import {
2
+ FloatingArrow,
3
+ arrow,
4
+ autoUpdate,
5
+ offset,
6
+ useDismiss,
7
+ useFloating,
8
+ useInteractions,
9
+ } from "@floating-ui/react";
1
10
  import { cross } from "@simplybusiness/icons";
11
+ import classNames from "classnames";
2
12
  import {
3
13
  ReactElement,
4
14
  ReactNode,
5
15
  Ref,
6
16
  RefAttributes,
7
17
  cloneElement,
8
- useCallback,
9
- useEffect,
10
18
  useRef,
11
19
  useState,
12
20
  } from "react";
13
- import classNames from "classnames";
14
- import { Popover as TinyPopover } from "react-tiny-popover";
15
21
  import { useWindowEvent } from "../../hooks";
16
- import { DOMProps } from "../../types/dom";
22
+ import { DOMProps } from "../../types";
17
23
  import { Button } from "../Button";
18
24
  import { Icon } from "../Icon";
19
25
 
@@ -24,7 +30,6 @@ export interface PopoverProps
24
30
  RefAttributes<PopoverElementType> {
25
31
  children?: ReactNode;
26
32
  trigger: ReactElement;
27
- isOpen?: boolean;
28
33
  /** Callback that fires each time the accordion is opened */
29
34
  onOpen?: () => void;
30
35
  /** Callback that fires each time the accordion is closed */
@@ -35,87 +40,83 @@ export interface PopoverProps
35
40
 
36
41
  export type PopoverRef = Ref<PopoverElementType>;
37
42
 
43
+ const OFFSET_FROM_CONTENT_DEFAULT = 10;
44
+
38
45
  export const Popover = (props: PopoverProps) => {
39
- const popoverRef = useRef(null);
40
- const triggerRef = useRef(null);
41
- const { trigger, children, isOpen, onOpen, onClose, className } = props;
42
- const previousIsOpen = useRef<boolean | undefined>(isOpen);
43
- const hasExternalState = typeof isOpen !== "undefined";
44
- const [open, setOpen] = useState(isOpen);
46
+ const { trigger, children, onOpen, onClose, className } = props;
47
+ const arrowRef = useRef(null);
48
+ const [isOpen, setIsOpen] = useState(false);
49
+ const { refs, floatingStyles, context } = useFloating({
50
+ open: isOpen,
51
+ onOpenChange: setIsOpen,
52
+ whileElementsMounted: autoUpdate,
53
+ middleware: [
54
+ arrow({
55
+ element: arrowRef,
56
+ }),
57
+ offset(OFFSET_FROM_CONTENT_DEFAULT),
58
+ ],
59
+ });
60
+ const dismiss = useDismiss(context, {
61
+ bubbles: true,
62
+ outsidePress: (event: MouseEvent) => {
63
+ // Prevent 'onClose' from firing when clicking the toggle to close
64
+ const toggle = refs.reference.current as HTMLElement;
65
+ const isToggleClick = !toggle?.contains(event.target as HTMLElement);
66
+ if (isToggleClick) {
67
+ onClose?.();
68
+ }
69
+ return true;
70
+ },
71
+ });
72
+ const { getReferenceProps, getFloatingProps } = useInteractions([dismiss]);
73
+
45
74
  const containerClasses = classNames(
46
75
  "mobius",
47
76
  "mobius/PopoverContainer",
48
77
  className,
49
78
  );
50
- const noop = () => {};
51
-
52
- const openPopover = useCallback(() => {
53
- setOpen(true);
54
-
55
- if (onOpen) {
56
- onOpen();
57
- }
58
- }, [onOpen]);
59
-
60
- const closePopover = useCallback(() => {
61
- setOpen(false);
62
-
63
- if (onClose) {
64
- onClose();
65
- }
66
- }, [onClose]);
67
79
 
68
- const handleClick = () => {
69
- if (open) {
70
- closePopover();
80
+ const toggleVisibility = () => {
81
+ if (isOpen) {
82
+ setIsOpen(false);
83
+ onClose?.();
71
84
  return;
72
85
  }
73
86
 
74
- openPopover();
87
+ setIsOpen(true);
88
+ onOpen?.();
75
89
  };
76
90
 
77
91
  const triggerComponent = cloneElement(trigger, {
78
- className: "mobius/PopoverToggle",
79
- onClick: hasExternalState ? noop : handleClick,
92
+ ref: refs.setReference,
93
+ className: classNames(trigger.props.className, "mobius/PopoverToggle"),
94
+ onClick: toggleVisibility,
95
+ ...getReferenceProps(),
80
96
  });
81
97
 
82
98
  useWindowEvent("keydown", e => {
83
- if (open && e.key === "Escape") {
84
- closePopover();
99
+ if (e.key === "Escape") {
100
+ onClose?.();
85
101
  }
86
102
  });
87
103
 
88
- useEffect(() => {
89
- if (isOpen) {
90
- openPopover();
91
- return;
92
- }
93
-
94
- // Prevent 'onClose' being called when
95
- // 'isOpen === false' on initial render
96
- if (previousIsOpen.current === isOpen) {
97
- previousIsOpen.current = undefined;
98
- return;
99
- }
100
-
101
- if (!isOpen) {
102
- closePopover();
103
- }
104
- }, [isOpen, closePopover, openPopover]);
105
-
106
104
  return (
107
- <TinyPopover
108
- isOpen={!!open}
109
- positions={["bottom"]}
110
- ref={triggerRef}
111
- content={
112
- <div className={containerClasses} ref={popoverRef}>
105
+ <>
106
+ {triggerComponent}
107
+ {isOpen && (
108
+ <div
109
+ className={containerClasses}
110
+ ref={refs.setFloating}
111
+ style={floatingStyles}
112
+ {...getFloatingProps()}
113
+ >
113
114
  <div className="mobius/Popover">
114
115
  <header className="mobius/PopoverHeader">
115
116
  <Button
116
117
  type="button"
117
118
  className="mobius/PopoverCloseButton"
118
- onClick={handleClick}
119
+ onClick={toggleVisibility}
119
120
  aria-label="Close"
120
121
  variant="ghost"
121
122
  >
@@ -128,10 +129,9 @@ export const Popover = (props: PopoverProps) => {
128
129
  </header>
129
130
  <div className="mobius/PopoverBody">{children}</div>
130
131
  </div>
132
+ <FloatingArrow ref={arrowRef} context={context} width={20} />
131
133
  </div>
132
- }
133
- >
134
- {triggerComponent}
135
- </TinyPopover>
134
+ )}
135
+ </>
136
136
  );
137
137
  };