Skip to content

Commit

Permalink
Shopping: display product pill in editor (#11838)
Browse files Browse the repository at this point in the history
  • Loading branch information
swissspidy authored Jun 30, 2022
1 parent 21efe88 commit 512328a
Show file tree
Hide file tree
Showing 6 changed files with 247 additions and 55 deletions.
2 changes: 1 addition & 1 deletion packages/element-library/src/product/display.js
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ const pulseDotAfter = keyframes`
}
`;

// See https://github.com/ampproject/amphtml/blob/160dcd9c93d34bb7da073a1240c287fc0cf14591/extensions/amp-story-shopping/0.1/amp-story-shopping-tag.css#L56-L154
// See https://github.com/ampproject/amphtml/blob/b2dbb6b805529b7cf699dad3a91f6d7556131543/extensions/amp-story-shopping/0.1/amp-story-shopping-tag.css
const ShoppingTagDot = styled.div`
border-radius: 100%;
position: relative;
Expand Down
113 changes: 60 additions & 53 deletions packages/story-editor/src/components/canvas/frameElement.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ import {
useDropTargets,
} from '../../app';
import WithLink from '../elementLink/frame';
import WithProductPill from '../shopping/frame';
import useDoubleClick from '../../utils/useDoubleClick';
import usePerformanceTracking from '../../utils/usePerformanceTracking';
import { TRACKING_EVENTS } from '../../constants';
Expand Down Expand Up @@ -304,60 +305,66 @@ function FrameElement({ id }) {
);

return (
<WithLink element={element} active={isLinkActive} anchorRef={elementRef}>
{Controls && (
<Controls
isTransforming={isTransforming}
box={box}
elementRef={elementRef}
element={element}
isRTL={isRTL}
topOffset={topOffset}
isActive={isActive}
/>
)}
{/* eslint-disable-next-line styled-components-a11y/click-events-have-key-events -- False positive */}
<Wrapper
ref={combinedFocusGroupRef}
data-element-id={id}
{...box}
tabIndex={-1}
role="button"
aria-label={elementLabel}
hasMask={isMaskable}
isClickable={isClickable}
data-testid="frameElement"
onMouseDown={handleMouseDown}
onFocus={handleFocus}
onPointerEnter={onPointerEnter}
onPointerLeave={onPointerLeave}
onClick={isMedia ? handleMediaClick(id) : null}
>
<WithMask
element={element}
fill
flip={flip}
draggingResource={draggingResource}
activeDropTargetId={activeDropTargetId}
isDropSource={isDropSource}
registerDropTarget={registerDropTarget}
unregisterDropTarget={unregisterDropTarget}
isSelected={isSelected}
<WithProductPill
element={element}
active={isLinkActive}
anchorRef={elementRef}
>
<WithLink element={element} active={isLinkActive} anchorRef={elementRef}>
{Controls && (
<Controls
isTransforming={isTransforming}
box={box}
elementRef={elementRef}
element={element}
isRTL={isRTL}
topOffset={topOffset}
isActive={isActive}
/>
)}
{/* eslint-disable-next-line styled-components-a11y/click-events-have-key-events -- False positive */}
<Wrapper
ref={combinedFocusGroupRef}
data-element-id={id}
{...box}
tabIndex={-1}
role="button"
aria-label={elementLabel}
hasMask={isMaskable}
isClickable={isClickable}
data-testid="frameElement"
onMouseDown={handleMouseDown}
onFocus={handleFocus}
onPointerEnter={onPointerEnter}
onPointerLeave={onPointerLeave}
onClick={isMedia ? handleMediaClick(id) : null}
>
{Frame ? (
<Frame
wrapperRef={elementRef}
element={element}
box={box}
isOnlySelectedElement={isOnlySelectedElement}
setEditingElementWithState={setEditingElementWithState}
/>
) : (
<EmptyFrame />
)}
</WithMask>
</Wrapper>
</WithLink>
<WithMask
element={element}
fill
flip={flip}
draggingResource={draggingResource}
activeDropTargetId={activeDropTargetId}
isDropSource={isDropSource}
registerDropTarget={registerDropTarget}
unregisterDropTarget={unregisterDropTarget}
isSelected={isSelected}
>
{Frame ? (
<Frame
wrapperRef={elementRef}
element={element}
box={box}
isOnlySelectedElement={isOnlySelectedElement}
setEditingElementWithState={setEditingElementWithState}
/>
) : (
<EmptyFrame />
)}
</WithMask>
</Wrapper>
</WithLink>
</WithProductPill>
);
}

Expand Down
3 changes: 3 additions & 0 deletions packages/story-editor/src/components/canvas/test/_utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,9 @@ export function TestFrameElement({
video: [],
...(inputConfigContext && inputConfigContext.allowedMimeTypes),
},
locale: {
locale: 'en-US',
},
};
const storyContext = {
...inputStoryContext,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
/**
* External dependencies
*/
import { waitFor } from '@testing-library/react';
import { waitFor, within } from '@testing-library/react';
/**
* Internal dependencies
*/
Expand Down Expand Up @@ -206,5 +206,39 @@ describe('Shopping integration', () => {

expect(firstOption.textContent).toContain('WordPress Pennant');
});

it('should display product pill tooltip when adding product on canvas', async () => {
await insertProduct('Big Logo Collection');

const container = fixture.container;
// Unselect element.
const fullbleedElements = await within(container).findAllByTestId(
'fullbleed',
{
timeout: 2000,
}
);
// There are three fullbleed elements; [0](Display layer), [1](Frames layer), and [2](Edit layer),
const { left, top } = fullbleedElements[1].getBoundingClientRect();
await fixture.events.mouse.click(left - 5, top - 5);

// Move mouse to hover over the element.
const frame = await waitFor(() => {
const frameNode = fixture.editor.canvas.framesLayer.frames[1].node;
if (!frameNode) {
throw new Error('node not ready');
}
expect(frameNode).toBeTruthy();
return frameNode;
});

const tooltipText = '$28.00';

await fixture.events.mouse.moveRel(frame, 10, 10);

const tooltip = await fixture.screen.findByText(tooltipText);
expect(tooltip).toHaveTextContent(tooltipText);
await fixture.snapshot('Product pill tooltip on hover');
});
});
});
147 changes: 147 additions & 0 deletions packages/story-editor/src/components/shopping/frame.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
/*
* Copyright 2020 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

/**
* External dependencies
*/
import styled from 'styled-components';
import PropTypes from 'prop-types';
import { TOOLTIP_PLACEMENT } from '@googleforcreators/design-system';
import { ELEMENT_TYPES } from '@googleforcreators/elements';
import { useEffect } from '@googleforcreators/react';

/**
* Internal dependencies
*/
import StoryPropTypes from '../../types';
import Tooltip from '../tooltip';
import { useConfig, useFont } from '../../app';
import PillIcon from './icons/pill.svg';

// See https://github.com/ampproject/amphtml/blob/b2dbb6b805529b7cf699dad3a91f6d7556131543/extensions/amp-story-shopping/0.1/amp-story-shopping-tag.css

const StyledTooltip = styled(Tooltip)`
align-items: center;
p {
display: flex;
max-width: 100%;
color: #fff;
font-family: 'Poppins', sans-serif;
font-weight: 700;
font-size: 14px;
line-height: 24px;
padding: 0;
}
background-color: rgba(125, 125, 125, 0.75);
border-radius: 18px;
padding-inline-start: 6px;
padding-inline-end: 12px;
cursor: pointer;
height: 36px;
`;

const PillImage = styled.span`
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
background-color: rgba(0, 0, 0, 0.6);
border-radius: 100%;
flex-shrink: 0;
flex-shrink: 0;
margin-inline-end: 4px;
`;

function TooltipTitle({ productTagText }) {
return (
<>
<PillImage>
<PillIcon width={14} height={14} aria-hidden />
</PillImage>
{productTagText}
</>
);
}

TooltipTitle.propTypes = {
productTagText: PropTypes.string.isRequired,
};

function WithProductPill({ element, active, children, anchorRef }) {
const {
actions: { maybeEnqueueFontStyle },
} = useFont();

const {
locale: { locale },
} = useConfig();

const elementType = element.type;
useEffect(() => {
if (elementType !== ELEMENT_TYPES.PRODUCT) {
return;
}

maybeEnqueueFontStyle([
{
font: {
family: 'Poppins',
service: 'fonts.google.com',
weights: [700],
styles: ['regular'],
variants: [[0, 700]],
},
fontWeight: 700,
},
]);
}, [elementType, maybeEnqueueFontStyle]);

if (elementType !== ELEMENT_TYPES.PRODUCT) {
return children;
}

const { productPrice, productPriceCurrency, productTitle } = element.product;

const productTagText =
productPrice && productPriceCurrency && locale
? new Intl.NumberFormat(locale, {
style: 'currency',
currency: productPriceCurrency,
}).format(productPrice)
: productTitle;

return (
<StyledTooltip
forceAnchorRef={anchorRef}
placement={TOOLTIP_PLACEMENT.RIGHT}
hasTail={false}
title={active ? <TooltipTitle productTagText={productTagText} /> : null}
>
{children}
</StyledTooltip>
);
}

WithProductPill.propTypes = {
element: StoryPropTypes.element.isRequired,
anchorRef: PropTypes.object,
active: PropTypes.bool.isRequired,
children: PropTypes.node.isRequired,
};

export default WithProductPill;
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit 512328a

Please sign in to comment.