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

feat(Toolbar): add popup prop #1480

Merged
merged 10 commits into from
Jun 13, 2019
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
### Features
- Define types for accessibility behaviors props. Do not render `aria-disabled` if the value is `false` @sophieH29 ([#1481](https://github.com/stardust-ui/react/pull/1481))
- Add `Toolbar` component @miroslavstastny ([#1408](https://github.com/stardust-ui/react/pull/1408))
- Add `popup` prop to `Toolbar` component @miroslavstastny ([#1408](https://github.com/stardust-ui/react/pull/1480))
- Add `disableAnimations` boolean prop on the `Provider` @mnajdova ([#1377](https://github.com/stardust-ui/react/pull/1377))
- Integrate `Dropdown` with `Form.Field` @silviuavram ([#1446](https://github.com/stardust-ui/react/pull/1446))
- Add expand/collapse and navigation with `ArrowUp` and `ArrowDown` to `Tree` @silviuavram ([#1457](https://github.com/stardust-ui/react/pull/1457))
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,54 @@
import * as React from 'react'
import { Toolbar } from '@stardust-ui/react'
import { Toolbar, Input, Button, Form } from '@stardust-ui/react'

const fields = [
{
label: 'First name',
name: 'firstName',
id: 'first-name-inline-shorthand',
key: 'first-name',
required: true,
inline: true,
},
{
label: 'Last name',
name: 'lastName',
id: 'last-name-inline-shorthand',
key: 'last-name',
required: true,
inline: true,
},
{
label: 'I agree to the Terms and Conditions',
control: {
as: 'input',
},
type: 'checkbox',
id: 'conditions-inline-shorthand',
key: 'conditions',
},
{
control: {
as: Button,
content: 'Submit',
},
key: 'submit',
},
]

const HighlightPopup = ({ onConfirm }) => {
return <Form onSubmit={onConfirm} fields={fields} />
}

const ToolbarExampleShorthand = () => {
const [isBold, setBold] = React.useState(true)
const [isItalic, setItalic] = React.useState(false)
const [isUnderline, setUnderline] = React.useState(false)
const [isStrike, setStrike] = React.useState(false)

const [highlightOpen, setHighlightOpen] = React.useState(false)
const [fontColorActive, setFontColorActive] = React.useState(false)

return (
<Toolbar
items={[
Expand Down Expand Up @@ -44,8 +86,37 @@ const ToolbarExampleShorthand = () => {
},
},
{ key: 'divider1', kind: 'divider' },
{ key: 'highlight', icon: { name: 'highlight', outline: true } },
{ key: 'font-color', icon: { name: 'font-color', outline: true } },
{
key: 'highlight',
icon: { name: 'highlight', outline: true },
active: highlightOpen,
popup: {
content: {
content: (
miroslavstastny marked this conversation as resolved.
Show resolved Hide resolved
<HighlightPopup
onConfirm={() => {
setHighlightOpen(false)
}}
/>
),
},
onOpenChange: (e, { open }) => {
setHighlightOpen(open)
},
open: highlightOpen,
},
},
{
key: 'font-color',
icon: { name: 'font-color', outline: true },
active: fontColorActive,
popup: {
content: { content: <Input icon="search" placeholder="Search..." /> },
onOpenChange: () => {
setFontColorActive(!fontColorActive)
},
},
},
{ key: 'font-size', icon: { name: 'font-size', outline: true } },
{ key: 'remove-format', icon: { name: 'remove-format', outline: true } },
{ key: 'divider2', kind: 'divider' },
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { ToolbarItem } from '@stardust-ui/react'

const config: ScreenerTestsConfig = {
themes: ['teams', 'teamsDark', 'teamsHighContrast'],
steps: [
builder =>
builder
.click(`.${ToolbarItem.className}:nth-child(1)`)
.snapshot('Shows first popup')
.click(`.${ToolbarItem.className}:nth-child(2)`)
.snapshot('Shows second popup'),
],
}

export default config
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import * as React from 'react'
import { Toolbar, Input, Button, Form } from '@stardust-ui/react'

const HighlightPopup = ({ onConfirm }) => {
return (
<Form
onSubmit={onConfirm}
fields={[
{
label: 'First name',
name: 'firstName',
id: 'first-name-inline-shorthand',
key: 'first-name',
required: true,
inline: true,
},
{
control: {
as: Button,
content: 'Submit',
},
key: 'submit',
},
]}
/>
)
}

const ToolbarExamplePopupShorthand = () => {
const [highlightOpen, setHighlightOpen] = React.useState(false)
const [fontColorActive, setFontColorActive] = React.useState(false)
return (
<Toolbar
items={[
{
key: 'highlight',
icon: { name: 'highlight', outline: true },
active: highlightOpen,
popup: {
content: {
content: (
<HighlightPopup
onConfirm={() => {
setHighlightOpen(false)
}}
/>
),
},
onOpenChange: (e, { open }) => {
setHighlightOpen(open)
},
open: highlightOpen,
},
},
{
key: 'font-color',
icon: { name: 'font-color', outline: true },
active: fontColorActive,
popup: {
content: { content: <Input icon="search" placeholder="Search..." /> },
onOpenChange: () => {
setFontColorActive(!fontColorActive)
},
},
},
]}
/>
)
}

export default ToolbarExamplePopupShorthand
11 changes: 11 additions & 0 deletions docs/src/examples/components/Toolbar/Types/index.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,20 @@
import * as React from 'react'
import { Link } from 'react-router-dom'
import ComponentExample from 'docs/src/components/ComponentDoc/ComponentExample'
import ExampleSection from 'docs/src/components/ComponentDoc/ExampleSection'

const Types = () => (
<ExampleSection title="Types">
<ComponentExample
title="Toolbar can contain a popup"
description={
<>
Toolbar item can open a popup. See <Link to="/components/popup">Popup</Link> component for
more details.
</>
}
examplePath="components/Toolbar/Types/ToolbarExamplePopup"
/>
<ComponentExample
title="Text editor toolbar"
description="A Toolbar use case for a text editor."
Expand Down
16 changes: 13 additions & 3 deletions packages/react-proptypes/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,11 @@ export const as = PropTypes.oneOfType([
*/
export const domNode = (props: ObjectOf<any>, propName: string) => {
// skip if prop is undefined
if (props[propName] === undefined) return
if (props[propName] === undefined) return undefined
// skip if prop is valid
if (props[propName] instanceof Element) return
if (props[propName] instanceof Element) return undefined

throw new Error(`Invalid prop "${propName}" supplied, expected a DOM node.`)
return new Error(`Invalid prop "${propName}" supplied, expected a DOM node.`)
}

/**
Expand Down Expand Up @@ -113,6 +113,16 @@ export const suggest = (suggestions: string[]) => {
}
}

/**
* The prop cannot be used.
* Similar to `deprecate` but with different error message.
*/
export const never = (props: ObjectOf<any>, propName: string, componentName: string) => {
if (_.isNil(props[propName]) || props[propName] === false) return undefined

return new Error(`Prop \`${propName}\` in \`${componentName}\` cannot be used.`)
}

/**
* Disallow other props from being defined with this prop.
* @param {string[]} disallowedProps An array of props that cannot be used with this prop.
Expand Down
5 changes: 5 additions & 0 deletions packages/react/src/components/Popup/Popup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import {

import { Accessibility } from '../../lib/accessibility/types'
import { ReactAccessibilityBehavior } from '../../lib/accessibility/reactTypes'
import { createShorthandFactory } from '../../lib/factories'

export type PopupEvents = 'click' | 'hover' | 'focus'
export type RestrictedClickEvents = 'click' | 'focus'
Expand Down Expand Up @@ -129,6 +130,8 @@ export default class Popup extends AutoControlledComponent<PopupProps, PopupStat

static className = 'ui-popup'

static create: Function

static slotClassNames: PopupSlotClassNames = {
content: `${Popup.className}__content`,
}
Expand Down Expand Up @@ -529,3 +532,5 @@ export default class Popup extends AutoControlledComponent<PopupProps, PopupStat
: this.triggerRef.current
}
}

Popup.create = createShorthandFactory({ Component: Popup, mappedProp: 'content' })
45 changes: 41 additions & 4 deletions packages/react/src/components/Toolbar/ToolbarItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,18 @@ import {
childrenExist,
isFromKeyboard,
} from '../../lib'
import { ComponentEventHandler, ShorthandValue, WithAsProp, withSafeTypeForAs } from '../../types'
import {
ComponentEventHandler,
ShorthandValue,
WithAsProp,
withSafeTypeForAs,
Omit,
} from '../../types'
import { Accessibility } from '../../lib/accessibility/types'
import { defaultBehavior } from '../../lib/accessibility'
import { defaultBehavior, popupFocusTrapBehavior } from '../../lib/accessibility'

import Icon from '../Icon/Icon'
import Popup, { PopupProps } from '../Popup/Popup'

export interface ToolbarItemProps
extends UIComponentProps,
Expand Down Expand Up @@ -58,6 +65,14 @@ export interface ToolbarItemProps
* @param {object} data - All props.
*/
onBlur?: ComponentEventHandler<ToolbarItemProps>

/**
* Attaches a `Popup` component to the ToolbarItem.
* Accepts all props as a `Popup`, except `trigger` and `children`.
* Sets `accessibility` to `popupFocusTrapBehavior` by default.
* @see PopupProps
*/
popup?: Omit<PopupProps, 'trigger' | 'children'> | string
}

export interface ToolbarItemState {
Expand All @@ -79,6 +94,14 @@ class ToolbarItem extends UIComponent<WithAsProp<ToolbarItemProps>, ToolbarItemS
onClick: PropTypes.func,
onFocus: PropTypes.func,
onBlur: PropTypes.func,
popup: PropTypes.oneOfType([
PropTypes.shape({
...Popup.propTypes,
trigger: customPropTypes.never,
children: customPropTypes.never,
}),
PropTypes.string,
]),
}

static defaultProps = {
Expand All @@ -87,8 +110,8 @@ class ToolbarItem extends UIComponent<WithAsProp<ToolbarItemProps>, ToolbarItemS
}

renderComponent({ ElementType, classes, unhandledProps, accessibility }) {
const { icon, children, disabled } = this.props
return (
const { icon, children, disabled, popup } = this.props
const renderedItem = (
<ElementType
{...accessibility.attributes.root}
{...unhandledProps}
Expand All @@ -101,6 +124,20 @@ class ToolbarItem extends UIComponent<WithAsProp<ToolbarItemProps>, ToolbarItemS
{childrenExist(children) ? children : Icon.create(icon)}
</ElementType>
)

if (popup) {
miroslavstastny marked this conversation as resolved.
Show resolved Hide resolved
return Popup.create(popup, {
defaultProps: {
accessibility: popupFocusTrapBehavior,
},
overrideProps: {
trigger: renderedItem,
children: undefined, // force-reset `children` defined for `Popup` as it collides with the `trigger
miroslavstastny marked this conversation as resolved.
Show resolved Hide resolved
},
})
}

return renderedItem
}

handleBlur = (e: React.SyntheticEvent) => {
Expand Down
4 changes: 1 addition & 3 deletions packages/react/src/themes/types.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as CSSType from 'csstype'
import { IRenderer as FelaRenderer } from 'fela'
import * as React from 'react'
import { Extendable, ObjectOf, ObjectOrFunc } from '../types'
import { Extendable, ObjectOf, ObjectOrFunc, Omit } from '../types'

// Themes go through 3 phases.
// 1. Input - (from the user), variable and style objects/functions, some values optional
Expand Down Expand Up @@ -214,8 +214,6 @@ export interface StardustAnimationName {
params?: object
}

type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>

export type CSSProperties = Omit<React.CSSProperties, 'animationName'> & {
animationName?: StardustAnimationName | string | 'none'
}
Expand Down
2 changes: 2 additions & 0 deletions packages/react/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ export type ObjectOf<T> = { [key: string]: T }

export type ObjectOrFunc<TResult, TArg = {}> = ((arg: TArg) => TResult) | TResult

export type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>

// ========================================================
// Props
// ========================================================
Expand Down