From e1716c15c123a769047c2adff5ad078e7c2ee98c Mon Sep 17 00:00:00 2001 From: Amela Kodzic Date: Mon, 26 Apr 2021 15:06:01 +0200 Subject: [PATCH 01/22] Add Popover component --- .../circuit-ui/components/Popover/Popover.tsx | 24 ++++++++++++++ .../circuit-ui/components/Popover/index.tsx | 33 +++++++++++++++++++ 2 files changed, 57 insertions(+) create mode 100644 packages/circuit-ui/components/Popover/Popover.tsx create mode 100644 packages/circuit-ui/components/Popover/index.tsx diff --git a/packages/circuit-ui/components/Popover/Popover.tsx b/packages/circuit-ui/components/Popover/Popover.tsx new file mode 100644 index 0000000000..f507cc7241 --- /dev/null +++ b/packages/circuit-ui/components/Popover/Popover.tsx @@ -0,0 +1,24 @@ +/** + * Copyright 2021, SumUp Ltd. + * 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 + * + * http://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. + */ + +import React from 'react'; + +export const PopoverItem = ({ icon, label, onClick }) => ( + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + +); diff --git a/packages/circuit-ui/components/Popover/index.tsx b/packages/circuit-ui/components/Popover/index.tsx new file mode 100644 index 0000000000..d34471a317 --- /dev/null +++ b/packages/circuit-ui/components/Popover/index.tsx @@ -0,0 +1,33 @@ +/** + * Copyright 2021, SumUp Ltd. + * 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 + * + * http://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. + */ + +/** + * Copyright 2021, SumUp Ltd. + * 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 + * + * http://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. + */ + +import Popover from './Popover'; + +export default Popover; From 7ca09cff4c372e1d592db25ea2ca94c7af7285eb Mon Sep 17 00:00:00 2001 From: Amela Kodzic Date: Mon, 17 May 2021 17:46:38 +0200 Subject: [PATCH 02/22] Remove old Popover component --- .../circuit-ui/components/Popover/Popover.js | 263 ----------- .../components/Popover/Popover.spec.js | 116 ----- .../components/Popover/Popover.stories.js | 81 ---- .../components/Popover/PopoverService.js | 25 - .../__snapshots__/Popover.spec.js.snap | 443 ------------------ .../circuit-ui/components/Popover/index.js | 18 - 6 files changed, 946 deletions(-) delete mode 100644 packages/circuit-ui/components/Popover/Popover.js delete mode 100644 packages/circuit-ui/components/Popover/Popover.spec.js delete mode 100644 packages/circuit-ui/components/Popover/Popover.stories.js delete mode 100644 packages/circuit-ui/components/Popover/PopoverService.js delete mode 100644 packages/circuit-ui/components/Popover/__snapshots__/Popover.spec.js.snap delete mode 100644 packages/circuit-ui/components/Popover/index.js diff --git a/packages/circuit-ui/components/Popover/Popover.js b/packages/circuit-ui/components/Popover/Popover.js deleted file mode 100644 index a2cae2d94d..0000000000 --- a/packages/circuit-ui/components/Popover/Popover.js +++ /dev/null @@ -1,263 +0,0 @@ -/** - * Copyright 2019, SumUp Ltd. - * 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 - * - * http://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. - */ - -import React, { Component } from 'react'; -import PropTypes from 'prop-types'; -import { Manager, Reference, Popper } from 'react-popper'; -import styled from '@emotion/styled'; -import { css } from '@emotion/core'; - -import Portal from '../Portal'; -import { positionPropType, alignPropType } from '../../util/shared-prop-types'; - -import { toPopperPlacement, popperModifiers } from './PopoverService'; - -const ReferenceWrapper = styled('div')` - label: popover__button-wrapper; - display: inline-block; - - &:focus { - outline: none; - } - - ${({ referenceWrapperStyles, ...rest }) => referenceWrapperStyles(rest)}; -`; - -const basePopoverWrapperStyles = ({ theme }) => css` - label: popover; - z-index: ${theme.zIndex.popover}; -`; - -const customZIndexWrapperStyles = ({ zIndex }) => - zIndex && - css` - z-index: ${zIndex}; - `; - -const PopoverWrapper = styled('div')( - basePopoverWrapperStyles, - customZIndexWrapperStyles, -); - -const arrowUpStyles = css` - position: absolute; - top: 0; - left: 50%; - transform: translate(-50%, -100%); -`; - -const arrowDownStyles = css` - position: absolute; - bottom: 0; - left: 50%; - transform: translate(-50%, 100%); -`; - -const arrowLeftStyles = css` - position: absolute; - left: 0; - top: 50%; - transform: translate(-100%, -50%); -`; - -const arrowRightStyles = css` - position: absolute; - right: 0; - top: 50%; - transform: translate(100%, -50%); -`; - -const oppositeDirection = { - left: 'right', - right: 'left', - top: 'down', - bottom: 'up', -}; - -const arrowStyles = { - up: arrowUpStyles, - down: arrowDownStyles, - left: arrowLeftStyles, - right: arrowRightStyles, -}; - -class Popover extends Component { - static propTypes = { - /** - * isOpen controlled prop - */ - isOpen: PropTypes.bool, - /** - * function rendering the popover - */ - renderPopover: PropTypes.func.isRequired, - /** - * function rendering the reference (button or something clickable) - */ - renderReference: PropTypes.func, - /** - * placement of the popover relative to the reference - */ - position: positionPropType, - /** - * alignment of the popover relative to the reference - */ - align: alignPropType, - /** - * A callback that is called when the popover should be closed when reference is clicked in an open state - */ - onReferenceClickClose: PropTypes.func.isRequired, - /** - * A callback that is called on click outside the popover wrapper or the reference - */ - onOutsideClickClose: PropTypes.func.isRequired, - /** - * A custom z-index for the popover - */ - zIndex: PropTypes.number, - onClose: PropTypes.func, - usePortal: PropTypes.bool, - modifiers: PropTypes.shape(), - arrowRenderer: PropTypes.func, - referenceWrapperStyles: PropTypes.func, - referenceElement: PropTypes.element, - }; - - static defaultProps = { - isOpen: false, - position: 'bottom', - align: 'start', - zIndex: null, - onClose: () => {}, - usePortal: false, - modifiers: {}, - arrowRenderer: () => null, - renderReference: () => null, - referenceElement: null, - referenceWrapperStyles: () => null, - }; - - buttonRef = null; - - popoverRef = null; - - componentDidMount() { - document.addEventListener('click', this.handleDocumentClick, true); - } - - componentWillUnmount() { - document.removeEventListener('click', this.handleDocumentClick, true); - } - - handleDocumentClick = ({ target }) => { - const isWithinPopover = this.popoverRef && this.popoverRef.contains(target); - const isWithinButton = this.buttonRef && this.buttonRef.contains(target); - - if (this.props.isOpen && !isWithinButton && !isWithinPopover) { - this.props.onOutsideClickClose(target); - } - }; - - receiveButtonRef = (ref) => { - this.buttonRef = ref; - }; - - receivePopoverRef = (ref) => { - this.popoverRef = ref; - }; - - handleReferenceClick = () => { - const { isOpen } = this.props; - if (isOpen) { - this.props.onReferenceClickClose(); - } - }; - - render() { - const { - arrowRenderer, - renderPopover, - renderReference, - referenceElement, - referenceWrapperStyles, - position, - align, - isOpen, - zIndex, - modifiers, - usePortal, - ...props - } = this.props; - - const reference = !referenceElement && ( - - {({ ref }) => ( - -
{renderReference()}
-
- )} -
- ); - - const popper = isOpen && ( - - {({ ref, style }) => - isOpen && ( - -
- {renderPopover()} - {!!arrowRenderer && - arrowRenderer( - arrowStyles[oppositeDirection[position]], - oppositeDirection[position], - )} -
-
- ) - } -
- ); - - return ( - - {reference} - {usePortal ? {popper} : popper} - - ); - } -} - -/** - * @component - */ -export default Popover; diff --git a/packages/circuit-ui/components/Popover/Popover.spec.js b/packages/circuit-ui/components/Popover/Popover.spec.js deleted file mode 100644 index d77831d7d3..0000000000 --- a/packages/circuit-ui/components/Popover/Popover.spec.js +++ /dev/null @@ -1,116 +0,0 @@ -/** - * Copyright 2019, SumUp Ltd. - * 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 - * - * http://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. - */ - -import React from 'react'; - -import Popover from '.'; - -const positions = ['top', 'bottom', 'left', 'right']; -const alignments = ['start', 'end', 'center']; - -const defaultProps = { - // eslint-disable-next-line react/display-name - renderReference: () => , - // eslint-disable-next-line react/display-name - renderPopover: () =>
, - onReferenceClickClose: () => {}, - onOutsideClickClose: () => {}, -}; - -// FMI: https://github.com/FezVrasta/popper.js/issues/478 -jest.mock('popper.js', () => { - const PopperJS = jest.requireActual('popper.js'); - - return class Popper { - static placements = PopperJS.placements; - - constructor() { - return { - destroy: () => {}, - scheduleUpdate: () => {}, - }; - } - }; -}); - -describe('Popover', () => { - beforeEach(() => { - jest.resetAllMocks(); - }); - - /** - * Style tests. - */ - it('should render with default styles', () => { - const actual = create(); - expect(actual).toMatchSnapshot(); - }); - - positions.forEach((position) => { - alignments.forEach((alignment) => { - it(`should render with position ${position} and alignment ${alignment}`, () => { - const actual = create( - , - ); - expect(actual).toMatchSnapshot(); - }); - }); - }); - - it('should render nothing without isOpen=false', () => { - const { queryByTestId } = render( - , - ); - expect(queryByTestId('popover-child')).toBeNull(); - }); - - it('should call onReferenceClickClose on clicked reference when isOpen=true', () => { - const onReferenceClickClose = jest.fn(); - const { getByTestId } = render( - , - ); - - act(() => { - fireEvent.click(getByTestId('popover-reference')); - }); - - expect(onReferenceClickClose).toHaveBeenCalledTimes(1); - }); - - it('should not render component when referenceElement is passed', () => { - const { queryByTestId } = render( - } />, - ); - expect(queryByTestId('popover-reference')).toBeNull(); - }); - - /** - * Accessibility tests. - */ - it('should meet accessibility guidelines', async () => { - const wrapper = renderToHtml(); - const actual = await axe(wrapper); - expect(actual).toHaveNoViolations(); - }); -}); diff --git a/packages/circuit-ui/components/Popover/Popover.stories.js b/packages/circuit-ui/components/Popover/Popover.stories.js deleted file mode 100644 index 2526b0f5ff..0000000000 --- a/packages/circuit-ui/components/Popover/Popover.stories.js +++ /dev/null @@ -1,81 +0,0 @@ -/** - * Copyright 2019, SumUp Ltd. - * 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 - * - * http://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. - */ - -import React, { useState } from 'react'; -import { findByText } from '@testing-library/dom'; -import userEvent from '@testing-library/user-event'; - -import Button from '../Button'; -import Card from '../Card'; - -import Popover from './Popover'; - -const interactionTasks = [ - { - name: 'Open popover', - description: - 'Click the popover and wait until the popover content is shown.', - run: async ({ container }) => { - const button = container.querySelector('[data-testid=button]'); - userEvent.click(button); - await findByText(container, 'Popover Content'); - }, - }, -]; - -export default { - title: 'Components/Popover', - component: Popover, - parameters: { - performance: { - interactions: interactionTasks, - }, - }, -}; - -export const Base = (args) => { - const [open, setOpen] = useState(false); - - const { closeOnButtonClick, ...props } = args; - - return ( - Popover Content} - renderReference={() => ( - - )} - onReferenceClickClose={() => { - if (closeOnButtonClick) { - setOpen(false); - } - }} - onOutsideClickClose={() => setOpen(false)} - /> - ); -}; - -Base.args = { - position: 'bottom', - align: 'start', - closeOnButtonClick: false, -}; diff --git a/packages/circuit-ui/components/Popover/PopoverService.js b/packages/circuit-ui/components/Popover/PopoverService.js deleted file mode 100644 index 6d4831cbf0..0000000000 --- a/packages/circuit-ui/components/Popover/PopoverService.js +++ /dev/null @@ -1,25 +0,0 @@ -/** - * Copyright 2019, SumUp Ltd. - * 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 - * - * http://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. - */ - -// popper.js modifiers -// read more: https://popper.js.org/popper-documentation.html#modifiers -export const popperModifiers = { - offset: { enabled: true, offset: '0,10' }, - flip: { enabled: true }, -}; - -export function toPopperPlacement(placement, align) { - return placement + (align !== 'center' ? `-${align}` : ''); -} diff --git a/packages/circuit-ui/components/Popover/__snapshots__/Popover.spec.js.snap b/packages/circuit-ui/components/Popover/__snapshots__/Popover.spec.js.snap deleted file mode 100644 index b3835fdb0e..0000000000 --- a/packages/circuit-ui/components/Popover/__snapshots__/Popover.spec.js.snap +++ /dev/null @@ -1,443 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Popover should render with default styles 1`] = ` -HTMLCollection [ - .circuit-0 { - display: inline-block; -} - -.circuit-0:focus { - outline: none; -} - -
-
- -
-
, - .circuit-0 { - z-index: 30; -} - -
-
-
-
-
, -] -`; - -exports[`Popover should render with position bottom and alignment center 1`] = ` -HTMLCollection [ - .circuit-0 { - display: inline-block; -} - -.circuit-0:focus { - outline: none; -} - -
-
- -
-
, - .circuit-0 { - z-index: 30; -} - -
-
-
-
-
, -] -`; - -exports[`Popover should render with position bottom and alignment end 1`] = ` -HTMLCollection [ - .circuit-0 { - display: inline-block; -} - -.circuit-0:focus { - outline: none; -} - -
-
- -
-
, - .circuit-0 { - z-index: 30; -} - -
-
-
-
-
, -] -`; - -exports[`Popover should render with position bottom and alignment start 1`] = ` -HTMLCollection [ - .circuit-0 { - display: inline-block; -} - -.circuit-0:focus { - outline: none; -} - -
-
- -
-
, - .circuit-0 { - z-index: 30; -} - -
-
-
-
-
, -] -`; - -exports[`Popover should render with position left and alignment center 1`] = ` -HTMLCollection [ - .circuit-0 { - display: inline-block; -} - -.circuit-0:focus { - outline: none; -} - -
-
- -
-
, - .circuit-0 { - z-index: 30; -} - -
-
-
-
-
, -] -`; - -exports[`Popover should render with position left and alignment end 1`] = ` -HTMLCollection [ - .circuit-0 { - display: inline-block; -} - -.circuit-0:focus { - outline: none; -} - -
-
- -
-
, - .circuit-0 { - z-index: 30; -} - -
-
-
-
-
, -] -`; - -exports[`Popover should render with position left and alignment start 1`] = ` -HTMLCollection [ - .circuit-0 { - display: inline-block; -} - -.circuit-0:focus { - outline: none; -} - -
-
- -
-
, - .circuit-0 { - z-index: 30; -} - -
-
-
-
-
, -] -`; - -exports[`Popover should render with position right and alignment center 1`] = ` -HTMLCollection [ - .circuit-0 { - display: inline-block; -} - -.circuit-0:focus { - outline: none; -} - -
-
- -
-
, - .circuit-0 { - z-index: 30; -} - -
-
-
-
-
, -] -`; - -exports[`Popover should render with position right and alignment end 1`] = ` -HTMLCollection [ - .circuit-0 { - display: inline-block; -} - -.circuit-0:focus { - outline: none; -} - -
-
- -
-
, - .circuit-0 { - z-index: 30; -} - -
-
-
-
-
, -] -`; - -exports[`Popover should render with position right and alignment start 1`] = ` -HTMLCollection [ - .circuit-0 { - display: inline-block; -} - -.circuit-0:focus { - outline: none; -} - -
-
- -
-
, - .circuit-0 { - z-index: 30; -} - -
-
-
-
-
, -] -`; - -exports[`Popover should render with position top and alignment center 1`] = ` -HTMLCollection [ - .circuit-0 { - display: inline-block; -} - -.circuit-0:focus { - outline: none; -} - -
-
- -
-
, - .circuit-0 { - z-index: 30; -} - -
-
-
-
-
, -] -`; - -exports[`Popover should render with position top and alignment end 1`] = ` -HTMLCollection [ - .circuit-0 { - display: inline-block; -} - -.circuit-0:focus { - outline: none; -} - -
-
- -
-
, - .circuit-0 { - z-index: 30; -} - -
-
-
-
-
, -] -`; - -exports[`Popover should render with position top and alignment start 1`] = ` -HTMLCollection [ - .circuit-0 { - display: inline-block; -} - -.circuit-0:focus { - outline: none; -} - -
-
- -
-
, - .circuit-0 { - z-index: 30; -} - -
-
-
-
-
, -] -`; diff --git a/packages/circuit-ui/components/Popover/index.js b/packages/circuit-ui/components/Popover/index.js deleted file mode 100644 index 1b804ada8d..0000000000 --- a/packages/circuit-ui/components/Popover/index.js +++ /dev/null @@ -1,18 +0,0 @@ -/** - * Copyright 2019, SumUp Ltd. - * 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 - * - * http://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. - */ - -import Popover from './Popover'; - -export default Popover; From 58d8214bae8cad276c30e8832c4f543f0af6e7c4 Mon Sep 17 00:00:00 2001 From: Amela Kodzic Date: Mon, 17 May 2021 17:47:24 +0200 Subject: [PATCH 03/22] Add listItem style mixin --- packages/circuit-ui/styles/style-mixins.ts | 26 ++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/packages/circuit-ui/styles/style-mixins.ts b/packages/circuit-ui/styles/style-mixins.ts index 70366114d6..3d62c7f9dc 100644 --- a/packages/circuit-ui/styles/style-mixins.ts +++ b/packages/circuit-ui/styles/style-mixins.ts @@ -334,3 +334,29 @@ export const inputOutline = ( } `; }; + +export const listItem = (args: ThemeArgs): SerializedStyles => { + const theme = getTheme(args); + return css` + background-color: ${theme.colors.white}; + padding: ${theme.spacings.kilo} ${theme.spacings.tera} + ${theme.spacings.kilo} ${theme.spacings.mega}; + border: 0; + color: ${theme.colors.bodyColor}; + text-decoration: none; + + &:hover { + background-color: ${theme.colors.n100}; + cursor: pointer; + } + + &:active { + background-color: ${theme.colors.n200}; + } + + &:focus { + background-color: ${theme.colors.white}; + ${focusOutline({ theme })}; + } + `; +}; From f5e07ddd59ba8363be248eee7498e0049f20c606 Mon Sep 17 00:00:00 2001 From: Amela Kodzic Date: Mon, 17 May 2021 17:48:50 +0200 Subject: [PATCH 04/22] Add PopoverItem component --- .../circuit-ui/components/Popover/Popover.tsx | 89 +++++++++++++++++-- .../circuit-ui/components/Popover/index.tsx | 4 +- packages/circuit-ui/index.ts | 1 - 3 files changed, 83 insertions(+), 11 deletions(-) diff --git a/packages/circuit-ui/components/Popover/Popover.tsx b/packages/circuit-ui/components/Popover/Popover.tsx index f507cc7241..46c97c308f 100644 --- a/packages/circuit-ui/components/Popover/Popover.tsx +++ b/packages/circuit-ui/components/Popover/Popover.tsx @@ -13,12 +13,85 @@ * limitations under the License. */ -import React from 'react'; - -export const PopoverItem = ({ icon, label, onClick }) => ( - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - +/** @jsx jsx */ +import { css, jsx } from '@emotion/core'; +import { Theme } from '@sumup/design-tokens'; +import { FC, HTMLProps, ReactNode, SVGProps, MouseEvent } from 'react'; +import { Dispatch as TrackingProps } from '@sumup/collector'; + +import styled from '../../styles/styled'; +import { listItem } from '../../styles/style-mixins'; +import { useComponents } from '../ComponentsContext'; +import useClickHandler from '../../hooks/use-click-handler'; + +export interface BaseProps { + children: ReactNode; + /** + * Display an icon in addition to the label. + */ + icon: FC>; + /** + * Additional data that is dispatched with the tracking event. + */ + tracking?: TrackingProps; + /** + * The ref to the html dom element, it can be a button an anchor or a span, typed as any for now because of complex js manipulation with styled components + */ + ref?: React.Ref; +} + +type LinkElProps = Omit, 'size' | 'type'>; +type ButtonElProps = Omit, 'size' | 'type'>; + +export type PopoverItemProps = BaseProps & LinkElProps & ButtonElProps; +type PopoverItemWrapperProps = LinkElProps & ButtonElProps; + +const wrapperStyles = () => css` + label: popover-item; + display: flex; + justify-content: center; + align-items: flex-start; + width: 100%; +`; + +const PopoverItemWrapper = styled('button')( + listItem, + wrapperStyles, ); + +const iconStyles = (theme: Theme) => css` + label: popover__icon; + margin-right: ${theme.spacings.byte}; +`; + +export const PopoverItem = ({ + children, + icon: Icon, + onClick, + tracking, + ...props +}: PopoverItemProps) => { + const components = useComponents(); + + // Need to typecast here because the PopoverItemWrapper expects a button-like + // component for its `as` prop. It's safe to ignore that constraint here. + /* eslint-disable @typescript-eslint/no-unsafe-assignment */ + const Link = components.Link as any; + + const handleClick = useClickHandler>( + onClick, + tracking, + 'popover-item', + ); + + return ( + + {Icon && } + {children} + + ); +}; diff --git a/packages/circuit-ui/components/Popover/index.tsx b/packages/circuit-ui/components/Popover/index.tsx index d34471a317..6912702d53 100644 --- a/packages/circuit-ui/components/Popover/index.tsx +++ b/packages/circuit-ui/components/Popover/index.tsx @@ -28,6 +28,6 @@ * limitations under the License. */ -import Popover from './Popover'; +import { PopoverItem } from './Popover'; -export default Popover; +export default PopoverItem; diff --git a/packages/circuit-ui/index.ts b/packages/circuit-ui/index.ts index 76eb53272b..bd19c45e3a 100644 --- a/packages/circuit-ui/index.ts +++ b/packages/circuit-ui/index.ts @@ -120,7 +120,6 @@ export { default as ProgressBar } from './components/ProgressBar'; export type { ProgressBarProps } from './components/ProgressBar'; export { default as Tag } from './components/Tag'; export type { TagProps } from './components/Tag'; -export { default as Popover } from './components/Popover'; export { default as Tooltip } from './components/Tooltip'; export { default as BaseStyles } from './components/BaseStyles'; export { From a86f7e635a70eb9fb984c6e31e4fc0841edf1ea6 Mon Sep 17 00:00:00 2001 From: Amela Kodzic Date: Mon, 17 May 2021 17:49:30 +0200 Subject: [PATCH 05/22] Add stories and tests for PopoverItem --- .../components/Popover/Popover.spec.tsx | 65 ++++++++ .../components/Popover/Popover.stories.tsx | 36 +++++ .../__snapshots__/Popover.spec.tsx.snap | 140 ++++++++++++++++++ 3 files changed, 241 insertions(+) create mode 100644 packages/circuit-ui/components/Popover/Popover.spec.tsx create mode 100644 packages/circuit-ui/components/Popover/Popover.stories.tsx create mode 100644 packages/circuit-ui/components/Popover/__snapshots__/Popover.spec.tsx.snap diff --git a/packages/circuit-ui/components/Popover/Popover.spec.tsx b/packages/circuit-ui/components/Popover/Popover.spec.tsx new file mode 100644 index 0000000000..7fcfa005a3 --- /dev/null +++ b/packages/circuit-ui/components/Popover/Popover.spec.tsx @@ -0,0 +1,65 @@ +/** + * Copyright 2020, SumUp Ltd. + * 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 + * + * http://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. + */ + +import { Zap } from '@sumup/icons'; +import React from 'react'; + +import { create, renderToHtml, axe, RenderFn } from '../../util/test-utils'; + +import { PopoverItem, PopoverItemProps } from './Popover'; + +describe('PopoverItem', () => { + function renderPopoverItem( + renderFn: RenderFn, + props: PopoverItemProps, + ) { + return renderFn(); + } + + const baseProps = { children: 'PopoverItem' }; + + describe('styles', () => { + it('should render as Link when an href (and onClick) is passed', () => { + const props = { + ...baseProps, + href: 'https://sumup.com', + onClick: jest.fn(), + icon: Zap, + }; + const actual = renderPopoverItem(create, props); + expect(actual).toMatchSnapshot(); + }); + + it('should render as a `button` when an onClick is passed', () => { + const props = { ...baseProps, onClick: jest.fn(), icon: Zap }; + const actual = renderPopoverItem(create, props); + expect(actual).toMatchSnapshot(); + }); + }); + + describe('accessibility', () => { + it('should meet accessibility guidelines', async () => { + const props = { + ...baseProps, + href: 'https://sumup.com', + onClick: jest.fn(), + icon: Zap, + }; + const wrapper = renderPopoverItem(renderToHtml, props); + const actual = await axe(wrapper); + expect(actual).toHaveNoViolations(); + }); + }); +}); diff --git a/packages/circuit-ui/components/Popover/Popover.stories.tsx b/packages/circuit-ui/components/Popover/Popover.stories.tsx new file mode 100644 index 0000000000..ccba41a89a --- /dev/null +++ b/packages/circuit-ui/components/Popover/Popover.stories.tsx @@ -0,0 +1,36 @@ +/** + * Copyright 2019, SumUp Ltd. + * 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 + * + * http://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. + */ + +import React from 'react'; +import { Zap } from '@sumup/icons'; + +import { PopoverItem } from './Popover'; + +export default { + title: 'Components/Popover', + component: PopoverItem, + argTypes: { + children: { control: 'text' }, + }, +}; + +export const Base = (args) => ; + +Base.args = { + onClick: () => alert('Hello'), + href: 'https://sumup.com/', + children: 'Label', + icon: Zap, +}; diff --git a/packages/circuit-ui/components/Popover/__snapshots__/Popover.spec.tsx.snap b/packages/circuit-ui/components/Popover/__snapshots__/Popover.spec.tsx.snap new file mode 100644 index 0000000000..6c8646a40e --- /dev/null +++ b/packages/circuit-ui/components/Popover/__snapshots__/Popover.spec.tsx.snap @@ -0,0 +1,140 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`PopoverItem styles should render as a \`button\` when an onClick is passed 1`] = ` +.circuit-1 { + background-color: #FFF; + padding: 12px 32px 12px 16px; + border: 0; + color: #1A1A1A; + -webkit-text-decoration: none; + text-decoration: none; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; + -webkit-align-items: flex-start; + -webkit-box-align: flex-start; + -ms-flex-align: flex-start; + align-items: flex-start; + width: 100%; +} + +.circuit-1:hover { + background-color: #F5F5F5; + cursor: pointer; +} + +.circuit-1:active { + background-color: #E6E6E6; +} + +.circuit-1:focus { + background-color: #FFF; + outline: 0; + box-shadow: 0 0 0 4px #AFD0FE; +} + +.circuit-1:focus::-moz-focus-inner { + border: 0; +} + +.circuit-0 { + margin-right: 8px; +} + + +`; + +exports[`PopoverItem styles should render as an \`a\` when an href (and onClick) is passed 1`] = ` +.circuit-1 { + background-color: #FFF; + padding: 12px 32px 12px 16px; + border: 0; + color: #1A1A1A; + -webkit-text-decoration: none; + text-decoration: none; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; + -webkit-align-items: flex-start; + -webkit-box-align: flex-start; + -ms-flex-align: flex-start; + align-items: flex-start; + width: 100%; +} + +.circuit-1:hover { + background-color: #F5F5F5; + cursor: pointer; +} + +.circuit-1:active { + background-color: #E6E6E6; +} + +.circuit-1:focus { + background-color: #FFF; + outline: 0; + box-shadow: 0 0 0 4px #AFD0FE; +} + +.circuit-1:focus::-moz-focus-inner { + border: 0; +} + +.circuit-0 { + margin-right: 8px; +} + + + + + + PopoverItem + +`; From cc99ed2b7384719fe092c7ccd2bdafb1660f0c40 Mon Sep 17 00:00:00 2001 From: Amela Kodzic Date: Thu, 20 May 2021 18:04:29 +0200 Subject: [PATCH 06/22] Upgrade react-popper --- packages/circuit-ui/package.json | 3 +- yarn.lock | 53 +++----------------------------- 2 files changed, 7 insertions(+), 49 deletions(-) diff --git a/packages/circuit-ui/package.json b/packages/circuit-ui/package.json index dfa64ecf66..282ab8a5bb 100644 --- a/packages/circuit-ui/package.json +++ b/packages/circuit-ui/package.json @@ -36,6 +36,7 @@ "static-styles": "cross-env BABEL_ENV=static babel-node --extensions '.js,.ts,.tsx' ./scripts/static-styles/cli.js" }, "dependencies": { + "@popperjs/core": "^2.9.2", "cross-spawn": "^7.0.3", "jscodeshift": "^0.12.0", "lodash": "^4.17.11", @@ -45,7 +46,7 @@ "react-dates": "^21.2.0", "react-modal": "^3.8.1", "react-number-format": "^4.4.1", - "react-popper": "^1.3.3", + "react-popper": "^2.2.5", "tiny-warning": "^1.0.3", "yargs": "^17.0.1" }, diff --git a/yarn.lock b/yarn.lock index 528cce7b63..c3d0edaade 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1215,7 +1215,7 @@ core-js-pure "^3.0.0" regenerator-runtime "^0.13.4" -"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.10.2", "@babel/runtime@^7.10.4", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.13", "@babel/runtime@^7.12.5", "@babel/runtime@^7.13.10", "@babel/runtime@^7.3.1", "@babel/runtime@^7.5.0", "@babel/runtime@^7.5.5", "@babel/runtime@^7.7.2", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2": +"@babel/runtime@^7.0.0", "@babel/runtime@^7.10.2", "@babel/runtime@^7.10.4", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.13", "@babel/runtime@^7.12.5", "@babel/runtime@^7.13.10", "@babel/runtime@^7.3.1", "@babel/runtime@^7.5.0", "@babel/runtime@^7.5.5", "@babel/runtime@^7.7.2", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2": version "7.13.10" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.13.10.tgz#47d42a57b6095f4468da440388fdbad8bebf0d7d" integrity sha512-4QPkjJq6Ns3V/RgpEahRk+AGfL0eO6RHHtTWoNNr5mO49G6B5+X6d6THgWEAvTrznU5xYpbAlVKRYcsCgh/Akw== @@ -1629,14 +1629,6 @@ minimatch "^3.0.4" strip-json-comments "^3.1.1" -"@hypnosphi/create-react-context@^0.3.1": - version "0.3.1" - resolved "https://registry.yarnpkg.com/@hypnosphi/create-react-context/-/create-react-context-0.3.1.tgz#f8bfebdc7665f5d426cba3753e0e9c7d3154d7c6" - integrity sha512-V1klUed202XahrWJLLOT3EXNeCpFHCcJntdFGI15ntCwau+jfT386w7OFTMaCqOgXUH1fa0w/I1oZs+i/Rfr0A== - dependencies: - gud "^1.0.0" - warning "^4.0.3" - "@icons/material@^0.2.4": version "0.2.4" resolved "https://registry.yarnpkg.com/@icons/material/-/material-0.2.4.tgz#e90c9f71768b3736e76d7dd6783fc6c2afa88bc8" @@ -3003,7 +2995,7 @@ schema-utils "^2.6.5" source-map "^0.7.3" -"@popperjs/core@^2.5.4", "@popperjs/core@^2.6.0": +"@popperjs/core@^2.5.4", "@popperjs/core@^2.6.0", "@popperjs/core@^2.9.2": version "2.9.2" resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.9.2.tgz#adea7b6953cbb34651766b0548468e743c6a2353" integrity sha512-VZMYa7+fXHdwIq1TDhSXoVmSPEGM/aa+6Aiq3nVVJ9bXr24zScr+NlKFKC3iPljA7ho/GAZr+d2jOf5GIRC30Q== @@ -7603,18 +7595,6 @@ dedent@^0.7.0: resolved "https://registry.yarnpkg.com/dedent/-/dedent-0.7.0.tgz#2495ddbaf6eb874abb0e1be9df22d2e5a544326c" integrity sha1-JJXduvbrh0q7Dhvp3yLS5aVEMmw= -deep-equal@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.1.1.tgz#b5c98c942ceffaf7cb051e24e1434a25a2e6076a" - integrity sha512-yd9c5AdiqVcR+JjcwUQb9DkhJc8ngNr0MahEBGvDiJw8puWab2yZlh+nkasOnZP+EGTAP6rRp2JzJhJZzvNF8g== - dependencies: - is-arguments "^1.0.4" - is-date-object "^1.0.1" - is-regex "^1.0.4" - object-is "^1.0.1" - object-keys "^1.1.1" - regexp.prototype.flags "^1.2.0" - deep-extend@^0.6.0: version "0.6.0" resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac" @@ -10642,7 +10622,7 @@ is-alphanumerical@^1.0.0: is-alphabetical "^1.0.0" is-decimal "^1.0.0" -is-arguments@^1.0.4, is-arguments@^1.1.0: +is-arguments@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/is-arguments/-/is-arguments-1.1.0.tgz#62353031dfbee07ceb34656a6bde59efecae8dd9" integrity sha512-1Ij4lOMPl/xB5kBDn7I+b2ttPMKa8szhEIrXDuXQD/oe3HJLTLhqhgGspwgyGd6MOywBUqVvYicF72lkgDnIHg== @@ -14695,11 +14675,6 @@ polished@^4.0.5: dependencies: "@babel/runtime" "^7.12.5" -popper.js@^1.14.4: - version "1.16.1" - resolved "https://registry.yarnpkg.com/popper.js/-/popper.js-1.16.1.tgz#2a223cb3dc7b6213d740e40372be40de43e65b1b" - integrity sha512-Wb4p1J4zyFTbM+u6WuO4XstYx4Ky9Cewe4DWrel7B0w6VVICvPwdOpotjzcf6eD8TsckVnIMNONQyPIUFOUbCQ== - posix-character-classes@^0.1.0: version "0.1.1" resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab" @@ -15461,20 +15436,7 @@ react-popper-tooltip@^3.1.1: "@popperjs/core" "^2.5.4" react-popper "^2.2.4" -react-popper@^1.3.3: - version "1.3.11" - resolved "https://registry.yarnpkg.com/react-popper/-/react-popper-1.3.11.tgz#a2cc3f0a67b75b66cfa62d2c409f9dd1fcc71ffd" - integrity sha512-VSA/bS+pSndSF2fiasHK/PTEEAyOpX60+H5EPAjoArr8JGm+oihu4UbrqcEBpQibJxBVCpYyjAX7abJ+7DoYVg== - dependencies: - "@babel/runtime" "^7.1.2" - "@hypnosphi/create-react-context" "^0.3.1" - deep-equal "^1.1.1" - popper.js "^1.14.4" - prop-types "^15.6.1" - typed-styles "^0.0.7" - warning "^4.0.2" - -react-popper@^2.2.4: +react-popper@^2.2.4, react-popper@^2.2.5: version "2.2.5" resolved "https://registry.yarnpkg.com/react-popper/-/react-popper-2.2.5.tgz#1214ef3cec86330a171671a4fbcbeeb65ee58e96" integrity sha512-kxGkS80eQGtLl18+uig1UIf9MKixFSyPxglsgLBxlYnyDf65BiY9B3nZSc6C9XUNDgStROB0fMQlTEz1KxGddw== @@ -15897,7 +15859,7 @@ regex-not@^1.0.0, regex-not@^1.0.2: extend-shallow "^3.0.2" safe-regex "^1.1.0" -regexp.prototype.flags@^1.2.0, regexp.prototype.flags@^1.3.1: +regexp.prototype.flags@^1.3.1: version "1.3.1" resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.3.1.tgz#7ef352ae8d159e758c0eadca6f8fcb4eef07be26" integrity sha512-JiBdRBq91WlY7uRJ0ds7R+dU02i6LKi8r3BuQhNXn+kmeLN+EfHhfjqMRis1zJxnlu88hq/4dx0P2OP3APRTOA== @@ -18061,11 +18023,6 @@ type-is@~1.6.17, type-is@~1.6.18: media-typer "0.3.0" mime-types "~2.1.24" -typed-styles@^0.0.7: - version "0.0.7" - resolved "https://registry.yarnpkg.com/typed-styles/-/typed-styles-0.0.7.tgz#93392a008794c4595119ff62dde6809dbc40a3d9" - integrity sha512-pzP0PWoZUhsECYjABgCGQlRGL1n7tOHsgwYv3oIiEpJwGhFTuty/YNeduxQYzXXa3Ge5BdT6sHYIQYpl4uJ+5Q== - typedarray-to-buffer@^3.1.5: version "3.1.5" resolved "https://registry.yarnpkg.com/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz#a97ee7a9ff42691b9f783ff1bc5112fe3fca9080" From 2d51dc249e047826ff3b792603105580de243440 Mon Sep 17 00:00:00 2001 From: Amela Kodzic Date: Thu, 20 May 2021 18:05:11 +0200 Subject: [PATCH 07/22] Add Popover component and update stories --- .../components/Popover/Popover.stories.tsx | 61 ++++++++++++- .../circuit-ui/components/Popover/Popover.tsx | 90 ++++++++++++++++++- .../circuit-ui/components/Popover/index.tsx | 4 +- 3 files changed, 146 insertions(+), 9 deletions(-) diff --git a/packages/circuit-ui/components/Popover/Popover.stories.tsx b/packages/circuit-ui/components/Popover/Popover.stories.tsx index ccba41a89a..15493ec076 100644 --- a/packages/circuit-ui/components/Popover/Popover.stories.tsx +++ b/packages/circuit-ui/components/Popover/Popover.stories.tsx @@ -13,14 +13,16 @@ * limitations under the License. */ -import React from 'react'; +import React, { useState } from 'react'; import { Zap } from '@sumup/icons'; -import { PopoverItem } from './Popover'; +import Button from '../Button'; + +import { Popover, PopoverItem } from './Popover'; export default { title: 'Components/Popover', - component: PopoverItem, + component: Popover, argTypes: { children: { control: 'text' }, }, @@ -34,3 +36,56 @@ Base.args = { children: 'Label', icon: Zap, }; + +export const Example = (args) => { + const [isOpen, setOpen] = useState(false); + const [referenceElement, setReferenceElement] = useState( + null, + ); + + const handleClick = () => { + setOpen((prev) => !prev); + }; + + const handleClose = () => { + console.log('Work please'); + setOpen((prev) => !prev); + }; + + return ( + <> + + + + ); +}; + +Example.args = { + actions: [ + { + onClick: () => alert('Hello'), + href: 'https://sumup.com/', + children: 'Label', + icon: Zap, + }, + { + onClick: () => alert('Hello'), + href: 'https://sumup.com/', + children: 'Label', + icon: Zap, + }, + { + onClick: () => alert('Hello'), + children: 'Label', + icon: Zap, + }, + { type: 'divider' }, + ], +}; diff --git a/packages/circuit-ui/components/Popover/Popover.tsx b/packages/circuit-ui/components/Popover/Popover.tsx index 46c97c308f..e3bb9a8d64 100644 --- a/packages/circuit-ui/components/Popover/Popover.tsx +++ b/packages/circuit-ui/components/Popover/Popover.tsx @@ -16,13 +16,23 @@ /** @jsx jsx */ import { css, jsx } from '@emotion/core'; import { Theme } from '@sumup/design-tokens'; -import { FC, HTMLProps, ReactNode, SVGProps, MouseEvent } from 'react'; +import { + useState, + FC, + HTMLProps, + ReactNode, + SVGProps, + MouseEvent, +} from 'react'; import { Dispatch as TrackingProps } from '@sumup/collector'; +import { usePopper } from 'react-popper'; +import { Placement } from '@popperjs/core'; -import styled from '../../styles/styled'; +import styled, { StyleProps } from '../../styles/styled'; import { listItem } from '../../styles/style-mixins'; import { useComponents } from '../ComponentsContext'; import useClickHandler from '../../hooks/use-click-handler'; +import Hr from '../Hr'; export interface BaseProps { children: ReactNode; @@ -46,7 +56,7 @@ type ButtonElProps = Omit, 'size' | 'type'>; export type PopoverItemProps = BaseProps & LinkElProps & ButtonElProps; type PopoverItemWrapperProps = LinkElProps & ButtonElProps; -const wrapperStyles = () => css` +const itemWrapperStyles = () => css` label: popover-item; display: flex; justify-content: center; @@ -56,7 +66,7 @@ const wrapperStyles = () => css` const PopoverItemWrapper = styled('button')( listItem, - wrapperStyles, + itemWrapperStyles, ); const iconStyles = (theme: Theme) => css` @@ -95,3 +105,75 @@ export const PopoverItem = ({ ); }; + +const wrapperStyles = ({ theme }: StyleProps) => css` + label: popover; + display: flex; + flex-direction: column; + align-items: flex-start; + padding: ${theme.spacings.byte}; + border: 1px solid #e6e6e6; + box-sizing: border-box; + box-shadow: 0px 3px 8px rgba(0, 0, 0, 0.2); + border-radius: ${theme.spacings.byte}; +`; + +const overlayStyles = () => css` + bottom: 0; + left: 0; + position: fixed; + right: 0; + top: 0; +`; + +const Overlay = styled('div')(overlayStyles); + +const PopoverWrapper = styled('div')(wrapperStyles); + +type Divider = { type: 'divider' }; +type Action = PopoverItemProps | Divider; + +function isDivider(action: Action): action is Divider { + return 'type' in action && action.type === 'divider'; +} + +export interface PopoverProps { + isOpen: boolean; + actions: []; + referenceElement: HTMLElement | null; + placement: Placement; + handleClose: () => any; +} + +export const Popover = ({ + isOpen, + handleClose, + actions, + referenceElement, + placement = 'bottom', + ...props +}: PopoverProps) => { + const [popperElement, setPopperElement] = useState(null); + const { styles, attributes } = usePopper(referenceElement, popperElement, { + placement, + }); + + if (!isOpen) { + return null; + } + + return ( + + + {actions.map((action, index) => + isDivider(action) ?
: , + )} +
+
+ ); +}; diff --git a/packages/circuit-ui/components/Popover/index.tsx b/packages/circuit-ui/components/Popover/index.tsx index 6912702d53..8e95e5779d 100644 --- a/packages/circuit-ui/components/Popover/index.tsx +++ b/packages/circuit-ui/components/Popover/index.tsx @@ -28,6 +28,6 @@ * limitations under the License. */ -import { PopoverItem } from './Popover'; +import { Popover } from './Popover'; -export default PopoverItem; +export default Popover; From be77fc11aa278ed49ba871df3487edba6aeba365 Mon Sep 17 00:00:00 2001 From: Amela Kodzic Date: Tue, 25 May 2021 16:04:37 +0200 Subject: [PATCH 08/22] update popover with overlay --- .../circuit-ui/components/Popover/Popover.tsx | 41 +++++++++---------- 1 file changed, 20 insertions(+), 21 deletions(-) diff --git a/packages/circuit-ui/components/Popover/Popover.tsx b/packages/circuit-ui/components/Popover/Popover.tsx index e3bb9a8d64..cf612ae447 100644 --- a/packages/circuit-ui/components/Popover/Popover.tsx +++ b/packages/circuit-ui/components/Popover/Popover.tsx @@ -116,18 +116,19 @@ const wrapperStyles = ({ theme }: StyleProps) => css` box-sizing: border-box; box-shadow: 0px 3px 8px rgba(0, 0, 0, 0.2); border-radius: ${theme.spacings.byte}; -`; -const overlayStyles = () => css` - bottom: 0; - left: 0; - position: fixed; - right: 0; - top: 0; + ::before { + content: ''; + position: fixed; + bottom: 0; + left: 0; + right: 0; + top: 0; + background: rgba(0, 0, 0, 0.5); + pointer-events: none; + } `; -const Overlay = styled('div')(overlayStyles); - const PopoverWrapper = styled('div')(wrapperStyles); type Divider = { type: 'divider' }; @@ -163,17 +164,15 @@ export const Popover = ({ } return ( - - - {actions.map((action, index) => - isDivider(action) ?
: , - )} -
-
+ + {actions.map((action, index) => + isDivider(action) ?
: , + )} +
); }; From 6a296521bf39b705f93ee7827e0f59089c1f07f6 Mon Sep 17 00:00:00 2001 From: Amela Kodzic Date: Mon, 31 May 2021 16:07:01 +0200 Subject: [PATCH 09/22] Add Overlay and mobile styles --- .../circuit-ui/components/Popover/Popover.tsx | 71 +++++++++++++------ 1 file changed, 49 insertions(+), 22 deletions(-) diff --git a/packages/circuit-ui/components/Popover/Popover.tsx b/packages/circuit-ui/components/Popover/Popover.tsx index cf612ae447..be75a8d2c7 100644 --- a/packages/circuit-ui/components/Popover/Popover.tsx +++ b/packages/circuit-ui/components/Popover/Popover.tsx @@ -23,10 +23,12 @@ import { ReactNode, SVGProps, MouseEvent, + Fragment, } from 'react'; import { Dispatch as TrackingProps } from '@sumup/collector'; import { usePopper } from 'react-popper'; -import { Placement } from '@popperjs/core'; +import { Placement, State, Modifier } from '@popperjs/core'; +import { useTheme } from 'emotion-theming'; import styled, { StyleProps } from '../../styles/styled'; import { listItem } from '../../styles/style-mixins'; @@ -116,21 +118,24 @@ const wrapperStyles = ({ theme }: StyleProps) => css` box-sizing: border-box; box-shadow: 0px 3px 8px rgba(0, 0, 0, 0.2); border-radius: ${theme.spacings.byte}; - - ::before { - content: ''; - position: fixed; - bottom: 0; - left: 0; - right: 0; - top: 0; - background: rgba(0, 0, 0, 0.5); - pointer-events: none; - } + background-color: white; `; const PopoverWrapper = styled('div')(wrapperStyles); +const Overlay = styled.div( + ({ theme }: StyleProps) => css` + ${theme.mq.untilKilo} { + position: fixed; + top: 0; + bottom: 0; + left: 0; + right: 0; + background-color: ${theme.colors.overlay}; + } + `, +); + type Divider = { type: 'divider' }; type Action = PopoverItemProps | Divider; @@ -154,9 +159,28 @@ export const Popover = ({ placement = 'bottom', ...props }: PopoverProps) => { + const theme: Theme = useTheme(); + console.log(window.matchMedia(theme.mq.untilKilo).matches); + const mobilePosition: Modifier<'mobilePosition', { state: State }> = { + name: 'mobilePosition', + enabled: true, + phase: 'write', + fn({ state }) { + if (window.matchMedia(theme.mq.untilKilo).matches) { + state.styles.popper = { + width: '100%', + left: '0px', + right: '0px', + bottom: '0px', + position: 'fixed', + }; + } + }, + }; const [popperElement, setPopperElement] = useState(null); const { styles, attributes } = usePopper(referenceElement, popperElement, { placement, + modifiers: [mobilePosition], }); if (!isOpen) { @@ -164,15 +188,18 @@ export const Popover = ({ } return ( - - {actions.map((action, index) => - isDivider(action) ?
: , - )} -
+ + + + {actions.map((action, index) => + isDivider(action) ?
: , + )} +
+
); }; From d5c3d3476dd15188c04f6a436a357c2112335bc5 Mon Sep 17 00:00:00 2001 From: Amela Kodzic Date: Thu, 10 Jun 2021 18:09:07 +0200 Subject: [PATCH 10/22] Add Popper support for positioning, mobile styles and divider styles --- docs/introduction/getting-started.stories.mdx | 4 +- .../circuit-ui/components/Popover/Popover.tsx | 134 ++++++++++---- packages/circuit-ui/package.json | 4 +- yarn.lock | 164 +++++++++++++++++- 4 files changed, 266 insertions(+), 40 deletions(-) diff --git a/docs/introduction/getting-started.stories.mdx b/docs/introduction/getting-started.stories.mdx index 848c639feb..d58beb3fd8 100644 --- a/docs/introduction/getting-started.stories.mdx +++ b/docs/introduction/getting-started.stories.mdx @@ -28,13 +28,13 @@ yarn add @sumup/circuit-ui npm install @sumup/circuit-ui ``` -Circuit UI relies on some mandatory peer dependencies, namely [@sumup/collector](https://www.npmjs.com/package/@sumup/collector), [@sumup/design-tokens](https://www.npmjs.com/package/@sumup/design-tokens), [@sumup/icons](https://www.npmjs.com/package/@sumup/icons), [@sumup/intl](https://www.npmjs.com/package/@sumup/intl), [React](https://reactjs.org/), and [Emotion](https://emotion.sh/). You should install them with the following command: +Circuit UI relies on some mandatory peer dependencies, namely [@sumup/collector](https://www.npmjs.com/package/@sumup/collector), [@sumup/design-tokens](https://www.npmjs.com/package/@sumup/design-tokens), [@sumup/icons](https://www.npmjs.com/package/@sumup/icons), [@sumup/intl](https://www.npmjs.com/package/@sumup/intl), [React](https://reactjs.org/), [Emotion](https://emotion.sh/), and [react-use](https://streamich.github.io/react-use/). You should install them with the following command: ```sh # With yarn: yarn add @sumup/collector @sumup/design-tokens @sumup/icons react react-dom @emotion/core @emotion/styled emotion-theming # With npm: -npm install --save @sumup/collector @sumup/design-tokens @sumup/icons react react-dom @emotion/core @emotion/styled emotion-theming +npm install --save @sumup/collector @sumup/design-tokens @sumup/icons react react-dom react-use @emotion/core @emotion/styled emotion-theming ``` ### Configuring the theme diff --git a/packages/circuit-ui/components/Popover/Popover.tsx b/packages/circuit-ui/components/Popover/Popover.tsx index be75a8d2c7..fdf2a78315 100644 --- a/packages/circuit-ui/components/Popover/Popover.tsx +++ b/packages/circuit-ui/components/Popover/Popover.tsx @@ -16,7 +16,7 @@ /** @jsx jsx */ import { css, jsx } from '@emotion/core'; import { Theme } from '@sumup/design-tokens'; -import { +import React, { useState, FC, HTMLProps, @@ -24,14 +24,17 @@ import { SVGProps, MouseEvent, Fragment, + useMemo, + RefObject, } from 'react'; +import { useClickAway, useLatest } from 'react-use'; import { Dispatch as TrackingProps } from '@sumup/collector'; import { usePopper } from 'react-popper'; import { Placement, State, Modifier } from '@popperjs/core'; import { useTheme } from 'emotion-theming'; import styled, { StyleProps } from '../../styles/styled'; -import { listItem } from '../../styles/style-mixins'; +import { listItem, textMega } from '../../styles/style-mixins'; import { useComponents } from '../ComponentsContext'; import useClickHandler from '../../hooks/use-click-handler'; import Hr from '../Hr'; @@ -41,7 +44,9 @@ export interface BaseProps { /** * Display an icon in addition to the label. */ - icon: FC>; + icon?: FC>; + + destructive?: boolean; /** * Additional data that is dispatched with the tracking event. */ @@ -63,12 +68,12 @@ const itemWrapperStyles = () => css` display: flex; justify-content: center; align-items: flex-start; - width: 100%; `; const PopoverItemWrapper = styled('button')( listItem, itemWrapperStyles, + textMega, ); const iconStyles = (theme: Theme) => css` @@ -82,7 +87,7 @@ export const PopoverItem = ({ onClick, tracking, ...props -}: PopoverItemProps) => { +}: PopoverItemProps): JSX.Element => { const components = useComponents(); // Need to typecast here because the PopoverItemWrapper expects a button-like @@ -112,17 +117,28 @@ const wrapperStyles = ({ theme }: StyleProps) => css` label: popover; display: flex; flex-direction: column; + justify-content: start; align-items: flex-start; - padding: ${theme.spacings.byte}; - border: 1px solid #e6e6e6; + padding: ${theme.spacings.byte} 0px; + border: 1px solid ${theme.colors.n200}; box-sizing: border-box; box-shadow: 0px 3px 8px rgba(0, 0, 0, 0.2); - border-radius: ${theme.spacings.byte}; - background-color: white; + border-radius: 8px; + background-color: ${theme.colors.white}; + + ${theme.mq.untilKilo} { + border-bottom-right-radius: 0; + border-bottom-left-radius: 0; + } `; const PopoverWrapper = styled('div')(wrapperStyles); +const dividerStyles = (theme: Theme) => css` + margin: ${theme.spacings.byte} ${theme.spacings.mega}; + width: calc(100% - ${theme.spacings.mega}*2); +`; + const Overlay = styled.div( ({ theme }: StyleProps) => css` ${theme.mq.untilKilo} { @@ -145,42 +161,84 @@ function isDivider(action: Action): action is Divider { export interface PopoverProps { isOpen: boolean; - actions: []; - referenceElement: HTMLElement | null; - placement: Placement; - handleClose: () => any; + actions: Action[]; + referenceElement: RefObject; + placement?: Placement; + onClose: (event: Event) => void; } export const Popover = ({ isOpen, - handleClose, + onClose, actions, referenceElement, placement = 'bottom', ...props -}: PopoverProps) => { +}: PopoverProps): JSX.Element | null => { const theme: Theme = useTheme(); - console.log(window.matchMedia(theme.mq.untilKilo).matches); - const mobilePosition: Modifier<'mobilePosition', { state: State }> = { - name: 'mobilePosition', - enabled: true, - phase: 'write', - fn({ state }) { - if (window.matchMedia(theme.mq.untilKilo).matches) { - state.styles.popper = { - width: '100%', - left: '0px', - right: '0px', - bottom: '0px', - position: 'fixed', - }; - } - }, - }; + + // Popper custom modifier to apply bottom sheet for mobile. + // The window.matchMedia() is a useful API for this, it allows you to change the styles based on a condition. + // useMemo hook is used in order to prevent the render loop, more https://popper.js.org/react-popper/v2/faq/#why-i-get-render-loop-whenever-i-put-a-function-inside-the-popper-configuration + const mobilePosition: Modifier<'mobilePosition', { state: State }> = useMemo( + () => ({ + name: 'mobilePosition', + enabled: true, + phase: 'write', + fn({ state }) { + if (window.matchMedia(`${theme.breakpoints.untilKilo}`).matches) { + // eslint-disable-next-line no-param-reassign + state.styles.popper = { + width: '100%', + left: '0px', + right: '0px', + bottom: '0px', + position: 'fixed', + }; + } else { + // eslint-disable-next-line no-param-reassign + state.styles.popper.width = 'auto'; + } + }, + }), + [theme], + ); + // Note: the usePopper hook intentionally takes the DOM node, not refs, in order to be able to update when the nodes change. + // A callback ref is used here to permit this behaviour, and useState is an appropriate way to implement this. const [popperElement, setPopperElement] = useState(null); - const { styles, attributes } = usePopper(referenceElement, popperElement, { - placement, - modifiers: [mobilePosition], + const { styles, attributes } = usePopper( + referenceElement.current, + popperElement, + { + placement, + modifiers: [ + mobilePosition, + // The flip modifier is used if the popper has placement set to bottom, but there isn't enough space to position the popper in that direction. + // By default, it will change the popper placement to top. As soon as enough space is detected, the placement will be reverted to the originally defined (or preferred) one. + // You can also define fallback placements by providing a list of placements to try. When no space is available on the preferred placement, the modifier will test the ones provided in the list, and use the first useful one. + { + name: 'flip', + options: { + fallbackPlacements: ['top', 'right', 'left'], + }, + }, + ], + }, + ); + + // This is a performance optimization to prevent event listeners from being + // re-attached on every render. + const popperRef = useLatest(popperElement); + + useClickAway(popperRef, (event) => { + // The reference element has its own click handler to toggle the popover. + if ( + !referenceElement.current || + referenceElement.current.contains(event.target as Node) + ) { + return; + } + onClose(event); }); if (!isOpen) { @@ -197,7 +255,11 @@ export const Popover = ({ {...attributes.popper} > {actions.map((action, index) => - isDivider(action) ?
: , + isDivider(action) ? ( +
+ ) : ( + + ), )} diff --git a/packages/circuit-ui/package.json b/packages/circuit-ui/package.json index 282ab8a5bb..993eb2d673 100644 --- a/packages/circuit-ui/package.json +++ b/packages/circuit-ui/package.json @@ -88,6 +88,7 @@ "react-dom": "^17.0.1", "react-svg-loader": "^3.0.3", "react-swipeable": "^6.1.0", + "react-use": "^17.2.4", "style-loader": "^2.0.0", "ts-jest": "^26.4.4", "ts-loader": "^9.0.0", @@ -105,6 +106,7 @@ "@sumup/intl": "1.x", "emotion-theming": "10.x", "react": ">=16.8.0 <18.0.0", - "react-dom": ">=16.8.0 <18.0.0" + "react-dom": ">=16.8.0 <18.0.0", + "react-use": "17x" } } diff --git a/yarn.lock b/yarn.lock index c3d0edaade..ff6a3d4091 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1222,6 +1222,13 @@ dependencies: regenerator-runtime "^0.13.4" +"@babel/runtime@^7.1.2": + version "7.14.0" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.14.0.tgz#46794bc20b612c5f75e62dd071e24dfd95f1cbe6" + integrity sha512-JELkvo/DlpNdJ7dlyw/eY7E0suy5i5GQH+Vlxaq1nsNJ+H7f4Vtv3jMeCEgRhZZQFXTjldYfQgv2qmM6M1v5wA== + dependencies: + regenerator-runtime "^0.13.4" + "@babel/template@^7.12.13", "@babel/template@^7.12.7", "@babel/template@^7.3.3": version "7.12.13" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.12.13.tgz#530265be8a2589dbb37523844c5bcb55947fb327" @@ -4318,6 +4325,11 @@ resolved "https://registry.yarnpkg.com/@types/jest/-/jest-23.3.14.tgz#37daaf78069e7948520474c87b80092ea912520a" integrity sha512-Q5hTcfdudEL2yOmluA1zaSyPbzWPmJ3XfSWeP3RyoYvS9hnje1ZyagrZOuQ6+1nQC1Gw+7gap3pLNL3xL6UBug== +"@types/js-cookie@^2.2.6": + version "2.2.6" + resolved "https://registry.yarnpkg.com/@types/js-cookie/-/js-cookie-2.2.6.tgz#f1a1cb35aff47bc5cfb05cb0c441ca91e914c26f" + integrity sha512-+oY0FDTO2GYKEV0YPvSshGq9t7YozVkgvXLty7zogQNuCxBhT9/3INX9Q7H1aRZ4SUDRXAKlJuA4EA5nTt7SNw== + "@types/jscodeshift@^0.11.0": version "0.11.0" resolved "https://registry.yarnpkg.com/@types/jscodeshift/-/jscodeshift-0.11.0.tgz#7224cf1a4d0383b4fb2694ffed52f57b45c3325b" @@ -4863,6 +4875,11 @@ "@webassemblyjs/wast-parser" "1.9.0" "@xtuc/long" "4.2.2" +"@xobotyi/scrollbar-width@^1.9.5": + version "1.9.5" + resolved "https://registry.yarnpkg.com/@xobotyi/scrollbar-width/-/scrollbar-width-1.9.5.tgz#80224a6919272f405b87913ca13b92929bdf3c4d" + integrity sha512-N8tkAACJx2ww8vFMneJmaAgmjAG1tnVBZJRLRcx061tmsLRZHSEZSLuGWnwPtunsSLvSqXQ2wfp7Mgqg1I+2dQ== + "@xstate/react@1.0.0-rc.3": version "1.0.0-rc.3" resolved "https://registry.yarnpkg.com/@xstate/react/-/react-1.0.0-rc.3.tgz#a5974c651849604636fd9ed4a371f8fac538b8d6" @@ -7291,6 +7308,14 @@ css-color-keywords@^1.0.0: resolved "https://registry.yarnpkg.com/css-color-keywords/-/css-color-keywords-1.0.0.tgz#fea2616dc676b2962686b3af8dbdbe180b244e05" integrity sha1-/qJhbcZ2spYmhrOvjb2+GAskTgU= +css-in-js-utils@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/css-in-js-utils/-/css-in-js-utils-2.0.1.tgz#3b472b398787291b47cfe3e44fecfdd9e914ba99" + integrity sha512-PJF0SpJT+WdbVVt0AOYp9C8GnuruRlL/UFW7932nLWmFLQTaWEzTBQEx7/hn4BuV+WON75iAViSUJLiU3PKbpA== + dependencies: + hyphenate-style-name "^1.0.2" + isobject "^3.0.1" + css-loader@^3.6.0: version "3.6.0" resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-3.6.0.tgz#2e4b2c7e6e2d27f8c8f28f61bffcd2e6c91ef645" @@ -7450,6 +7475,11 @@ csstype@^3.0.2: resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.0.7.tgz#2a5fb75e1015e84dd15692f71e89a1450290950b" integrity sha512-KxnUB0ZMlnUWCsx2Z8MUsr6qV6ja1w9ArPErJaJaF8a5SOWoHLIszeCTKGRGRgtLgYrs1E8CHkNSP1VZTTPc9g== +csstype@^3.0.6: + version "3.0.8" + resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.0.8.tgz#d2266a792729fb227cd216fb572f43728e1ad340" + integrity sha512-jXKhWqXPmlUeoQnF/EhTtTl4C9SnrxSH/jZUih3jmO6lBKr99rP3/+FmrMj4EFpOXzMtXHAZkd3x0E6h6Fgflw== + csv-generate@^3.3.0: version "3.3.0" resolved "https://registry.yarnpkg.com/csv-generate/-/csv-generate-3.3.0.tgz#0e25658f1bb9806d94fec7b270896a35c7eedf1a" @@ -8991,11 +9021,21 @@ fast-levenshtein@^2.0.6, fast-levenshtein@~2.0.6: resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc= +fast-shallow-equal@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fast-shallow-equal/-/fast-shallow-equal-1.0.0.tgz#d4dcaf6472440dcefa6f88b98e3251e27f25628b" + integrity sha512-HPtaa38cPgWvaCFmRNhlc6NG7pv6NUHqjPgVAkWGoB9mQMwYB27/K0CvOM5Czy+qpT3e8XJ6Q4aPAnzpNpzNaw== + fast-xml-parser@^3.12.17: version "3.16.0" resolved "https://registry.yarnpkg.com/fast-xml-parser/-/fast-xml-parser-3.16.0.tgz#d905e7e6b28fc4648cabebcb074363867fb56ee2" integrity sha512-U+bpScacfgnfNfIKlWHDu4u6rtOaCyxhblOLJ8sZPkhsjgGqdZmVPBhdOyvdMGCDt8CsAv+cssOP3NzQptNt2w== +fastest-stable-stringify@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/fastest-stable-stringify/-/fastest-stable-stringify-2.0.2.tgz#3757a6774f6ec8de40c4e86ec28ea02417214c76" + integrity sha512-bijHueCGd0LqqNK9b5oCMHc0MluJAx0cwqASgbWMvkO01lCYgIhacVRLcaDz3QnyYIRNJRDwMb41VuT6pHJ91Q== + fastq@^1.6.0: version "1.11.0" resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.11.0.tgz#bb9fb955a07130a918eb63c1f5161cc32a5d0858" @@ -10319,6 +10359,11 @@ husky@^4.0.0: slash "^3.0.0" which-pm-runs "^1.0.0" +hyphenate-style-name@^1.0.2: + version "1.0.4" + resolved "https://registry.yarnpkg.com/hyphenate-style-name/-/hyphenate-style-name-1.0.4.tgz#691879af8e220aea5750e8827db4ef62a54e361d" + integrity sha512-ygGZLjmXfPHj+ZWh6LwbC37l43MhfztxetbFCoYTM2VjkIUpeHgSNn7QIyVFj7YQ1Wl9Cbw5sholVJPzWvC2MQ== + iconv-lite@0.4.24, iconv-lite@^0.4.17, iconv-lite@^0.4.24: version "0.4.24" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" @@ -10484,6 +10529,13 @@ inline-style-parser@0.1.1: resolved "https://registry.yarnpkg.com/inline-style-parser/-/inline-style-parser-0.1.1.tgz#ec8a3b429274e9c0a1f1c4ffa9453a7fef72cea1" integrity sha512-7NXolsK4CAS5+xvdj5OMMbI962hU/wvwoxk+LWR9Ek9bVtyuuYScDN6eS0rUm6TxApFpw7CX1o4uJzcd4AyD3Q== +inline-style-prefixer@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/inline-style-prefixer/-/inline-style-prefixer-6.0.0.tgz#f73d5dbf2855733d6b153a4d24b7b47a73e9770b" + integrity sha512-XTHvRUS4ZJNzC1GixJRmOlWSS45fSt+DJoyQC9ytj0WxQfcgofQtDtyKKYxHUqEsWCs+LIWftPF1ie7+i012Fg== + dependencies: + css-in-js-utils "^2.0.0" + inquirer@^3.3.0: version "3.3.0" resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-3.3.0.tgz#9dd2f2ad765dcab1ff0443b491442a20ba227dc9" @@ -11718,6 +11770,11 @@ jest@^26.6.3: import-local "^3.0.2" jest-cli "^26.6.3" +js-cookie@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/js-cookie/-/js-cookie-2.2.1.tgz#69e106dc5d5806894562902aa5baec3744e9b2b8" + integrity sha512-HvdH2LzI/EAZcUwA8+0nKNtWHqS+ZmijLA30RwZA0bo7ToCckjK5MkGhjED9KoRcXO6BaGI3I9UIzSA1FKFPOQ== + js-string-escape@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/js-string-escape/-/js-string-escape-1.0.1.tgz#e2625badbc0d67c7533e9edc1068c587ae4137ef" @@ -13274,6 +13331,20 @@ nan@^2.12.1: resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.2.tgz#f5376400695168f4cc694ac9393d0c9585eeea19" integrity sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ== +nano-css@^5.3.1: + version "5.3.1" + resolved "https://registry.yarnpkg.com/nano-css/-/nano-css-5.3.1.tgz#b709383e07ad3be61f64edffacb9d98250b87a1f" + integrity sha512-ENPIyNzANQRyYVvb62ajDd7PAyIgS2LIUnT9ewih4yrXSZX4hKoUwssy8WjUH++kEOA5wUTMgNnV7ko5n34kUA== + dependencies: + css-tree "^1.1.2" + csstype "^3.0.6" + fastest-stable-stringify "^2.0.2" + inline-style-prefixer "^6.0.0" + rtl-css-js "^1.14.0" + sourcemap-codec "^1.4.8" + stacktrace-js "^2.0.2" + stylis "^4.0.6" + nanoid@^3.1.23: version "3.1.23" resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.23.tgz#f744086ce7c2bc47ee0a8472574d5c78e4183a81" @@ -15512,6 +15583,31 @@ react-textarea-autosize@^8.1.1, react-textarea-autosize@^8.3.0: use-composed-ref "^1.0.0" use-latest "^1.0.0" +react-universal-interface@^0.6.2: + version "0.6.2" + resolved "https://registry.yarnpkg.com/react-universal-interface/-/react-universal-interface-0.6.2.tgz#5e8d438a01729a4dbbcbeeceb0b86be146fe2b3b" + integrity sha512-dg8yXdcQmvgR13RIlZbTRQOoUrDciFVoSBZILwjE2LFISxZZ8loVJKAkuzswl5js8BHda79bIb2b84ehU8IjXw== + +react-use@^17.2.4: + version "17.2.4" + resolved "https://registry.yarnpkg.com/react-use/-/react-use-17.2.4.tgz#1f89be3db0a8237c79253db0a15e12bbe3cfeff1" + integrity sha512-vQGpsAM0F5UIlshw5UI8ULGPS4yn5rm7/qvn3T1Gnkrz7YRMEEMh+ynKcmRloOyiIeLvKWiQjMiwRGtdbgs5qQ== + dependencies: + "@types/js-cookie" "^2.2.6" + "@xobotyi/scrollbar-width" "^1.9.5" + copy-to-clipboard "^3.3.1" + fast-deep-equal "^3.1.3" + fast-shallow-equal "^1.0.0" + js-cookie "^2.2.1" + nano-css "^5.3.1" + react-universal-interface "^0.6.2" + resize-observer-polyfill "^1.5.1" + screenfull "^5.1.0" + set-harmonic-interval "^1.0.1" + throttle-debounce "^3.0.1" + ts-easing "^0.2.0" + tslib "^2.1.0" + react-with-direction@^1.3.1: version "1.3.1" resolved "https://registry.yarnpkg.com/react-with-direction/-/react-with-direction-1.3.1.tgz#9fd414564f0ffe6947e5ff176f6132dd83f8b8df" @@ -16076,6 +16172,11 @@ reselect@^4.0.0: resolved "https://registry.yarnpkg.com/reselect/-/reselect-4.0.0.tgz#f2529830e5d3d0e021408b246a206ef4ea4437f7" integrity sha512-qUgANli03jjAyGlnbYVAV5vvnOmJnODyABz51RdBN7M4WaVu8mecZWgyQNkG8Yqe3KRGRt0l4K4B3XVEULC4CA== +resize-observer-polyfill@^1.5.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz#0e9020dd3d21024458d4ebd27e23e40269810464" + integrity sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg== + resolve-cwd@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-3.0.0.tgz#0f0075f1bb2544766cf73ba6a6e2adfebcb13f2d" @@ -16187,6 +16288,13 @@ rsvp@^4.8.4: resolved "https://registry.yarnpkg.com/rsvp/-/rsvp-4.8.5.tgz#c8f155311d167f68f21e168df71ec5b083113734" integrity sha512-nfMOlASu9OnRJo1mbEk2cz0D56a1MBNrJ7orjRZQG10XDyuvwksKbuXNp6qa+kbn839HwjwhBzhFmdsaEAfauA== +rtl-css-js@^1.14.0: + version "1.14.1" + resolved "https://registry.yarnpkg.com/rtl-css-js/-/rtl-css-js-1.14.1.tgz#f79781d6a0c510abe73fde60aa3cbe9dfd134a45" + integrity sha512-G9N1s/6329FpJr8k9e1U/Lg0IDWThv99sb7k0IrXHjSnubxe01h52/ajsPRafJK1/2Vqrhz3VKLe3E1dx6jS9Q== + dependencies: + "@babel/runtime" "^7.1.2" + run-async@^2.2.0, run-async@^2.4.0: version "2.4.1" resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.4.1.tgz#8440eccf99ea3e70bd409d49aab88e10c189a455" @@ -16330,6 +16438,11 @@ schema-utils@^3.0.0: ajv "^6.12.5" ajv-keywords "^3.5.2" +screenfull@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/screenfull/-/screenfull-5.1.0.tgz#85c13c70f4ead4c1b8a935c70010dfdcd2c0e5c8" + integrity sha512-dYaNuOdzr+kc6J6CFcBrzkLCfyGcMg+gWkJ8us93IQ7y1cevhQAugFsaCdMHb6lw8KV3xPzSxzH7zM1dQap9mA== + select@^1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/select/-/select-1.1.2.tgz#0e7350acdec80b1108528786ec1d4418d11b396d" @@ -16468,6 +16581,11 @@ set-blocking@^2.0.0, set-blocking@~2.0.0: resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc= +set-harmonic-interval@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/set-harmonic-interval/-/set-harmonic-interval-1.0.1.tgz#e1773705539cdfb80ce1c3d99e7f298bb3995249" + integrity sha512-AhICkFV84tBP1aWqPwLZqFvAwqEoVA9kxNMniGEUvzOlm4vLmOFLiTT3UZ6bziJTy4bOVpzWGTfSCbmaayGx8g== + set-value@^2.0.0, set-value@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/set-value/-/set-value-2.0.1.tgz#a18d40530e6f07de4228c7defe4227af8cad005b" @@ -16743,6 +16861,11 @@ source-map-url@^0.4.0: resolved "https://registry.yarnpkg.com/source-map-url/-/source-map-url-0.4.1.tgz#0af66605a745a5a2f91cf1bbf8a7afbc283dec56" integrity sha512-cPiFOTLUKvJFIg4SKVScy4ilPPW6rFgMgfuZJPNoDuMs3nC1HbMUycBoJw77xFIp6z1UJQJOfx6C9GMH80DiTw== +source-map@0.5.6: + version "0.5.6" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.6.tgz#75ce38f52bf0733c5a7f0c118d81334a2bb5f412" + integrity sha1-dc449SvwczxafwwRjYEzSiu19BI= + source-map@^0.5.0, source-map@^0.5.6, source-map@^0.5.7: version "0.5.7" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" @@ -16758,6 +16881,11 @@ source-map@^0.7.3: resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.3.tgz#5302f8169031735226544092e64981f751750383" integrity sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ== +sourcemap-codec@^1.4.8: + version "1.4.8" + resolved "https://registry.yarnpkg.com/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz#ea804bd94857402e6992d05a38ef1ae35a9ab4c4" + integrity sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA== + space-separated-tokens@^1.0.0: version "1.1.5" resolved "https://registry.yarnpkg.com/space-separated-tokens/-/space-separated-tokens-1.1.5.tgz#85f32c3d10d9682007e917414ddc5c26d1aa6899" @@ -16902,6 +17030,13 @@ stable@^0.1.8: resolved "https://registry.yarnpkg.com/stable/-/stable-0.1.8.tgz#836eb3c8382fe2936feaf544631017ce7d47a3cf" integrity sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w== +stack-generator@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/stack-generator/-/stack-generator-2.0.5.tgz#fb00e5b4ee97de603e0773ea78ce944d81596c36" + integrity sha512-/t1ebrbHkrLrDuNMdeAcsvynWgoH/i4o8EGGfX7dEYDoTXOYVAkEpFdtshlvabzc6JlJ8Kf9YdFEoz7JkzGN9Q== + dependencies: + stackframe "^1.1.1" + stack-utils@^1.0.1: version "1.0.4" resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-1.0.4.tgz#4b600971dcfc6aed0cbdf2a8268177cc916c87c8" @@ -16921,6 +17056,23 @@ stackframe@^1.1.1: resolved "https://registry.yarnpkg.com/stackframe/-/stackframe-1.2.0.tgz#52429492d63c62eb989804c11552e3d22e779303" integrity sha512-GrdeshiRmS1YLMYgzF16olf2jJ/IzxXY9lhKOskuVziubpTYcYqyOwYeJKzQkwy7uN0fYSsbsC4RQaXf9LCrYA== +stacktrace-gps@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/stacktrace-gps/-/stacktrace-gps-3.0.4.tgz#7688dc2fc09ffb3a13165ebe0dbcaf41bcf0c69a" + integrity sha512-qIr8x41yZVSldqdqe6jciXEaSCKw1U8XTXpjDuy0ki/apyTn/r3w9hDAAQOhZdxvsC93H+WwwEu5cq5VemzYeg== + dependencies: + source-map "0.5.6" + stackframe "^1.1.1" + +stacktrace-js@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/stacktrace-js/-/stacktrace-js-2.0.2.tgz#4ca93ea9f494752d55709a081d400fdaebee897b" + integrity sha512-Je5vBeY4S1r/RnLydLl0TBTi3F2qdfWmYsGvtfZgEI+SCprPppaIhQf5nGcal4gI4cGpCV/duLcAzT1np6sQqg== + dependencies: + error-stack-parser "^2.0.6" + stack-generator "^2.0.5" + stacktrace-gps "^3.0.4" + state-toggle@^1.0.0: version "1.0.3" resolved "https://registry.yarnpkg.com/state-toggle/-/state-toggle-1.0.3.tgz#e123b16a88e143139b09c6852221bc9815917dfe" @@ -17278,6 +17430,11 @@ styled-components@^5.1.1: shallowequal "^1.1.0" supports-color "^5.5.0" +stylis@^4.0.6: + version "4.0.10" + resolved "https://registry.yarnpkg.com/stylis/-/stylis-4.0.10.tgz#446512d1097197ab3f02fb3c258358c3f7a14240" + integrity sha512-m3k+dk7QeJw660eIKRRn3xPF6uuvHs/FFzjX3HQ5ove0qYsiygoAhwn5a3IYKaZPo5LrYD0rfVmtv1gNY1uYwg== + supports-color@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7" @@ -17810,6 +17967,11 @@ ts-dedent@^2.0.0: resolved "https://registry.yarnpkg.com/ts-dedent/-/ts-dedent-2.1.1.tgz#6dd56870bb5493895171334fa5d7e929107e5bbc" integrity sha512-riHuwnzAUCfdIeTBNUq7+Yj+ANnrMXo/7+Z74dIdudS7ys2k8aSGMzpJRMFDF7CLwUTbtvi1ZZff/Wl+XxmqIA== +ts-easing@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/ts-easing/-/ts-easing-0.2.0.tgz#c8a8a35025105566588d87dbda05dd7fbfa5a4ec" + integrity sha512-Z86EW+fFFh/IFB1fqQ3/+7Zpf9t2ebOAxNI/V6Wo7r5gqiqtxmgTlQ1qbqQcjLKYeSHPTsEmvlJUDg/EuL0uHQ== + ts-essentials@^2.0.3: version "2.0.12" resolved "https://registry.yarnpkg.com/ts-essentials/-/ts-essentials-2.0.12.tgz#c9303f3d74f75fa7528c3d49b80e089ab09d8745" @@ -17900,7 +18062,7 @@ tslib@^1.8.1, tslib@^1.9.0: resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== -tslib@^2.0.0, tslib@^2.0.3: +tslib@^2.0.0, tslib@^2.0.3, tslib@^2.1.0: version "2.2.0" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.2.0.tgz#fb2c475977e35e241311ede2693cee1ec6698f5c" integrity sha512-gS9GVHRU+RGn5KQM2rllAlR3dU6m7AcpJKdtH8gFvQiC4Otgk98XnmMU+nZenHt/+VhnBPWwgrJsyrdcw6i23w== From 82c5ec149e839741529824b9362ba16d1264feee Mon Sep 17 00:00:00 2001 From: Amela Kodzic Date: Thu, 10 Jun 2021 18:09:45 +0200 Subject: [PATCH 11/22] Add destructive option in listItem style mixin --- packages/circuit-ui/styles/style-mixins.ts | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/packages/circuit-ui/styles/style-mixins.ts b/packages/circuit-ui/styles/style-mixins.ts index 3d62c7f9dc..8cce8447ee 100644 --- a/packages/circuit-ui/styles/style-mixins.ts +++ b/packages/circuit-ui/styles/style-mixins.ts @@ -314,7 +314,6 @@ export const inputOutline = ( focus: theme.colors.p500, active: theme.colors.p500, }; - break; } } @@ -335,14 +334,28 @@ export const inputOutline = ( `; }; -export const listItem = (args: ThemeArgs): SerializedStyles => { +/** + * Visually communicates that the listItem (eg. Popover or Dropdown component) is hovered, active or focused. + */ +export const listItem = ( + args: + | Theme + | { + theme: Theme; + destructive?: boolean; + }, +): SerializedStyles => { const theme = getTheme(args); + const options = isTheme(args) ? { destructive: false } : args; + return css` background-color: ${theme.colors.white}; padding: ${theme.spacings.kilo} ${theme.spacings.tera} ${theme.spacings.kilo} ${theme.spacings.mega}; border: 0; - color: ${theme.colors.bodyColor}; + color: ${options.destructive + ? theme.colors.danger + : theme.colors.bodyColor}; text-decoration: none; &:hover { @@ -357,6 +370,7 @@ export const listItem = (args: ThemeArgs): SerializedStyles => { &:focus { background-color: ${theme.colors.white}; ${focusOutline({ theme })}; + z-index: ${theme.zIndex.absolute}; } `; }; From a514c8183b8bb38b8946f1522a1ad0cf314f783d Mon Sep 17 00:00:00 2001 From: Amela Kodzic Date: Thu, 10 Jun 2021 18:10:37 +0200 Subject: [PATCH 12/22] Add unit tests and stories --- .../components/Popover/Popover.docs.mdx | 14 + .../components/Popover/Popover.spec.tsx | 167 +++- .../components/Popover/Popover.stories.tsx | 30 +- .../__snapshots__/Popover.spec.tsx.snap | 784 +++++++++++++++++- 4 files changed, 969 insertions(+), 26 deletions(-) create mode 100644 packages/circuit-ui/components/Popover/Popover.docs.mdx diff --git a/packages/circuit-ui/components/Popover/Popover.docs.mdx b/packages/circuit-ui/components/Popover/Popover.docs.mdx new file mode 100644 index 0000000000..0a741e5801 --- /dev/null +++ b/packages/circuit-ui/components/Popover/Popover.docs.mdx @@ -0,0 +1,14 @@ +import { Status, Props, Story } from '../../../../.storybook/components'; +import { Popover } from '.'; + +# Popover + +The popover component displays a list of subsequent action options, when interacting wuith an actionable component. + + + + + +## Usage guidelines + +Triggers of Popover can be primary, secondary, tertiary buttons, an overflow icons, or components with embedded buttons such as image upload component. diff --git a/packages/circuit-ui/components/Popover/Popover.spec.tsx b/packages/circuit-ui/components/Popover/Popover.spec.tsx index 7fcfa005a3..c681679974 100644 --- a/packages/circuit-ui/components/Popover/Popover.spec.tsx +++ b/packages/circuit-ui/components/Popover/Popover.spec.tsx @@ -13,12 +13,24 @@ * limitations under the License. */ -import { Zap } from '@sumup/icons'; -import React from 'react'; +import { Bin, CirclePlus, PenStroke, ThumbUp, Zap } from '@sumup/icons'; +import { fireEvent } from '@testing-library/dom'; +import React, { useRef, useState } from 'react'; -import { create, renderToHtml, axe, RenderFn } from '../../util/test-utils'; +import { + create, + renderToHtml, + axe, + RenderFn, + render, + act, + userEvent, +} from '../../util/test-utils'; +import Button from '../Button'; -import { PopoverItem, PopoverItemProps } from './Popover'; +import { PopoverItem, PopoverItemProps, Popover } from './Popover'; + +const placements = ['auto', 'top', 'bottom', 'left', 'right']; describe('PopoverItem', () => { function renderPopoverItem( @@ -63,3 +75,150 @@ describe('PopoverItem', () => { }); }); }); + +describe('Popover', () => { + const Default = (props) => { + const referenceElement = useRef( + null, + ); + + return ( + <> + + + + ); + }; + + const Interactive = () => { + const [isOpen, setOpen] = useState(true); + const referenceElement = useRef( + null, + ); + + const handleClick = () => { + setOpen((prev) => !prev); + }; + + const onClose = () => { + setOpen(false); + }; + + return ( + <> + + alert('Hello'), + children: 'Add', + icon: CirclePlus, + }, + { + onClick: () => alert('Hello'), + children: 'Edit', + icon: PenStroke, + }, + { type: 'divider' }, + { + onClick: () => alert('Hello'), + children: 'Delete', + icon: Bin, + destructive: true, + }, + ]} + onClose={onClose} + isOpen={isOpen} + referenceElement={referenceElement} + /> + + ); + }; + + describe('styles', () => { + it('should render with default styles', () => { + const props = { + actions: [ + { + onClick: () => alert('Hello'), + children: 'Add', + icon: CirclePlus, + }, + ], + isOpen: true, + onClose: jest.fn(), + }; + + const actual = create(); + expect(actual).toMatchSnapshot(); + }); + + placements.forEach((placement) => { + it(`should render popover on ${placement}`, () => { + const props = { + actions: [ + { + onClick: () => alert('Hello'), + children: 'Add', + icon: CirclePlus, + }, + ], + isOpen: true, + onClose: jest.fn(), + }; + const actual = create(); + expect(actual).toMatchSnapshot(); + }); + }); + + it('should render popover when isOpen=true', () => { + const { queryByText } = render(); + expect(queryByText('Edit')).not.toBeNull(); + }); + + it('should close popover when passing a click outside', () => { + const { queryByText } = render(); + expect(queryByText('Edit')).not.toBeNull(); + + userEvent.click(document.body); + + expect(queryByText('Add')).toBeNull(); + }); + + it('should close popover when passing a click to a reference element', () => { + const { queryByText, getAllByRole } = render(); + expect(queryByText('Edit')).not.toBeNull(); + + act(() => { + fireEvent.click(getAllByRole('button')[0]); + }); + + expect(queryByText('Add')).toBeNull(); + }); + + it('should render nothing when isOpen=false', () => { + const props = { + actions: [ + { + onClick: () => alert('Hello'), + children: 'Add', + icon: CirclePlus, + }, + ], + isOpen: false, + onClose: jest.fn(), + }; + + const { queryByTestId } = render(); + expect(queryByTestId('popover-child')).toBeNull(); + }); + }); +}); diff --git a/packages/circuit-ui/components/Popover/Popover.stories.tsx b/packages/circuit-ui/components/Popover/Popover.stories.tsx index 15493ec076..413589693f 100644 --- a/packages/circuit-ui/components/Popover/Popover.stories.tsx +++ b/packages/circuit-ui/components/Popover/Popover.stories.tsx @@ -13,8 +13,8 @@ * limitations under the License. */ -import React, { useState } from 'react'; -import { Zap } from '@sumup/icons'; +import React, { useState, useRef } from 'react'; +import { ThumbUp, Zap } from '@sumup/icons'; import Button from '../Button'; @@ -39,27 +39,30 @@ Base.args = { export const Example = (args) => { const [isOpen, setOpen] = useState(false); - const [referenceElement, setReferenceElement] = useState( - null, - ); + const referenceElement = useRef(null); const handleClick = () => { setOpen((prev) => !prev); }; - const handleClose = () => { - console.log('Work please'); - setOpen((prev) => !prev); + const onClose = () => { + setOpen(false); }; return ( <> - @@ -71,7 +74,7 @@ Example.args = { actions: [ { onClick: () => alert('Hello'), - href: 'https://sumup.com/', + href: '', children: 'Label', icon: Zap, }, @@ -81,11 +84,12 @@ Example.args = { children: 'Label', icon: Zap, }, + { type: 'divider' }, { onClick: () => alert('Hello'), children: 'Label', icon: Zap, + destructive: true, }, - { type: 'divider' }, ], }; diff --git a/packages/circuit-ui/components/Popover/__snapshots__/Popover.spec.tsx.snap b/packages/circuit-ui/components/Popover/__snapshots__/Popover.spec.tsx.snap index 6c8646a40e..e96d31a645 100644 --- a/packages/circuit-ui/components/Popover/__snapshots__/Popover.spec.tsx.snap +++ b/packages/circuit-ui/components/Popover/__snapshots__/Popover.spec.tsx.snap @@ -1,6 +1,768 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`PopoverItem styles should render as a \`button\` when an onClick is passed 1`] = ` +exports[`Popover styles should render popover on auto 1`] = ` +HTMLCollection [ + , + @media (max-width:479px) { + .circuit-0 { + position: fixed; + top: 0; + bottom: 0; + left: 0; + right: 0; + background-color: rgba(12,15,20,0.21); + } +} + +
, + .circuit-1 { + background-color: #FFF; + padding: 12px 32px 12px 16px; + border: 0; + color: #1A1A1A; + -webkit-text-decoration: none; + text-decoration: none; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; + -webkit-align-items: flex-start; + -webkit-box-align: flex-start; + -ms-flex-align: flex-start; + align-items: flex-start; + font-size: 16px; + line-height: 24px; +} + +.circuit-1:hover { + background-color: #F5F5F5; + cursor: pointer; +} + +.circuit-1:active { + background-color: #E6E6E6; +} + +.circuit-1:focus { + background-color: #FFF; + outline: 0; + box-shadow: 0 0 0 4px #AFD0FE; + z-index: 1; +} + +.circuit-1:focus::-moz-focus-inner { + border: 0; +} + +.circuit-0 { + margin-right: 8px; +} + +.circuit-2 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + -webkit-box-pack: start; + -webkit-justify-content: start; + -ms-flex-pack: start; + justify-content: start; + -webkit-align-items: flex-start; + -webkit-box-align: flex-start; + -ms-flex-align: flex-start; + align-items: flex-start; + padding: 8px 0px; + border: 1px solid #e6e6e6; + box-sizing: border-box; + box-shadow: 0px 3px 8px rgba(0,0,0,0.2); + border-radius: 8px; + background-color: #FFF; +} + +@media (max-width:479px) { + .circuit-2 { + border-bottom-right-radius: 0; + border-bottom-left-radius: 0; + } +} + +
+ +
, +] +`; + +exports[`Popover styles should render popover on bottom 1`] = ` +HTMLCollection [ + , + @media (max-width:479px) { + .circuit-0 { + position: fixed; + top: 0; + bottom: 0; + left: 0; + right: 0; + background-color: rgba(12,15,20,0.21); + } +} + +
, + .circuit-1 { + background-color: #FFF; + padding: 12px 32px 12px 16px; + border: 0; + color: #1A1A1A; + -webkit-text-decoration: none; + text-decoration: none; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; + -webkit-align-items: flex-start; + -webkit-box-align: flex-start; + -ms-flex-align: flex-start; + align-items: flex-start; + font-size: 16px; + line-height: 24px; +} + +.circuit-1:hover { + background-color: #F5F5F5; + cursor: pointer; +} + +.circuit-1:active { + background-color: #E6E6E6; +} + +.circuit-1:focus { + background-color: #FFF; + outline: 0; + box-shadow: 0 0 0 4px #AFD0FE; + z-index: 1; +} + +.circuit-1:focus::-moz-focus-inner { + border: 0; +} + +.circuit-0 { + margin-right: 8px; +} + +.circuit-2 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + -webkit-box-pack: start; + -webkit-justify-content: start; + -ms-flex-pack: start; + justify-content: start; + -webkit-align-items: flex-start; + -webkit-box-align: flex-start; + -ms-flex-align: flex-start; + align-items: flex-start; + padding: 8px 0px; + border: 1px solid #e6e6e6; + box-sizing: border-box; + box-shadow: 0px 3px 8px rgba(0,0,0,0.2); + border-radius: 8px; + background-color: #FFF; +} + +@media (max-width:479px) { + .circuit-2 { + border-bottom-right-radius: 0; + border-bottom-left-radius: 0; + } +} + +
+ +
, +] +`; + +exports[`Popover styles should render popover on left 1`] = ` +HTMLCollection [ + , + @media (max-width:479px) { + .circuit-0 { + position: fixed; + top: 0; + bottom: 0; + left: 0; + right: 0; + background-color: rgba(12,15,20,0.21); + } +} + +
, + .circuit-1 { + background-color: #FFF; + padding: 12px 32px 12px 16px; + border: 0; + color: #1A1A1A; + -webkit-text-decoration: none; + text-decoration: none; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; + -webkit-align-items: flex-start; + -webkit-box-align: flex-start; + -ms-flex-align: flex-start; + align-items: flex-start; + font-size: 16px; + line-height: 24px; +} + +.circuit-1:hover { + background-color: #F5F5F5; + cursor: pointer; +} + +.circuit-1:active { + background-color: #E6E6E6; +} + +.circuit-1:focus { + background-color: #FFF; + outline: 0; + box-shadow: 0 0 0 4px #AFD0FE; + z-index: 1; +} + +.circuit-1:focus::-moz-focus-inner { + border: 0; +} + +.circuit-0 { + margin-right: 8px; +} + +.circuit-2 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + -webkit-box-pack: start; + -webkit-justify-content: start; + -ms-flex-pack: start; + justify-content: start; + -webkit-align-items: flex-start; + -webkit-box-align: flex-start; + -ms-flex-align: flex-start; + align-items: flex-start; + padding: 8px 0px; + border: 1px solid #e6e6e6; + box-sizing: border-box; + box-shadow: 0px 3px 8px rgba(0,0,0,0.2); + border-radius: 8px; + background-color: #FFF; +} + +@media (max-width:479px) { + .circuit-2 { + border-bottom-right-radius: 0; + border-bottom-left-radius: 0; + } +} + +
+ +
, +] +`; + +exports[`Popover styles should render popover on right 1`] = ` +HTMLCollection [ + , + @media (max-width:479px) { + .circuit-0 { + position: fixed; + top: 0; + bottom: 0; + left: 0; + right: 0; + background-color: rgba(12,15,20,0.21); + } +} + +
, + .circuit-1 { + background-color: #FFF; + padding: 12px 32px 12px 16px; + border: 0; + color: #1A1A1A; + -webkit-text-decoration: none; + text-decoration: none; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; + -webkit-align-items: flex-start; + -webkit-box-align: flex-start; + -ms-flex-align: flex-start; + align-items: flex-start; + font-size: 16px; + line-height: 24px; +} + +.circuit-1:hover { + background-color: #F5F5F5; + cursor: pointer; +} + +.circuit-1:active { + background-color: #E6E6E6; +} + +.circuit-1:focus { + background-color: #FFF; + outline: 0; + box-shadow: 0 0 0 4px #AFD0FE; + z-index: 1; +} + +.circuit-1:focus::-moz-focus-inner { + border: 0; +} + +.circuit-0 { + margin-right: 8px; +} + +.circuit-2 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + -webkit-box-pack: start; + -webkit-justify-content: start; + -ms-flex-pack: start; + justify-content: start; + -webkit-align-items: flex-start; + -webkit-box-align: flex-start; + -ms-flex-align: flex-start; + align-items: flex-start; + padding: 8px 0px; + border: 1px solid #e6e6e6; + box-sizing: border-box; + box-shadow: 0px 3px 8px rgba(0,0,0,0.2); + border-radius: 8px; + background-color: #FFF; +} + +@media (max-width:479px) { + .circuit-2 { + border-bottom-right-radius: 0; + border-bottom-left-radius: 0; + } +} + +
+ +
, +] +`; + +exports[`Popover styles should render popover on top 1`] = ` +HTMLCollection [ + , + @media (max-width:479px) { + .circuit-0 { + position: fixed; + top: 0; + bottom: 0; + left: 0; + right: 0; + background-color: rgba(12,15,20,0.21); + } +} + +
, + .circuit-1 { + background-color: #FFF; + padding: 12px 32px 12px 16px; + border: 0; + color: #1A1A1A; + -webkit-text-decoration: none; + text-decoration: none; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; + -webkit-align-items: flex-start; + -webkit-box-align: flex-start; + -ms-flex-align: flex-start; + align-items: flex-start; + font-size: 16px; + line-height: 24px; +} + +.circuit-1:hover { + background-color: #F5F5F5; + cursor: pointer; +} + +.circuit-1:active { + background-color: #E6E6E6; +} + +.circuit-1:focus { + background-color: #FFF; + outline: 0; + box-shadow: 0 0 0 4px #AFD0FE; + z-index: 1; +} + +.circuit-1:focus::-moz-focus-inner { + border: 0; +} + +.circuit-0 { + margin-right: 8px; +} + +.circuit-2 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + -webkit-box-pack: start; + -webkit-justify-content: start; + -ms-flex-pack: start; + justify-content: start; + -webkit-align-items: flex-start; + -webkit-box-align: flex-start; + -ms-flex-align: flex-start; + align-items: flex-start; + padding: 8px 0px; + border: 1px solid #e6e6e6; + box-sizing: border-box; + box-shadow: 0px 3px 8px rgba(0,0,0,0.2); + border-radius: 8px; + background-color: #FFF; +} + +@media (max-width:479px) { + .circuit-2 { + border-bottom-right-radius: 0; + border-bottom-left-radius: 0; + } +} + +
+ +
, +] +`; + +exports[`Popover styles should render with default styles 1`] = ` +HTMLCollection [ + , + @media (max-width:479px) { + .circuit-0 { + position: fixed; + top: 0; + bottom: 0; + left: 0; + right: 0; + background-color: rgba(12,15,20,0.21); + } +} + +
, + .circuit-1 { + background-color: #FFF; + padding: 12px 32px 12px 16px; + border: 0; + color: #1A1A1A; + -webkit-text-decoration: none; + text-decoration: none; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; + -webkit-align-items: flex-start; + -webkit-box-align: flex-start; + -ms-flex-align: flex-start; + align-items: flex-start; + font-size: 16px; + line-height: 24px; +} + +.circuit-1:hover { + background-color: #F5F5F5; + cursor: pointer; +} + +.circuit-1:active { + background-color: #E6E6E6; +} + +.circuit-1:focus { + background-color: #FFF; + outline: 0; + box-shadow: 0 0 0 4px #AFD0FE; + z-index: 1; +} + +.circuit-1:focus::-moz-focus-inner { + border: 0; +} + +.circuit-0 { + margin-right: 8px; +} + +.circuit-2 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + -webkit-box-pack: start; + -webkit-justify-content: start; + -ms-flex-pack: start; + justify-content: start; + -webkit-align-items: flex-start; + -webkit-box-align: flex-start; + -ms-flex-align: flex-start; + align-items: flex-start; + padding: 8px 0px; + border: 1px solid #e6e6e6; + box-sizing: border-box; + box-shadow: 0px 3px 8px rgba(0,0,0,0.2); + border-radius: 8px; + background-color: #FFF; +} + +@media (max-width:479px) { + .circuit-2 { + border-bottom-right-radius: 0; + border-bottom-left-radius: 0; + } +} + +
+ +
, +] +`; + +exports[`PopoverItem styles should render as Link when an href (and onClick) is passed 1`] = ` .circuit-1 { background-color: #FFF; padding: 12px 32px 12px 16px; @@ -20,7 +782,8 @@ exports[`PopoverItem styles should render as a \`button\` when an onClick is pas -webkit-box-align: flex-start; -ms-flex-align: flex-start; align-items: flex-start; - width: 100%; + font-size: 16px; + line-height: 24px; } .circuit-1:hover { @@ -36,6 +799,7 @@ exports[`PopoverItem styles should render as a \`button\` when an onClick is pas background-color: #FFF; outline: 0; box-shadow: 0 0 0 4px #AFD0FE; + z-index: 1; } .circuit-1:focus::-moz-focus-inner { @@ -46,8 +810,9 @@ exports[`PopoverItem styles should render as a \`button\` when an onClick is pas margin-right: 8px; } - + `; -exports[`PopoverItem styles should render as an \`a\` when an href (and onClick) is passed 1`] = ` +exports[`PopoverItem styles should render as a \`button\` when an onClick is passed 1`] = ` .circuit-1 { background-color: #FFF; padding: 12px 32px 12px 16px; @@ -89,7 +854,8 @@ exports[`PopoverItem styles should render as an \`a\` when an href (and onClick) -webkit-box-align: flex-start; -ms-flex-align: flex-start; align-items: flex-start; - width: 100%; + font-size: 16px; + line-height: 24px; } .circuit-1:hover { @@ -105,6 +871,7 @@ exports[`PopoverItem styles should render as an \`a\` when an href (and onClick) background-color: #FFF; outline: 0; box-shadow: 0 0 0 4px #AFD0FE; + z-index: 1; } .circuit-1:focus::-moz-focus-inner { @@ -115,9 +882,8 @@ exports[`PopoverItem styles should render as an \`a\` when an href (and onClick) margin-right: 8px; } - PopoverItem - + `; From 59d59ecae432c75717a1222b5209ba88a427cf16 Mon Sep 17 00:00:00 2001 From: Amela Kodzic Date: Wed, 16 Jun 2021 10:23:13 +0200 Subject: [PATCH 13/22] update styles and modifier, move react-use to dependencies --- docs/introduction/getting-started.stories.mdx | 4 +- .../circuit-ui/components/Popover/Popover.tsx | 58 +++++++++++-------- packages/circuit-ui/index.ts | 1 + packages/circuit-ui/package.json | 5 +- 4 files changed, 38 insertions(+), 30 deletions(-) diff --git a/docs/introduction/getting-started.stories.mdx b/docs/introduction/getting-started.stories.mdx index d58beb3fd8..848c639feb 100644 --- a/docs/introduction/getting-started.stories.mdx +++ b/docs/introduction/getting-started.stories.mdx @@ -28,13 +28,13 @@ yarn add @sumup/circuit-ui npm install @sumup/circuit-ui ``` -Circuit UI relies on some mandatory peer dependencies, namely [@sumup/collector](https://www.npmjs.com/package/@sumup/collector), [@sumup/design-tokens](https://www.npmjs.com/package/@sumup/design-tokens), [@sumup/icons](https://www.npmjs.com/package/@sumup/icons), [@sumup/intl](https://www.npmjs.com/package/@sumup/intl), [React](https://reactjs.org/), [Emotion](https://emotion.sh/), and [react-use](https://streamich.github.io/react-use/). You should install them with the following command: +Circuit UI relies on some mandatory peer dependencies, namely [@sumup/collector](https://www.npmjs.com/package/@sumup/collector), [@sumup/design-tokens](https://www.npmjs.com/package/@sumup/design-tokens), [@sumup/icons](https://www.npmjs.com/package/@sumup/icons), [@sumup/intl](https://www.npmjs.com/package/@sumup/intl), [React](https://reactjs.org/), and [Emotion](https://emotion.sh/). You should install them with the following command: ```sh # With yarn: yarn add @sumup/collector @sumup/design-tokens @sumup/icons react react-dom @emotion/core @emotion/styled emotion-theming # With npm: -npm install --save @sumup/collector @sumup/design-tokens @sumup/icons react react-dom react-use @emotion/core @emotion/styled emotion-theming +npm install --save @sumup/collector @sumup/design-tokens @sumup/icons react react-dom @emotion/core @emotion/styled emotion-theming ``` ### Configuring the theme diff --git a/packages/circuit-ui/components/Popover/Popover.tsx b/packages/circuit-ui/components/Popover/Popover.tsx index fdf2a78315..595a152e71 100644 --- a/packages/circuit-ui/components/Popover/Popover.tsx +++ b/packages/circuit-ui/components/Popover/Popover.tsx @@ -20,7 +20,6 @@ import React, { useState, FC, HTMLProps, - ReactNode, SVGProps, MouseEvent, Fragment, @@ -40,25 +39,38 @@ import useClickHandler from '../../hooks/use-click-handler'; import Hr from '../Hr'; export interface BaseProps { - children: ReactNode; + children: string; + /** + * Function that's called when the button is clicked. + */ + onClick?: (event: MouseEvent | KeyboardEvent) => void; /** * Display an icon in addition to the label. */ icon?: FC>; - + /** + * Destructive variant, changes the color of label and icon from blue to red to signal to the user that the action + * is irreversible or otherwise dangerous. Interactive states are the same for destructive variant. + */ destructive?: boolean; /** * Additional data that is dispatched with the tracking event. */ tracking?: TrackingProps; /** - * The ref to the html dom element, it can be a button an anchor or a span, typed as any for now because of complex js manipulation with styled components + * The ref to the HTML DOM element, can be a button or an anchor. */ ref?: React.Ref; } -type LinkElProps = Omit, 'size' | 'type'>; -type ButtonElProps = Omit, 'size' | 'type'>; +type LinkElProps = Omit< + HTMLProps, + 'size' | 'type' | 'onClick' +>; +type ButtonElProps = Omit< + HTMLProps, + 'size' | 'type' | 'onClick' +>; export type PopoverItemProps = BaseProps & LinkElProps & ButtonElProps; type PopoverItemWrapperProps = LinkElProps & ButtonElProps; @@ -66,8 +78,9 @@ type PopoverItemWrapperProps = LinkElProps & ButtonElProps; const itemWrapperStyles = () => css` label: popover-item; display: flex; - justify-content: center; - align-items: flex-start; + justify-content: flex-start; + align-items: center; + width: 100%; `; const PopoverItemWrapper = styled('button')( @@ -95,7 +108,7 @@ export const PopoverItem = ({ /* eslint-disable @typescript-eslint/no-unsafe-assignment */ const Link = components.Link as any; - const handleClick = useClickHandler>( + const handleClick = useClickHandler( onClick, tracking, 'popover-item', @@ -115,10 +128,6 @@ export const PopoverItem = ({ const wrapperStyles = ({ theme }: StyleProps) => css` label: popover; - display: flex; - flex-direction: column; - justify-content: start; - align-items: flex-start; padding: ${theme.spacings.byte} 0px; border: 1px solid ${theme.colors.n200}; box-sizing: border-box; @@ -203,6 +212,16 @@ export const Popover = ({ }), [theme], ); + + // The flip modifier is used if the popper has placement set to bottom, but there isn't enough space to position the popper in that direction. + // By default, it will change the popper placement to top. More at https://popper.js.org/docs/v2/modifiers/flip/ + const flip = { + name: 'flip', + options: { + fallbackPlacements: ['top', 'right', 'left'], + }, + }; + // Note: the usePopper hook intentionally takes the DOM node, not refs, in order to be able to update when the nodes change. // A callback ref is used here to permit this behaviour, and useState is an appropriate way to implement this. const [popperElement, setPopperElement] = useState(null); @@ -211,18 +230,7 @@ export const Popover = ({ popperElement, { placement, - modifiers: [ - mobilePosition, - // The flip modifier is used if the popper has placement set to bottom, but there isn't enough space to position the popper in that direction. - // By default, it will change the popper placement to top. As soon as enough space is detected, the placement will be reverted to the originally defined (or preferred) one. - // You can also define fallback placements by providing a list of placements to try. When no space is available on the preferred placement, the modifier will test the ones provided in the list, and use the first useful one. - { - name: 'flip', - options: { - fallbackPlacements: ['top', 'right', 'left'], - }, - }, - ], + modifiers: [mobilePosition, flip], }, ); diff --git a/packages/circuit-ui/index.ts b/packages/circuit-ui/index.ts index bd19c45e3a..76eb53272b 100644 --- a/packages/circuit-ui/index.ts +++ b/packages/circuit-ui/index.ts @@ -120,6 +120,7 @@ export { default as ProgressBar } from './components/ProgressBar'; export type { ProgressBarProps } from './components/ProgressBar'; export { default as Tag } from './components/Tag'; export type { TagProps } from './components/Tag'; +export { default as Popover } from './components/Popover'; export { default as Tooltip } from './components/Tooltip'; export { default as BaseStyles } from './components/BaseStyles'; export { diff --git a/packages/circuit-ui/package.json b/packages/circuit-ui/package.json index 993eb2d673..242352218d 100644 --- a/packages/circuit-ui/package.json +++ b/packages/circuit-ui/package.json @@ -47,6 +47,7 @@ "react-modal": "^3.8.1", "react-number-format": "^4.4.1", "react-popper": "^2.2.5", + "react-use": "^17.2.4", "tiny-warning": "^1.0.3", "yargs": "^17.0.1" }, @@ -88,7 +89,6 @@ "react-dom": "^17.0.1", "react-svg-loader": "^3.0.3", "react-swipeable": "^6.1.0", - "react-use": "^17.2.4", "style-loader": "^2.0.0", "ts-jest": "^26.4.4", "ts-loader": "^9.0.0", @@ -106,7 +106,6 @@ "@sumup/intl": "1.x", "emotion-theming": "10.x", "react": ">=16.8.0 <18.0.0", - "react-dom": ">=16.8.0 <18.0.0", - "react-use": "17x" + "react-dom": ">=16.8.0 <18.0.0" } } From a520e99a110ff1f6be642b922c05fb3605138637 Mon Sep 17 00:00:00 2001 From: Amela Kodzic Date: Wed, 16 Jun 2021 10:23:56 +0200 Subject: [PATCH 14/22] update tests and stories --- .../components/Popover/Popover.docs.mdx | 1 - .../components/Popover/Popover.spec.tsx | 167 ++++++------------ .../components/Popover/Popover.stories.tsx | 89 +++++++++- .../__snapshots__/Popover.spec.tsx.snap | 155 +--------------- packages/circuit-ui/jest.setup.js | 17 ++ 5 files changed, 159 insertions(+), 270 deletions(-) diff --git a/packages/circuit-ui/components/Popover/Popover.docs.mdx b/packages/circuit-ui/components/Popover/Popover.docs.mdx index 0a741e5801..c83f3b0f80 100644 --- a/packages/circuit-ui/components/Popover/Popover.docs.mdx +++ b/packages/circuit-ui/components/Popover/Popover.docs.mdx @@ -6,7 +6,6 @@ import { Popover } from '.'; The popover component displays a list of subsequent action options, when interacting wuith an actionable component. - ## Usage guidelines diff --git a/packages/circuit-ui/components/Popover/Popover.spec.tsx b/packages/circuit-ui/components/Popover/Popover.spec.tsx index c681679974..94186fa102 100644 --- a/packages/circuit-ui/components/Popover/Popover.spec.tsx +++ b/packages/circuit-ui/components/Popover/Popover.spec.tsx @@ -13,9 +13,8 @@ * limitations under the License. */ -import { Bin, CirclePlus, PenStroke, ThumbUp, Zap } from '@sumup/icons'; -import { fireEvent } from '@testing-library/dom'; -import React, { useRef, useState } from 'react'; +import { CirclePlus, Zap } from '@sumup/icons'; +import React, { useRef } from 'react'; import { create, @@ -23,10 +22,8 @@ import { axe, RenderFn, render, - act, userEvent, } from '../../util/test-utils'; -import Button from '../Button'; import { PopoverItem, PopoverItemProps, Popover } from './Popover'; @@ -50,14 +47,16 @@ describe('PopoverItem', () => { onClick: jest.fn(), icon: Zap, }; - const actual = renderPopoverItem(create, props); - expect(actual).toMatchSnapshot(); + const { container } = renderPopoverItem(render, props); + const anchorEl = container.querySelector('a'); + expect(anchorEl).toBeVisible(); }); it('should render as a `button` when an onClick is passed', () => { const props = { ...baseProps, onClick: jest.fn(), icon: Zap }; - const actual = renderPopoverItem(create, props); - expect(actual).toMatchSnapshot(); + const { container } = renderPopoverItem(render, props); + const buttonEl = container.querySelector('button'); + expect(buttonEl).toBeVisible(); }); }); @@ -74,6 +73,21 @@ describe('PopoverItem', () => { expect(actual).toHaveNoViolations(); }); }); + + describe('business logic', () => { + it('should call onClick when rendered as Link', () => { + const props = { + ...baseProps, + href: 'https://sumup.com', + onClick: jest.fn(), + icon: Zap, + }; + const { container } = renderPopoverItem(render, props); + const anchorEl = container.querySelector('a'); + userEvent.click(anchorEl); + expect(props.onClick).toHaveBeenCalledTimes(1); + }); + }); }); describe('Popover', () => { @@ -90,135 +104,58 @@ describe('Popover', () => { ); }; - const Interactive = () => { - const [isOpen, setOpen] = useState(true); - const referenceElement = useRef( - null, - ); - - const handleClick = () => { - setOpen((prev) => !prev); - }; - - const onClose = () => { - setOpen(false); - }; - - return ( - <> - - alert('Hello'), - children: 'Add', - icon: CirclePlus, - }, - { - onClick: () => alert('Hello'), - children: 'Edit', - icon: PenStroke, - }, - { type: 'divider' }, - { - onClick: () => alert('Hello'), - children: 'Delete', - icon: Bin, - destructive: true, - }, - ]} - onClose={onClose} - isOpen={isOpen} - referenceElement={referenceElement} - /> - - ); + const baseProps = { + actions: [ + { + onClick: () => alert('Hello'), + children: 'Add', + icon: CirclePlus, + }, + ], + isOpen: true, + onClose: jest.fn(), }; describe('styles', () => { it('should render with default styles', () => { - const props = { - actions: [ - { - onClick: () => alert('Hello'), - children: 'Add', - icon: CirclePlus, - }, - ], - isOpen: true, - onClose: jest.fn(), - }; - - const actual = create(); + const actual = create(); expect(actual).toMatchSnapshot(); }); placements.forEach((placement) => { it(`should render popover on ${placement}`, () => { - const props = { - actions: [ - { - onClick: () => alert('Hello'), - children: 'Add', - icon: CirclePlus, - }, - ], - isOpen: true, - onClose: jest.fn(), - }; - const actual = create(); + const actual = create(); expect(actual).toMatchSnapshot(); }); }); + }); - it('should render popover when isOpen=true', () => { - const { queryByText } = render(); - expect(queryByText('Edit')).not.toBeNull(); - }); - + describe('business logic', () => { it('should close popover when passing a click outside', () => { - const { queryByText } = render(); - expect(queryByText('Edit')).not.toBeNull(); + const { queryByText } = render(); + expect(queryByText('Add')).not.toBeNull(); userEvent.click(document.body); - expect(queryByText('Add')).toBeNull(); + expect(baseProps.onClose).toHaveBeenCalledTimes(1); }); it('should close popover when passing a click to a reference element', () => { - const { queryByText, getAllByRole } = render(); - expect(queryByText('Edit')).not.toBeNull(); + const { queryByText, getAllByRole } = render(); + expect(queryByText('Add')).not.toBeNull(); - act(() => { - fireEvent.click(getAllByRole('button')[0]); - }); + userEvent.click(getAllByRole('button')[0]); - expect(queryByText('Add')).toBeNull(); + expect(baseProps.onClose).toHaveBeenCalledTimes(1); }); + }); - it('should render nothing when isOpen=false', () => { - const props = { - actions: [ - { - onClick: () => alert('Hello'), - children: 'Add', - icon: CirclePlus, - }, - ], - isOpen: false, - onClose: jest.fn(), - }; - - const { queryByTestId } = render(); - expect(queryByTestId('popover-child')).toBeNull(); - }); + /** + * Accessibility tests. + */ + it('should meet accessibility guidelines', async () => { + const wrapper = renderToHtml(); + const actual = await axe(wrapper); + expect(actual).toHaveNoViolations(); }); }); diff --git a/packages/circuit-ui/components/Popover/Popover.stories.tsx b/packages/circuit-ui/components/Popover/Popover.stories.tsx index 413589693f..4d58610429 100644 --- a/packages/circuit-ui/components/Popover/Popover.stories.tsx +++ b/packages/circuit-ui/components/Popover/Popover.stories.tsx @@ -14,9 +14,18 @@ */ import React, { useState, useRef } from 'react'; -import { ThumbUp, Zap } from '@sumup/icons'; +import { + More, + ThumbUp, + Zap, + CirclePlus, + PenStroke, + Share, + Bin, +} from '@sumup/icons'; import Button from '../Button'; +import IconButton from '../IconButton'; import { Popover, PopoverItem } from './Popover'; @@ -28,16 +37,24 @@ export default { }, }; -export const Base = (args) => ; +export const PopoverItemAnchor = (args) => ; -Base.args = { +PopoverItemAnchor.args = { onClick: () => alert('Hello'), href: 'https://sumup.com/', children: 'Label', icon: Zap, }; -export const Example = (args) => { +export const PopoverItemButton = (args) => ; + +PopoverItemButton.args = { + onClick: () => alert('Hello'), + children: 'Label', + icon: Zap, +}; + +export const PopoverBase = (args) => { const [isOpen, setOpen] = useState(false); const referenceElement = useRef(null); @@ -70,7 +87,7 @@ export const Example = (args) => { ); }; -Example.args = { +PopoverBase.args = { actions: [ { onClick: () => alert('Hello'), @@ -93,3 +110,65 @@ Example.args = { }, ], }; + +export const PopoverIconTrigger = (args) => { + const [isOpen, setOpen] = useState(false); + const referenceElement = useRef(null); + + const handleClick = () => { + setOpen((prev) => !prev); + }; + + const onClose = () => { + setOpen(false); + }; + + return ( + <> + + + + + + ); +}; + +PopoverIconTrigger.args = { + actions: [ + { + onClick: () => alert('Hello'), + href: '', + children: 'Add', + icon: CirclePlus, + }, + { + onClick: () => alert('Hello'), + href: 'https://sumup.com/', + children: 'Edit', + icon: PenStroke, + }, + { + onClick: () => alert('Hello'), + href: 'https://sumup.com/', + children: 'Upload', + icon: Share, + }, + { type: 'divider' }, + { + onClick: () => alert('Hello'), + children: 'Delete', + icon: Bin, + destructive: true, + }, + ], +}; diff --git a/packages/circuit-ui/components/Popover/__snapshots__/Popover.spec.tsx.snap b/packages/circuit-ui/components/Popover/__snapshots__/Popover.spec.tsx.snap index e96d31a645..d7c1272a56 100644 --- a/packages/circuit-ui/components/Popover/__snapshots__/Popover.spec.tsx.snap +++ b/packages/circuit-ui/components/Popover/__snapshots__/Popover.spec.tsx.snap @@ -83,7 +83,7 @@ HTMLCollection [ -ms-flex-align: flex-start; align-items: flex-start; padding: 8px 0px; - border: 1px solid #e6e6e6; + border: 1px solid #E6E6E6; box-sizing: border-box; box-shadow: 0px 3px 8px rgba(0,0,0,0.2); border-radius: 8px; @@ -210,7 +210,7 @@ HTMLCollection [ -ms-flex-align: flex-start; align-items: flex-start; padding: 8px 0px; - border: 1px solid #e6e6e6; + border: 1px solid #E6E6E6; box-sizing: border-box; box-shadow: 0px 3px 8px rgba(0,0,0,0.2); border-radius: 8px; @@ -337,7 +337,7 @@ HTMLCollection [ -ms-flex-align: flex-start; align-items: flex-start; padding: 8px 0px; - border: 1px solid #e6e6e6; + border: 1px solid #E6E6E6; box-sizing: border-box; box-shadow: 0px 3px 8px rgba(0,0,0,0.2); border-radius: 8px; @@ -464,7 +464,7 @@ HTMLCollection [ -ms-flex-align: flex-start; align-items: flex-start; padding: 8px 0px; - border: 1px solid #e6e6e6; + border: 1px solid #E6E6E6; box-sizing: border-box; box-shadow: 0px 3px 8px rgba(0,0,0,0.2); border-radius: 8px; @@ -591,7 +591,7 @@ HTMLCollection [ -ms-flex-align: flex-start; align-items: flex-start; padding: 8px 0px; - border: 1px solid #e6e6e6; + border: 1px solid #E6E6E6; box-sizing: border-box; box-shadow: 0px 3px 8px rgba(0,0,0,0.2); border-radius: 8px; @@ -718,7 +718,7 @@ HTMLCollection [ -ms-flex-align: flex-start; align-items: flex-start; padding: 8px 0px; - border: 1px solid #e6e6e6; + border: 1px solid #E6E6E6; box-sizing: border-box; box-shadow: 0px 3px 8px rgba(0,0,0,0.2); border-radius: 8px; @@ -761,146 +761,3 @@ HTMLCollection [
, ] `; - -exports[`PopoverItem styles should render as Link when an href (and onClick) is passed 1`] = ` -.circuit-1 { - background-color: #FFF; - padding: 12px 32px 12px 16px; - border: 0; - color: #1A1A1A; - -webkit-text-decoration: none; - text-decoration: none; - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; - display: flex; - -webkit-box-pack: center; - -webkit-justify-content: center; - -ms-flex-pack: center; - justify-content: center; - -webkit-align-items: flex-start; - -webkit-box-align: flex-start; - -ms-flex-align: flex-start; - align-items: flex-start; - font-size: 16px; - line-height: 24px; -} - -.circuit-1:hover { - background-color: #F5F5F5; - cursor: pointer; -} - -.circuit-1:active { - background-color: #E6E6E6; -} - -.circuit-1:focus { - background-color: #FFF; - outline: 0; - box-shadow: 0 0 0 4px #AFD0FE; - z-index: 1; -} - -.circuit-1:focus::-moz-focus-inner { - border: 0; -} - -.circuit-0 { - margin-right: 8px; -} - - - - - - PopoverItem - -`; - -exports[`PopoverItem styles should render as a \`button\` when an onClick is passed 1`] = ` -.circuit-1 { - background-color: #FFF; - padding: 12px 32px 12px 16px; - border: 0; - color: #1A1A1A; - -webkit-text-decoration: none; - text-decoration: none; - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; - display: flex; - -webkit-box-pack: center; - -webkit-justify-content: center; - -ms-flex-pack: center; - justify-content: center; - -webkit-align-items: flex-start; - -webkit-box-align: flex-start; - -ms-flex-align: flex-start; - align-items: flex-start; - font-size: 16px; - line-height: 24px; -} - -.circuit-1:hover { - background-color: #F5F5F5; - cursor: pointer; -} - -.circuit-1:active { - background-color: #E6E6E6; -} - -.circuit-1:focus { - background-color: #FFF; - outline: 0; - box-shadow: 0 0 0 4px #AFD0FE; - z-index: 1; -} - -.circuit-1:focus::-moz-focus-inner { - border: 0; -} - -.circuit-0 { - margin-right: 8px; -} - - -`; diff --git a/packages/circuit-ui/jest.setup.js b/packages/circuit-ui/jest.setup.js index bd09bed28e..b1416a55e8 100644 --- a/packages/circuit-ui/jest.setup.js +++ b/packages/circuit-ui/jest.setup.js @@ -59,3 +59,20 @@ expect.addSnapshotSerializer( }, }), ); + +global.matchMedia = jest.fn().mockImplementation((query) => ({ + matches: false, + media: query, + onchange: null, + /** + * @deprecated + */ + addListener: jest.fn(), + /** + * @deprecated + */ + removeListener: jest.fn(), + addEventListener: jest.fn(), + removeEventListener: jest.fn(), + dispatchEvent: jest.fn(), +})); From 29e01460fd2c3020317a0adc8cf73587afb1ec1b Mon Sep 17 00:00:00 2001 From: Amela Kodzic Date: Wed, 16 Jun 2021 17:37:16 +0200 Subject: [PATCH 15/22] update docs --- .../components/Popover/Popover.docs.mdx | 63 +++++++++++++++++-- .../components/Popover/Popover.stories.tsx | 26 +++----- .../circuit-ui/components/Popover/Popover.tsx | 4 +- packages/circuit-ui/styles/style-mixins.ts | 1 + 4 files changed, 71 insertions(+), 23 deletions(-) diff --git a/packages/circuit-ui/components/Popover/Popover.docs.mdx b/packages/circuit-ui/components/Popover/Popover.docs.mdx index c83f3b0f80..bc7bf5f813 100644 --- a/packages/circuit-ui/components/Popover/Popover.docs.mdx +++ b/packages/circuit-ui/components/Popover/Popover.docs.mdx @@ -1,13 +1,66 @@ import { Status, Props, Story } from '../../../../.storybook/components'; -import { Popover } from '.'; +import { Popover } from '@sumup/circuit-ui'; # Popover -The popover component displays a list of subsequent action options, when interacting wuith an actionable component. +Popover menus are a common pattern to display a list of subsequent action options, when interacting with an actionable component. - - + + + + + +- Triggers of Popover can be primary, secondary, tertiary buttons, an overflow icons, or components with embedded buttons such as image upload component. +- Each Popover action item is represented by an appropriate HTML element (button or a). +- If needed the dividing line can be used to separate Popover action items. +- The leading icon is optional. +- The Popover is powered by React Popper [https://popper.js.org/docs/v2/] which means you can easily change the Popover placement by simply passing the placement prop. ## Usage guidelines -Triggers of Popover can be primary, secondary, tertiary buttons, an overflow icons, or components with embedded buttons such as image upload component. +- **Do** popover menu label should be clear, actionable, concise and understandable +- **Do** always think about the priority of the action option to be taken and put the option order in logical order + +## Usage in code + +```js +import React from 'react'; +import { + Popover, + Button, +} from '@sumup/circuit-ui'; + + const [isOpen, setOpen] = useState(false); + const referenceElement = useRef(null); + + const handleClick = () => { + setOpen((prev) => !prev); + }; + + const onClose = () => { + setOpen(false); + }; + + const props = { + actions: [ + { + onClick: () => alert('Hello'), + children: 'Add', + icon: CirclePlus, + }, + ], + isOpen: true, + onClose: jest.fn(), + }; + + return ( + <> + + + + ); +}; +``` diff --git a/packages/circuit-ui/components/Popover/Popover.stories.tsx b/packages/circuit-ui/components/Popover/Popover.stories.tsx index 4d58610429..f4ce3b190a 100644 --- a/packages/circuit-ui/components/Popover/Popover.stories.tsx +++ b/packages/circuit-ui/components/Popover/Popover.stories.tsx @@ -37,23 +37,6 @@ export default { }, }; -export const PopoverItemAnchor = (args) => ; - -PopoverItemAnchor.args = { - onClick: () => alert('Hello'), - href: 'https://sumup.com/', - children: 'Label', - icon: Zap, -}; - -export const PopoverItemButton = (args) => ; - -PopoverItemButton.args = { - onClick: () => alert('Hello'), - children: 'Label', - icon: Zap, -}; - export const PopoverBase = (args) => { const [isOpen, setOpen] = useState(false); const referenceElement = useRef(null); @@ -172,3 +155,12 @@ PopoverIconTrigger.args = { }, ], }; + +export const PopoverItemBase = (args) => ; + +PopoverItemBase.args = { + onClick: () => alert('Hello'), + href: 'https://sumup.com/', + children: 'Label', + icon: Zap, +}; diff --git a/packages/circuit-ui/components/Popover/Popover.tsx b/packages/circuit-ui/components/Popover/Popover.tsx index 595a152e71..52434a63f5 100644 --- a/packages/circuit-ui/components/Popover/Popover.tsx +++ b/packages/circuit-ui/components/Popover/Popover.tsx @@ -173,6 +173,7 @@ export interface PopoverProps { actions: Action[]; referenceElement: RefObject; placement?: Placement; + fallbackPlacements?: Placement[]; onClose: (event: Event) => void; } @@ -182,6 +183,7 @@ export const Popover = ({ actions, referenceElement, placement = 'bottom', + fallbackPlacements = ['top', 'right', 'left'], ...props }: PopoverProps): JSX.Element | null => { const theme: Theme = useTheme(); @@ -218,7 +220,7 @@ export const Popover = ({ const flip = { name: 'flip', options: { - fallbackPlacements: ['top', 'right', 'left'], + fallbackPlacements, }, }; diff --git a/packages/circuit-ui/styles/style-mixins.ts b/packages/circuit-ui/styles/style-mixins.ts index 8cce8447ee..a15055df63 100644 --- a/packages/circuit-ui/styles/style-mixins.ts +++ b/packages/circuit-ui/styles/style-mixins.ts @@ -357,6 +357,7 @@ export const listItem = ( ? theme.colors.danger : theme.colors.bodyColor}; text-decoration: none; + position: relative; &:hover { background-color: ${theme.colors.n100}; From 1bc98a572262fce0df8e30a6ae4154fc24d72d2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robin=20M=C3=A9tral?= Date: Thu, 17 Jun 2021 11:39:08 +0200 Subject: [PATCH 16/22] Fix tests --- .../components/Popover/Popover.spec.tsx | 16 +- .../__snapshots__/Popover.spec.tsx.snap | 210 ++++++------------ 2 files changed, 78 insertions(+), 148 deletions(-) diff --git a/packages/circuit-ui/components/Popover/Popover.spec.tsx b/packages/circuit-ui/components/Popover/Popover.spec.tsx index 94186fa102..0b791f782f 100644 --- a/packages/circuit-ui/components/Popover/Popover.spec.tsx +++ b/packages/circuit-ui/components/Popover/Popover.spec.tsx @@ -15,6 +15,7 @@ import { CirclePlus, Zap } from '@sumup/icons'; import React, { useRef } from 'react'; +import { Placement } from '@popperjs/core'; import { create, @@ -25,9 +26,14 @@ import { userEvent, } from '../../util/test-utils'; -import { PopoverItem, PopoverItemProps, Popover } from './Popover'; +import { + PopoverItem, + PopoverItemProps, + Popover, + PopoverProps, +} from './Popover'; -const placements = ['auto', 'top', 'bottom', 'left', 'right']; +const placements: Placement[] = ['auto', 'top', 'bottom', 'left', 'right']; describe('PopoverItem', () => { function renderPopoverItem( @@ -84,14 +90,16 @@ describe('PopoverItem', () => { }; const { container } = renderPopoverItem(render, props); const anchorEl = container.querySelector('a'); - userEvent.click(anchorEl); + if (anchorEl) { + userEvent.click(anchorEl); + } expect(props.onClick).toHaveBeenCalledTimes(1); }); }); }); describe('Popover', () => { - const Default = (props) => { + const Default = (props: Omit) => { const referenceElement = useRef( null, ); diff --git a/packages/circuit-ui/components/Popover/__snapshots__/Popover.spec.tsx.snap b/packages/circuit-ui/components/Popover/__snapshots__/Popover.spec.tsx.snap index d7c1272a56..b4ac89309f 100644 --- a/packages/circuit-ui/components/Popover/__snapshots__/Popover.spec.tsx.snap +++ b/packages/circuit-ui/components/Popover/__snapshots__/Popover.spec.tsx.snap @@ -12,7 +12,7 @@ HTMLCollection [ bottom: 0; left: 0; right: 0; - background-color: rgba(12,15,20,0.21); + background-color: rgba(0,0,0,0.4); } } @@ -26,18 +26,20 @@ HTMLCollection [ color: #1A1A1A; -webkit-text-decoration: none; text-decoration: none; + position: relative; display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; display: flex; - -webkit-box-pack: center; - -webkit-justify-content: center; - -ms-flex-pack: center; - justify-content: center; - -webkit-align-items: flex-start; - -webkit-box-align: flex-start; - -ms-flex-align: flex-start; - align-items: flex-start; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + width: 100%; font-size: 16px; line-height: 24px; } @@ -67,21 +69,6 @@ HTMLCollection [ } .circuit-2 { - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; - display: flex; - -webkit-flex-direction: column; - -ms-flex-direction: column; - flex-direction: column; - -webkit-box-pack: start; - -webkit-justify-content: start; - -ms-flex-pack: start; - justify-content: start; - -webkit-align-items: flex-start; - -webkit-box-align: flex-start; - -ms-flex-align: flex-start; - align-items: flex-start; padding: 8px 0px; border: 1px solid #E6E6E6; box-sizing: border-box; @@ -139,7 +126,7 @@ HTMLCollection [ bottom: 0; left: 0; right: 0; - background-color: rgba(12,15,20,0.21); + background-color: rgba(0,0,0,0.4); } } @@ -153,18 +140,20 @@ HTMLCollection [ color: #1A1A1A; -webkit-text-decoration: none; text-decoration: none; + position: relative; display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; display: flex; - -webkit-box-pack: center; - -webkit-justify-content: center; - -ms-flex-pack: center; - justify-content: center; - -webkit-align-items: flex-start; - -webkit-box-align: flex-start; - -ms-flex-align: flex-start; - align-items: flex-start; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + width: 100%; font-size: 16px; line-height: 24px; } @@ -194,21 +183,6 @@ HTMLCollection [ } .circuit-2 { - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; - display: flex; - -webkit-flex-direction: column; - -ms-flex-direction: column; - flex-direction: column; - -webkit-box-pack: start; - -webkit-justify-content: start; - -ms-flex-pack: start; - justify-content: start; - -webkit-align-items: flex-start; - -webkit-box-align: flex-start; - -ms-flex-align: flex-start; - align-items: flex-start; padding: 8px 0px; border: 1px solid #E6E6E6; box-sizing: border-box; @@ -266,7 +240,7 @@ HTMLCollection [ bottom: 0; left: 0; right: 0; - background-color: rgba(12,15,20,0.21); + background-color: rgba(0,0,0,0.4); } } @@ -280,18 +254,20 @@ HTMLCollection [ color: #1A1A1A; -webkit-text-decoration: none; text-decoration: none; + position: relative; display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; display: flex; - -webkit-box-pack: center; - -webkit-justify-content: center; - -ms-flex-pack: center; - justify-content: center; - -webkit-align-items: flex-start; - -webkit-box-align: flex-start; - -ms-flex-align: flex-start; - align-items: flex-start; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + width: 100%; font-size: 16px; line-height: 24px; } @@ -321,21 +297,6 @@ HTMLCollection [ } .circuit-2 { - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; - display: flex; - -webkit-flex-direction: column; - -ms-flex-direction: column; - flex-direction: column; - -webkit-box-pack: start; - -webkit-justify-content: start; - -ms-flex-pack: start; - justify-content: start; - -webkit-align-items: flex-start; - -webkit-box-align: flex-start; - -ms-flex-align: flex-start; - align-items: flex-start; padding: 8px 0px; border: 1px solid #E6E6E6; box-sizing: border-box; @@ -393,7 +354,7 @@ HTMLCollection [ bottom: 0; left: 0; right: 0; - background-color: rgba(12,15,20,0.21); + background-color: rgba(0,0,0,0.4); } } @@ -407,18 +368,20 @@ HTMLCollection [ color: #1A1A1A; -webkit-text-decoration: none; text-decoration: none; + position: relative; display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; display: flex; - -webkit-box-pack: center; - -webkit-justify-content: center; - -ms-flex-pack: center; - justify-content: center; - -webkit-align-items: flex-start; - -webkit-box-align: flex-start; - -ms-flex-align: flex-start; - align-items: flex-start; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + width: 100%; font-size: 16px; line-height: 24px; } @@ -448,21 +411,6 @@ HTMLCollection [ } .circuit-2 { - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; - display: flex; - -webkit-flex-direction: column; - -ms-flex-direction: column; - flex-direction: column; - -webkit-box-pack: start; - -webkit-justify-content: start; - -ms-flex-pack: start; - justify-content: start; - -webkit-align-items: flex-start; - -webkit-box-align: flex-start; - -ms-flex-align: flex-start; - align-items: flex-start; padding: 8px 0px; border: 1px solid #E6E6E6; box-sizing: border-box; @@ -520,7 +468,7 @@ HTMLCollection [ bottom: 0; left: 0; right: 0; - background-color: rgba(12,15,20,0.21); + background-color: rgba(0,0,0,0.4); } } @@ -534,18 +482,20 @@ HTMLCollection [ color: #1A1A1A; -webkit-text-decoration: none; text-decoration: none; + position: relative; display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; display: flex; - -webkit-box-pack: center; - -webkit-justify-content: center; - -ms-flex-pack: center; - justify-content: center; - -webkit-align-items: flex-start; - -webkit-box-align: flex-start; - -ms-flex-align: flex-start; - align-items: flex-start; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + width: 100%; font-size: 16px; line-height: 24px; } @@ -575,21 +525,6 @@ HTMLCollection [ } .circuit-2 { - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; - display: flex; - -webkit-flex-direction: column; - -ms-flex-direction: column; - flex-direction: column; - -webkit-box-pack: start; - -webkit-justify-content: start; - -ms-flex-pack: start; - justify-content: start; - -webkit-align-items: flex-start; - -webkit-box-align: flex-start; - -ms-flex-align: flex-start; - align-items: flex-start; padding: 8px 0px; border: 1px solid #E6E6E6; box-sizing: border-box; @@ -647,7 +582,7 @@ HTMLCollection [ bottom: 0; left: 0; right: 0; - background-color: rgba(12,15,20,0.21); + background-color: rgba(0,0,0,0.4); } } @@ -661,18 +596,20 @@ HTMLCollection [ color: #1A1A1A; -webkit-text-decoration: none; text-decoration: none; + position: relative; display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; display: flex; - -webkit-box-pack: center; - -webkit-justify-content: center; - -ms-flex-pack: center; - justify-content: center; - -webkit-align-items: flex-start; - -webkit-box-align: flex-start; - -ms-flex-align: flex-start; - align-items: flex-start; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + width: 100%; font-size: 16px; line-height: 24px; } @@ -702,21 +639,6 @@ HTMLCollection [ } .circuit-2 { - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; - display: flex; - -webkit-flex-direction: column; - -ms-flex-direction: column; - flex-direction: column; - -webkit-box-pack: start; - -webkit-justify-content: start; - -ms-flex-pack: start; - justify-content: start; - -webkit-align-items: flex-start; - -webkit-box-align: flex-start; - -ms-flex-align: flex-start; - align-items: flex-start; padding: 8px 0px; border: 1px solid #E6E6E6; box-sizing: border-box; From 92f6ca0531e64602d4306f99eb9ca6c82ef3ec6e Mon Sep 17 00:00:00 2001 From: Amela Kodzic Date: Thu, 17 Jun 2021 11:50:04 +0200 Subject: [PATCH 17/22] use new typography stylemixin --- packages/circuit-ui/components/Popover/Popover.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/circuit-ui/components/Popover/Popover.tsx b/packages/circuit-ui/components/Popover/Popover.tsx index 52434a63f5..896a15b444 100644 --- a/packages/circuit-ui/components/Popover/Popover.tsx +++ b/packages/circuit-ui/components/Popover/Popover.tsx @@ -33,7 +33,7 @@ import { Placement, State, Modifier } from '@popperjs/core'; import { useTheme } from 'emotion-theming'; import styled, { StyleProps } from '../../styles/styled'; -import { listItem, textMega } from '../../styles/style-mixins'; +import { listItem, typography } from '../../styles/style-mixins'; import { useComponents } from '../ComponentsContext'; import useClickHandler from '../../hooks/use-click-handler'; import Hr from '../Hr'; @@ -86,7 +86,7 @@ const itemWrapperStyles = () => css` const PopoverItemWrapper = styled('button')( listItem, itemWrapperStyles, - textMega, + typography('one'), ); const iconStyles = (theme: Theme) => css` From efa11d2e749bd90dfdf39825edac60b5435b5268 Mon Sep 17 00:00:00 2001 From: Amela Kodzic Date: Thu, 17 Jun 2021 11:52:27 +0200 Subject: [PATCH 18/22] add changeset --- .changeset/tame-mayflies-move.md | 61 ++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 .changeset/tame-mayflies-move.md diff --git a/.changeset/tame-mayflies-move.md b/.changeset/tame-mayflies-move.md new file mode 100644 index 0000000000..fe367a8c3e --- /dev/null +++ b/.changeset/tame-mayflies-move.md @@ -0,0 +1,61 @@ +--- +'@sumup/circuit-ui': major +--- + +--- + +## "@sumup/circuit-ui": major + +Popover menus are a common pattern to display additional actions commonly when interacting with an actionable component. The old popover component was removed and replaced with the new one. + +The Popover Item component was added as an action item represented by an appropriate HTML element (button or a) with an optional leading icon and label. + +The new Popover component takes isOpen and onClose as props to control the open Popover, actions to distinguish PopoverItem and Divider. +The Popover is powered by React Popper which means you can easily change the Popover placement by simply passing the placement prop. +The triggers or the reference elements of the Popover can be primary, secondary, tertiary buttons, an overflow icons, or components with embedded buttons such as image upload component. +The states of the popover are added and can be Default, Hover, Active, Focus. +The Destructive variant is added that changes the color of label and icon from blue to red to signal to the user that the action is irreversible or otherwise dangerous. + +Usage example: + +``` +import React from 'react'; +import { + Popover, + Button, +} from '@sumup/circuit-ui'; + + const [isOpen, setOpen] = useState(false); + const referenceElement = useRef(null); + + const handleClick = () => { + setOpen((prev) => !prev); + }; + + const onClose = () => { + setOpen(false); + }; + + const props = { + actions: [ + { + onClick: () => alert('Hello'), + children: 'Add', + icon: CirclePlus, + }, + ], + isOpen: true, + onClose: jest.fn(), + }; + + return ( + <> + + + + ); +}; +``` From 9ece58dae27c0b8233bd3f979c94d1d4590f1da2 Mon Sep 17 00:00:00 2001 From: Amela Kodzic Date: Thu, 17 Jun 2021 16:30:00 +0200 Subject: [PATCH 19/22] fix bug on mobile --- packages/circuit-ui/components/Popover/Popover.tsx | 1 + .../components/Popover/__snapshots__/Popover.spec.tsx.snap | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/packages/circuit-ui/components/Popover/Popover.tsx b/packages/circuit-ui/components/Popover/Popover.tsx index 896a15b444..8507419e0b 100644 --- a/packages/circuit-ui/components/Popover/Popover.tsx +++ b/packages/circuit-ui/components/Popover/Popover.tsx @@ -157,6 +157,7 @@ const Overlay = styled.div( left: 0; right: 0; background-color: ${theme.colors.overlay}; + pointer-events: none; } `, ); diff --git a/packages/circuit-ui/components/Popover/__snapshots__/Popover.spec.tsx.snap b/packages/circuit-ui/components/Popover/__snapshots__/Popover.spec.tsx.snap index b4ac89309f..146ec84d00 100644 --- a/packages/circuit-ui/components/Popover/__snapshots__/Popover.spec.tsx.snap +++ b/packages/circuit-ui/components/Popover/__snapshots__/Popover.spec.tsx.snap @@ -13,6 +13,7 @@ HTMLCollection [ left: 0; right: 0; background-color: rgba(0,0,0,0.4); + pointer-events: none; } } @@ -127,6 +128,7 @@ HTMLCollection [ left: 0; right: 0; background-color: rgba(0,0,0,0.4); + pointer-events: none; } } @@ -241,6 +243,7 @@ HTMLCollection [ left: 0; right: 0; background-color: rgba(0,0,0,0.4); + pointer-events: none; } } @@ -355,6 +358,7 @@ HTMLCollection [ left: 0; right: 0; background-color: rgba(0,0,0,0.4); + pointer-events: none; } } @@ -469,6 +473,7 @@ HTMLCollection [ left: 0; right: 0; background-color: rgba(0,0,0,0.4); + pointer-events: none; } } @@ -583,6 +588,7 @@ HTMLCollection [ left: 0; right: 0; background-color: rgba(0,0,0,0.4); + pointer-events: none; } } From f87b11d627bac2d49c92b76a78394973a2ccf508 Mon Sep 17 00:00:00 2001 From: Amela Kodzic Date: Thu, 17 Jun 2021 16:43:19 +0200 Subject: [PATCH 20/22] fix ui bugs --- .../circuit-ui/components/Popover/Popover.stories.tsx | 4 ++++ packages/circuit-ui/styles/style-mixins.ts | 11 +++++------ 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/packages/circuit-ui/components/Popover/Popover.stories.tsx b/packages/circuit-ui/components/Popover/Popover.stories.tsx index f4ce3b190a..dad60444da 100644 --- a/packages/circuit-ui/components/Popover/Popover.stories.tsx +++ b/packages/circuit-ui/components/Popover/Popover.stories.tsx @@ -28,10 +28,14 @@ import Button from '../Button'; import IconButton from '../IconButton'; import { Popover, PopoverItem } from './Popover'; +import docs from './Popover.docs.mdx'; export default { title: 'Components/Popover', component: Popover, + parameters: { + docs: { page: docs }, + }, argTypes: { children: { control: 'text' }, }, diff --git a/packages/circuit-ui/styles/style-mixins.ts b/packages/circuit-ui/styles/style-mixins.ts index a15055df63..de1268df2a 100644 --- a/packages/circuit-ui/styles/style-mixins.ts +++ b/packages/circuit-ui/styles/style-mixins.ts @@ -364,14 +364,13 @@ export const listItem = ( cursor: pointer; } - &:active { - background-color: ${theme.colors.n200}; - } - &:focus { - background-color: ${theme.colors.white}; - ${focusOutline({ theme })}; + ${focusOutline('inset')({ theme })}; z-index: ${theme.zIndex.absolute}; } + + &:active { + background-color: ${theme.colors.n200}; + } `; }; From 4aa838cb450b144e5e544bcc10866bebc60a6b1a Mon Sep 17 00:00:00 2001 From: Amela Kodzic Date: Thu, 17 Jun 2021 17:56:54 +0200 Subject: [PATCH 21/22] add props description and update docs --- .changeset/tame-mayflies-move.md | 58 +------------- .../components/Popover/Popover.docs.mdx | 51 +----------- .../components/Popover/Popover.spec.tsx | 10 +-- .../components/Popover/Popover.stories.tsx | 29 +++---- .../circuit-ui/components/Popover/Popover.tsx | 50 ++++++++---- .../__snapshots__/Popover.spec.tsx.snap | 78 +++++++++---------- 6 files changed, 93 insertions(+), 183 deletions(-) diff --git a/.changeset/tame-mayflies-move.md b/.changeset/tame-mayflies-move.md index fe367a8c3e..ce3fc78355 100644 --- a/.changeset/tame-mayflies-move.md +++ b/.changeset/tame-mayflies-move.md @@ -2,60 +2,4 @@ '@sumup/circuit-ui': major --- ---- - -## "@sumup/circuit-ui": major - -Popover menus are a common pattern to display additional actions commonly when interacting with an actionable component. The old popover component was removed and replaced with the new one. - -The Popover Item component was added as an action item represented by an appropriate HTML element (button or a) with an optional leading icon and label. - -The new Popover component takes isOpen and onClose as props to control the open Popover, actions to distinguish PopoverItem and Divider. -The Popover is powered by React Popper which means you can easily change the Popover placement by simply passing the placement prop. -The triggers or the reference elements of the Popover can be primary, secondary, tertiary buttons, an overflow icons, or components with embedded buttons such as image upload component. -The states of the popover are added and can be Default, Hover, Active, Focus. -The Destructive variant is added that changes the color of label and icon from blue to red to signal to the user that the action is irreversible or otherwise dangerous. - -Usage example: - -``` -import React from 'react'; -import { - Popover, - Button, -} from '@sumup/circuit-ui'; - - const [isOpen, setOpen] = useState(false); - const referenceElement = useRef(null); - - const handleClick = () => { - setOpen((prev) => !prev); - }; - - const onClose = () => { - setOpen(false); - }; - - const props = { - actions: [ - { - onClick: () => alert('Hello'), - children: 'Add', - icon: CirclePlus, - }, - ], - isOpen: true, - onClose: jest.fn(), - }; - - return ( - <> - - - - ); -}; -``` +Replaced the old Popover component with a new one. It uses Popper v2 and comes with a refreshed component API. diff --git a/packages/circuit-ui/components/Popover/Popover.docs.mdx b/packages/circuit-ui/components/Popover/Popover.docs.mdx index bc7bf5f813..3a26fb50f8 100644 --- a/packages/circuit-ui/components/Popover/Popover.docs.mdx +++ b/packages/circuit-ui/components/Popover/Popover.docs.mdx @@ -6,11 +6,9 @@ import { Popover } from '@sumup/circuit-ui'; Popover menus are a common pattern to display a list of subsequent action options, when interacting with an actionable component. - - -- Triggers of Popover can be primary, secondary, tertiary buttons, an overflow icons, or components with embedded buttons such as image upload component. +- Triggers of Popover can be primary, secondary, tertiary buttons, an overflow icon, or components with embedded buttons such as the [ImageInput component](Forms/ImageInput). - Each Popover action item is represented by an appropriate HTML element (button or a). - If needed the dividing line can be used to separate Popover action items. - The leading icon is optional. @@ -18,49 +16,8 @@ Popover menus are a common pattern to display a list of subsequent action option ## Usage guidelines -- **Do** popover menu label should be clear, actionable, concise and understandable +- **Do** use clear, concise and actionable labels for Popover items - **Do** always think about the priority of the action option to be taken and put the option order in logical order -## Usage in code - -```js -import React from 'react'; -import { - Popover, - Button, -} from '@sumup/circuit-ui'; - - const [isOpen, setOpen] = useState(false); - const referenceElement = useRef(null); - - const handleClick = () => { - setOpen((prev) => !prev); - }; - - const onClose = () => { - setOpen(false); - }; - - const props = { - actions: [ - { - onClick: () => alert('Hello'), - children: 'Add', - icon: CirclePlus, - }, - ], - isOpen: true, - onClose: jest.fn(), - }; - - return ( - <> - - - - ); -}; -``` + + diff --git a/packages/circuit-ui/components/Popover/Popover.spec.tsx b/packages/circuit-ui/components/Popover/Popover.spec.tsx index 0b791f782f..aff17dad74 100644 --- a/packages/circuit-ui/components/Popover/Popover.spec.tsx +++ b/packages/circuit-ui/components/Popover/Popover.spec.tsx @@ -99,15 +99,13 @@ describe('PopoverItem', () => { }); describe('Popover', () => { - const Default = (props: Omit) => { - const referenceElement = useRef( - null, - ); + const Default = (props: Omit) => { + const triggerRef = useRef(null); return ( <> - - + + ); }; diff --git a/packages/circuit-ui/components/Popover/Popover.stories.tsx b/packages/circuit-ui/components/Popover/Popover.stories.tsx index dad60444da..f45dd91f86 100644 --- a/packages/circuit-ui/components/Popover/Popover.stories.tsx +++ b/packages/circuit-ui/components/Popover/Popover.stories.tsx @@ -14,6 +14,7 @@ */ import React, { useState, useRef } from 'react'; +import { action } from '@storybook/addon-actions'; import { More, ThumbUp, @@ -43,7 +44,7 @@ export default { export const PopoverBase = (args) => { const [isOpen, setOpen] = useState(false); - const referenceElement = useRef(null); + const triggerRef = useRef(null); const handleClick = () => { setOpen((prev) => !prev); @@ -57,9 +58,9 @@ export const PopoverBase = (args) => { <> - + ); }; @@ -78,20 +57,20 @@ export const PopoverBase = (args) => { PopoverBase.args = { actions: [ { - onClick: () => action('Button Click'), + onClick: action('Button Click'), href: '', children: 'Label', icon: Zap, }, { - onClick: () => action('Button Click'), + onClick: action('Button Click'), href: 'https://sumup.com/', children: 'Label', icon: Zap, }, { type: 'divider' }, { - onClick: () => action('Button Click'), + onClick: action('Button Click'), children: 'Label', icon: Zap, destructive: true, @@ -99,7 +78,7 @@ PopoverBase.args = { ], }; -export const PopoverIconTrigger = (args) => { +export const PopoverInteractive = (args) => { const [isOpen, setOpen] = useState(false); const triggerRef = useRef(null); @@ -131,29 +110,29 @@ export const PopoverIconTrigger = (args) => { ); }; -PopoverIconTrigger.args = { +PopoverInteractive.args = { actions: [ { - onClick: () => action('Button Click'), + onClick: action('Button Click'), href: '', children: 'Add', icon: CirclePlus, }, { - onClick: () => action('Button Click'), + onClick: action('Button Click'), href: 'https://sumup.com/', children: 'Edit', icon: PenStroke, }, { - onClick: () => action('Button Click'), + onClick: action('Button Click'), href: 'https://sumup.com/', children: 'Upload', icon: Share, }, { type: 'divider' }, { - onClick: () => alert('Hello'), + onClick: action('Button Click'), children: 'Delete', icon: Bin, destructive: true, @@ -164,7 +143,7 @@ PopoverIconTrigger.args = { export const PopoverItemBase = (args) => ; PopoverItemBase.args = { - onClick: () => action('Button Click'), + onClick: action('Button Click'), href: 'https://sumup.com/', children: 'Label', icon: Zap,