@scality/core-ui 0.182.0 → 0.183.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.
@@ -3,12 +3,13 @@ export declare const COPY_STATE_IDLE = "idle";
3
3
  export declare const COPY_STATE_SUCCESS = "success";
4
4
  export declare const COPY_STATE_UNSUPPORTED = "unsupported";
5
5
  export declare const useClipboard: () => {
6
- copy: (text: any) => void;
6
+ copy: (text: string, asHtml?: boolean) => void;
7
7
  copyStatus: string;
8
8
  };
9
- export declare const CopyButton: ({ label, textToCopy, variant, ...props }: {
9
+ export declare const CopyButton: ({ label, textToCopy, copyAsHtml, variant, ...props }: {
10
10
  label?: string;
11
11
  textToCopy: string;
12
+ copyAsHtml?: boolean;
12
13
  variant?: "outline" | "ghost";
13
14
  } & Props) => import("react/jsx-runtime").JSX.Element;
14
15
  //# sourceMappingURL=CopyButton.component.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"CopyButton.component.d.ts","sourceRoot":"","sources":["../../../src/lib/components/buttonv2/CopyButton.component.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAU,KAAK,EAAE,MAAM,sBAAsB,CAAC;AAErD,eAAO,MAAM,eAAe,SAAS,CAAC;AACtC,eAAO,MAAM,kBAAkB,YAAY,CAAC;AAC5C,eAAO,MAAM,sBAAsB,gBAAgB,CAAC;AACpD,eAAO,MAAM,YAAY;;;CAuBxB,CAAC;AAEF,eAAO,MAAM,UAAU,6CAKpB;IACD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,SAAS,GAAG,OAAO,CAAC;CAC/B,GAAG,KAAK,4CA+CR,CAAC"}
1
+ {"version":3,"file":"CopyButton.component.d.ts","sourceRoot":"","sources":["../../../src/lib/components/buttonv2/CopyButton.component.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAU,KAAK,EAAE,MAAM,sBAAsB,CAAC;AAErD,eAAO,MAAM,eAAe,SAAS,CAAC;AACtC,eAAO,MAAM,kBAAkB,YAAY,CAAC;AAC5C,eAAO,MAAM,sBAAsB,gBAAgB,CAAC;AACpD,eAAO,MAAM,YAAY;iBASQ,MAAM,WAAW,OAAO;;CAoCxD,CAAC;AAEF,eAAO,MAAM,UAAU,yDAMpB;IACD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,OAAO,CAAC,EAAE,SAAS,GAAG,OAAO,CAAC;CAC/B,GAAG,KAAK,4CA+CR,CAAC"}
@@ -13,20 +13,41 @@ export const useClipboard = () => {
13
13
  }, 2000);
14
14
  return () => clearTimeout(timer);
15
15
  }, [copyStatus]);
16
- const copyToClipboard = (text) => {
16
+ const copyToClipboard = (text, asHtml) => {
17
17
  if (!navigator || !navigator.clipboard) {
18
18
  setCopyStatus(COPY_STATE_UNSUPPORTED);
19
19
  return;
20
20
  }
21
- navigator.clipboard.writeText(text);
22
- setCopyStatus(COPY_STATE_SUCCESS);
21
+ if (asHtml) {
22
+ // Copy as HTML with plain text fallback
23
+ const el = document.createElement('div');
24
+ el.innerHTML = text;
25
+ const plainText = el.innerText;
26
+ const clipboardItem = new ClipboardItem({
27
+ 'text/html': new Blob([text], { type: 'text/html' }),
28
+ 'text/plain': new Blob([plainText], { type: 'text/plain' }),
29
+ });
30
+ navigator.clipboard
31
+ .write([clipboardItem])
32
+ .then(() => {
33
+ setCopyStatus(COPY_STATE_SUCCESS);
34
+ })
35
+ .catch(() => {
36
+ setCopyStatus(COPY_STATE_UNSUPPORTED);
37
+ });
38
+ }
39
+ else {
40
+ // Copy as plain text only
41
+ navigator.clipboard.writeText(text);
42
+ setCopyStatus(COPY_STATE_SUCCESS);
43
+ }
23
44
  };
24
45
  return {
25
46
  copy: copyToClipboard,
26
47
  copyStatus: copyStatus,
27
48
  };
28
49
  };
