@thecb/components 7.0.2-beta.5 → 7.0.2-beta.6

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thecb/components",
3
- "version": "7.0.2-beta.5",
3
+ "version": "7.0.2-beta.6",
4
4
  "description": "Common lib for CityBase react components",
5
5
  "main": "dist/index.cjs.js",
6
6
  "typings": "dist/index.d.ts",
@@ -1,4 +1,4 @@
1
- import React, { useState } from "react";
1
+ import React, { useEffect, useState } from "react";
2
2
  import ButtonWithAction from "../../atoms/button-with-action";
3
3
  import ClipboardIcon from "../../atoms/icons/ClipboardIcon";
4
4
  import Popover from "../popover";
@@ -6,12 +6,22 @@ import Stack from "../../atoms/layouts/Stack";
6
6
  import Text from "../../atoms/text";
7
7
 
8
8
  /*
9
- This component will render `text` and a clipboard icon. When hovered, a popover with content `initialPopoverContent` will be displayed.
10
- When clicked, `text` will be copied to the clipboard, the popover content will change to `copiedPopoverContent` for `copiedPopoverContentDuration` milliseconds,
11
- and the `onCopy` callback will be executed.
9
+ This component will render `content` and a clipboard icon.
10
+ When hovered, a popover with content `initialPopoverContent` will be displayed.
11
+ to the left of the content.
12
+
13
+ When clicked, `content` will be copied to the clipboard, the popover content
14
+ will change to `copySuccessPopoverContent` for `copiedPopoverContentDuration`
15
+ milliseconds, and the `onCopy` callback will be executed.
16
+
17
+ If the content was unable to be copied to the clipboard, the popover will instead
18
+ display `copyErrorPopoverContent`
19
+
20
+ If you only want the copy to clipboard behaviour without a popover, pass the prop
21
+ `hasPopover={false}`
12
22
  */
13
23
 
