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 8 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
36 changes: 24 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. */
activeIndicator?: ShorthandValue
}

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

static defaultProps = {}

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, activeIndicator, active } = this.props
const submenuIndicatorWithDefault = activeIndicator === undefined ? {} : activeIndicator

if (childrenExist(children)) {
return (
<ElementType {...unhandledProps} className={classes.root} onClick={this.handleClick}>
{children}
</ElementType>
)
}
const contentElement = (
<>
{Indicator.create(submenuIndicatorWithDefault, {
defaultProps: {
direction: active ? 'bottom' : 'forward',
styles: styles.activeIndicator,
},
})}
{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
87 changes: 87 additions & 0 deletions src/components/Indicator/Indicator.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import * as React from 'react'
import * as PropTypes from 'prop-types'

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

export interface IndicatorProps extends UIComponentProps, ColorComponentProps {
/** The indicator can be direction in different directions. */
direction?: 'forward' | 'back' | 'top' | 'bottom'
Copy link
Contributor

Choose a reason for hiding this comment

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

For me, it seems a bit confusable, what is 'bottom' and what is 'back'. I guess it's left but seems not very intuitive.
Maybe just 'right', 'left', 'top', 'bottom' - simple and straightforward

Copy link
Member

Choose a reason for hiding this comment

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

right and left are not RTL safe.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Would like to avoid the right and left, as in rtl it won't make sense to use them.. I am open to other suggestion. This one were gathered from the browser arrows for navigation (back and forward). Maybe start and end? What do you think?

Copy link
Contributor

Choose a reason for hiding this comment

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

Well, we use 'right' and 'left' for the Popup too.
I don't think start end is better as it more describes a position in regards to something. If no other ideas then let's leave 'back' and 'forward'. If treat it as video player buttons it makes more sense 😄

Copy link
Member

Choose a reason for hiding this comment

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

I vote for start and end as these are the direction names we use.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@miroslavstastny agree, let's strive for consistency, although it seems really strange for me to have the '<' indicator a start, and '>' as end.. We can change it later anyway...


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

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

static create: Function

static className = 'ui-indicator'

static directionMap = {
forward: { unicode: '25B8', rotation: -90 },
back: { 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(['forward', 'back', '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, rotate: Indicator.directionMap[direction].rotation },
}),
}
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 === 'forward') return 'back'
if (direction === 'back') return 'forward'
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

/** Indicates whether the submenuIndicator should be shown, or defines an icon for it. */
submenuIndicator?: 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,
submenuIndicator: customPropTypes.itemShorthand,
}

static defaultProps = {
Expand Down Expand Up @@ -145,6 +149,7 @@ class Menu extends AutoControlledComponent<ReactProps<MenuProps>, MenuState> {
underlined,
vertical,
submenu,
submenuIndicator,
} = 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,
submenuIndicator,
},
overrideProps: this.handleItemOverrides,
})
Expand Down
27 changes: 26 additions & 1 deletion 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. */
submenuIndicator?: 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,
submenuIndicator: customPropTypes.itemShorthand,
}

static defaultProps = {
Expand Down Expand Up @@ -172,7 +177,19 @@ class MenuItem extends AutoControlledComponent<ReactProps<MenuItemProps>, MenuIt
}

