diff --git a/src/app/dim-ui/PressTip.tsx b/src/app/dim-ui/PressTip.tsx index 5df8b232dc..98e6cb98db 100644 --- a/src/app/dim-ui/PressTip.tsx +++ b/src/app/dim-ui/PressTip.tsx @@ -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. */ @@ -41,6 +45,7 @@ interface Props { type ControlProps = Props & React.HTMLAttributes & { + events: React.HTMLAttributes; open: boolean; triggerRef: React.RefObject; }; @@ -65,10 +70,12 @@ type ControlProps = Props & function Control({ tooltip, open, + disabled, triggerRef, children, elementType: Component = 'div', className, + events, ...rest }: ControlProps) { const tooltipContents = useRef(null); @@ -94,8 +101,10 @@ function Control({ // TODO: or use framer motion layout animations? return ( - {children} +
{children}
{open && + !disabled && + tooltip && ReactDOM.createPortal(
{_.isFunction(tooltip) ? tooltip() : tooltip}
@@ -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), []); @@ -181,12 +195,12 @@ function PressTip(props: Props) { onClick: absorbClick, } : { - onMouseEnter: hover, + onMouseOver: hover, onMouseUp: closeToolTip, onMouseLeave: closeToolTip, }; - return ; + return ; } export default PressTip; diff --git a/src/app/infuse/InfusionFinder.tsx b/src/app/infuse/InfusionFinder.tsx index 6132caf60a..d2364f4834 100644 --- a/src/app/infuse/InfusionFinder.tsx +++ b/src/app/infuse/InfusionFinder.tsx @@ -229,15 +229,23 @@ export default function InfusionFinder() {
- {effectiveTarget ? : missingItem} + {effectiveTarget ? ( + + ) : ( + missingItem + )}
- {effectiveSource ? : missingItem} + {effectiveSource ? ( + + ) : ( + missingItem + )}
- {result ? : missingItem} + {result ? : missingItem}
); diff --git a/src/app/inventory/ConnectedInventoryItem.tsx b/src/app/inventory/ConnectedInventoryItem.tsx index 9938fda756..b5bdab1b1d 100644 --- a/src/app/inventory/ConnectedInventoryItem.tsx +++ b/src/app/inventory/ConnectedInventoryItem.tsx @@ -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; onClick?(e: React.MouseEvent): void; onShiftClick?(e: React.MouseEvent): void; @@ -71,6 +72,7 @@ function ConnectedInventoryItem({ onDoubleClick, searchHidden, ignoreSelectedPerks, + includeTooltip, innerRef, }: Props) { return ( @@ -86,6 +88,7 @@ function ConnectedInventoryItem({ onDoubleClick={onDoubleClick} searchHidden={searchHidden} ignoreSelectedPerks={ignoreSelectedPerks} + includeTooltip={includeTooltip} innerRef={innerRef} /> ); diff --git a/src/app/inventory/InventoryItem.tsx b/src/app/inventory/InventoryItem.tsx index 90dabf85c7..e5029609de 100644 --- a/src/app/inventory/InventoryItem.tsx +++ b/src/app/inventory/InventoryItem.tsx @@ -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'; @@ -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'; @@ -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; /** TODO: item locked needs to be passed in */ onClick?(e: React.MouseEvent): void; @@ -43,6 +47,7 @@ export default function InventoryItem({ notes, searchHidden, wishlistRoll, + includeTooltip, ignoreSelectedPerks, onClick, onShiftClick, @@ -115,16 +120,23 @@ export default function InventoryItem({ ); }, [isNew, item, notes, subclassPath, tag, wishlistRoll]); - return ( + const tooltip = includeTooltip ?? false; + const inner = (
{contents}
); + + return ( + }> + {inner} + + ); } diff --git a/src/app/inventory/ItemTooltip.m.scss b/src/app/inventory/ItemTooltip.m.scss new file mode 100644 index 0000000000..43ac358faf --- /dev/null +++ b/src/app/inventory/ItemTooltip.m.scss @@ -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; + } +} diff --git a/src/app/inventory/ItemTooltip.m.scss.d.ts b/src/app/inventory/ItemTooltip.m.scss.d.ts new file mode 100644 index 0000000000..c6cb703e1b --- /dev/null +++ b/src/app/inventory/ItemTooltip.m.scss.d.ts @@ -0,0 +1,11 @@ +// This file is automatically generated. +// Please do not change this file! +interface CssExports { + 'note': string; + 'notes': string; + 'perkSelected': string; + 'perks': string; + 'stats': string; +} +export const cssExports: CssExports; +export default cssExports; diff --git a/src/app/inventory/ItemTooltip.tsx b/src/app/inventory/ItemTooltip.tsx new file mode 100644 index 0000000000..057010ac0f --- /dev/null +++ b/src/app/inventory/ItemTooltip.tsx @@ -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) => ( +
+ {socket.plugOptions.map((p) => ( +
1 && p === socket.plugged, + })} + data-perk-name={p.plugDef.displayProperties.name} + > + {p.plugDef.displayProperties.name} +
+ ))} +
+ )); + + return ; + } else if (item.bucket.sort === 'Armor' && item.stats?.length) { + const renderStat = (stat: DimStat) => ( +
+ {stat.displayProperties.hasIcon ? ( + + + + ) : ( + stat.displayProperties.name + ': ' + )} + {stat.base} +
+ ); + + const contents = ( +
+
{item.stats?.filter((s) => s.statHash > 0).map(renderStat)}
+
{item.stats?.filter((s) => s.statHash < 0).map(renderStat)}
+
+ ); + + return ; + } else { + return ; + } +} + +function Tooltip({ + def, + notes, + contents, +}: { + def: DestinyInventoryItemDefinition; + notes?: string; + contents?: React.ReactNode; +}) { + return ( + <> +

{def.displayProperties.name}

+ {def.itemTypeDisplayName &&

{def.itemTypeDisplayName}

} + {notes && ( +
+ + {notes} +
+ )} + {contents} + + ); +} diff --git a/src/app/inventory/StoreInventoryItem.tsx b/src/app/inventory/StoreInventoryItem.tsx index 520ecc5da9..dd2a359dbf 100644 --- a/src/app/inventory/StoreInventoryItem.tsx +++ b/src/app/inventory/StoreInventoryItem.tsx @@ -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'; @@ -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)); }; @@ -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) diff --git a/src/app/item-picker/ItemPicker.tsx b/src/app/item-picker/ItemPicker.tsx index 5ca35c03e8..e0b6dc3bf9 100644 --- a/src/app/item-picker/ItemPicker.tsx +++ b/src/app/item-picker/ItemPicker.tsx @@ -104,6 +104,7 @@ function ItemPicker({ onItemSelectedFn(item, onClose)} + includeTooltip ignoreSelectedPerks={ignoreSelectedPerks} /> {item.type === 'Class' && (