14
- const CopyableText = ({ text, onClick }) => {
24
+ const CopyableContent = ({ content, onClick }) => {
15
25
  return (
16
26
  <ButtonWithAction
17
27
  data-testid="copyable-trigger"
@@ -21,7 +31,7 @@ const CopyableText = ({ text, onClick }) => {
21
31
  extraStyles="padding: 0; margin: 0; min-height: initial; min-width: initial"
22
32
  >
23
33
  <Stack direction="row" childGap="0.25rem">
24
- <Text>{text}</Text>
34
+ <Text>{content}</Text>
25
35
  <ClipboardIcon />
26
36
  </Stack>
27
37
  </ButtonWithAction>
@@ -29,56 +39,103 @@ const CopyableText = ({ text, onClick }) => {
29
39
  };
30
40
 
31
41
  const Copyable = ({
32
- text,
42
+ content,
33
43
  onCopy,
34
44
  initialPopoverContent = "Click to copy to clipboard",
35
- copiedPopoverContent = "Copied!",
36
- withPopover = true,
37
- popoverMinWidth = "210px",
45
+ copySuccessPopoverContent = "Copied!",
46
+ copyErrorPopoverContent = "Unable to copy value",
38
47
  copiedPopoverContentDuration = 1000,
48
+ hasPopover = true,
49
+ popoverMinWidth = "210px",
39
50
  popoverID = 0,
40
- extraStyles,
51
+ popoverExtraStyles
41
52
  }) => {
42
- const popoverPosition = {
43
- top: "-65px",
44
- right: "auto",
45
- bottom: "auto",
46
- left: "50%",
47
- };
48
- const popoverArrowPosition = {
49
- arrowTop: "auto",
50
- arrowRight: "auto",
51
- arrowBottom: "-8px",
52
- arrowLeft: "calc(50% - 4px)", // Popover arrow is 8px wide. This places the middle of the arrow in the middle of the popover.
53
- };
54
53
  const [popoverContent, setPopoverContent] = useState(initialPopoverContent);
55
- const [timer, setTimer] = useState(undefined);
56
- const onClick = () => {
57
- setPopoverContent(copiedPopoverContent);
58
- navigator.clipboard.writeText(text).catch((e) => console.error(e));
59
- onCopy?.();
60
- if (timer) {
61
- clearTimeout(timer);
54
+ const [startTimer, setStartTimer] = useState(false);
55
+ const [attemptCopy, setAttemptCopy] = useState(false);
56
+ const [timeoutId, setTimeoutId] = useState(null);
57
+
58
+ const cleanup = () => {
59
+ if (timeoutId) {
60
+ // console.log(`Clearing timeout ${timeoutId}`);
61
+ clearTimeout(timeoutId);
62
+ }
63
+ };
64
+
65
+ useEffect(() => {
66
+ if (startTimer) {
67
+ if (timeoutId) {
68
+ // If there's an active timeout, clean it up.
69
+ cleanup();
70
+ }
71
+ // Start a timeout to restore popover content to the initial value.
72
+ // Record the ID of the timeout so it can be cleaned up later.
73
+ // console.log("Starting timeout");
74
+ setTimeoutId(
75
+ setTimeout(() => {
76
+ // console.log(`Timeout ${timeoutId} finished`);
77
+ setPopoverContent(initialPopoverContent);
78
+ }, copiedPopoverContentDuration)
79
+ );
80
+
81
+ // Set startTimer to false immediately after starting the timer,
82
+ // so subsequent clicks will start a new timer, refreshing the duration.
83
+ setStartTimer(false);
84
+ }
85
+
86
+ // Return cleanup function so timeout will be cleared when component unmoumnts.
87
+ return cleanup;
88
+ }, [startTimer]);
89
+
90
+ useEffect(() => {
91
+ if (attemptCopy) {
92
+ navigator.clipboard
93
+ .writeText(content)
94
+ .then(() => {
95
+ setPopoverContent(copySuccessPopoverContent);
96
+ onCopy?.();
97
+ setAttemptCopy(false);
98
+ })
99
+ .catch(error => {
100
+ console.error(error);
101
+ setPopoverContent(copyErrorPopoverContent);
102
+ setAttemptCopy(false);
103
+ });
62
104
  }
63
- setTimer(
64
- setTimeout(() => {
65
- setPopoverContent(initialPopoverContent);
66
- setTimer(undefined);
67
- }, copiedPopoverContentDuration)
68
- );
105
+ }, [attemptCopy]);
106
+
107
+ const onClick = () => {
108
+ setStartTimer(true);
109
+ setAttemptCopy(true);
69
110
  };
70
- return withPopover ? (
111
+
112
+ const popoverArrowWidth = "8px";
113
+
114
+ return hasPopover ? (
71
115
  <Popover
72
116
  minWidth={popoverMinWidth}
73
- position={popoverPosition}
74
- arrowPosition={popoverArrowPosition}
117
+ position={{
118
+ top: "auto",
119
+ right: "auto",
120
+ bottom: "auto",
121
+ left: `calc(-${popoverArrowWidth} - 8px)`
122
+ }}
123
+ arrowPosition={{
124
+ arrowTop: "auto",
125
+ arrowRight: "-8px",
126
+ arrowBottom: `calc(50% - ${popoverArrowWidth})`,
127
+ arrowLeft: "auto"
128
+ }}
129
+ transform="translate(-100%, -75%)"
130
+ arrowDirection="right"
75
131
  popoverID={popoverID}
76
- extraStyles={`> button#btnPopover${popoverID} { margin: 0 }; > #Disclosed${popoverID} { transform: translateX(-50%); }; ${extraStyles}`}
77
- triggerText={CopyableText({ text, onClick })}
132
+ buttonExtraStyles="margin: 0"
133
+ extraStyles={popoverExtraStyles}
134
+ triggerText={CopyableContent({ content, onClick })}
78
135
  content={popoverContent}
79
136
  ></Popover>
80
137
  ) : (
81
- CopyableText({ text, onClick })
138
+ CopyableContent({ content, onClick })
82
139
  );
83
140
  };
84
141
 
@@ -2,15 +2,16 @@ import React from "react";
2
2
  import Expand from "../../../util/expand";
3
3
 
4
4
  export interface CopyableProps {
5
- text: string;
5
+ content: string;
6
+ onCopy?: () => void;
6
7
  initialPopoverContent?: string;
7
- copiedPopoverContent?: string;
8
+ copySuccessPopoverContent?: string;
9
+ copyErrorPopoverContent?: string;
8
10
  copiedPopoverContentDuration?: number;
9
- withPopover?: boolean;
11
+ hasPopover?: boolean;
10
12
  popoverID?: number;
11
13
  popoverMinWidth?: string;
12
- onCopy?: () => void;
13
- extraStyles?: string;
14
+ popoverExtraStyles?: string;
14
15
  }
15
16
 
16
17
  export const Copyable: React.FC<Expand<CopyableProps> &
@@ -8,6 +8,20 @@ import { useOutsideClick } from "../../../util";
8
8
  import { noop } from "../../../util/general";
9
9
  import { fallbackValues } from "./Popover.theme";
10
10
 
11
+ const arrowBorder = (direction, width = "8px") => {
12
+ const angle = `${width} solid transparent`;
13
+ const straight = `${width} solid rgba(255, 255, 255, 0.85)`;
14
+ if (direction == "down") {
15
+ return `border-left: ${angle}; border-right: ${angle}; border-top: ${straight}`;
16
+ } else if (direction == "up") {
17
+ return `border-left: ${angle}; border-right: ${angle}; border-bottom: ${straight}`;
18
+ } else if (direction == "left") {
19
+ return `border-top: ${angle}; border-bottom: ${angle}; border-right: ${straight}`;
20
+ } else if (direction == "right") {
21
+ return `border-top: ${angle}; border-bottom: ${angle}; border-left: ${straight}`;
22
+ }
23
+ };
24
+
11
25
  const Popover = ({
12
26
  themeValues,
13
27
  triggerText = "",
@@ -23,7 +37,10 @@ const Popover = ({
23
37
  maxWidth = "300px",
24
38
  height = "auto",
25
39
  position, // { top: string, right: string, bottom: string, left: string }
26
- arrowPosition // { top: string, right: string, bottom: string, left: string }
40
+ arrowPosition, // { top: string, right: string, bottom: string, left: string }
41
+ arrowDirection = "down",
42
+ transform,
43
+ buttonExtraStyles
27
44
  }) => {
28
45
  const { hoverColor, activeColor, popoverTriggerColor } = themeValues;
29
46
  const { top = "-110px", right = "auto", bottom = "auto", left = "-225px" } =
@@ -73,6 +90,7 @@ const Popover = ({
73
90
  aria-describedby={`Disclosure${popoverID}`}
74
91
  aria-controls={`Disclosed${popoverID}`}
75
92
  ref={triggerRef}
93
+ extraStyles={buttonExtraStyles}
76
94
  >
77
95
  {hasIcon && (
78
96
  <>
@@ -109,12 +127,24 @@ const Popover = ({
109
127
  bottom: ${bottom};
110
128
  left: ${left};
111
129
  height: ${height};
130
+ transform: ${transform ? transform : "none"};
112
131
  `}
113
132
  >
114
133
  <Paragraph>{content}</Paragraph>
115
134
  <Box
116
135
  padding="0"
117
- extraStyles={`position: absolute; content: ""; width: 0; height: 0; border-left: 8px solid transparent; border-right: 8px solid transparent; border-top: 8px solid rgba(255, 255, 255, 0.85); filter: drop-shadow(2px 8px 14px black); bottom: ${arrowBottom}; right: ${arrowRight}; top: ${arrowTop}; left: ${arrowLeft};`}
136
+ extraStyles={`
137
+ position: absolute;
138
+ content: "";
139
+ width: 0;
140
+ height: 0;
141
+ ${arrowBorder(arrowDirection, "8px")};
142
+ filter: drop-shadow(2px 8px 14px black);
143
+ bottom: ${arrowBottom};
144
+ right: ${arrowRight};
145
+ top: ${arrowTop};
146
+ left: ${arrowLeft};
147
+ `}
118
148
  />
119
149
  </Box>
120
150
  </Box>
@@ -20,6 +20,9 @@ export interface PopoverProps {
20
20
  arrowBottom: string;
21
21
  arrowLeft: string;
22
22
  };
23
+ arrowDirection?: "left" | "right" | "top" | "bottom";
24
+ transform?: string;
25
+ disclosedExtraStyles?: string;
23
26
  }
24
27
 
25
28
  export const Popover: React.FC<Expand<PopoverProps> &