29
- export const CopyButton = ({ label, textToCopy, variant, ...props }) => {
50
+ export const CopyButton = ({ label, textToCopy, copyAsHtml, variant, ...props }) => {
30
51
  const { copy, copyStatus } = useClipboard();
31
52
  return (_jsx(Button, { ...props, variant: variant === 'outline' ? 'outline' : undefined, style: {
32
53
  minWidth:
@@ -38,7 +59,7 @@ export const CopyButton = ({ label, textToCopy, variant, ...props }) => {
38
59
  ? copyStatus === COPY_STATE_SUCCESS
39
60
  ? `Copied${label ? ' ' + label + '' : ''}!`
40
61
  : `Copy${label ? ' ' + label : ''}`
41
- : undefined, icon: _jsx(Icon, { name: copyStatus === COPY_STATE_SUCCESS ? 'Check' : 'Copy', color: copyStatus === COPY_STATE_SUCCESS ? 'statusHealthy' : undefined }), disabled: copyStatus === COPY_STATE_SUCCESS || props.disabled, onClick: () => copy(textToCopy), type: "button", tooltip: variant !== 'outline'
62
+ : undefined, icon: _jsx(Icon, { name: copyStatus === COPY_STATE_SUCCESS ? 'Check' : 'Copy', color: copyStatus === COPY_STATE_SUCCESS ? 'statusHealthy' : undefined }), disabled: copyStatus === COPY_STATE_SUCCESS || props.disabled, onClick: () => copy(textToCopy, copyAsHtml), type: "button", tooltip: variant !== 'outline'
42
63
  ? {
43
64
  overlay: copyStatus === COPY_STATE_SUCCESS
44
65
  ? 'Copied !'
@@ -5,6 +5,11 @@ export declare const TextArea: import("react").ForwardRefExoticComponent<Textare
5
5
  variant?: TextAreaVariant;
6
6
  width?: CSSProperties["width"];
7
7
  height?: CSSProperties["height"];
8
+ /**
9
+ * Automatically adjust height to fit content
10
+ * When enabled, the textarea will grow/shrink to show all content
11
+ */
12
+ autoGrow?: boolean;
8
13
  } & import("react").RefAttributes<RefType>>;
9
14
  export {};
10
15
  //# sourceMappingURL=TextArea.component.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"TextArea.component.d.ts","sourceRoot":"","sources":["../../../src/lib/components/textarea/TextArea.component.tsx"],"names":[],"mappings":"AAAA,OAAO,EACL,aAAa,EAEb,sBAAsB,EAEvB,MAAM,OAAO,CAAC;AAIf,KAAK,eAAe,GAAG,MAAM,GAAG,MAAM,CAAC;AAMvC,KAAK,OAAO,GAAG,mBAAmB,GAAG,IAAI,CAAC;AA4F1C,eAAO,MAAM,QAAQ;cAhGT,eAAe;YACjB,aAAa,CAAC,OAAO,CAAC;aACrB,aAAa,CAAC,QAAQ,CAAC;2CA8FiC,CAAC"}
1
+ {"version":3,"file":"TextArea.component.d.ts","sourceRoot":"","sources":["../../../src/lib/components/textarea/TextArea.component.tsx"],"names":[],"mappings":"AAAA,OAAO,EACL,aAAa,EAEb,sBAAsB,EAMvB,MAAM,OAAO,CAAC;AAIf,KAAK,eAAe,GAAG,MAAM,GAAG,MAAM,CAAC;AAWvC,KAAK,OAAO,GAAG,mBAAmB,GAAG,IAAI,CAAC;AAyJ1C,eAAO,MAAM,QAAQ;cAlKT,eAAe;YACjB,aAAa,CAAC,OAAO,CAAC;aACrB,aAAa,CAAC,QAAQ,CAAC;IAChC;;;OAGG;eACQ,OAAO;2CA2J+C,CAAC"}
@@ -1,5 +1,5 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
- import { forwardRef, } from 'react';
2
+ import { forwardRef, useEffect, useRef, useImperativeHandle, useCallback, } from 'react';
3
3
  import styled, { css } from 'styled-components';
4
4
  import { spacing } from '../../spacing';
5
5
  const TextAreaContainer = styled.textarea `
@@ -25,6 +25,11 @@ const TextAreaContainer = styled.textarea `
25
25
  height: ${props.height};
26
26
  `}
27
27
 
28
+ ${(props) => props.autoGrow &&
29
+ css `
30
+ overflow: hidden;
31
+ `}
32
+
28
33
  &:placeholder-shown {
29
34
  font-style: italic;
30
35
  }
@@ -52,10 +57,36 @@ const TextAreaContainer = styled.textarea `
52
57
  `;
53
58
  }}
54
59
  `;
55
- function TextAreaElement({ rows = 3, cols = 20, width, height, variant = 'code', ...rest }, ref) {
60
+ function TextAreaElement({ rows = 3, cols = 20, width, height, variant = 'code', autoGrow = false, value, defaultValue, onChange, ...rest }, ref) {
61
+ const internalRef = useRef(null);
62
+ // Expose the textarea element to parent components via forwarded ref
63
+ useImperativeHandle(ref, () => internalRef.current);
64
+ // Adjust height on mount and when value changes (for controlled components)
65
+ const adjustHeight = useCallback(() => {
66
+ const textarea = internalRef.current;
67
+ if (!textarea || !autoGrow)
68
+ return;
69
+ // Reset height to auto to get the correct scrollHeight
70
+ textarea.style.height = 'auto';
71
+ // Set the height to match the content
72
+ const newHeight = textarea.scrollHeight;
73
+ textarea.style.height = `${newHeight}px`;
74
+ }, [autoGrow]);
75
+ useEffect(() => {
76
+ adjustHeight();
77
+ }, [adjustHeight, value]);
78
+ // Handle onChange to support both controlled and uncontrolled components
79
+ const handleChange = useCallback((event) => {
80
+ if (autoGrow) {
81
+ adjustHeight();
82
+ }
83
+ if (onChange) {
84
+ onChange(event);
85
+ }
86
+ }, [autoGrow, adjustHeight, onChange]);
56
87
  if (width || height) {
57
- return (_jsx(TextAreaContainer, { className: "sc-textarea", width: width, height: height, variant: variant, ...rest, ref: ref }));
88
+ return (_jsx(TextAreaContainer, { className: "sc-textarea", width: width, height: height, variant: variant, autoGrow: autoGrow, value: value, defaultValue: defaultValue, onChange: handleChange, ...rest, ref: internalRef }));
58
89
  }
59
- return (_jsx(TextAreaContainer, { className: "sc-textarea", rows: rows, cols: cols, variant: variant, ...rest, ref: ref }));
90
+ return (_jsx(TextAreaContainer, { className: "sc-textarea", rows: rows, cols: cols, variant: variant, autoGrow: autoGrow, value: value, defaultValue: defaultValue, onChange: handleChange, ...rest, ref: internalRef }));
60
91
  }
61
92
  export const TextArea = forwardRef(TextAreaElement);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scality/core-ui",
3
- "version": "0.182.0",
3
+ "version": "0.183.0",
4
4
  "description": "Scality common React component library",
5
5
  "author": "Scality Engineering",
6
6
  "license": "SEE LICENSE IN LICENSE",
@@ -14,14 +14,36 @@ export const useClipboard = () => {
14
14
  return () => clearTimeout(timer);
15
15
  }, [copyStatus]);
16
16
 
17
- const copyToClipboard = (text) => {
17
+ const copyToClipboard = (text: string, asHtml?: boolean) => {
18
18
  if (!navigator || !navigator.clipboard) {
19
19
  setCopyStatus(COPY_STATE_UNSUPPORTED);
20
20
  return;
21
21
  }
22
22
 
23
- navigator.clipboard.writeText(text);
24
- setCopyStatus(COPY_STATE_SUCCESS);
23
+ if (asHtml) {
24
+ // Copy as HTML with plain text fallback
25
+ const el = document.createElement('div');
26
+ el.innerHTML = text;
27
+ const plainText = el.innerText;
28
+
29
+ const clipboardItem = new ClipboardItem({
30
+ 'text/html': new Blob([text], { type: 'text/html' }),
31
+ 'text/plain': new Blob([plainText], { type: 'text/plain' }),
32
+ });
33
+
34
+ navigator.clipboard
35
+ .write([clipboardItem])
36
+ .then(() => {
37
+ setCopyStatus(COPY_STATE_SUCCESS);
38
+ })
39
+ .catch(() => {
40
+ setCopyStatus(COPY_STATE_UNSUPPORTED);
41
+ });
42
+ } else {
43
+ // Copy as plain text only
44
+ navigator.clipboard.writeText(text);
45
+ setCopyStatus(COPY_STATE_SUCCESS);
46
+ }
25
47
  };
26
48
 
27
49
  return {
@@ -33,11 +55,13 @@ export const useClipboard = () => {
33
55
  export const CopyButton = ({
34
56
  label,
35
57
  textToCopy,
58
+ copyAsHtml,
36
59
  variant,
37
60
  ...props
38
61
  }: {
39
62
  label?: string;
40
63
  textToCopy: string;
64
+ copyAsHtml?: boolean;
41
65
  variant?: 'outline' | 'ghost';
42
66
  } & Props) => {
43
67
  const { copy, copyStatus } = useClipboard();
@@ -68,7 +92,7 @@ export const CopyButton = ({
68
92
  />
69
93
  }
70
94
  disabled={copyStatus === COPY_STATE_SUCCESS || props.disabled}
71
- onClick={() => copy(textToCopy)}
95
+ onClick={() => copy(textToCopy, copyAsHtml)}
72
96
  type="button"
73
97
  tooltip={
74
98
  variant !== 'outline'
@@ -3,6 +3,10 @@ import {
3
3
  forwardRef,
4
4
  TextareaHTMLAttributes,
5
5
  ForwardedRef,
6
+ useEffect,
7
+ useRef,
8
+ useImperativeHandle,
9
+ useCallback,
6
10
  } from 'react';
7
11
  import styled, { css } from 'styled-components';
8
12
  import { spacing } from '../../spacing';
@@ -12,6 +16,11 @@ type Props = TextareaHTMLAttributes<HTMLTextAreaElement> & {
12
16
  variant?: TextAreaVariant;
13
17
  width?: CSSProperties['width'];
14
18
  height?: CSSProperties['height'];
19
+ /**
20
+ * Automatically adjust height to fit content
21
+ * When enabled, the textarea will grow/shrink to show all content
22
+ */
23
+ autoGrow?: boolean;
15
24
  };
16
25
  type RefType = HTMLTextAreaElement | null;
17
26
 
@@ -19,6 +28,7 @@ const TextAreaContainer = styled.textarea<{
19
28
  variant: TextAreaVariant;
20
29
  width?: CSSProperties['width'];
21
30
  height?: CSSProperties['height'];
31
+ autoGrow?: boolean;
22
32
  }>`
23
33
  padding: ${spacing.r12} ${spacing.r8};
24
34
  border-radius: 4px;
@@ -46,6 +56,12 @@ const TextAreaContainer = styled.textarea<{
46
56
  height: ${props.height};
47
57
  `}
48
58
 
59
+ ${(props) =>
60
+ props.autoGrow &&
61
+ css`
62
+ overflow: hidden;
63
+ `}
64
+
49
65
  &:placeholder-shown {
50
66
  font-style: italic;
51
67
  }
@@ -77,9 +93,55 @@ const TextAreaContainer = styled.textarea<{
77
93
  `;
78
94
 
79
95
  function TextAreaElement(
80
- { rows = 3, cols = 20, width, height, variant = 'code', ...rest }: Props,
96
+ {
97
+ rows = 3,
98
+ cols = 20,
99
+ width,
100
+ height,
101
+ variant = 'code',
102
+ autoGrow = false,
103
+ value,
104
+ defaultValue,
105
+ onChange,
106
+ ...rest
107
+ }: Props,
81
108
  ref: ForwardedRef<RefType>,
82
109
  ) {
110
+ const internalRef = useRef<HTMLTextAreaElement>(null);
111
+
112
+ // Expose the textarea element to parent components via forwarded ref
113
+ useImperativeHandle(ref, () => internalRef.current as HTMLTextAreaElement);
114
+
115
+ // Adjust height on mount and when value changes (for controlled components)
116
+ const adjustHeight = useCallback(() => {
117
+ const textarea = internalRef.current;
118
+ if (!textarea || !autoGrow) return;
119
+
120
+ // Reset height to auto to get the correct scrollHeight
121
+ textarea.style.height = 'auto';
122
+
123
+ // Set the height to match the content
124
+ const newHeight = textarea.scrollHeight;
125
+ textarea.style.height = `${newHeight}px`;
126
+ }, [autoGrow]);
127
+
128
+ useEffect(() => {
129
+ adjustHeight();
130
+ }, [adjustHeight, value]);
131
+
132
+ // Handle onChange to support both controlled and uncontrolled components
133
+ const handleChange = useCallback(
134
+ (event: React.ChangeEvent<HTMLTextAreaElement>) => {
135
+ if (autoGrow) {
136
+ adjustHeight();
137
+ }
138
+ if (onChange) {
139
+ onChange(event);
140
+ }
141
+ },
142
+ [autoGrow, adjustHeight, onChange],
143
+ );
144
+
83
145
  if (width || height) {
84
146
  return (
85
147
  <TextAreaContainer
@@ -87,8 +149,12 @@ function TextAreaElement(
87
149
  width={width}
88
150
  height={height}
89
151
  variant={variant}
152
+ autoGrow={autoGrow}
153
+ value={value}
154
+ defaultValue={defaultValue}
155
+ onChange={handleChange}
90
156
  {...rest}
91
- ref={ref}
157
+ ref={internalRef}
92
158
  />
93
159
  );
94
160
  }
@@ -99,8 +165,12 @@ function TextAreaElement(
99
165
  rows={rows}
100
166
  cols={cols}
101
167
  variant={variant}
168
+ autoGrow={autoGrow}
169
+ value={value}
170
+ defaultValue={defaultValue}
171
+ onChange={handleChange}
102
172
  {...rest}
103
- ref={ref}
173
+ ref={internalRef}
104
174
  />
105
175
  );
106
176
  }
@@ -32,14 +32,14 @@ export const Playground = {};
32
32
 
33
33
  export const DefaultTextArea = {
34
34
  args: {
35
- value: 'Some text',
35
+ defaultValue: 'Some text',
36
36
  },
37
37
  };
38
38
 
39
39
  export const TextVariantTextArea = {
40
40
  args: {
41
41
  variant: 'text',
42
- value: 'Text area with "text" variant',
42
+ defaultValue: 'Text area with "text" variant',
43
43
  },
44
44
  };
45
45
 
@@ -67,3 +67,57 @@ export const RowsAndColsSet = {
67
67
  placeholder: 'With rows = 20 and cols = 40',
68
68
  },
69
69
  };
70
+
71
+ /**
72
+ * Auto-growing textarea adjusts its height based on content
73
+ * Perfect for displaying commands or long text where you want the entire content visible
74
+ * Simply set autoGrow={true} and the textarea will grow to show all content
75
+ */
76
+ export const AutoGrowTextArea = {
77
+ args: {
78
+ autoGrow: true,
79
+ placeholder:
80
+ 'Type or paste content here...\nThe textarea will automatically grow to fit all the content.',
81
+ defaultValue: `docker run -d \\
82
+ --name my-container \\
83
+ -p 8080:80 \\
84
+ -v /host/path:/container/path \\
85
+ -e ENV_VAR=value \\
86
+ my-image:latest`,
87
+ width: '500px',
88
+ },
89
+ };
90
+
91
+ /**
92
+ * Auto-growing textarea with long command example
93
+ * The entire command is visible without scrolling
94
+ */
95
+ export const AutoGrowWithLongCommand = {
96
+ args: {
97
+ autoGrow: true,
98
+ variant: 'code',
99
+ defaultValue: `kubectl apply -f - <<EOF
100
+ apiVersion: v1
101
+ kind: Pod
102
+ metadata:
103
+ name: my-pod
104
+ labels:
105
+ app: myapp
106
+ spec:
107
+ containers:
108
+ - name: nginx
109
+ image: nginx:1.14.2
110
+ ports:
111
+ - containerPort: 80
112
+ env:
113
+ - name: DATABASE_URL
114
+ value: "postgresql://user:password@localhost:5432/db"
115
+ - name: API_KEY
116
+ valueFrom:
117
+ secretKeyRef:
118
+ name: api-secret
119
+ key: api-key
120
+ EOF`,
121
+ width: '600px',
122
+ },
123
+ };