renderComponent({ ElementType, classes, accessibility, unhandledProps, styles }) {
const { children, content, icon, wrapper, menu, primary, secondary, active } = this.props
const {
children,
content,
icon,
wrapper,
menu,
primary,
secondary,
active,
vertical,
submenuIndicator,
} = this.props
const submenuIndicatorWithDefault = submenuIndicator === undefined ? {} : submenuIndicator

const { menuOpen } = this.state

Expand All @@ -193,6 +210,13 @@ class MenuItem extends AutoControlledComponent<ReactProps<MenuItemProps>, MenuIt
defaultProps: { xSpacing: !!content ? 'after' : 'none' },
})}
{content}
{menu &&
Indicator.create(submenuIndicatorWithDefault, {
defaultProps: {
direction: vertical ? 'forward' : 'bottom',
styles: styles.submenuIndicator,
},
})}
</ElementType>
</Ref>
)
Expand All @@ -207,6 +231,7 @@ class MenuItem extends AutoControlledComponent<ReactProps<MenuItemProps>, MenuIt
secondary,
styles: styles.menu,
submenu: true,
submenuIndicator,
},
})}
</Ref>
Expand Down
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,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
1 change: 1 addition & 0 deletions src/themes/base/componentStyles.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export { default as Loader } from './components/Loader/loaderStyles'
export { default as Text } from './components/Text/textStyles'
export { default as Indicator } from './components/Indicator/indicatorStyles'
1 change: 1 addition & 0 deletions src/themes/base/componentVariables.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export { default as Loader } from './components/Loader/loaderVariables'
export { default as Text } from './components/Text/textVariables'
export { default as UnicodeCharacter } from './components/Indicator/indicatorVariables'
15 changes: 15 additions & 0 deletions src/themes/base/components/Indicator/indicatorStyles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import * as _ from 'lodash'
import { ComponentStyleFunctionParam, ICSSInJSStyle } from '../../../types'
import { IndicatorVariables } from './indicatorVariables'
import { IndicatorProps } from '@stardust-ui/react'

export default {
root: ({
props: { color },
variables: v,
}: ComponentStyleFunctionParam<IndicatorProps, IndicatorVariables>): ICSSInJSStyle => {
return {
...(color && { color: _.get(v.colors, color) }),
}
},
}
14 changes: 14 additions & 0 deletions src/themes/base/components/Indicator/indicatorVariables.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { ColorValues } from '../../../types'
import { mapColorsToScheme } from '../../../../lib'

export interface IndicatorVariables {
colors: ColorValues<string>
}

export default (siteVariables): IndicatorVariables => {
const colorVariant = 500

return {
colors: mapColorsToScheme(siteVariables, colorVariant),
}
}
27 changes: 9 additions & 18 deletions src/themes/teams/components/Accordion/accordionTitleStyles.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,13 @@
import { ICSSInJSStyle } from '../../../types'
import { getSideArrow } from '../../utils'

const accordionTitleStyles = {
root: ({ props, theme }): ICSSInJSStyle => {
const { active } = props
const { arrowDown } = theme.siteVariables
const sideArrow = getSideArrow(theme)
return {
display: 'inline-block',
verticalAlign: 'middle',
padding: '.5rem 0',
cursor: 'pointer',
'::before': {
userSelect: 'none',
content: active ? `"${arrowDown}"` : `"${sideArrow}"`,
},
}
},
root: () => ({
display: 'inline-block',
verticalAlign: 'middle',
padding: '.5rem 0',
cursor: 'pointer',
}),
activeIndicator: () => ({
userSelect: 'none',
}),
}

export default accordionTitleStyles
5 changes: 4 additions & 1 deletion src/themes/teams/components/Icon/iconStyles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,11 +92,12 @@ const getIconColor = (colorProp: string, variables: IconVariables) =>

const iconStyles: ComponentSlotStylesInput<IconProps, IconVariables> = {
root: ({
props: { disabled, name, size, bordered, circular, color, xSpacing },
props: { disabled, name, size, bordered, circular, color, xSpacing, rotate },
variables: v,
theme,
}): ICSSInJSStyle => {
const iconSpec = theme.icons[name]
const rtl = theme.rtl
const isFontBased = !iconSpec || !iconSpec.isSvg

return {
Expand All @@ -120,6 +121,8 @@ const iconStyles: ComponentSlotStylesInput<IconProps, IconVariables> = {

...((bordered || v.borderColor || circular) &&
getBorderedStyles(circular, v.borderColor || getIconColor(color, v))),

transform: `${rtl ? 'scaleX(-1)' : ''} rotate(${rtl ? -1 * rotate : rotate}deg)`,
}
},

Expand Down
Loading