forked from patternfly/patternfly-react
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathLabel.tsx
347 lines (326 loc) · 11.5 KB
/
Label.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
import * as React from 'react';
import { useState } from 'react';
import styles from '@patternfly/react-styles/css/components/Label/label';
import labelGrpStyles from '@patternfly/react-styles/css/components/Label/label-group';
import { Button } from '../Button';
import { Tooltip, TooltipPosition } from '../Tooltip';
import { css } from '@patternfly/react-styles';
import TimesIcon from '@patternfly/react-icons/dist/esm/icons/times-icon';
import { useIsomorphicLayoutEffect } from '../../helpers';
import cssTextMaxWidth from '@patternfly/react-tokens/dist/esm/c_label__text_MaxWidth';
export interface LabelProps extends React.HTMLProps<HTMLSpanElement> {
/** Content rendered inside the label. */
children?: React.ReactNode;
/** Additional classes added to the label. */
className?: string;
/** Color of the label. */
color?: 'blue' | 'cyan' | 'green' | 'orange' | 'purple' | 'red' | 'grey' | 'gold';
/** Variant of the label. */
variant?: 'outline' | 'filled';
/** Flag indicating the label is compact. */
isCompact?: boolean;
/** Flag indicating the label is disabled. Works only on clickable labels, so either href or onClick props must be passed in. */
isDisabled?: boolean;
/** @beta Flag indicating the label is editable. */
isEditable?: boolean;
/** @beta Additional props passed to the editable label text div. Optionally passing onInput and onBlur callbacks will allow finer custom text input control. */
editableProps?: any;
/** @beta Callback when an editable label completes an edit. */
onEditComplete?: (event: MouseEvent | KeyboardEvent, newText: string) => void;
/** @beta Callback when an editable label cancels an edit. */
onEditCancel?: (event: KeyboardEvent, previousText: string) => void;
/** The max width of the label before it is truncated. Can be any valid CSS unit, such as '100%', '100px', or '16ch'. */
textMaxWidth?: string;
/** Position of the tooltip which is displayed if text is truncated */
tooltipPosition?:
| TooltipPosition
| 'auto'
| 'top'
| 'bottom'
| 'left'
| 'right'
| 'top-start'
| 'top-end'
| 'bottom-start'
| 'bottom-end'
| 'left-start'
| 'left-end'
| 'right-start'
| 'right-end';
/** Icon added to the left of the label text. */
icon?: React.ReactNode;
/** Close click callback for removable labels. If present, label will have a close button. */
onClose?: (event: React.MouseEvent) => void;
/** Node for custom close button. */
closeBtn?: React.ReactNode;
/** Aria label for close button */
closeBtnAriaLabel?: string;
/** Additional properties for the default close button. */
closeBtnProps?: any;
/** Href for a label that is a link. If present, the label will change to an anchor element. This should not be passed in if the onClick prop is also passed in. */
href?: string;
/** Flag indicating if the label is an overflow label. */
isOverflowLabel?: boolean;
/** Callback for when the label is clicked. This should not be passed in if the href or isEditable props are also passed in. */
onClick?: (event: React.MouseEvent) => void;
/** Forwards the label content and className to rendered function. Use this prop for react router support.*/
render?: ({
className,
content,
componentRef
}: {
className: string;
content: React.ReactNode;
componentRef: any;
}) => React.ReactNode;
}
const colorStyles = {
blue: styles.modifiers.blue,
cyan: styles.modifiers.cyan,
green: styles.modifiers.green,
orange: styles.modifiers.orange,
purple: styles.modifiers.purple,
red: styles.modifiers.red,
gold: styles.modifiers.gold,
grey: ''
};
export const Label: React.FunctionComponent<LabelProps> = ({
children,
className = '',
color = 'grey',
variant = 'filled',
isCompact = false,
isDisabled = false,
isEditable = false,
editableProps,
textMaxWidth,
tooltipPosition,
icon,
onClose,
onClick: onLabelClick,
onEditCancel,
onEditComplete,
closeBtn,
closeBtnAriaLabel,
closeBtnProps,
href,
isOverflowLabel,
render,
...props
}: LabelProps) => {
const [isEditableActive, setIsEditableActive] = useState<boolean>(false);
const [currValue, setCurrValue] = useState(children);
const editableButtonRef = React.useRef<HTMLButtonElement>();
const editableInputRef = React.useRef<HTMLInputElement>();
React.useEffect(() => {
document.addEventListener('mousedown', onDocMouseDown);
document.addEventListener('keydown', onKeyDown);
return () => {
document.removeEventListener('mousedown', onDocMouseDown);
document.removeEventListener('keydown', onKeyDown);
};
});
React.useEffect(() => {
if (onLabelClick && href) {
// eslint-disable-next-line no-console
console.warn(
'Link labels cannot have onClick passed, this results in invalid HTML. Please remove either the href or onClick prop.'
);
} else if (onLabelClick && isEditable) {
// eslint-disable-next-line no-console
console.warn(
'Editable labels cannot have onClick passed, clicking starts the label edit process. Please remove either the isEditable or onClick prop.'
);
}
}, [onLabelClick, href, isEditable]);
const onDocMouseDown = (event: MouseEvent) => {
if (
isEditableActive &&
editableInputRef &&
editableInputRef.current &&
!editableInputRef.current.contains(event.target as Node)
) {
if (editableInputRef.current.value) {
onEditComplete && onEditComplete(event, editableInputRef.current.value);
}
setIsEditableActive(false);
}
};
const onKeyDown = (event: KeyboardEvent) => {
const key = event.key;
if (
(!isEditableActive &&
(!editableButtonRef ||
!editableButtonRef.current ||
!editableButtonRef.current.contains(event.target as Node))) ||
(isEditableActive &&
(!editableInputRef || !editableInputRef.current || !editableInputRef.current.contains(event.target as Node)))
) {
return;
}
if (isEditableActive && (key === 'Enter' || key === 'Tab')) {
event.preventDefault();
event.stopImmediatePropagation();
if (editableInputRef.current.value) {
onEditComplete && onEditComplete(event, editableInputRef.current.value);
}
setIsEditableActive(false);
editableButtonRef?.current?.focus();
}
if (isEditableActive && key === 'Escape') {
event.preventDefault();
event.stopImmediatePropagation();
// Reset div text to initial children prop - pre-edit
if (editableInputRef.current.value) {
editableInputRef.current.value = children as string;
onEditCancel && onEditCancel(event, children as string);
}
setIsEditableActive(false);
editableButtonRef?.current?.focus();
}
if (!isEditableActive && key === 'Enter') {
event.preventDefault();
event.stopImmediatePropagation();
setIsEditableActive(true);
// Set cursor position to end of text
const el = event.target as HTMLElement;
const range = document.createRange();
const sel = window.getSelection();
range.selectNodeContents(el);
range.collapse(false);
sel.removeAllRanges();
sel.addRange(range);
}
};
const isClickableDisabled = (href || onLabelClick) && isDisabled;
const defaultCloseButton = (
<Button
type="button"
variant="plain"
onClick={onClose}
aria-label={closeBtnAriaLabel || `Close ${children}`}
{...(isClickableDisabled && { isDisabled: true })}
{...closeBtnProps}
>
<TimesIcon />
</Button>
);
const closeButton = <span className={css(styles.labelActions)}>{closeBtn || defaultCloseButton}</span>;
const textRef = React.createRef<any>();
// ref to apply tooltip when rendered is used
const componentRef = React.useRef();
const [isTooltipVisible, setIsTooltipVisible] = React.useState(false);
useIsomorphicLayoutEffect(() => {
const currTextRef = isEditable ? editableButtonRef : textRef;
if (!isEditableActive) {
setIsTooltipVisible(currTextRef.current && currTextRef.current.offsetWidth < currTextRef.current.scrollWidth);
}
}, [isEditableActive]);
const content = (
<React.Fragment>
{icon && <span className={css(styles.labelIcon)}>{icon}</span>}
<span
ref={textRef}
className={css(styles.labelText)}
{...(textMaxWidth && {
style: {
[cssTextMaxWidth.name]: textMaxWidth
} as React.CSSProperties
})}
>
{children}
</span>
</React.Fragment>
);
React.useEffect(() => {
if (isEditableActive && editableInputRef) {
editableInputRef.current && editableInputRef.current.focus();
}
}, [editableInputRef, isEditableActive]);
const updateVal = () => {
setCurrValue(editableInputRef.current.value);
};
let LabelComponentChildElement = 'span';
if (href) {
LabelComponentChildElement = 'a';
} else if (isEditable || (onLabelClick && !isOverflowLabel)) {
LabelComponentChildElement = 'button';
}
const clickableLabelProps = {
type: 'button',
onClick: onLabelClick
};
const isButton = LabelComponentChildElement === 'button';
const labelComponentChildProps = {
className: css(styles.labelContent),
...(isTooltipVisible && { tabIndex: 0 }),
...(href && { href }),
// Need to prevent onClick since aria-disabled won't prevent AT from triggering the link
...(href && isDisabled && { onClick: (event: MouseEvent) => event.preventDefault() }),
...(isButton && clickableLabelProps),
...(isEditable && {
ref: editableButtonRef,
onClick: (e: React.MouseEvent) => {
setIsEditableActive(true);
e.stopPropagation();
},
...editableProps
}),
...(isClickableDisabled && isButton && { disabled: true }),
...(isClickableDisabled && href && { tabIndex: -1, 'aria-disabled': true })
};
let labelComponentChild = (
<LabelComponentChildElement {...labelComponentChildProps}>{content}</LabelComponentChildElement>
);
if (render) {
labelComponentChild = (
<React.Fragment>
{isTooltipVisible && <Tooltip triggerRef={componentRef} content={children} position={tooltipPosition} />}
{render({
className: styles.labelContent,
content,
componentRef
})}
</React.Fragment>
);
} else if (isTooltipVisible) {
labelComponentChild = (
<Tooltip content={children} position={tooltipPosition}>
{labelComponentChild}
</Tooltip>
);
}
const LabelComponent = (isOverflowLabel ? 'button' : 'span') as any;
return (
<LabelComponent
{...props}
className={css(
styles.label,
isClickableDisabled && styles.modifiers.disabled,
colorStyles[color],
variant === 'outline' && styles.modifiers.outline,
isOverflowLabel && styles.modifiers.overflow,
isCompact && styles.modifiers.compact,
isEditable && labelGrpStyles.modifiers.editable,
isEditableActive && styles.modifiers.editableActive,
className
)}
onClick={isOverflowLabel ? onLabelClick : undefined}
>
{!isEditableActive && labelComponentChild}
{!isEditableActive && onClose && closeButton}
{isEditableActive && (
<input
className={css(styles.labelContent)}
type="text"
id="editable-input"
ref={editableInputRef}
value={currValue}
onChange={updateVal}
{...editableProps}
/>
)}
</LabelComponent>
);
};
Label.displayName = 'Label';