diff --git a/polaris-react/src/components/HoverCard/HoverCard.module.scss b/polaris-react/src/components/HoverCard/HoverCard.module.scss index 1dc1c0080fc..d1ed72797da 100644 --- a/polaris-react/src/components/HoverCard/HoverCard.module.scss +++ b/polaris-react/src/components/HoverCard/HoverCard.module.scss @@ -12,7 +12,7 @@ $vertical-motion-offset: -5px; // stylelint-disable-next-line polaris/conventions/polaris/custom-property-allowed-list -- HoverCard CSS Custom Variables min-width: var(--pc-hovercard-min-width); background: var(--p-color-bg-surface); - will-change: opacity, left, top, width, height; + will-change: opacity, left, top; margin: var(--p-space-100) var(--p-space-200) var(--p-space-400); box-shadow: var(--p-shadow-500); border-radius: var(--p-border-radius-300); @@ -39,7 +39,6 @@ $vertical-motion-offset: -5px; .HoverCardOverlay-open { opacity: 1; - transform: none; transition: top var(--p-motion-duration-100) cubic-bezier(0.25, 0, 0.75, 1.35), opacity var(--p-motion-duration-100) ease-in-out, @@ -47,11 +46,11 @@ $vertical-motion-offset: -5px; height var(--p-motion-duration-100) ease-in-out; } -.measuring:not(.HoverCardOverlay-exiting) { +.measuring:not(.HoverCardOverlay-exited) { opacity: 0; } -.measured:not(.HoverCardOverlay-open) { +.measured { opacity: 1; transition: opacity var(--p-motion-duration-100) var(--p-motion-ease) var(--p-motion-duration-300); @@ -84,7 +83,6 @@ $vertical-motion-offset: -5px; } .Content { - opacity: 1; display: flex; width: 100%; flex-direction: column; diff --git a/polaris-react/src/components/HoverCard/HoverCard.stories.tsx b/polaris-react/src/components/HoverCard/HoverCard.stories.tsx index 979a1f82d0a..3c8423e780c 100644 --- a/polaris-react/src/components/HoverCard/HoverCard.stories.tsx +++ b/polaris-react/src/components/HoverCard/HoverCard.stories.tsx @@ -616,7 +616,6 @@ export function WithDynamicActivator() { ; toggleActive?(active: boolean): void; @@ -103,22 +101,15 @@ export function useHoverCardActivatorWrapperProps({ mouseEntered.current = true; - if (hoverDelay && !presenceList.hovercard) { + if (!presenceList.hovercard) { hoverDelayTimeout.current = setTimeout(() => { handleOpen(); - }, hoverDelay); + }, 100); } else { handleOpen(); } }, - [ - handleOpen, - hoverDelay, - hoverDelayTimeout, - presenceList, - mdUp, - providedActivatorRef, - ], + [handleOpen, hoverDelayTimeout, presenceList, mdUp, providedActivatorRef], ); // https://github.com/facebook/react/issues/10109 diff --git a/polaris-react/src/components/PositionedOverlay/PositionedOverlay.tsx b/polaris-react/src/components/PositionedOverlay/PositionedOverlay.tsx index 426c88250fd..59d2cf0165d 100644 --- a/polaris-react/src/components/PositionedOverlay/PositionedOverlay.tsx +++ b/polaris-react/src/components/PositionedOverlay/PositionedOverlay.tsx @@ -41,7 +41,6 @@ export interface PositionedOverlayProps { preventInteraction?: boolean; classNames?: string; zIndexOverride?: number; - transform?: string; render(overlayDetails: OverlayDetails): React.ReactNode; onScrollOut?(): void; } @@ -137,11 +136,9 @@ export class PositionedOverlay extends PureComponent< preventInteraction, classNames: propClassNames, zIndexOverride, - transform, } = this.props; const style = { - transform, top: top == null || isNaN(top) ? undefined : top, left: left == null || isNaN(left) ? undefined : left, right: right == null || isNaN(right) ? undefined : right, diff --git a/polaris.shopify.com/content/components/overlays/hovercard.mdx b/polaris.shopify.com/content/components/overlays/hovercard.mdx new file mode 100644 index 00000000000..fe3fe4b0282 --- /dev/null +++ b/polaris.shopify.com/content/components/overlays/hovercard.mdx @@ -0,0 +1,39 @@ +--- +title: HoverCard +shortDescription: Used to present a preview of a commerce object's key information when hovering a link to its detail page. +category: Overlays +keywords: + - hovercard + - link preview + - link details + - commerce object +examples: + - fileName: hovercard-with-child-activator.tsx + title: With child activator + description: A hover card that renders and is triggered to be `active` by its `children`. Use for commerce objects rendered by themselves within normal page content. + - fileName: hovercard-with-dynamic-activator.tsx + title: With dynamic activator + description: A HoverCard rendered without `children` that is triggered to be `active` and repositioned by dynamically setting the `activator` prop. Use when several of the same commerce object type are in close context, like a column of customer names in an index table of orders. +--- + +# {frontmatter.title} + + + +A hover card is an overlay only triggered by mouse over of a link. They are not triggered on focus, keyboard navigable, or visible to screen readers. Use to present a preview of a commerce object's key information when hovering a link to its detail page. + + + + + + + +## Best practices + +--- + +## Content guidelines + +--- + +## Related components diff --git a/polaris.shopify.com/pages/examples/hovercard-with-child-activator.tsx b/polaris.shopify.com/pages/examples/hovercard-with-child-activator.tsx new file mode 100644 index 00000000000..20d95466ea2 --- /dev/null +++ b/polaris.shopify.com/pages/examples/hovercard-with-child-activator.tsx @@ -0,0 +1,146 @@ +import React, {useState} from 'react'; +import { + ButtonGroup, + Box, + Button, + HoverCard, + Icon, + Link, + Text, + BlockStack, + InlineStack, + Card, +} from '@shopify/polaris'; +import type {PositionedOverlayProps} from '@shopify/polaris'; +import {LocationsMinor, OrdersMinor} from '@shopify/polaris-icons'; +import {withPolarisExample} from '../../src/components/PolarisExampleWrapper'; + +function HoverCardWithChildActivator() { + const [active, setActive] = useState(false); + const [position, setPosition] = + useState('below'); + + const handleChangePosition = + (position: PositionedOverlayProps['preferredPosition']) => () => { + setPosition(position); + }; + + const activator = ( + + Saul Goodman + + ); + + const customerHoverCardContent = ( + + + + + Saul Goodman + + + + help@bettercallsaul.com + + + + +1 505-842-5662 + + + + + + + + Albequerque, NM, USA + + + + + + + + 8 Orders + + + + + + + ); + + const positionControlBar = ( + + + Use the buttons below to change the hover card position + + + + + + + + + ); + + return ( +
+ + + {positionControlBar} + + + + + Customer + + + + {activator} + + + + + + +
+ ); +} + +export default withPolarisExample(HoverCardWithChildActivator); diff --git a/polaris.shopify.com/pages/examples/hovercard-with-dynamic-activator.tsx b/polaris.shopify.com/pages/examples/hovercard-with-dynamic-activator.tsx new file mode 100644 index 00000000000..9d3b67ea36b --- /dev/null +++ b/polaris.shopify.com/pages/examples/hovercard-with-dynamic-activator.tsx @@ -0,0 +1,530 @@ +import React, {useCallback, useState} from 'react'; +import { + Tag, + Thumbnail, + useIndexResourceState, + Box, + HoverCard, + useHoverCardActivatorWrapperProps, + Badge, + IndexTable, + Icon, + Link, + Text, + BlockStack, + InlineStack, + Card, +} from '@shopify/polaris'; +import { + ShipmentMajor, + LocationsMinor, + OrdersMinor, + ImageMajor, +} from '@shopify/polaris-icons'; +import {withPolarisExample} from '../../src/components/PolarisExampleWrapper'; + +export function HoverCardWithDynamicActivator() { + interface CustomerDetailPreview { + id: string; + name: string; + email: string; + phone: string; + location: string; + orders: number; + } + + interface LineItem { + quantity: number; + title: string; + imageSrc?: string; + variant?: string; + skuNumber?: string; + } + + interface OrderDetailPreview { + location?: string; + deliveryMethod: string; + fulfillmentStatus: React.ReactNode; + items: LineItem[]; + } + + const orders = [ + { + id: '1020', + title: ( + + #1020 + + ), + date: 'Jul 20 at 4:34pm', + customer: { + id: '4102', + email: 'yo@superduperkid.co', + phone: '+19171111111', + name: 'Colm Dillane', + location: 'Brooklyn, NY, USA', + orders: 27, + }, + channel: 'Online Store', + total: '$969.44', + paymentStatus: ( + + Partially paid + + ), + fulfillmentStatus: Fulfilled, + items: [ + { + quantity: 40, + title: 'Perforated Driving Glove - Lavender/White', + imageSrc: + 'https://cdn.shopify.com/s/files/1/2376/3301/files/3D_render_product_image_of_classic_two-tone_per_2d730775-1987-49ec-8571-3756d83ef508.png?v=1704767232', + variant: 'Size - S (8.5)', + skuNumber: '178988', + }, + { + quantity: 56, + title: 'Perforated Driving Glove - Lavender/White', + imageSrc: + 'https://cdn.shopify.com/s/files/1/2376/3301/files/3D_render_product_image_of_classic_two-tone_per_2d730775-1987-49ec-8571-3756d83ef508.png?v=1704767232', + variant: 'Size - M (9)', + skuNumber: '178988', + }, + { + quantity: 79, + title: 'Perforated Driving Glove - Lavender/White', + imageSrc: + 'https://cdn.shopify.com/s/files/1/2376/3301/files/3D_render_product_image_of_classic_two-tone_per_2d730775-1987-49ec-8571-3756d83ef508.png?v=1704767232', + variant: 'Size - L (9.5)', + skuNumber: '178988', + }, + { + quantity: 56, + title: 'Perforated Driving Glove - Lavender/White', + imageSrc: + 'https://cdn.shopify.com/s/files/1/2376/3301/files/3D_render_product_image_of_classic_two-tone_per_2d730775-1987-49ec-8571-3756d83ef508.png?v=1704767232', + variant: 'Size - XL (10)', + skuNumber: '178988', + }, + ], + deliveryStatus: 'Complete', + deliveryMethod: 'Local Pickup', + location: 'Ridgewood Factory', + tags: ['VIP', 'wholesale', 'Net 30', 'pickup', 'priority'], + }, + { + id: '1019', + title: ( + + #1019 + + ), + date: 'Jul 20 at 3:46pm', + customer: { + id: '2564', + name: 'Al Chemist', + email: 'foodvillain@idontwantthat.com', + phone: '+12122222222', + location: 'Los Angeles, CA, USA', + orders: 19, + }, + channel: 'Online Store', + total: '$701.19', + paymentStatus: Paid, + fulfillmentStatus: ( + + Unfulfilled + + ), + items: [ + { + quantity: 1, + title: 'Perforated Motocross Glove - Brown/Cognac/White', + imageSrc: + 'https://cdn.shopify.com/s/files/1/2376/3301/files/3D_render_product_image_for_luxury_keyhole_5_fi_0b5f3b16-fb54-47b6-855d-c19097586c8b.png?v=1704767257', + variant: 'Size - L (9.5)', + skuNumber: '176400', + }, + ], + deliveryStatus: 'Tracking added', + deliveryMethod: 'UPS 2 Day Air', + location: 'BK Warehouse - Williamsburg', + tags: ['VIP'], + }, + { + id: '1018', + title: ( + + #1018 + + ), + date: 'Jul 20 at 3.44pm', + customer: { + id: '2563', + name: 'Larry June', + email: 'yeehee@unclelarry.com', + phone: '+1415NUMBERS', + location: 'San Francisco, CA, USA', + orders: 22, + }, + channel: 'Instagram', + total: '$798.24', + paymentStatus: Paid, + fulfillmentStatus: ( + + Unfulfilled + + ), + items: [ + { + quantity: 1, + title: 'Perforated Motocross Glove - Brown/Cognac/White', + imageSrc: + 'https://cdn.shopify.com/s/files/1/2376/3301/files/3D_render_product_image_for_luxury_keyhole_5_fi_0b5f3b16-fb54-47b6-855d-c19097586c8b.png?v=1704767257', + variant: 'Size - XL (10)', + skuNumber: '176400', + }, + ], + deliveryStatus: 'Tracking added', + deliveryMethod: 'UPS Ground', + location: 'BK Warehouse - Williamsburg', + tags: ['VIP'], + }, + ]; + + const resourceName = { + singular: 'order', + plural: 'orders', + }; + + const {selectedResources, allResourcesSelected, handleSelectionChange} = + useIndexResourceState(orders); + + const [activeHoverCard, setActiveHoverCard] = useState<{ + customer?: CustomerDetailPreview | null; + order?: OrderDetailPreview | null; + }>({ + customer: null, + order: null, + }); + + const { + className, + activatorElement, + handleMouseEnterActivator, + handleMouseLeaveActivator, + } = useHoverCardActivatorWrapperProps({snapToParent: true}); + + const handleMouseEnterCustomer = useCallback( + (customer: CustomerDetailPreview) => + (event: React.MouseEvent) => { + setActiveHoverCard({customer, order: null}); + handleMouseEnterActivator?.(event); + }, + [handleMouseEnterActivator], + ); + + const handleMouseEnterItems = useCallback( + (order: OrderDetailPreview) => (event: React.MouseEvent) => { + setActiveHoverCard({order, customer: null}); + handleMouseEnterActivator?.(event); + }, + [handleMouseEnterActivator], + ); + + let hoverCardContent: React.ReactNode = null; + + if (activeHoverCard.customer) { + const {name, phone, email, location, orders} = activeHoverCard?.customer; + + hoverCardContent = ( + + + + + {name} + + + {email} + + + {phone} + + + + + + + + + + {location} + + + + + + + + {`${orders} Orders`} + + + + + + + ); + } else if (activeHoverCard.order) { + const {location, deliveryMethod, fulfillmentStatus, items} = + activeHoverCard.order; + + hoverCardContent = ( + + + {fulfillmentStatus} + + + + + +
+ +
+ + Location + +
+ + {location} + +
+ + +
+ +
+ + Delivery method + +
+ + {deliveryMethod} + +
+
+
+ + {items.map( + ({quantity, title, imageSrc, variant, skuNumber}, index) => ( + + +
+ {imageSrc ? ( + + ) : ( +
+ +
+ )} +
+ + + {`${quantity}`} + + +
+
+ + + + {title} + + + + {variant} + + +
+
+ ), + )} +
+
+
+ ); + } + + const rowMarkup = orders.map( + ( + { + id, + title, + date, + customer, + channel, + total, + paymentStatus, + fulfillmentStatus, + items, + deliveryStatus, + deliveryMethod, + location, + tags, + }, + index, + ) => { + const customerLinkMarkup = ( +
+ + {customer.name} + +
+ ); + + const itemLinkMarkup = ( +
+ + {`${items.length} items`} + +
+ ); + + return ( + + {title} + {date} + {customerLinkMarkup} + {channel} + + + {total} + + + {paymentStatus} + {fulfillmentStatus} + {itemLinkMarkup} + {deliveryStatus} + {deliveryMethod} + + + {tags.map((tag, index) => ( + {tag} + ))} + + + + ); + }, + ); + + return ( + <> + + + + {rowMarkup} + + + + + ); +} + +export default withPolarisExample(HoverCardWithDynamicActivator);