Skip to content

Commit

Permalink
feat: Text view, tooltip for enrichments (#530)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>
  • Loading branch information
dorianmiller and drdorianm authored Sep 8, 2023
1 parent 6e8821f commit 8033a29
Show file tree
Hide file tree
Showing 10 changed files with 110 additions and 30 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import Section, { OnFieldClickFn } from '../Section/Section';
import VirtualScroll from '../VirtualScroll/VirtualScroll';
import { defaultTheme, Theme } from 'utils/theme';
import { SectionType, ItemMap, HighlightWithMeta } from 'components/CIDocument/types';
import { FacetInfoMap } from '../../../DocumentPreview/types';
import { getId as getLocationId } from 'utils/document/idUtils';

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

Expand All @@ -50,6 +52,7 @@ const CIDocumentContent: FC<CIDocumentContentProps> = ({
documentId = '',
onItemClick = (): void => {},
combinedHighlights,
facetInfoMap,
activeColor
}) => {
const virtualScrollRef = useRef<any>();
Expand All @@ -65,6 +68,7 @@ const CIDocumentContent: FC<CIDocumentContentProps> = ({
}, [activeIds, activeMetadataIds, activePartIds, itemMap]);

const loading = !sections || sections.length === 0;

return (
<div className={cx(baseClassName, className, { skeleton: loading })}>
{loading ? (
Expand All @@ -73,7 +77,7 @@ const CIDocumentContent: FC<CIDocumentContentProps> = ({
<>
<style data-testid="style">{docStyles}</style>
{!!combinedHighlights && combinedHighlights.length > 0 && (
<style>{highlightColoringFullArray(combinedHighlights)}</style>
<style>{highlightColoringFullArray(combinedHighlights).join('\n')}</style>
)}
{(!combinedHighlights || combinedHighlights.length <= 0) && (
<style>
Expand Down Expand Up @@ -124,7 +128,11 @@ const CIDocumentContent: FC<CIDocumentContentProps> = ({
ref={virtualScrollRef}
>
{({ index }): ReactElement => (
<Section section={sections[index]} onFieldClick={onItemClick} />
<Section
section={sections[index]}
onFieldClick={onItemClick}
facetInfoMap={facetInfoMap}
/>
)}
</VirtualScroll>
)}
Expand All @@ -144,9 +152,8 @@ function createStyleRules(idList: string[], rules: string[]): string {
function highlightColoringFullArray(combinedHighlightsWithMeta: HighlightWithMeta[]) {
return combinedHighlightsWithMeta.map(highlightWithMeta => {
const locationId = getHighlightLocationId(highlightWithMeta);
// Set z-index to -1 in order to push non-active fields back
const rules = `.${baseClassName} .field[data-field-id="${locationId}"] > * {background-color: ${highlightWithMeta.color}; z-index: -1;}`;
return <style>{rules}</style>;
const rules = `.${baseClassName} .field[data-field-id="${locationId}"] > * {background-color: ${highlightWithMeta.color}; border: 2px solid ${highlightWithMeta.color};}`;
return rules;
});
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ import React, {
MouseEvent,
useEffect,
useRef,
useState
useState,
useCallback
} from 'react';
import cx from 'classnames';
import debounce from 'debounce';
Expand All @@ -21,6 +22,9 @@ import { createFieldRects, findOffsetInDOM } from 'utils/document/documentUtils'
import { clearNodeChildren } from 'utils/dom';
import elementFromPoint from 'components/CIDocument/utils/elementFromPoint';
import { SectionType, Field, Item } from 'components/CIDocument/types';
import { FacetInfoMap } from '../../../DocumentPreview/types';
import { TooltipAction, TooltipEvent, OnTooltipShowFn } from '../../../TooltipHighlight/types';
import { TooltipHighlight, calcToolTipContent } from '../../../TooltipHighlight/TooltipHighlight';

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

Expand All @@ -35,9 +39,14 @@ interface SectionProps {
* Function to call when a field is clicked
*/
onFieldClick?: OnFieldClickFn;

/**
* Meta-data on facets
*/
facetInfoMap?: FacetInfoMap;
}

export const Section: FC<SectionProps> = ({ section, onFieldClick }) => {
export const Section: FC<SectionProps> = ({ section, onFieldClick, facetInfoMap = {} }) => {
const { html } = section;

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

const [tooltipAction, setTooltipAction] = useState<TooltipAction>({
tooltipEvent: TooltipEvent.LEAVE,
rectActiveElement: new DOMRect(),
tooltipContent: <div></div>
});

const onTooltipAction = useCallback(
(tooltipAction: TooltipAction) => {
const updateTooltipAction: TooltipAction = {
...{
tooltipEvent: tooltipAction.tooltipEvent || TooltipEvent.LEAVE,
rectActiveElement: tooltipAction.rectActiveElement || new DOMRect()
},
...(tooltipAction.tooltipContent && { tooltipContent: tooltipAction.tooltipContent })
};
setTooltipAction(updateTooltipAction);
},
[setTooltipAction]
);

useEffect(() => {
createSectionFields();
// Run every time this section changes
Expand All @@ -71,10 +100,11 @@ export const Section: FC<SectionProps> = ({ section, onFieldClick }) => {
<div
className={cx(`${baseClassName}`, { hasTable: hasTable(html) })}
ref={sectionNode}
onMouseMove={mouseMoveListener(hoveredField, setHoveredField)}
onMouseLeave={mouseLeaveListener(hoveredField, setHoveredField)}
onMouseMove={mouseMoveListener(hoveredField, setHoveredField, onTooltipAction, facetInfoMap)}
onMouseLeave={mouseLeaveListener(hoveredField, setHoveredField, onTooltipAction)}
onClick={mouseClickListener(onFieldClick)}
>
<TooltipHighlight parentDiv={sectionNode} tooltipAction={tooltipAction} />
<div className="fields" ref={fieldsNode} />
<div
className="content htmlReset htmlOverride"
Expand All @@ -88,7 +118,9 @@ export const Section: FC<SectionProps> = ({ section, onFieldClick }) => {

function mouseMoveListener(
hoveredField: HTMLElement | null,
setHoveredField: Dispatch<SetStateAction<HTMLElement | null>>
setHoveredField: Dispatch<SetStateAction<HTMLElement | null>>,
onTooltipShow: OnTooltipShowFn,
facetInfoMap: FacetInfoMap
) {
return function _mouseMoveListener(event: MouseEvent): void {
const fieldRect = elementFromPoint(
Expand All @@ -102,6 +134,7 @@ function mouseMoveListener(
hoveredField.classList.remove('hover');
setHoveredField(null);
document.body.style.cursor = 'initial';
onTooltipShow({ tooltipEvent: TooltipEvent.LEAVE });
}
return;
}
Expand All @@ -110,10 +143,22 @@ function mouseMoveListener(
if (hoveredField !== fieldNode) {
if (hoveredField) {
hoveredField.classList.remove('hover');
onTooltipShow({ tooltipEvent: TooltipEvent.LEAVE });
}
setHoveredField(fieldNode as HTMLElement);
if (fieldNode) {
fieldNode.classList.add('hover');
const enrichValue = fieldNode.getAttribute('data-field-value') || '';
const enrichFacetId = fieldNode.getAttribute('data-field-type') || '';
const tooltipContent = calcToolTipContent(facetInfoMap, enrichFacetId, enrichValue);
const fieldNodeContent = fieldNode?.firstElementChild;
onTooltipShow({
...{
tooltipEvent: TooltipEvent.ENTER,
rectActiveElement: fieldNodeContent?.getBoundingClientRect()
},
...(tooltipContent && { tooltipContent: tooltipContent })
});
}
document.body.style.cursor = 'pointer';
}
Expand All @@ -122,13 +167,15 @@ function mouseMoveListener(

function mouseLeaveListener(
hoveredField: HTMLElement | null,
setHoveredField: Dispatch<SetStateAction<HTMLElement | null>>
setHoveredField: Dispatch<SetStateAction<HTMLElement | null>>,
onTooltipShow: OnTooltipShowFn
) {
return function _mouseLeaveListener(): void {
if (hoveredField) {
hoveredField.classList.remove('hover');
setHoveredField(null);
document.body.style.cursor = 'initial';
onTooltipShow({ tooltipEvent: TooltipEvent.LEAVE });
}
};
}
Expand Down Expand Up @@ -191,6 +238,7 @@ function renderSectionFields(
for (const field of section.enrichments) {
try {
const fieldType = field.__type;
const fieldValue = field.value || '';
const { begin, end } = field.location;

const offsets = findOffsetInDOM(contentNode, begin, end);
Expand All @@ -199,6 +247,7 @@ function renderSectionFields(
fragment,
parentRect: sectionRect as DOMRect,
fieldType,
fieldValue,
fieldId: getId(field as unknown as Item),
...offsets
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export interface RelationItem extends Item {
export interface Enrichment {
__type: string;
location: Location;
value?: string;
}

export interface SectionType {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,7 @@ export const HtmlView = forwardRef<any, Props>(
fragment,
parentRect,
fieldType: 'highlight',
fieldValue: '',
fieldId: begin.toString(),
...offsets
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -150,9 +150,11 @@ const Highlight: FC<{
// Create tooltip content to display
const tooltipContent = calcToolTipContent(facetInfoMap, enrichFacetId, enrichValue);
onTooltipShow({
tooltipEvent: TooltipEvent.ENTER,
rectActiveElement: divEle?.getBoundingClientRect(),
tooltipContent
...{
tooltipEvent: TooltipEvent.ENTER,
rectActiveElement: divEle?.getBoundingClientRect()
},
...(tooltipContent && { tooltipContent: tooltipContent })
});
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,7 @@ export const SimpleDocument = forwardRef<any, Props>(
fragment,
parentRect,
fieldType: 'passage',
fieldValue: '',
fieldId: begin.toString(),
...offsets
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ import { defaultMessages } from 'components/TooltipHighlight/messages';
// TooltipInfo is the internal state of the TooltipHightlight
interface TooltipInfo {
rectTooltipArea: DOMRect;
tooltipContent: JSX.Element;
isOpen: boolean;
tooltipContent?: JSX.Element;
}

type Props = {
Expand All @@ -21,6 +21,12 @@ type Props = {
tooltipAction: TooltipAction;
};

// Longer strings will be truncated with ellipsis in the middle of the term.
// This way a user sees the start and end of the string and can map it to the document view
const MAX_CONTENT_LENGTH = 30;
const ELLIPSIS = '...';
const KEYWORDS_CATEGORY = 'Keywords';

const baseTooltipPlaceContent = `${settings.prefix}--tooltip-place-content`;
const baseTooltipCustomContent = `${settings.prefix}--tooltip-custom-content`;
const baseTooltipContentHeader = `${settings.prefix}--tooltip-content-header`;
Expand All @@ -31,7 +37,6 @@ const baseTooltipContentCellBuffer = `${settings.prefix}--tooltip-content-cell-b
export const TooltipHighlight: FC<Props> = ({ parentDiv, tooltipAction }) => {
const [tooltipInfo, setTooltipInfo] = useState<TooltipInfo>({
rectTooltipArea: new DOMRect(),
tooltipContent: <div></div>,
isOpen: false
});

Expand All @@ -47,9 +52,11 @@ export const TooltipHighlight: FC<Props> = ({ parentDiv, tooltipAction }) => {
clickRect?.height
);
const tooltipUpdate = {
rectTooltipArea: tooltipRect,
tooltipContent: tooltipAction.tooltipContent || <div></div>,
isOpen: !!tooltipAction.tooltipContent && isOpen
...{
rectTooltipArea: tooltipRect,
isOpen: !!tooltipAction.tooltipContent && isOpen
},
...(tooltipAction.tooltipContent && { tooltipContent: tooltipAction.tooltipContent })
};
setTooltipInfo(tooltipUpdate);
}, [tooltipAction, setTooltipInfo, parentDiv]);
Expand Down Expand Up @@ -96,6 +103,12 @@ export function calcToolTipContent(
if (facetInfoMap[facetId]) {
enrichColor = facetInfoMap[facetId].color;
enrichFacetDisplayname = facetInfoMap[facetId].displayName;
if (
enrichFacetDisplayname.localeCompare(enrichValue, undefined, { sensitivity: 'base' }) == 0
) {
// This case applies to keywords
enrichFacetDisplayname = KEYWORDS_CATEGORY;
}
// Will have multiple entries after overlapping is implemented
tableContent.push({
enrichColor: enrichColor,
Expand All @@ -114,28 +127,27 @@ export function calcToolTipContent(
</div>
<table>
{tableContent.map((oneRow, index) => {
let rowBorderClass = {};
if (index < tableContent.length - 1) {
rowBorderClass = {
borderBottom: `1px solid #7A7979`
};
}
const isDivider = index < tableContent.length - 1;
const classObj = {
[`${baseTooltipContentCell}`]: true,
[`${settings.prefix}--tooltip-content-divider`]: isDivider
};
return (
<tr>
<td className={cx(baseTooltipContentCell)} style={rowBorderClass}>
<td className={cx(classObj)}>
<div
className={cx(baseTooltipBoxColor)}
style={{
backgroundColor: oneRow.enrichColor
}}
/>
</td>
<td className={cx(baseTooltipContentCell)} style={rowBorderClass}>
<td className={cx(classObj)}>
<span className={cx(baseTooltipContentCellBuffer)}>
{oneRow.enrichFacetDisplayname}
</span>
</td>
<td className={cx(baseTooltipContentCell)} style={rowBorderClass}>
<td className={cx(classObj)}>
{oneRow.enrichValue &&
oneRow.enrichValue.localeCompare(oneRow.enrichFacetDisplayname) !== 0 &&
`${oneRow.enrichValue}`}
Expand All @@ -151,8 +163,6 @@ export function calcToolTipContent(
}

function ellipsisMiddle(text: string) {
const MAX_CONTENT_LENGTH = 30; // even number
const ELLIPSIS = '...';
let ellipsisText = text;
// account for the new string being extended by the ellipsis
if (text.length > MAX_CONTENT_LENGTH + ELLIPSIS.length) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ interface CreateFieldRectsProps {
fragment: DocumentFragment;
parentRect: DOMRect;
fieldType: string;
fieldValue: string;
fieldId: string;
beginTextNode: Text;
beginOffset: number;
Expand All @@ -130,6 +131,7 @@ interface CreateFieldRectsProps {
* @param args.fragment DocumentFragment or Node in which to create field rects
* @param args.parentRect dimensions of parent of field rects
* @param args.fieldType type string for field rects
* @param args.fieldValue displayed in tooltip
* @param args.fieldId id string for field rects
* @param args.beginTextNode
* @param args.beginOffset
Expand All @@ -140,6 +142,7 @@ export function createFieldRects({
fragment,
parentRect,
fieldType,
fieldValue,
fieldId,
beginTextNode,
beginOffset,
Expand All @@ -150,6 +153,7 @@ export function createFieldRects({
const fieldNode = document.createElement('div');
fieldNode.className = 'field';
fieldNode.dataset.fieldType = fieldType;
fieldNode.dataset.fieldValue = fieldValue;
fieldNode.dataset.fieldId = fieldId;
fieldNode.setAttribute('data-testid', `field-${fieldId}`);
fragment.appendChild(fieldNode);
Expand Down
Loading

0 comments on commit 8033a29

Please sign in to comment.