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

Commit

Permalink
feat(Toolbar): add custom kind of ToolbarItem (#1558)
Browse files Browse the repository at this point in the history
* feat(Toolbar) - add `custom` kind of item

* simplify example

* add mention about `custom` kind

* add `focusable` prop, update warning and example

* fix classname

* fix UTs

* add changelog entry

* fix changelog entry

* update message
  • Loading branch information
miroslavstastny authored and layershifter committed Jul 2, 2019
1 parent ee412c7 commit 9e11693
Show file tree
Hide file tree
Showing 13 changed files with 268 additions and 2 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
- Add ARIA attributes and focus handling for `RadioGroup` in `Toolbar` @sophieH29 ([#1526](https://github.com/stardust-ui/react/pull/1526))
- Add ARIA attributes and keyboard navigation for `Menu` in `Toolbar` @sophieH29 ([#1553](https://github.com/stardust-ui/react/pull/1553))
- Add `alert`, `info`, `share-alt` and `microsoft-stream` icons to Teams theme @marst89 ([#1544](https://github.com/stardust-ui/react/pull/1544))
- Add `custom` `kind` for `items` in `Toolbar` component @miroslavstastny ([#1558](https://github.com/stardust-ui/react/pull/1558))

<!--------------------------------[ v0.34.0 ]------------------------------- -->
## [v0.34.0](https://github.com/stardust-ui/react/tree/v0.34.0) (2019-06-26)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import * as React from 'react'
import { Button, Text, Toolbar } from '@stardust-ui/react'

const ToolbarExampleCustomContentShorthand = () => (
<Toolbar
items={[
{ key: 'bold', icon: 'bold' },
{
key: 'custom-text',
content: <Text content="Text" />,
kind: 'custom',
},
{
key: 'custom-focusable-text',
content: <Text content="Focusable" />,
focusable: true,
kind: 'custom',
},
{
key: 'custom-button',
kind: 'custom',
content: <Button content="Button" />,
fitted: 'horizontally',
},
]}
/>
)

export default ToolbarExampleCustomContentShorthand
16 changes: 16 additions & 0 deletions docs/src/examples/components/Toolbar/Content/index.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { Alert } from '@stardust-ui/react'
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'

Expand All @@ -25,6 +27,20 @@ const Content = () => (
description="Toolbar items can be grouped into radio group. Up/Down arrow keys can be used to cycle between radio items. Only one of the radio items can be selected at a time, should be implemented additionally."
examplePath="components/Toolbar/Content/ToolbarExampleRadioGroup"
/>
<ComponentExample
title="Toolbar can contain custom content"
description="Toolbar item can contain custom content."
examplePath="components/Toolbar/Content/ToolbarExampleCustomContent"
>
<Alert warning>
<p>
When <code>custom</code> kind is used it is the responsibility of the consumer to verify
accessibility and styling aspects of the component and handle them correctly. This kind of
items can't be actionable, but actionable components might be added to the{' '}
<code>content</code> slot.
</p>
</Alert>
</ComponentExample>
</ExampleSection>
)

Expand Down
14 changes: 12 additions & 2 deletions packages/react/src/components/Toolbar/Toolbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,15 @@ import { Accessibility } from '../../lib/accessibility/types'
import { toolbarBehavior, toggleButtonBehavior } from '../../lib/accessibility'
import { ShorthandCollection, WithAsProp, withSafeTypeForAs } from '../../types'

import ToolbarCustomItem from './ToolbarCustomItem'
import ToolbarDivider from './ToolbarDivider'
import ToolbarItem from './ToolbarItem'
import ToolbarMenu from './ToolbarMenu'
import ToolbarMenuDivider from './ToolbarMenuDivider'
import ToolbarMenuItem from './ToolbarMenuItem'
import ToolbarRadioGroup from './ToolbarRadioGroup'

export type ToolbarItemShorthandKinds = 'divider' | 'item' | 'group' | 'toggle'
export type ToolbarItemShorthandKinds = 'divider' | 'item' | 'group' | 'toggle' | 'custom'

export interface ToolbarProps
extends UIComponentProps,
Expand All @@ -51,13 +52,20 @@ class Toolbar extends UIComponent<WithAsProp<ToolbarProps>, any> {

static propTypes = {
...commonPropTypes.createCommon(),
items: customPropTypes.collectionShorthandWithKindProp(['divider', 'item', 'group', 'toggle']),
items: customPropTypes.collectionShorthandWithKindProp([
'divider',
'item',
'group',
'toggle',
'custom',
]),
}

static defaultProps = {
accessibility: toolbarBehavior,
}

static CustomItem = ToolbarCustomItem
static Divider = ToolbarDivider
static Item = ToolbarItem
static Menu = ToolbarMenu
Expand All @@ -84,6 +92,8 @@ class Toolbar extends UIComponent<WithAsProp<ToolbarProps>, any> {
defaultProps: { accessibility: toggleButtonBehavior },
overrideProps: itemOverridesFn,
})
case 'custom':
return ToolbarCustomItem.create(item, { overrideProps: itemOverridesFn })
default:
return ToolbarItem.create(item, { overrideProps: itemOverridesFn })
}
Expand Down
122 changes: 122 additions & 0 deletions packages/react/src/components/Toolbar/ToolbarCustomItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import * as React from 'react'
import * as PropTypes from 'prop-types'

import {
ChildrenComponentProps,
ContentComponentProps,
createShorthandFactory,
UIComponentProps,
UIComponent,
childrenExist,
commonPropTypes,
isFromKeyboard,
} from '../../lib'

import { ComponentEventHandler, WithAsProp, withSafeTypeForAs } from '../../types'
import { Accessibility } from '../../lib/accessibility/types'
import { defaultBehavior } from '../../lib/accessibility'
import { IS_FOCUSABLE_ATTRIBUTE } from '../../lib/accessibility/FocusZone'
import * as _ from 'lodash'

export interface ToolbarCustomItemProps
extends UIComponentProps,
ChildrenComponentProps,
ContentComponentProps {
/**
* Accessibility behavior if overridden by the user.
*/
accessibility?: Accessibility

/** A custom item can remove element padding, vertically or horizontally. */
fitted?: boolean | 'horizontally' | 'vertically'

/** A custom item can be focused. */
focusable?: boolean

/** A custom item can't be actionable. */
onClick: never

/**
* Called after user's focus. Will be called only if the item is focusable.
* @param {SyntheticEvent} event - React's original SyntheticEvent.
* @param {object} data - All props.
*/
onFocus?: ComponentEventHandler<ToolbarCustomItemProps>

/**
* Called after item blur. Will be called only if the item is focusable.
* @param {SyntheticEvent} event - React's original SyntheticEvent.
* @param {object} data - All props.
*/
onBlur?: ComponentEventHandler<ToolbarCustomItemProps>
}

interface ToolbarCustomItemState {
isFromKeyboard: boolean
}

class ToolbarCustomItem extends UIComponent<
WithAsProp<ToolbarCustomItemProps>,
ToolbarCustomItemState
> {
static displayName = 'ToolbarCustomItem'

static className = 'ui-toolbar__customitem'

static create: Function

static propTypes = {
...commonPropTypes.createCommon(),
fitted: PropTypes.oneOfType([PropTypes.bool, PropTypes.oneOf(['horizontally', 'vertically'])]),
focusable: PropTypes.bool,
onFocus: PropTypes.func,
onBlur: PropTypes.func,
}

static defaultProps = {
accessibility: defaultBehavior,
}

handleBlur = (e: React.SyntheticEvent) => {
if (this.props.focusable) {
this.setState({ isFromKeyboard: false })
_.invoke(this.props, 'onBlur', e, this.props)
}
}

handleFocus = (e: React.SyntheticEvent) => {
if (this.props.focusable) {
this.setState({ isFromKeyboard: isFromKeyboard() })
_.invoke(this.props, 'onFocus', e, this.props)
}
}

renderComponent({ ElementType, classes, variables, accessibility, unhandledProps }) {
const { children, content, focusable } = this.props
return (
<ElementType
{...accessibility.attributes.root}
{...{ [IS_FOCUSABLE_ATTRIBUTE]: focusable }}
{...unhandledProps}
className={classes.root}
onBlur={this.handleBlur}
onFocus={this.handleFocus}
>
{childrenExist(children) ? children : content}
</ElementType>
)
}
}

ToolbarCustomItem.create = createShorthandFactory({
Component: ToolbarCustomItem,
mappedProp: 'content',
})

/**
* Custom toolbar item.
* The item renders as a non-focusable div with custom content inside.
*/
export default withSafeTypeForAs<typeof ToolbarCustomItem, ToolbarCustomItemProps>(
ToolbarCustomItem,
)
2 changes: 2 additions & 0 deletions packages/react/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,8 @@ export { default as Animation } from './components/Animation/Animation'

export * from './components/Toolbar/Toolbar'
export { default as Toolbar } from './components/Toolbar/Toolbar'
export * from './components/Toolbar/ToolbarCustomItem'
export { default as ToolbarCustomItem } from './components/Toolbar/ToolbarCustomItem'
export * from './components/Toolbar/ToolbarDivider'
export { default as ToolbarDivider } from './components/Toolbar/ToolbarDivider'
export * from './components/Toolbar/ToolbarItem'
Expand Down
1 change: 1 addition & 0 deletions packages/react/src/themes/teams/componentStyles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ export { default as Status } from './components/Status/statusStyles'

export { default as Text } from './components/Text/textStyles'

export { default as ToolbarCustomItem } from './components/Toolbar/toolbarCustomItemStyles'
export { default as ToolbarDivider } from './components/Toolbar/toolbarDividerStyles'
export { default as ToolbarItem } from './components/Toolbar/toolbarItemStyles'
export { default as ToolbarMenu } from './components/Toolbar/toolbarMenuStyles'
Expand Down
1 change: 1 addition & 0 deletions packages/react/src/themes/teams/componentVariables.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ export { default as Status } from './components/Status/statusVariables'
export { default as Text } from './components/Text/textVariables'

export { default as Toolbar } from './components/Toolbar/toolbarVariables'
export { default as ToolbarCustomItem } from './components/Toolbar/toolbarCustomItemVariables'
export { default as ToolbarItem } from './components/Toolbar/toolbarItemVariables'
export { default as ToolbarDivider } from './components/Toolbar/toolbarDividerVariables'
export { default as ToolbarMenu } from './components/Toolbar/toolbarMenuVariables'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { ComponentSlotStylesInput, ICSSInJSStyle } from '../../../types'
import { ToolbarCustomItemProps } from '../../../../components/Toolbar/ToolbarCustomItem'
import { ToolbarVariables } from './toolbarVariables'
import { getColorScheme } from '../../colors'

const toolbarCustomItemStyles: ComponentSlotStylesInput<
ToolbarCustomItemProps,
ToolbarVariables
> = {
root: ({ props: p, variables: v }): ICSSInJSStyle => {
const colors = getColorScheme(v.colorScheme)

return {
backgroundColor: v.background,
borderColor: 'transparent',
borderWidth: v.borderWidth,
borderStyle: 'solid',
height: v.itemHeight,
color: v.foreground || colors.foreground1,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
...(p.fitted !== true &&
p.fitted !== 'horizontally' && {
paddingLeft: v.customItemHorizontalPadding,
paddingRight: v.customItemHorizontalPadding,
}),
...(p.fitted !== true &&
p.fitted !== 'vertically' && {
paddingTop: v.customItemVerticalPadding,
paddingBottom: v.customItemVerticalPadding,
}),

':focus': {
outline: 0,
},

...(p.isFromKeyboard && {
color: v.foregroundFocus || colors.foregroundFocus,
backgroundColor: v.backgroundFocus || colors.backgroundFocus,
borderColor: v.borderFocus || colors.borderFocus,
}),
}
},
}

export default toolbarCustomItemStyles
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import toolbarVariables from './toolbarVariables'

export default toolbarVariables
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,9 @@ export interface ToolbarVariables {

menuDividerBorder: string // border color
menuDividerMargin: string

customItemHorizontalPadding: string
customItemVerticalPadding: string
}

export default (siteVars: any): ToolbarVariables => ({
Expand Down Expand Up @@ -135,4 +138,7 @@ export default (siteVars: any): ToolbarVariables => ({

menuDividerBorder: undefined,
menuDividerMargin: `${pxToRem(8)} 0`,

customItemHorizontalPadding: pxToRem(16),
customItemVerticalPadding: pxToRem(4),
})
19 changes: 19 additions & 0 deletions packages/react/test/specs/components/Toolbar/Toolbar-test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,13 @@ describe('Toolbar', () => {
.prop('variables') as Function)(),
).toEqual(expect.objectContaining({ a: 'toolbar', b: 'overwritten', c: 'item' }))

expect(
(toolbar
.find('ToolbarCustomItem')
.first()
.prop('variables') as Function)(),
).toEqual(expect.objectContaining({ a: 'toolbar', b: 'overwritten', c: 'customItem' }))

expect(
(toolbar
.find('ToolbarDivider')
Expand All @@ -38,6 +45,12 @@ describe('Toolbar', () => {
variables={{ a: 'toolbar', b: 'toolbar' }}
items={[
{ key: 1, content: 'toolbar item', variables: { b: 'overwritten', c: 'item' } },
{
key: 'custom',
kind: 'custom',
content: 'custom toolbar item',
variables: { b: 'overwritten', c: 'customItem' },
},
{
key: 'd1',
kind: 'divider',
Expand All @@ -61,6 +74,12 @@ describe('Toolbar', () => {
variables={() => ({ a: 'toolbar', b: 'toolbar' })}
items={[
{ key: 1, content: 'toolbar item', variables: () => ({ b: 'overwritten', c: 'item' }) },
{
key: 'custom',
kind: 'custom',
content: 'custom toolbar item',
variables: () => ({ b: 'overwritten', c: 'customItem' }),
},
{
key: 'd1',
kind: 'divider',
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { isConformant } from 'test/specs/commonTests'

import ToolbarCustomItem from 'src/components/Toolbar/ToolbarCustomItem'

describe('ToolbarCustomItem', () => {
isConformant(ToolbarCustomItem, {
requiredProps: { focusable: true },
})
})

0 comments on commit 9e11693

Please sign in to comment.