Skip to content
This repository was archived by the owner on Apr 18, 2024. It is now read-only.

Commit 8d3f6a0

Browse files
feat: PROD-309: Add hint for Choice (#1359)
* feat: PROD-309: Add hint for Choice * Update testing library * test: PROD-309: Add tests for displaying hints on Choice * feat: PROD-309: Fix mouseEnterDelay parameter and use it for Tooltip on Choice * Add resetting taxonomy snapshot cache on destroy * Comb props Co-authored-by: yyassi-heartex <[email protected]> * Refactor code Co-authored-by: yyassi-heartex <[email protected]> * Update testing library --------- Co-authored-by: yyassi-heartex <[email protected]>
1 parent 8e51769 commit 8d3f6a0

File tree

13 files changed

+318
-82
lines changed

13 files changed

+318
-82
lines changed

src/common/Tooltip/Tooltip.tsx

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,12 @@ export interface TooltipProps {
1111
children: JSX.Element;
1212
theme?: 'light' | 'dark';
1313
defaultVisible?: boolean;
14+
// activates intent detecting mode
1415
mouseEnterDelay?: number;
1516
enabled?: boolean;
1617
style?: CSSProperties;
18+
// allows to convert triggerElementRef into a real HTMLElement for listeners and getting bbox
19+
triggerElementGetter?: (refValue:any)=>HTMLElement;
1720
}
1821

1922
export const Tooltip = forwardRef<HTMLElement, TooltipProps>(({
@@ -24,6 +27,7 @@ export const Tooltip = forwardRef<HTMLElement, TooltipProps>(({
2427
enabled = true,
2528
theme = 'dark',
2629
style,
30+
triggerElementGetter = refValue => refValue as HTMLElement,
2731
}, ref) => {
2832
if (!children || Array.isArray(children)) {
2933
throw new Error('Tooltip does accept a single child only');
@@ -35,10 +39,11 @@ export const Tooltip = forwardRef<HTMLElement, TooltipProps>(({
3539
const [visibility, setVisibility] = useState(defaultVisible ? 'visible' : null);
3640
const [injected, setInjected] = useState(false);
3741
const [align, setAlign] = useState<ElementAlignment>('top-center');
42+
const mouseEnterTimeoutRef = useRef<number|undefined>();
3843

3944
const calculatePosition = useCallback(() => {
4045
const { left, top, align: resultAlign } = alignElements(
41-
triggerElement.current,
46+
triggerElementGetter(triggerElement.current),
4247
tooltipElement.current!,
4348
align,
4449
10,
@@ -113,19 +118,23 @@ export const Tooltip = forwardRef<HTMLElement, TooltipProps>(({
113118
}, [injected]);
114119

115120
useEffect(() => {
116-
const el = triggerElement.current;
121+
const el = triggerElementGetter(triggerElement.current);
117122

118123
const handleTooltipAppear = () => {
119124
if (enabled === false) return;
120125

121-
setTimeout(() => {
126+
mouseEnterTimeoutRef.current = window.setTimeout(() => {
127+
mouseEnterTimeoutRef.current = undefined;
122128
setInjected(true);
123129
}, mouseEnterDelay);
124130
};
125131

126132
const handleTooltipHiding = () => {
127133
if (enabled === false) return;
128134

135+
if (mouseEnterTimeoutRef.current) {
136+
mouseEnterTimeoutRef.current = window.clearTimeout(mouseEnterTimeoutRef.current);
137+
}
129138
performAnimation(false);
130139
};
131140

src/components/Taxonomy/Taxonomy.tsx

Lines changed: 107 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,12 @@
1-
import React, { FormEvent, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
1+
import React, {
2+
FormEvent,
3+
useCallback,
4+
useContext,
5+
useEffect,
6+
useMemo,
7+
useRef,
8+
useState
9+
} from 'react';
210
import { Dropdown, Menu } from 'antd';
311

412
import { useToggle } from '../../hooks/useToggle';
@@ -7,7 +15,9 @@ import { LsChevron } from '../../assets/icons';
715
import TreeStructure from '../TreeStructure/TreeStructure';
816

917
import styles from './Taxonomy.module.scss';
10-
import { FF_DEV_4075, isFF } from '../../utils/feature-flags';
18+
import { FF_DEV_4075, FF_PROD_309, isFF } from '../../utils/feature-flags';
19+
import { Tooltip } from '../../common/Tooltip/Tooltip';
20+
import { CNTagName } from '../../utils/bem';
1121

1222
type TaxonomyPath = string[];
1323
type onAddLabelCallback = (path: string[]) => any;
@@ -19,6 +29,7 @@ type TaxonomyItem = {
1929
depth: number,
2030
children?: TaxonomyItem[],
2131
origin?: 'config' | 'user' | 'session',
32+
hint?: string,
2233
};
2334

2435
type TaxonomyOptions = {
@@ -74,6 +85,7 @@ interface RowProps {
7485
padding: number,
7586
isLeaf: boolean,
7687
origin?: any,
88+
hint?: string,
7789
},
7890
children?: any,
7991
toggle: (id: string) => void,
@@ -148,9 +160,38 @@ function isSubArray(item: string[], parent: string[]) {
148160
return parent.every((n, i) => item[i] === n);
149161
}
150162

163+
type HintTooltipProps = {
164+
// Without title there is no tooltip at all as a component
165+
title?: string,
166+
// wrapper is used as a tag in JSX to wrap child elements to make Tooltip to work with the single child element
167+
// it can be a real tag or a component that provides real HTMLElement (not a text) as the result
168+
wrapper?: CNTagName,
169+
children: JSX.Element,
170+
}
171+
172+
export const HintTooltip: React.FC<HintTooltipProps> = ({
173+
title,
174+
wrapper: Wrapper,
175+
children,
176+
...rest
177+
}) => {
178+
if (!isFF(FF_PROD_309)) return children;
179+
180+
const content = Wrapper ? <Wrapper>{children}</Wrapper> : children;
181+
182+
if (title) {
183+
return (
184+
<Tooltip title={title} mouseEnterDelay={500} {...rest}>
185+
{content}
186+
</Tooltip>
187+
);
188+
}
189+
return content;
190+
};
191+
151192
const Item: React.FC<RowProps> = ({ style, item, dimensionCallback, maxWidth, isEditable }: RowProps) => {
152193
const {
153-
row: { id, isOpen, childCount, isFiltering, name, path, padding, isLeaf },
194+
row: { id, isOpen, childCount, isFiltering, name, path, padding, isLeaf, hint },
154195
toggle,
155196
addInside: addChild,
156197
} = item;
@@ -222,67 +263,69 @@ const Item: React.FC<RowProps> = ({ style, item, dimensionCallback, maxWidth, is
222263
</div>
223264
)}
224265
</div>
225-
<div className={[styles.taxonomy__item, customClassname].join(' ')}>
226-
<div className={styles.taxonomy__grouping} onClick={() => toggle(id)}>
227-
<LsChevron stroke="#09f" style={arrowStyle} />
228-
</div>
229-
<input
230-
className="item"
231-
id={id}
232-
name={id}
233-
type="checkbox"
234-
disabled={disabled}
235-
checked={checked}
236-
ref={setIndeterminate}
237-
onChange={e => {
238-
if (isEditable) {
239-
setSelected(path, e.currentTarget.checked);
240-
}
241-
}}
242-
/>
243-
<label
244-
htmlFor={id}
245-
style={isFF(FF_DEV_4075) ? {} : { maxWidth: `${labelMaxWidth}px` }}
246-
onClick={isEditable ? onClick : undefined}
247-
title={title}
248-
className={disabled ? styles.taxonomy__collapsable : undefined}
249-
>
250-
{name}
251-
</label>
252-
{!isFiltering && (
253-
<div className={styles.taxonomy__extra}>
254-
<span className={styles.taxonomy__extra_count}>{childCount}</span>
255-
{isEditable && onAddLabel && (
256-
<div className={styles.taxonomy__extra_actions}>
257-
<Dropdown
258-
destroyPopupOnHide // important for long interactions with huge taxonomy
259-
trigger={['click']}
260-
overlay={(
261-
<Menu>
262-
<Menu.Item
263-
key="add-inside"
264-
className={styles.taxonomy__action}
265-
onClick={() => {
266-
addChild(id);
267-
}}
268-
>
266+
<HintTooltip title={hint}>
267+
<div className={[styles.taxonomy__item, customClassname].join(' ')}>
268+
<div className={styles.taxonomy__grouping} onClick={() => toggle(id)}>
269+
<LsChevron stroke="#09f" style={arrowStyle} />
270+
</div>
271+
<input
272+
className="item"
273+
id={id}
274+
name={id}
275+
type="checkbox"
276+
disabled={disabled}
277+
checked={checked}
278+
ref={setIndeterminate}
279+
onChange={e => {
280+
if (isEditable) {
281+
setSelected(path, e.currentTarget.checked);
282+
}
283+
}}
284+
/>
285+
<label
286+
htmlFor={id}
287+
style={isFF(FF_DEV_4075) ? {} : { maxWidth: `${labelMaxWidth}px` }}
288+
onClick={isEditable ? onClick : undefined}
289+
title={title}
290+
className={disabled ? styles.taxonomy__collapsable : undefined}
291+
>
292+
{name}
293+
</label>
294+
{!isFiltering && (
295+
<div className={styles.taxonomy__extra}>
296+
<span className={styles.taxonomy__extra_count}>{childCount}</span>
297+
{isEditable && onAddLabel && (
298+
<div className={styles.taxonomy__extra_actions}>
299+
<Dropdown
300+
destroyPopupOnHide // important for long interactions with huge taxonomy
301+
trigger={['click']}
302+
overlay={(
303+
<Menu>
304+
<Menu.Item
305+
key="add-inside"
306+
className={styles.taxonomy__action}
307+
onClick={() => {
308+
addChild(id);
309+
}}
310+
>
269311
Add Inside
270-
</Menu.Item>
271-
{item.row.origin === 'session' && (
272-
<Menu.Item key="delete" className={styles.taxonomy__action} onClick={onDelete}>
273-
Delete
274312
</Menu.Item>
275-
)}
276-
</Menu>
277-
)}
278-
>
279-
<div>...</div>
280-
</Dropdown>
281-
</div>
282-
)}
283-
</div>
284-
)}
285-
</div>
313+
{item.row.origin === 'session' && (
314+
<Menu.Item key="delete" className={styles.taxonomy__action} onClick={onDelete}>
315+
Delete
316+
</Menu.Item>
317+
)}
318+
</Menu>
319+
)}
320+
>
321+
<div>...</div>
322+
</Dropdown>
323+
</div>
324+
)}
325+
</div>
326+
)}
327+
</div>
328+
</HintTooltip>
286329
</>
287330
) : (
288331
<UserLabelForm key="" onAddLabel={onAddLabel} onFinish={() => addChild()} path={path} />
@@ -358,7 +401,7 @@ const TaxonomyDropdown = ({ show, flatten, items, dropdownRef, isEditable }: Tax
358401
}, [show]);
359402

360403
const dataTransformation = ({
361-
node: { children, depth, label, origin, path },
404+
node: { children, depth, label, origin, path, hint },
362405
nestingLevel,
363406
isFiltering,
364407
isOpen,
@@ -381,6 +424,7 @@ const TaxonomyDropdown = ({ show, flatten, items, dropdownRef, isEditable }: Tax
381424
origin,
382425
padding: nestingLevel * 10 + 10,
383426
path,
427+
hint,
384428
});
385429

386430
return (

src/tags/control/Choice.js

Lines changed: 24 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,11 @@ import Tree from '../../core/Tree';
1313
import Types from '../../core/Types';
1414
import { AnnotationMixin } from '../../mixins/AnnotationMixin';
1515
import { TagParentMixin } from '../../mixins/TagParentMixin';
16-
import { FF_DEV_2007, FF_DEV_2244, FF_DEV_3391, isFF } from '../../utils/feature-flags';
16+
import { FF_DEV_2007, FF_DEV_2244, FF_DEV_3391, FF_PROD_309, isFF } from '../../utils/feature-flags';
1717
import { Block, Elem } from '../../utils/bem';
1818
import './Choice/Choice.styl';
1919
import { LsChevron } from '../../assets/icons';
20+
import { HintTooltip } from '../../components/Taxonomy/Taxonomy';
2021

2122
/**
2223
* The `Choice` tag represents a single choice for annotations. Use with the `Choices` tag or `Taxonomy` tag to provide specific choice options.
@@ -40,6 +41,7 @@ import { LsChevron } from '../../assets/icons';
4041
* @param {string} [alias] - Alias for the choice. If used, the alias replaces the choice value in the annotation results. Alias does not display in the interface.
4142
* @param {style} [style] - CSS style of the checkbox element
4243
* @param {string} [hotkey] - Hotkey for the selection
44+
* @param {string} [hint] - Hint for choice on hover (it works when fflag_feat_front_prod_309_choice_hint_080523_short is enabled)
4345
*/
4446
const TagAttrs = types.model({
4547
...(isFF(FF_DEV_3391) ? { id: types.identifier } : {}),
@@ -49,6 +51,7 @@ const TagAttrs = types.model({
4951
hotkey: types.maybeNull(types.string),
5052
style: types.maybeNull(types.string),
5153
...(isFF(FF_DEV_2007) ? { html: types.maybeNull(types.string) } : {}),
54+
...(isFF(FF_PROD_309) ? { hint: types.maybeNull(types.string) } : {}),
5255
});
5356

5457
const Model = types
@@ -162,6 +165,10 @@ const Model = types
162165

163166
const ChoiceModel = types.compose('ChoiceModel', TagParentMixin, TagAttrs, Model, ProcessAttrsMixin, AnnotationMixin);
164167

168+
function triggerElementGetter(el) {
169+
return el?.input?.parentNode?.parentNode;
170+
}
171+
165172
class HtxChoiceView extends Component {
166173
render() {
167174
const { item, store } = this.props;
@@ -194,19 +201,23 @@ class HtxChoiceView extends Component {
194201

195202
return (
196203
<Form.Item style={cStyle}>
197-
<Checkbox name={item._value} {...props} disabled={item.isReadOnly()}>
198-
{item._value}
199-
{showHotkey && <Hint>[{item.hotkey}]</Hint>}
200-
</Checkbox>
204+
<HintTooltip title={item.hint} triggerElementGetter={triggerElementGetter}>
205+
<Checkbox name={item._value} {...props} disabled={item.isReadOnly()}>
206+
{item._value}
207+
{showHotkey && <Hint>[{item.hotkey}]</Hint>}
208+
</Checkbox>
209+
</HintTooltip>
201210
</Form.Item>
202211
);
203212
} else {
204213
return (
205214
<div style={style}>
206-
<Radio value={item._value} style={{ display: 'inline-block', marginBottom: '0.5em' }} {...props}>
207-
{item._value}
208-
{showHotkey && <Hint>[{item.hotkey}]</Hint>}
209-
</Radio>
215+
<HintTooltip title={item.hint} triggerElementGetter={triggerElementGetter}>
216+
<Radio value={item._value} style={{ display: 'inline-block', marginBottom: '0.5em' }} {...props}>
217+
{item._value}
218+
{showHotkey && <Hint>[{item.hotkey}]</Hint>}
219+
</Radio>
220+
</HintTooltip>
210221
</div>
211222
);
212223
}
@@ -250,8 +261,10 @@ const HtxNewChoiceView = ({ item, store }) => {
250261
disabled={item.isReadOnly()}
251262
onChange={changeHandler}
252263
>
253-
{item.html ? <span dangerouslySetInnerHTML={{ __html: item.html }}/> : item._value }
254-
{showHotkey && (<Hint>[{item.hotkey}]</Hint>)}
264+
<HintTooltip title={item.hint} wrapper="span">
265+
{item.html ? <span dangerouslySetInnerHTML={{ __html: item.html }}/> : item._value }
266+
{showHotkey && (<Hint>[{item.hotkey}]</Hint>)}
267+
</HintTooltip>
255268
</Elem>
256269
{!item.isLeaf ? (
257270
<Elem name="toggle" mod={{ collapsed }} component={Button} type="text" onClick={toogleCollapsed}>

src/tags/control/Choices.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import DynamicChildrenMixin from '../../mixins/DynamicChildrenMixin';
2222
import { FF_DEV_2007, FF_DEV_2007_DEV_2008, isFF } from '../../utils/feature-flags';
2323
import { ReadOnlyControlMixin } from '../../mixins/ReadOnlyMixin';
2424
import SelectedChoiceMixin from '../../mixins/SelectedChoiceMixin';
25+
import { HintTooltip } from '../../components/Taxonomy/Taxonomy';
2526

2627
const { Option } = Select;
2728

@@ -277,7 +278,9 @@ const ChoicesSelectLayout = observer(({ item }) => {
277278
>
278279
{item.tiedChildren.map(i => (
279280
<Option key={i._value} value={i._value}>
280-
{i._value}
281+
<HintTooltip title={i.hint} wrapper="div">
282+
{i._value}
283+
</HintTooltip>
281284
</Option>
282285
))}
283286
</Select>

0 commit comments

Comments
 (0)