Skip to content
This repository has been archived by the owner on Mar 4, 2020. It is now read-only.

feat(Popup): add pointing prop #1198

Merged
merged 20 commits into from
Apr 15, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
- Move `Flex` styles to `base` theme @kuzhelov ([#1206](https://github.com/stardust-ui/react/pull/1206))
- Add file video icon on `Icon` component @luzhon ([#1205](https://github.com/stardust-ui/react/pull/1250))
- Export `call-missed-line` icon in Teams theme @96andrei ([#1203](https://github.com/stardust-ui/react/pull/1203))
- Add `pointing` prop to `Popup` ([#1198](https://github.com/stardust-ui/react/pull/1198))

<!--------------------------------[ v0.27.0 ]------------------------------- -->
## [v0.27.0](https://github.com/stardust-ui/react/tree/v0.27.0) (2019-04-10)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import * as React from 'react'
import { Button, Grid, Popup } from '@stardust-ui/react'

const PopupWithButton = props => (
<Popup
align={props.align}
content={{
content: (
<>
<p>A popup with a pointer.</p>
<p>
Is aligned to <code>{props.align}</code>.
</p>
</>
),
}}
pointing
position={props.position}
trigger={
<Button
icon={props.icon}
styles={{ padding: props.padding, height: '38px', minWidth: '64px' }}
/>
}
/>
)

const triggers = [
{ position: 'above', align: 'start', icon: 'arrow circle up', padding: '5px 42px 18px 5px' },
{ position: 'above', align: 'center', icon: 'arrow circle up', padding: '5px 5px 18px 5px' },
{ position: 'above', align: 'end', icon: 'arrow circle up', padding: '5px 5px 18px 42px' },
{ position: 'below', align: 'start', icon: 'arrow circle down', padding: '18px 42px 5px 5px' },
{ position: 'below', align: 'center', icon: 'arrow circle down', padding: '18px 5px 5px 5px' },
{ position: 'below', align: 'end', icon: 'arrow circle down', padding: '18px 5px 5px 42px' },
{ position: 'before', align: 'top', icon: 'arrow circle left', padding: '5px 42px 18px 5px' },
{ position: 'before', align: 'center', icon: 'arrow circle left', padding: '5px 42px 5px 5px' },
{ position: 'before', align: 'bottom', icon: 'arrow circle left', padding: '18px 42px 5px 5px' },
{ position: 'after', align: 'top', icon: 'arrow circle right', padding: '5px 5px 18px 42px' },
{ position: 'after', align: 'center', icon: 'arrow circle right', padding: '5px 5px 5px 42px' },
{ position: 'after', align: 'bottom', icon: 'arrow circle right', padding: '18px 5px 5px 42px' },
]

const PopupExamplePointing = () => (
<Grid columns="repeat(3, 30px)" variables={{ padding: '30px', gridGap: '80px' }}>
{triggers.map(({ position, align, icon, padding }) => (
<PopupWithButton
align={align}
icon={icon}
key={`${position}-${align}`}
padding={padding}
position={position}
/>
))}
</Grid>
)

export default PopupExamplePointing
5 changes: 5 additions & 0 deletions docs/src/examples/components/Popup/Types/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@ const Types = () => (
description="A default popup."
examplePath="components/Popup/Types/PopupExample"
/>
<ComponentExample
title="Pointing"
description="A popup can have a pointer."
examplePath="components/Popup/Types/PopupExamplePointing"
/>
<ComponentExample
title="Controlled"
description="Note that if Popup is controlled, then its 'open' prop value could be changed either by parent component, or by user actions (e.g. key press) - thus it is necessary to handle 'onOpenChange' event. Try to type some text into popup's input field and press ESC to see the effect."
Expand Down
19 changes: 16 additions & 3 deletions packages/react/src/components/Popup/Popup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,9 @@ export interface PopupProps
*/
onOpenChange?: ComponentEventHandler<PopupProps>

/** A popup can show a pointer to trigger. */
pointing?: boolean

/**
* Position for the popup. Position has higher priority than align. If position is vertical ('above' | 'below')
* and align is also vertical ('top' | 'bottom') or if both position and align are horizontal ('before' | 'after'
Expand Down Expand Up @@ -171,6 +174,7 @@ export default class Popup extends AutoControlledComponent<ReactProps<PopupProps
]),
open: PropTypes.bool,
onOpenChange: PropTypes.func,
pointing: PropTypes.bool,
position: PropTypes.oneOf(POSITIONS),
renderContent: PropTypes.func,
target: PropTypes.any,
Expand Down Expand Up @@ -438,9 +442,15 @@ export default class Popup extends AutoControlledComponent<ReactProps<PopupProps
rtl: boolean,
accessibility: AccessibilityBehavior,
// https://popper.js.org/popper-documentation.html#Popper.scheduleUpdate
{ ref, scheduleUpdate, style: popupPlacementStyles }: PopperChildrenProps,
{
arrowProps,
placement,
ref,
scheduleUpdate,
style: popupPlacementStyles,
}: PopperChildrenProps,
) => {
const { content: propsContent, renderContent, contentRef } = this.props
const { content: propsContent, renderContent, contentRef, pointing } = this.props
const content = renderContent ? renderContent(scheduleUpdate) : propsContent

const popupWrapperAttributes = {
Expand Down Expand Up @@ -471,8 +481,11 @@ export default class Popup extends AutoControlledComponent<ReactProps<PopupProps

const popupContent = Popup.Content.create(content, {
defaultProps: {
className: Popup.slotClassNames.content,
...popupContentAttributes,
placement,
pointing,
pointerRef: arrowProps.ref,
pointerStyle: arrowProps.style,
},
overrideProps: this.getContentProps,
})
Expand Down
46 changes: 44 additions & 2 deletions packages/react/src/components/Popup/PopupContent.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import * as React from 'react'
import { PopperChildrenProps } from 'react-popper'
import * as PropTypes from 'prop-types'
import * as _ from 'lodash'

Expand All @@ -16,6 +17,8 @@ import {
import { Accessibility } from '../../lib/accessibility/types'
import { defaultBehavior } from '../../lib/accessibility'
import { ReactProps, ComponentEventHandler } from '../../types'
import Box from '../Box/Box'
import Ref from '../Ref/Ref'

export interface PopupContentProps
extends UIComponentProps,
Expand All @@ -40,6 +43,18 @@ export interface PopupContentProps
* @param {object} data - All props.
*/
onMouseLeave?: ComponentEventHandler<PopupContentProps>

/** An actual placement value from Popper. */
placement?: PopperChildrenProps['placement']

/** A popup can show a pointer to trigger. */
pointing?: boolean

/** A ref to a pointer element. */
pointerRef?: PopperChildrenProps['arrowProps']['ref']

/** An object with positioning styles fof a pointer. */
pointerStyle?: PopperChildrenProps['arrowProps']['style']
}

/**
Expand All @@ -55,8 +70,12 @@ class PopupContent extends UIComponent<ReactProps<PopupContentProps>, any> {

public static propTypes = {
...commonPropTypes.createCommon(),
placement: PropTypes.string,
pointing: PropTypes.bool,
onMouseEnter: PropTypes.func,
onMouseLeave: PropTypes.func,
pointerRef: PropTypes.func,
pointerStyle: PropTypes.object,
}

static defaultProps = {
Expand All @@ -76,8 +95,9 @@ class PopupContent extends UIComponent<ReactProps<PopupContentProps>, any> {
ElementType,
classes,
unhandledProps,
styles,
}: RenderResultConfig<PopupContentProps>): React.ReactNode {
const { children, content } = this.props
const { children, content, pointing, pointerRef, pointerStyle } = this.props

return (
<ElementType
Expand All @@ -88,7 +108,29 @@ class PopupContent extends UIComponent<ReactProps<PopupContentProps>, any> {
onMouseEnter={this.handleMouseEnter}
onMouseLeave={this.handleMouseLeave}
>
{childrenExist(children) ? children : content}
{pointing && (
<Ref innerRef={pointerRef}>
{Box.create(
{},
{
defaultProps: {
style: pointerStyle,
styles: styles.pointer,
},
},
)}
</Ref>
)}

{Box.create(
{},
{
defaultProps: {
children: childrenExist(children) ? children : content,
styles: styles.content,
},
},
)}
</ElementType>
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,4 @@ export { default as TreeTitle } from './components/Tree/treeTitleVariables'
export { default as Menu } from './components/Menu/menuVariables'
export { default as Reaction } from './components/Reaction/reactionVariables'

export { default as Popup } from './components/Popup/popupVariables'
export { default as PopupContent } from './components/Popup/popupContentVariables'

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,4 @@ export { default as Menu } from './components/Menu/menuVariables'
export { default as RadioGroupItem } from './components/RadioGroup/radioGroupItemVariables'
export { default as Text } from './components/Text/textVariables'
export { default as TreeTitle } from './components/Tree/treeTitleVariables'
export { default as Popup } from './components/Popup/popupVariables'
export { default as Reaction } from './components/Reaction/reactionVariables'

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,20 +1,88 @@
import { PopperChildrenProps } from 'react-popper'
import { ComponentSlotStylesInput, ICSSInJSStyle } from '../../../types'
import { pxToRem } from '../../../../lib'
import { PopupContentProps } from '../../../../components/Popup/PopupContent'
import { PopupContentVariables } from './popupContentVariables'

const rtlMapping = {
left: 'right',
right: 'left',
}

const getPointerStyles = (
v: PopupContentVariables,
rtl: boolean,
popperPlacement?: PopperChildrenProps['placement'],
) => {
const placementValue = (popperPlacement || '').split('-', 1).pop()
const placement = (rtl && rtlMapping[placementValue]) || placementValue

const rootStyles = {
top: {
marginBottom: v.pointerMargin,
},
right: {
marginLeft: v.pointerMargin,
},
bottom: {
marginTop: v.pointerMargin,
},
left: {
marginRight: v.pointerMargin,
},
}
const pointerStyles = {
top: {
bottom: `-${v.pointerOffset}`,
transform: 'rotate(45deg)',
},
right: {
left: `-${v.pointerOffset}`,
transform: 'rotate(135deg)',
},
bottom: {
top: `-${v.pointerOffset}`,
transform: 'rotate(-135deg)',
},
left: {
right: `-${v.pointerOffset}`,
transform: 'rotate(-45deg)',
},
}

return {
root: rootStyles[placement],
pointer: pointerStyles[placement],
}
}

const popupContentStyles: ComponentSlotStylesInput<PopupContentProps, PopupContentVariables> = {
root: ({ variables }): ICSSInJSStyle => {
const { borderColor, padding } = variables

return {
display: 'block',
padding,
border: `1px solid ${borderColor}`,
borderRadius: pxToRem(3),
boxShadow: `0 2px 4px 0 ${borderColor}, 0 2px 10px 0 ${borderColor}`,
}
},
root: ({ props: p, theme: t, variables: v }): ICSSInJSStyle => ({
borderRadius: v.borderRadius,
display: 'block',

...(p.pointing && getPointerStyles(v, t.rtl, p.placement).root),
}),
pointer: ({ props: p, theme: t, variables: v }): ICSSInJSStyle => ({
display: 'block',
position: 'absolute',

backgroundColor: 'inherit',
borderBottom: `${v.borderSize} solid ${v.borderColor}`,
borderRight: `${v.borderSize} solid ${v.borderColor}`,

height: v.pointerSize,
width: v.pointerSize,

...getPointerStyles(v, t.rtl, p.placement).pointer,
}),
content: ({ props: p, variables: v }): ICSSInJSStyle => ({
display: 'block',
padding: v.padding,

border: `${v.borderSize} solid ${v.borderColor}`,
borderRadius: 'inherit',
boxShadow: `0 2px 4px 0 ${v.borderColor}, 0 2px 10px 0 ${v.borderColor}`,
}),
}

export default popupContentStyles
Original file line number Diff line number Diff line change
@@ -1,15 +1,25 @@
import { pxToRem } from '../../../../lib'

export interface PopupContentVariables {
[key: string]: string | number

borderColor: string
borderRadius: string
borderSize: string
padding: string

pointerMargin: string
pointerOffset: string
pointerSize: string
}

export default (siteVars: any): PopupContentVariables => {
return {
borderColor: siteVars.gray06,
borderRadius: pxToRem(3),
borderSize: '1px',
padding: `${pxToRem(10)} ${pxToRem(14)}`,

pointerOffset: pxToRem(5),
pointerMargin: pxToRem(10),
pointerSize: pxToRem(10),
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@ import { PopupVariables } from './popupVariables'
const popupStyles: ComponentSlotStylesInput<PopupProps, PopupVariables> = {
root: (): ICSSInJSStyle => ({}),

popup: ({ variables }): ICSSInJSStyle => ({
zIndex: variables.zIndex,
popup: ({ variables: v }): ICSSInJSStyle => ({
zIndex: v.zIndex,
position: 'absolute',
textAlign: 'left',
color: variables.contentColor,
background: variables.contentBackgroundColor,
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved to popupContentStyles...

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Talk to @kuzhelov. In #1121 he intentionally moved the styles from popupContentStyles to here.

Copy link
Contributor

@kuzhelov kuzhelov Apr 12, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

these styles should be left here, as we popup expects any content to be rendered with this set of styles, not just PopupContent. Also, please, ensure that PopupContent just CSS-inherits this set of colors.

These two requirements are necessary for introducing correct semantics around these styles, as well as for expected effects of theme switching.

FYI #1121

color: v.contentColor,
background: v.contentBackgroundColor,
}),
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
export interface PopupVariables {
[key: string]: string | number

zIndex: number
contentColor: string
contentBackgroundColor: string
Expand Down
Loading