Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 25 additions & 11 deletions src/app/dim-ui/PressTip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ interface Props {
* constructing the tree until the tooltip is shown.
*/
tooltip: React.ReactNode | (() => React.ReactNode);
/**
* Whether the presstip should react to events or not.
*/
disabled?: boolean;
/**
* The children of this component define the content that will trigger the tooltip.
*/
Expand All @@ -41,6 +45,7 @@ interface Props {

type ControlProps = Props &
React.HTMLAttributes<HTMLDivElement> & {
events: React.HTMLAttributes<HTMLDivElement>;
open: boolean;
triggerRef: React.RefObject<HTMLDivElement>;
};
Expand All @@ -65,10 +70,12 @@ type ControlProps = Props &
function Control({
tooltip,
open,
disabled,
triggerRef,
children,
elementType: Component = 'div',
className,
events,
...rest
}: ControlProps) {
const tooltipContents = useRef<HTMLDivElement>(null);
Expand All @@ -94,8 +101,10 @@ function Control({
// TODO: or use framer motion layout animations?
return (
<Component ref={triggerRef} className={clsx(styles.control, className)} {...rest}>
{children}
<div {...events}>{children}</div>
{open &&
!disabled &&
tooltip &&
ReactDOM.createPortal(
<div className={styles.tooltip} ref={tooltipContents}>
<div className={styles.content}>{_.isFunction(tooltip) ? tooltip() : tooltip}</div>
Expand Down Expand Up @@ -142,14 +151,19 @@ function PressTip(props: Props) {
timer.current = 0;
}, []);

const hover = useCallback((e: React.MouseEvent | React.TouchEvent | TouchEvent) => {
e.preventDefault();
clearTimeout(timer.current);
timer.current = window.setTimeout(() => {
setOpen(true);
}, hoverDelay);
touchStartTime.current = performance.now();
}, []);
const hover = useCallback(
(e: React.MouseEvent | React.TouchEvent | TouchEvent) => {
if (!props.disabled) {
e.preventDefault();
clearTimeout(timer.current);
timer.current = window.setTimeout(() => {
setOpen(true);
}, hoverDelay);
touchStartTime.current = performance.now();
}
},
[props.disabled]
);

// Stop the hover timer when the component unmounts
useEffect(() => () => clearTimeout(timer.current), []);
Expand Down Expand Up @@ -181,12 +195,12 @@ function PressTip(props: Props) {
onClick: absorbClick,
}
: {
onMouseEnter: hover,
onMouseOver: hover,
onMouseUp: closeToolTip,
onMouseLeave: closeToolTip,
};

return <Control open={open} triggerRef={ref} {...events} {...props} />;
return <Control open={open} triggerRef={ref} events={events} {...props} />;
}

export default PressTip;
16 changes: 12 additions & 4 deletions src/app/infuse/InfusionFinder.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -229,15 +229,23 @@ export default function InfusionFinder() {
<div className="infusionControls">
<div className="infuseTopRow">
<div className="infusionEquation">
{effectiveTarget ? <ConnectedInventoryItem item={effectiveTarget} /> : missingItem}
{effectiveTarget ? (
<ConnectedInventoryItem item={effectiveTarget} includeTooltip />
) : (
missingItem
)}
<div className="icon">
<AppIcon icon={plusIcon} />
</div>
{effectiveSource ? <ConnectedInventoryItem item={effectiveSource} /> : missingItem}
{effectiveSource ? (
<ConnectedInventoryItem item={effectiveSource} includeTooltip />
) : (
missingItem
)}
<div className="icon">
<AppIcon icon={faEquals} />
</div>
{result ? <ConnectedInventoryItem item={result} /> : missingItem}
{result ? <ConnectedInventoryItem item={result} includeTooltip /> : missingItem}
</div>
<div className="infuseActions">
<button type="button" className="dim-button" onClick={switchDirection}>
Expand Down Expand Up @@ -269,7 +277,7 @@ export default function InfusionFinder() {
className={clsx({ 'infuse-selected': item === target })}
onClick={() => selectItem(item)}
>
<ConnectedInventoryItem item={item} />
<ConnectedInventoryItem includeTooltip item={item} />
</div>
);

Expand Down
3 changes: 3 additions & 0 deletions src/app/inventory/ConnectedInventoryItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ interface ProvidedProps {
id?: string; // defaults to item.index - id is typically used for `itemPop`
allowFilter?: boolean;
ignoreSelectedPerks?: boolean;
includeTooltip?: boolean;
innerRef?: React.Ref<HTMLDivElement>;
onClick?(e: React.MouseEvent): void;
onShiftClick?(e: React.MouseEvent): void;
Expand Down Expand Up @@ -71,6 +72,7 @@ function ConnectedInventoryItem({
onDoubleClick,
searchHidden,
ignoreSelectedPerks,
includeTooltip,
innerRef,
}: Props) {
return (
Expand All @@ -86,6 +88,7 @@ function ConnectedInventoryItem({
onDoubleClick={onDoubleClick}
searchHidden={searchHidden}
ignoreSelectedPerks={ignoreSelectedPerks}
includeTooltip={includeTooltip}
innerRef={innerRef}
/>
);
Expand Down
16 changes: 14 additions & 2 deletions src/app/inventory/InventoryItem.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import PressTip from 'app/dim-ui/PressTip';
import clsx from 'clsx';
import React, { useMemo } from 'react';
import BungieImage from '../dim-ui/BungieImage';
Expand All @@ -9,6 +10,7 @@ import { TagValue } from './dim-item-info';
import styles from './InventoryItem.m.scss';
import { DimItem } from './item-types';
import ItemIcon from './ItemIcon';
import { DimItemTooltip } from './ItemTooltip';
import NewItemIndicator from './NewItemIndicator';
import { selectedSubclassPath } from './subclass';
import TagIcon from './TagIcon';
Expand All @@ -28,6 +30,8 @@ interface Props {
wishlistRoll?: InventoryWishListRoll;
/** Don't show information that relates to currently selected perks (only used for subclasses currently) */
ignoreSelectedPerks?: boolean;
/** Show a tooltip summarizing the item for when a click on the item has other effects than bringing up item popup */
includeTooltip?: boolean;
innerRef?: React.Ref<HTMLDivElement>;
/** TODO: item locked needs to be passed in */
onClick?(e: React.MouseEvent): void;
Expand All @@ -43,6 +47,7 @@ export default function InventoryItem({
notes,
searchHidden,
wishlistRoll,
includeTooltip,
ignoreSelectedPerks,
onClick,
onShiftClick,
Expand Down Expand Up @@ -115,16 +120,23 @@ export default function InventoryItem({
);
}, [isNew, item, notes, subclassPath, tag, wishlistRoll]);

return (
const tooltip = includeTooltip ?? false;
const inner = (
<div
id={id || item.index}
onClick={enhancedOnClick}
onDoubleClick={onDoubleClick}
title={`${item.name}\n${subtitle}`}
title={!tooltip ? `${item.name}\n${subtitle}` : undefined}
className={itemStyles}
ref={innerRef}
>
{contents}
</div>
);

return (
<PressTip disabled={!tooltip} tooltip={() => <DimItemTooltip item={item} />}>
{inner}
</PressTip>
);
}
40 changes: 40 additions & 0 deletions src/app/inventory/ItemTooltip.m.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
@import '../variables.scss';

.perks {
display: flex;
flex-flow: column;
margin: 4px 0;

border-left: 2px solid #888;
padding-left: 3px;

> div {
display: flex;
flex-flow: row;
align-items: center;
}

img {
height: 24px;
width: 24px;
}
}

.perkSelected {
font-weight: bold;
}

.notes {
margin-left: 4px;
}

.note {
margin-left: 2px;
}

.stats {
margin: 4px 0 0 0;
:global(.stat) {
line-height: 12px;
}
}
11 changes: 11 additions & 0 deletions src/app/inventory/ItemTooltip.m.scss.d.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

91 changes: 91 additions & 0 deletions src/app/inventory/ItemTooltip.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import BungieImage from 'app/dim-ui/BungieImage';
import { DimItem, DimStat } from 'app/inventory/item-types';
import { DefItemIcon } from 'app/inventory/ItemIcon';
import { useD2Definitions } from 'app/manifest/selectors';
import { AppIcon, stickyNoteIcon } from 'app/shell/icons';
import { isKillTrackerSocket } from 'app/utils/item-utils';
import { DestinyInventoryItemDefinition } from 'bungie-api-ts/destiny2';
import clsx from 'clsx';
import _ from 'lodash';
import React from 'react';
import { useSelector } from 'react-redux';
import { itemNoteSelector } from './dim-item-info';
import styles from './ItemTooltip.m.scss';

export function DimItemTooltip({ item }: { item: DimItem }) {
const defs = useD2Definitions()!;
const itemDef = defs.InventoryItem.get(item.hash);
const savedNotes = useSelector(itemNoteSelector(item));

if (item.bucket.sort === 'Weapons' && item.sockets) {
const perkSockets = item.sockets?.allSockets.filter((s) => s.isPerk && !isKillTrackerSocket(s));
const sockets = _.takeRight(perkSockets, 2);

const contents = sockets.map((socket) => (
<div key={socket.socketIndex} className={clsx(styles.perks)}>
{socket.plugOptions.map((p) => (
<div
key={p.plugDef.hash}
className={clsx(undefined, {
[styles.perkSelected]:
socket.isPerk && socket.plugOptions.length > 1 && p === socket.plugged,
})}
data-perk-name={p.plugDef.displayProperties.name}
>
<DefItemIcon itemDef={p.plugDef} borderless={true} /> {p.plugDef.displayProperties.name}
</div>
))}
</div>
));

return <Tooltip def={itemDef} notes={savedNotes} contents={contents} />;
} else if (item.bucket.sort === 'Armor' && item.stats?.length) {
const renderStat = (stat: DimStat) => (
<div key={stat.statHash} className="stat">
{stat.displayProperties.hasIcon ? (
<span title={stat.displayProperties.name}>
<BungieImage src={stat.displayProperties.icon} />
</span>
) : (
stat.displayProperties.name + ': '
)}
{stat.base}
</div>
);

const contents = (
<div className={clsx(styles.stats, 'stat-bars', 'destiny2')}>
<div className="stat-row">{item.stats?.filter((s) => s.statHash > 0).map(renderStat)}</div>
<div className="stat-row">{item.stats?.filter((s) => s.statHash < 0).map(renderStat)}</div>
</div>
);

return <Tooltip def={itemDef} notes={savedNotes} contents={contents} />;
} else {
return <Tooltip def={itemDef} notes={savedNotes} contents={undefined} />;
}
}

function Tooltip({
def,
notes,
contents,
}: {
def: DestinyInventoryItemDefinition;
notes?: string;
contents?: React.ReactNode;
}) {
return (
<>
<h2>{def.displayProperties.name}</h2>
{def.itemTypeDisplayName && <h3>{def.itemTypeDisplayName}</h3>}
{notes && (
<div className={clsx(styles.notes)}>
<AppIcon icon={stickyNoteIcon} />
<span className={clsx(styles.note)}>{notes}</span>
</div>
)}
{contents}
</>
);
}
4 changes: 4 additions & 0 deletions src/app/inventory/StoreInventoryItem.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { compareOpenSelector } from 'app/compare/selectors';
import { useThunkDispatch } from 'app/store/thunk-dispatch';
import React from 'react';
import { useSelector } from 'react-redux';
import ConnectedInventoryItem from './ConnectedInventoryItem';
import DraggableInventoryItem from './DraggableInventoryItem';
import { DimItem } from './item-types';
Expand All @@ -15,6 +17,7 @@ interface Props {
*/
export default function StoreInventoryItem({ item }: Props) {
const dispatch = useThunkDispatch();
const compareOpen = useSelector(compareOpenSelector);
const doubleClicked = (e: React.MouseEvent) => {
dispatch(moveItemToCurrentStore(item, e));
};
Expand All @@ -27,6 +30,7 @@ export default function StoreInventoryItem({ item }: Props) {
item={item}
allowFilter={true}
innerRef={ref}
includeTooltip={compareOpen}
onClick={onClick}
onDoubleClick={doubleClicked}
// for only StoreInventoryItems (the main inventory page)
Expand Down
1 change: 1 addition & 0 deletions src/app/item-picker/ItemPicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ function ItemPicker({
<ConnectedInventoryItem
item={item}
onClick={() => onItemSelectedFn(item, onClose)}
includeTooltip
ignoreSelectedPerks={ignoreSelectedPerks}
/>
{item.type === 'Class' && (
Expand Down