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

feat(MenuItem|AccordionTitle): add ability for removing and customizing the (active|submenu) indicator #721

Merged
merged 19 commits into from
Jan 21, 2019
Merged
Show file tree
Hide file tree
Changes from 15 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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm

## [Unreleased]

### Features
- Add `Indicator` component and used it in `MenuItem` and `AccordionTitle` @mnajdova ([#721](https://github.com/stardust-ui/react/pull/721))

<!--------------------------------[ v0.17.0 ]------------------------------- -->
## [v0.17.0](https://github.com/stardust-ui/react/tree/v0.17.0) (2019-01-17)
[Compare changes](https://github.com/stardust-ui/react/compare/v0.16.2...v0.17.0)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import * as React from 'react'
import { Indicator } from '@stardust-ui/react'

const IndicatorExample = () => <Indicator />

export default IndicatorExample
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import * as React from 'react'
import { Indicator } from '@stardust-ui/react'

const IndicatorExampleDirection = () => (
<div>
<Indicator direction="end" /> <Indicator direction="bottom" /> <Indicator direction="start" />{' '}
<Indicator direction="top" />{' '}
</div>
)

export default IndicatorExampleDirection
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import * as React from 'react'
import { Indicator } from '@stardust-ui/react'

const IndicatorExampleIcon = () => (
<div>
<Indicator icon="chevron down" direction="end" />{' '}
<Indicator icon="chevron down" direction="bottom" />{' '}
<Indicator icon="chevron down" direction="start" />{' '}
<Indicator icon="chevron down" direction="top" />{' '}
</div>
)
export default IndicatorExampleIcon
25 changes: 25 additions & 0 deletions docs/src/examples/components/Indicator/Types/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import * as React from 'react'
import ComponentExample from 'docs/src/components/ComponentDoc/ComponentExample'
import ExampleSection from 'docs/src/components/ComponentDoc/ExampleSection'

const Types = () => (
<ExampleSection title="Types">
<ComponentExample
title="Default"
description="A default Indicator ."
examplePath="components/Indicator/Types/IndicatorExample"
/>
<ComponentExample
title="Direction"
description="An Indicator may show towards different direction."
examplePath="components/Indicator/Types/IndicatorExampleDirection"
/>
<ComponentExample
title="Icon"
description="An Indicator may show icon instead of character."
examplePath="components/Indicator/Types/IndicatorExampleIcon"
/>
</ExampleSection>
)

export default Types
10 changes: 10 additions & 0 deletions docs/src/examples/components/Indicator/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import * as React from 'react'
import Types from './Types'

const IndicatorExamples = () => (
<div>
<Types />
</div>
)

export default IndicatorExamples
34 changes: 22 additions & 12 deletions src/components/Accordion/AccordionTitle.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,10 @@ import {
ContentComponentProps,
ChildrenComponentProps,
commonPropTypes,
customPropTypes,
} from '../../lib'
import { ReactProps, ComponentEventHandler } from '../../../types/utils'

import { ReactProps, ComponentEventHandler, ShorthandValue } from '../../../types/utils'
import Indicator from '../Indicator/Indicator'
export interface AccordionTitleProps
extends UIComponentProps,
ContentComponentProps,
Expand All @@ -30,6 +31,9 @@ export interface AccordionTitleProps
* @param {object} data - All props.
*/
onClick?: ComponentEventHandler<AccordionTitleProps>

/** Shorthand for the active indicator. */
indicator?: ShorthandValue
}

/**
Expand All @@ -47,26 +51,32 @@ class AccordionTitle extends UIComponent<ReactProps<AccordionTitleProps>, any> {
active: PropTypes.bool,
index: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
onClick: PropTypes.func,
indicator: customPropTypes.itemShorthand,
}

handleClick = e => {
_.invoke(this.props, 'onClick', e, this.props)
}

renderComponent({ ElementType, classes, unhandledProps }) {
const { children, content } = this.props
renderComponent({ ElementType, classes, unhandledProps, styles }) {
const { children, content, indicator, active } = this.props
const indicatorWithDefaults = indicator === undefined ? {} : indicator

if (childrenExist(children)) {
return (
<ElementType {...unhandledProps} className={classes.root} onClick={this.handleClick}>
{children}
</ElementType>
)
}
const contentElement = (
<>
{Indicator.create(indicatorWithDefaults, {
defaultProps: {
direction: active ? 'bottom' : 'end',
styles: styles.indicator,
},
})}
{content}
</>
)

return (
<ElementType {...unhandledProps} className={classes.root} onClick={this.handleClick}>
{content}
{childrenExist(children) ? children : contentElement}
</ElementType>
)
}
Expand Down
5 changes: 5 additions & 0 deletions src/components/Icon/Icon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ export interface IconProps extends UIComponentProps, ColorComponentProps {
/** Name of the icon. */
name?: string

/** An icon can be rotated by the degree specified as number. */
rotate?: number
Copy link
Member

Choose a reason for hiding this comment

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

String looks like a valid value, too:

<Indicator rotate='90' />

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The rotate property of the icon can be used for calculation as well, take a look in the Indicator component (the overrideProps function for the Icon). In that case we will need to cast this to number if it was provided as string. Do we really want this?


/** Size of the icon. */
size?: IconSize

Expand Down Expand Up @@ -65,6 +68,7 @@ class Icon extends UIComponent<ReactProps<IconProps>, any> {
circular: PropTypes.bool,
disabled: PropTypes.bool,
name: PropTypes.string,
rotate: PropTypes.number,
size: PropTypes.oneOf(['smallest', 'smaller', 'small', 'medium', 'large', 'larger', 'largest']),
xSpacing: PropTypes.oneOf(['none', 'before', 'after', 'both']),
}
Expand All @@ -73,6 +77,7 @@ class Icon extends UIComponent<ReactProps<IconProps>, any> {
as: 'span',
size: 'medium',
accessibility: iconBehavior,
rotate: 0,
}

private renderFontIcon(ElementType, classes, unhandledProps, accessibility): React.ReactNode {
Expand Down
89 changes: 89 additions & 0 deletions src/components/Indicator/Indicator.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import * as React from 'react'
import * as PropTypes from 'prop-types'

import {
createShorthandFactory,
UIComponent,
UIComponentProps,
commonPropTypes,
customPropTypes,
} from '../../lib'
import { ReactProps, ShorthandValue } from '../../../types/utils'
import Icon from '../Icon/Icon'

export interface IndicatorProps extends UIComponentProps {
/** The indicator can point towards different directions. */
direction?: 'start' | 'end' | 'top' | 'bottom'

/** The indicator can show specific icon if provided. */
icon?: ShorthandValue
}

/**
* An indicator is suggesting additional content next to the element it is used in.
*/
class Indicator extends UIComponent<ReactProps<IndicatorProps>, any> {
static displayName = 'Indicator'

static create: Function

static className = 'ui-indicator'

static directionMap = {
end: { unicode: '25B8', rotation: -90 },
start: { unicode: '25C2', rotation: 90 },
top: { unicode: '25B4', rotation: 180 },
bottom: { unicode: '25BE', rotation: 0 },
}

static propTypes = {
...commonPropTypes.createCommon({ children: false, content: false }),
direction: PropTypes.oneOf(['start', 'end', 'top', 'bottom']),
icon: customPropTypes.itemShorthand,
}

static defaultProps = {
as: 'span',
direction: 'bottom',
}

renderComponent({ ElementType, classes, unhandledProps, rtl }) {
const { direction, icon, color } = this.props
const hexUnicode =
direction && Indicator.directionMap[this.getDirectionBasedOnRtl(rtl, direction)].unicode
const contentProps = !icon
? {
dangerouslySetInnerHTML: {
__html: hexUnicode && this.isHex(hexUnicode) ? `&#x${hexUnicode};` : '',
},
}
: {
children: Icon.create(icon, {
defaultProps: { color },
overrideProps: ({ rotate }) => ({
rotate: (Indicator.directionMap[direction].rotation || 0) + (rotate || 0),
}),
}),
}
return <ElementType {...unhandledProps} className={classes.root} {...contentProps} />
}

private isHex(h) {
return (
parseInt(h, 16)
.toString(16)
.toUpperCase() === h.toUpperCase()
)
}

private getDirectionBasedOnRtl = (rtl: boolean, direction) => {
if (!rtl) return direction
if (direction === 'start') return 'end'
if (direction === 'end') return 'start'
return direction
}
}

Indicator.create = createShorthandFactory(Indicator, 'hex')

export default Indicator
8 changes: 7 additions & 1 deletion src/components/Menu/Menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import { menuBehavior } from '../../lib/accessibility'
import { Accessibility } from '../../lib/accessibility/types'

import { ComponentVariablesObject } from '../../themes/types'
import { ReactProps, ShorthandCollection } from '../../../types/utils'
import { ReactProps, ShorthandCollection, ShorthandValue } from '../../../types/utils'
import MenuDivider from './MenuDivider'

export type MenuShorthandKinds = 'divider' | 'item'
Expand Down Expand Up @@ -68,6 +68,9 @@ export interface MenuProps extends UIComponentProps, ChildrenComponentProps {

/** Indicates whether the menu is submenu. */
submenu?: boolean

/** Shorthand for the submenu indicator. */
indicator?: ShorthandValue
}

export interface MenuState {
Expand Down Expand Up @@ -103,6 +106,7 @@ class Menu extends AutoControlledComponent<ReactProps<MenuProps>, MenuState> {
underlined: PropTypes.bool,
vertical: PropTypes.bool,
submenu: PropTypes.bool,
indicator: customPropTypes.itemShorthand,
}

static defaultProps = {
Expand Down Expand Up @@ -145,6 +149,7 @@ class Menu extends AutoControlledComponent<ReactProps<MenuProps>, MenuState> {
underlined,
vertical,
submenu,
indicator,
} = this.props
const { activeIndex } = this.state

Expand Down Expand Up @@ -177,6 +182,7 @@ class Menu extends AutoControlledComponent<ReactProps<MenuProps>, MenuState> {
index,
active,
inSubmenu: submenu,
indicator,
},
overrideProps: this.handleItemOverrides,
})
Expand Down
16 changes: 16 additions & 0 deletions src/components/Menu/MenuItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { Accessibility, AccessibilityActionHandlers } from '../../lib/accessibil
import { ComponentEventHandler, ReactProps, ShorthandValue } from '../../../types/utils'
import { focusAsync } from '../../lib/accessibility/FocusZone'
import Ref from '../Ref/Ref'
import Indicator from '../Indicator/Indicator'

export interface MenuItemProps
extends UIComponentProps,
Expand Down Expand Up @@ -105,6 +106,9 @@ export interface MenuItemProps

/** Indicates whether the menu item is part of submenu. */
inSubmenu?: boolean

/** Shorthand for the submenu indicator. */
indicator?: ShorthandValue
}

export interface MenuItemState {
Expand Down Expand Up @@ -144,6 +148,7 @@ class MenuItem extends AutoControlledComponent<ReactProps<MenuItemProps>, MenuIt
defaultMenuOpen: PropTypes.bool,
onActiveChanged: PropTypes.func,
inSubmenu: PropTypes.bool,
indicator: customPropTypes.itemShorthand,
}

static defaultProps = {
Expand Down Expand Up @@ -181,8 +186,11 @@ class MenuItem extends AutoControlledComponent<ReactProps<MenuItemProps>, MenuIt
primary,
secondary,
active,
vertical,
indicator,
disabled,
} = this.props
const indicatorWithDefaults = indicator === undefined ? {} : indicator

const { menuOpen } = this.state

Expand All @@ -205,6 +213,13 @@ class MenuItem extends AutoControlledComponent<ReactProps<MenuItemProps>, MenuIt
defaultProps: { xSpacing: !!content ? 'after' : 'none' },
})}
{content}
{menu &&
Indicator.create(indicatorWithDefaults, {
defaultProps: {
direction: vertical ? 'end' : 'bottom',
styles: styles.indicator,
},
})}
</ElementType>
</Ref>
)
Expand All @@ -219,6 +234,7 @@ class MenuItem extends AutoControlledComponent<ReactProps<MenuItemProps>, MenuIt
secondary,
styles: styles.menu,
submenu: true,
indicator,
},
})}
</Ref>
Expand Down
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,8 @@ export { default as Animation, AnimationProps } from './components/Animation/Ani

export { default as Tree } from './components/Tree'

export { default as Indicator, IndicatorProps } from './components/Indicator/Indicator'

//
// Accessibility
//
Expand Down
Loading