Skip to content

Commit 8033a29

Browse files
feat: Text view, tooltip for enrichments (#530)
* feat: text active color and PDF highlight/active colors * feat: support active and highlight * fix: tweak merge * fix: pr review * feat: pdf, single tooltip * feat: pdf single tooltip * fix: pdf active color, css !important * fix: rename css classes and other pr feedback * fix: single line tooltip * feat: add tooltip for text. Next, update tooltip content * feat: tooltip for text view * fix: remove debug msg and clean styles * fix: refactor TooltipHighlight * fix: do not display empty tooltips or duplicate tooltip content * fix: update text highlight style * fix: pr feedback * fix: tooltip content with table to match design * fix: remove sample data for development * fix: pr feedback * fix: move static text to messages * fix: text view, rename TooltipAction to TooltipShow * fix: pr feedback * feat: for tooltip, ellipsis in the middle of long text * fix: pr feedback * fix: remove test data * fix: add testid for cypress test * fix: add Keyword, create style, move MAX_CONTENT_LENGTH * fix: hide tooltip sample data * fix: keyword case-insensitive * fix: account for empty tooltipContent * fix: pr feedback --------- Co-authored-by: DORIAN MILLER <millerbd@us.ibm.com>
1 parent 6e8821f commit 8033a29

10 files changed

Lines changed: 110 additions & 30 deletions

File tree

packages/discovery-react-components/src/components/CIDocument/components/CIDocumentContent/CIDocumentContent.tsx

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import Section, { OnFieldClickFn } from '../Section/Section';
1111
import VirtualScroll from '../VirtualScroll/VirtualScroll';
1212
import { defaultTheme, Theme } from 'utils/theme';
1313
import { SectionType, ItemMap, HighlightWithMeta } from 'components/CIDocument/types';
14+
import { FacetInfoMap } from '../../../DocumentPreview/types';
1415
import { getId as getLocationId } from 'utils/document/idUtils';
1516

1617
const baseClassName = `${settings.prefix}--ci-doc-content`;
@@ -31,6 +32,7 @@ export interface CIDocumentContentProps {
3132
documentId?: string;
3233
onItemClick?: OnFieldClickFn;
3334
combinedHighlights?: HighlightWithMeta[];
35+
facetInfoMap?: FacetInfoMap;
3436
activeColor?: string | null;
3537
}
3638

@@ -50,6 +52,7 @@ const CIDocumentContent: FC<CIDocumentContentProps> = ({
5052
documentId = '',
5153
onItemClick = (): void => {},
5254
combinedHighlights,
55+
facetInfoMap,
5356
activeColor
5457
}) => {
5558
const virtualScrollRef = useRef<any>();
@@ -65,6 +68,7 @@ const CIDocumentContent: FC<CIDocumentContentProps> = ({
6568
}, [activeIds, activeMetadataIds, activePartIds, itemMap]);
6669

6770
const loading = !sections || sections.length === 0;
71+
6872
return (
6973
<div className={cx(baseClassName, className, { skeleton: loading })}>
7074
{loading ? (
@@ -73,7 +77,7 @@ const CIDocumentContent: FC<CIDocumentContentProps> = ({
7377
<>
7478
<style data-testid="style">{docStyles}</style>
7579
{!!combinedHighlights && combinedHighlights.length > 0 && (
76-
<style>{highlightColoringFullArray(combinedHighlights)}</style>
80+
<style>{highlightColoringFullArray(combinedHighlights).join('\n')}</style>
7781
)}
7882
{(!combinedHighlights || combinedHighlights.length <= 0) && (
7983
<style>
@@ -124,7 +128,11 @@ const CIDocumentContent: FC<CIDocumentContentProps> = ({
124128
ref={virtualScrollRef}
125129
>
126130
{({ index }): ReactElement => (
127-
<Section section={sections[index]} onFieldClick={onItemClick} />
131+
<Section
132+
section={sections[index]}
133+
onFieldClick={onItemClick}
134+
facetInfoMap={facetInfoMap}
135+
/>
128136
)}
129137
</VirtualScroll>
130138
)}
@@ -144,9 +152,8 @@ function createStyleRules(idList: string[], rules: string[]): string {
144152
function highlightColoringFullArray(combinedHighlightsWithMeta: HighlightWithMeta[]) {
145153
return combinedHighlightsWithMeta.map(highlightWithMeta => {
146154
const locationId = getHighlightLocationId(highlightWithMeta);
147-
// Set z-index to -1 in order to push non-active fields back
148-
const rules = `.${baseClassName} .field[data-field-id="${locationId}"] > * {background-color: ${highlightWithMeta.color}; z-index: -1;}`;
149-
return <style>{rules}</style>;
155+
const rules = `.${baseClassName} .field[data-field-id="${locationId}"] > * {background-color: ${highlightWithMeta.color}; border: 2px solid ${highlightWithMeta.color};}`;
156+
return rules;
150157
});
151158
}
152159

packages/discovery-react-components/src/components/CIDocument/components/Section/Section.tsx

Lines changed: 55 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ import React, {
1010
MouseEvent,
1111
useEffect,
1212
useRef,
13-
useState
13+
useState,
14+
useCallback
1415
} from 'react';
1516
import cx from 'classnames';
1617
import debounce from 'debounce';
@@ -21,6 +22,9 @@ import { createFieldRects, findOffsetInDOM } from 'utils/document/documentUtils'
2122
import { clearNodeChildren } from 'utils/dom';
2223
import elementFromPoint from 'components/CIDocument/utils/elementFromPoint';
2324
import { SectionType, Field, Item } from 'components/CIDocument/types';
25+
import { FacetInfoMap } from '../../../DocumentPreview/types';
26+
import { TooltipAction, TooltipEvent, OnTooltipShowFn } from '../../../TooltipHighlight/types';
27+
import { TooltipHighlight, calcToolTipContent } from '../../../TooltipHighlight/TooltipHighlight';
2428

2529
export type OnFieldClickFn = (field: Field) => void;
2630

@@ -35,9 +39,14 @@ interface SectionProps {
3539
* Function to call when a field is clicked
3640
*/
3741
onFieldClick?: OnFieldClickFn;
42+
43+
/**
44+
* Meta-data on facets
45+
*/
46+
facetInfoMap?: FacetInfoMap;
3847
}
3948

40-
export const Section: FC<SectionProps> = ({ section, onFieldClick }) => {
49+
export const Section: FC<SectionProps> = ({ section, onFieldClick, facetInfoMap = {} }) => {
4150
const { html } = section;
4251

4352
const [hoveredField, setHoveredField] = useState<HTMLElement | null>(null);
@@ -55,6 +64,26 @@ export const Section: FC<SectionProps> = ({ section, onFieldClick }) => {
5564
}
5665
};
5766

67+
const [tooltipAction, setTooltipAction] = useState<TooltipAction>({
68+
tooltipEvent: TooltipEvent.LEAVE,
69+
rectActiveElement: new DOMRect(),
70+
tooltipContent: <div></div>
71+
});
72+
73+
const onTooltipAction = useCallback(
74+
(tooltipAction: TooltipAction) => {
75+
const updateTooltipAction: TooltipAction = {
76+
...{
77+
tooltipEvent: tooltipAction.tooltipEvent || TooltipEvent.LEAVE,
78+
rectActiveElement: tooltipAction.rectActiveElement || new DOMRect()
79+
},
80+
...(tooltipAction.tooltipContent && { tooltipContent: tooltipAction.tooltipContent })
81+
};
82+
setTooltipAction(updateTooltipAction);
83+
},
84+
[setTooltipAction]
85+
);
86+
5887
useEffect(() => {
5988
createSectionFields();
6089
// Run every time this section changes
@@ -71,10 +100,11 @@ export const Section: FC<SectionProps> = ({ section, onFieldClick }) => {
71100
<div
72101
className={cx(`${baseClassName}`, { hasTable: hasTable(html) })}
73102
ref={sectionNode}
74-
onMouseMove={mouseMoveListener(hoveredField, setHoveredField)}
75-
onMouseLeave={mouseLeaveListener(hoveredField, setHoveredField)}
103+
onMouseMove={mouseMoveListener(hoveredField, setHoveredField, onTooltipAction, facetInfoMap)}
104+
onMouseLeave={mouseLeaveListener(hoveredField, setHoveredField, onTooltipAction)}
76105
onClick={mouseClickListener(onFieldClick)}
77106
>
107+
<TooltipHighlight parentDiv={sectionNode} tooltipAction={tooltipAction} />
78108
<div className="fields" ref={fieldsNode} />
79109
<div
80110
className="content htmlReset htmlOverride"
@@ -88,7 +118,9 @@ export const Section: FC<SectionProps> = ({ section, onFieldClick }) => {
88118

89119
function mouseMoveListener(
90120
hoveredField: HTMLElement | null,
91-
setHoveredField: Dispatch<SetStateAction<HTMLElement | null>>
121+
setHoveredField: Dispatch<SetStateAction<HTMLElement | null>>,
122+
onTooltipShow: OnTooltipShowFn,
123+
facetInfoMap: FacetInfoMap
92124
) {
93125
return function _mouseMoveListener(event: MouseEvent): void {
94126
const fieldRect = elementFromPoint(
@@ -102,6 +134,7 @@ function mouseMoveListener(
102134
hoveredField.classList.remove('hover');
103135
setHoveredField(null);
104136
document.body.style.cursor = 'initial';
137+
onTooltipShow({ tooltipEvent: TooltipEvent.LEAVE });
105138
}
106139
return;
107140
}
@@ -110,10 +143,22 @@ function mouseMoveListener(
110143
if (hoveredField !== fieldNode) {
111144
if (hoveredField) {
112145
hoveredField.classList.remove('hover');
146+
onTooltipShow({ tooltipEvent: TooltipEvent.LEAVE });
113147
}
114148
setHoveredField(fieldNode as HTMLElement);
115149
if (fieldNode) {
116150
fieldNode.classList.add('hover');
151+
const enrichValue = fieldNode.getAttribute('data-field-value') || '';
152+
const enrichFacetId = fieldNode.getAttribute('data-field-type') || '';
153+
const tooltipContent = calcToolTipContent(facetInfoMap, enrichFacetId, enrichValue);
154+
const fieldNodeContent = fieldNode?.firstElementChild;
155+
onTooltipShow({
156+
...{
157+
tooltipEvent: TooltipEvent.ENTER,
158+
rectActiveElement: fieldNodeContent?.getBoundingClientRect()
159+
},
160+
...(tooltipContent && { tooltipContent: tooltipContent })
161+
});
117162
}
118163
document.body.style.cursor = 'pointer';
119164
}
@@ -122,13 +167,15 @@ function mouseMoveListener(
122167

123168
function mouseLeaveListener(
124169
hoveredField: HTMLElement | null,
125-
setHoveredField: Dispatch<SetStateAction<HTMLElement | null>>
170+
setHoveredField: Dispatch<SetStateAction<HTMLElement | null>>,
171+
onTooltipShow: OnTooltipShowFn
126172
) {
127173
return function _mouseLeaveListener(): void {
128174
if (hoveredField) {
129175
hoveredField.classList.remove('hover');
130176
setHoveredField(null);
131177
document.body.style.cursor = 'initial';
178+
onTooltipShow({ tooltipEvent: TooltipEvent.LEAVE });
132179
}
133180
};
134181
}
@@ -191,6 +238,7 @@ function renderSectionFields(
191238
for (const field of section.enrichments) {
192239
try {
193240
const fieldType = field.__type;
241+
const fieldValue = field.value || '';
194242
const { begin, end } = field.location;
195243

196244
const offsets = findOffsetInDOM(contentNode, begin, end);
@@ -199,6 +247,7 @@ function renderSectionFields(
199247
fragment,
200248
parentRect: sectionRect as DOMRect,
201249
fieldType,
250+
fieldValue,
202251
fieldId: getId(field as unknown as Item),
203252
...offsets
204253
});

packages/discovery-react-components/src/components/CIDocument/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ export interface RelationItem extends Item {
2828
export interface Enrichment {
2929
__type: string;
3030
location: Location;
31+
value?: string;
3132
}
3233

3334
export interface SectionType {

packages/discovery-react-components/src/components/DocumentPreview/components/HtmlView/HtmlView.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,7 @@ export const HtmlView = forwardRef<any, Props>(
173173
fragment,
174174
parentRect,
175175
fieldType: 'highlight',
176+
fieldValue: '',
176177
fieldId: begin.toString(),
177178
...offsets
178179
});

packages/discovery-react-components/src/components/DocumentPreview/components/PdfHighlight/PdfHighlight.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -150,9 +150,11 @@ const Highlight: FC<{
150150
// Create tooltip content to display
151151
const tooltipContent = calcToolTipContent(facetInfoMap, enrichFacetId, enrichValue);
152152
onTooltipShow({
153-
tooltipEvent: TooltipEvent.ENTER,
154-
rectActiveElement: divEle?.getBoundingClientRect(),
155-
tooltipContent
153+
...{
154+
tooltipEvent: TooltipEvent.ENTER,
155+
rectActiveElement: divEle?.getBoundingClientRect()
156+
},
157+
...(tooltipContent && { tooltipContent: tooltipContent })
156158
});
157159
};
158160

packages/discovery-react-components/src/components/DocumentPreview/components/SimpleDocument/SimpleDocument.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,7 @@ export const SimpleDocument = forwardRef<any, Props>(
167167
fragment,
168168
parentRect,
169169
fieldType: 'passage',
170+
fieldValue: '',
170171
fieldId: begin.toString(),
171172
...offsets
172173
});

packages/discovery-react-components/src/components/TooltipHighlight/TooltipHighlight.tsx

Lines changed: 26 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ import { defaultMessages } from 'components/TooltipHighlight/messages';
99
// TooltipInfo is the internal state of the TooltipHightlight
1010
interface TooltipInfo {
1111
rectTooltipArea: DOMRect;
12-
tooltipContent: JSX.Element;
1312
isOpen: boolean;
13+
tooltipContent?: JSX.Element;
1414
}
1515

1616
type Props = {
@@ -21,6 +21,12 @@ type Props = {
2121
tooltipAction: TooltipAction;
2222
};
2323

24+
// Longer strings will be truncated with ellipsis in the middle of the term.
25+
// This way a user sees the start and end of the string and can map it to the document view
26+
const MAX_CONTENT_LENGTH = 30;
27+
const ELLIPSIS = '...';
28+
const KEYWORDS_CATEGORY = 'Keywords';
29+
2430
const baseTooltipPlaceContent = `${settings.prefix}--tooltip-place-content`;
2531
const baseTooltipCustomContent = `${settings.prefix}--tooltip-custom-content`;
2632
const baseTooltipContentHeader = `${settings.prefix}--tooltip-content-header`;
@@ -31,7 +37,6 @@ const baseTooltipContentCellBuffer = `${settings.prefix}--tooltip-content-cell-b
3137
export const TooltipHighlight: FC<Props> = ({ parentDiv, tooltipAction }) => {
3238
const [tooltipInfo, setTooltipInfo] = useState<TooltipInfo>({
3339
rectTooltipArea: new DOMRect(),
34-
tooltipContent: <div></div>,
3540
isOpen: false
3641
});
3742

@@ -47,9 +52,11 @@ export const TooltipHighlight: FC<Props> = ({ parentDiv, tooltipAction }) => {
4752
clickRect?.height
4853
);
4954
const tooltipUpdate = {
50-
rectTooltipArea: tooltipRect,
51-
tooltipContent: tooltipAction.tooltipContent || <div></div>,
52-
isOpen: !!tooltipAction.tooltipContent && isOpen
55+
...{
56+
rectTooltipArea: tooltipRect,
57+
isOpen: !!tooltipAction.tooltipContent && isOpen
58+
},
59+
...(tooltipAction.tooltipContent && { tooltipContent: tooltipAction.tooltipContent })
5360
};
5461
setTooltipInfo(tooltipUpdate);
5562
}, [tooltipAction, setTooltipInfo, parentDiv]);
@@ -96,6 +103,12 @@ export function calcToolTipContent(
96103
if (facetInfoMap[facetId]) {
97104
enrichColor = facetInfoMap[facetId].color;
98105
enrichFacetDisplayname = facetInfoMap[facetId].displayName;
106+
if (
107+
enrichFacetDisplayname.localeCompare(enrichValue, undefined, { sensitivity: 'base' }) == 0
108+
) {
109+
// This case applies to keywords
110+
enrichFacetDisplayname = KEYWORDS_CATEGORY;
111+
}
99112
// Will have multiple entries after overlapping is implemented
100113
tableContent.push({
101114
enrichColor: enrichColor,
@@ -114,28 +127,27 @@ export function calcToolTipContent(
114127
</div>
115128
<table>
116129
{tableContent.map((oneRow, index) => {
117-
let rowBorderClass = {};
118-
if (index < tableContent.length - 1) {
119-
rowBorderClass = {
120-
borderBottom: `1px solid #7A7979`
121-
};
122-
}
130+
const isDivider = index < tableContent.length - 1;
131+
const classObj = {
132+
[`${baseTooltipContentCell}`]: true,
133+
[`${settings.prefix}--tooltip-content-divider`]: isDivider
134+
};
123135
return (
124136
<tr>
125-
<td className={cx(baseTooltipContentCell)} style={rowBorderClass}>
137+
<td className={cx(classObj)}>
126138
<div
127139
className={cx(baseTooltipBoxColor)}
128140
style={{
129141
backgroundColor: oneRow.enrichColor
130142
}}
131143
/>
132144
</td>
133-
<td className={cx(baseTooltipContentCell)} style={rowBorderClass}>
145+
<td className={cx(classObj)}>
134146
<span className={cx(baseTooltipContentCellBuffer)}>
135147
{oneRow.enrichFacetDisplayname}
136148
</span>
137149
</td>
138-
<td className={cx(baseTooltipContentCell)} style={rowBorderClass}>
150+
<td className={cx(classObj)}>
139151
{oneRow.enrichValue &&
140152
oneRow.enrichValue.localeCompare(oneRow.enrichFacetDisplayname) !== 0 &&
141153
`${oneRow.enrichValue}`}
@@ -151,8 +163,6 @@ export function calcToolTipContent(
151163
}
152164

153165
function ellipsisMiddle(text: string) {
154-
const MAX_CONTENT_LENGTH = 30; // even number
155-
const ELLIPSIS = '...';
156166
let ellipsisText = text;
157167
// account for the new string being extended by the ellipsis
158168
if (text.length > MAX_CONTENT_LENGTH + ELLIPSIS.length) {

packages/discovery-react-components/src/utils/document/documentUtils.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ interface CreateFieldRectsProps {
117117
fragment: DocumentFragment;
118118
parentRect: DOMRect;
119119
fieldType: string;
120+
fieldValue: string;
120121
fieldId: string;
121122
beginTextNode: Text;
122123
beginOffset: number;
@@ -130,6 +131,7 @@ interface CreateFieldRectsProps {
130131
* @param args.fragment DocumentFragment or Node in which to create field rects
131132
* @param args.parentRect dimensions of parent of field rects
132133
* @param args.fieldType type string for field rects
134+
* @param args.fieldValue displayed in tooltip
133135
* @param args.fieldId id string for field rects
134136
* @param args.beginTextNode
135137
* @param args.beginOffset
@@ -140,6 +142,7 @@ export function createFieldRects({
140142
fragment,
141143
parentRect,
142144
fieldType,
145+
fieldValue,
143146
fieldId,
144147
beginTextNode,
145148
beginOffset,
@@ -150,6 +153,7 @@ export function createFieldRects({
150153
const fieldNode = document.createElement('div');
151154
fieldNode.className = 'field';
152155
fieldNode.dataset.fieldType = fieldType;
156+
fieldNode.dataset.fieldValue = fieldValue;
153157
fieldNode.dataset.fieldId = fieldId;
154158
fieldNode.setAttribute('data-testid', `field-${fieldId}`);
155159
fragment.appendChild(fieldNode);

0 commit comments

Comments
 (0)