diff --git a/CHANGELOG.md b/CHANGELOG.md index e83c6d9c98..7e3c5c0420 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm ### Features - Rename `Slot` component to `Box` and export it @Bugaa92 ([#713](https://github.com/stardust-ui/react/pull/713)) +- Add `Indicator` component and used it in `MenuItem` and `AccordionTitle` @mnajdova ([#721](https://github.com/stardust-ui/react/pull/721)) ## [v0.17.0](https://github.com/stardust-ui/react/tree/v0.17.0) (2019-01-17) diff --git a/docs/src/examples/components/Indicator/Types/IndicatorExample.shorthand.tsx b/docs/src/examples/components/Indicator/Types/IndicatorExample.shorthand.tsx new file mode 100644 index 0000000000..5e5dd0572a --- /dev/null +++ b/docs/src/examples/components/Indicator/Types/IndicatorExample.shorthand.tsx @@ -0,0 +1,6 @@ +import * as React from 'react' +import { Indicator } from '@stardust-ui/react' + +const IndicatorExample = () => + +export default IndicatorExample diff --git a/docs/src/examples/components/Indicator/Types/IndicatorExampleDirection.shorthand.tsx b/docs/src/examples/components/Indicator/Types/IndicatorExampleDirection.shorthand.tsx new file mode 100644 index 0000000000..5a0df21481 --- /dev/null +++ b/docs/src/examples/components/Indicator/Types/IndicatorExampleDirection.shorthand.tsx @@ -0,0 +1,11 @@ +import * as React from 'react' +import { Indicator } from '@stardust-ui/react' + +const IndicatorExampleDirection = () => ( + <> + {' '} + {' '} + +) + +export default IndicatorExampleDirection diff --git a/docs/src/examples/components/Indicator/Types/IndicatorExampleIcon.shorthand.tsx b/docs/src/examples/components/Indicator/Types/IndicatorExampleIcon.shorthand.tsx new file mode 100644 index 0000000000..a2a611cf92 --- /dev/null +++ b/docs/src/examples/components/Indicator/Types/IndicatorExampleIcon.shorthand.tsx @@ -0,0 +1,12 @@ +import * as React from 'react' +import { Indicator } from '@stardust-ui/react' + +const IndicatorExampleIcon = () => ( + <> + {' '} + {' '} + {' '} + {' '} + +) +export default IndicatorExampleIcon diff --git a/docs/src/examples/components/Indicator/Types/index.tsx b/docs/src/examples/components/Indicator/Types/index.tsx new file mode 100644 index 0000000000..0f540d4608 --- /dev/null +++ b/docs/src/examples/components/Indicator/Types/index.tsx @@ -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 = () => ( + + + + + +) + +export default Types diff --git a/docs/src/examples/components/Indicator/index.tsx b/docs/src/examples/components/Indicator/index.tsx new file mode 100644 index 0000000000..8c27ea542f --- /dev/null +++ b/docs/src/examples/components/Indicator/index.tsx @@ -0,0 +1,10 @@ +import * as React from 'react' +import Types from './Types' + +const IndicatorExamples = () => ( + <> + + +) + +export default IndicatorExamples diff --git a/src/components/Accordion/AccordionTitle.tsx b/src/components/Accordion/AccordionTitle.tsx index 7e5d024310..db5a604703 100644 --- a/src/components/Accordion/AccordionTitle.tsx +++ b/src/components/Accordion/AccordionTitle.tsx @@ -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, @@ -30,6 +31,9 @@ export interface AccordionTitleProps * @param {object} data - All props. */ onClick?: ComponentEventHandler + + /** Shorthand for the active indicator. */ + indicator?: ShorthandValue } /** @@ -47,26 +51,32 @@ class AccordionTitle extends UIComponent, 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 ( - - {children} - - ) - } + const contentElement = ( + <> + {Indicator.create(indicatorWithDefaults, { + defaultProps: { + direction: active ? 'bottom' : 'end', + styles: styles.indicator, + }, + })} + {content} + + ) return ( - {content} + {childrenExist(children) ? children : contentElement} ) } diff --git a/src/components/Icon/Icon.tsx b/src/components/Icon/Icon.tsx index e554e3ad91..74f42616d4 100644 --- a/src/components/Icon/Icon.tsx +++ b/src/components/Icon/Icon.tsx @@ -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 + /** Size of the icon. */ size?: IconSize @@ -65,6 +68,7 @@ class Icon extends UIComponent, 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']), } @@ -73,6 +77,7 @@ class Icon extends UIComponent, any> { as: 'span', size: 'medium', accessibility: iconBehavior, + rotate: 0, } private renderFontIcon(ElementType, classes, unhandledProps, accessibility): React.ReactNode { diff --git a/src/components/Indicator/Indicator.tsx b/src/components/Indicator/Indicator.tsx new file mode 100644 index 0000000000..f0b93896dc --- /dev/null +++ b/src/components/Indicator/Indicator.tsx @@ -0,0 +1,79 @@ +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, any> { + static displayName = 'Indicator' + + static create: Function + + static className = 'ui-indicator' + + static directionMap = { + end: { unicode: '\u25B8', rotation: -90 }, + start: { unicode: '\u25C2', rotation: 90 }, + top: { unicode: '\u25B4', rotation: 180 }, + bottom: { unicode: '\u25BE', 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 + + return ( + + {icon + ? Icon.create(icon, { + defaultProps: { color }, + overrideProps: ({ rotate }) => ({ + rotate: (Indicator.directionMap[direction].rotation || 0) + (rotate || 0), + }), + }) + : hexUnicode} + + ) + } + + 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 diff --git a/src/components/Menu/Menu.tsx b/src/components/Menu/Menu.tsx index 414b76b00a..6dc9ffc374 100644 --- a/src/components/Menu/Menu.tsx +++ b/src/components/Menu/Menu.tsx @@ -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' @@ -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 { @@ -103,6 +106,7 @@ class Menu extends AutoControlledComponent, MenuState> { underlined: PropTypes.bool, vertical: PropTypes.bool, submenu: PropTypes.bool, + indicator: customPropTypes.itemShorthand, } static defaultProps = { @@ -145,6 +149,7 @@ class Menu extends AutoControlledComponent, MenuState> { underlined, vertical, submenu, + indicator, } = this.props const { activeIndex } = this.state @@ -177,6 +182,7 @@ class Menu extends AutoControlledComponent, MenuState> { index, active, inSubmenu: submenu, + indicator, }, overrideProps: this.handleItemOverrides, }) diff --git a/src/components/Menu/MenuItem.tsx b/src/components/Menu/MenuItem.tsx index ba8ef2e875..96357f87fa 100644 --- a/src/components/Menu/MenuItem.tsx +++ b/src/components/Menu/MenuItem.tsx @@ -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, @@ -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 { @@ -144,6 +148,7 @@ class MenuItem extends AutoControlledComponent, MenuIt defaultMenuOpen: PropTypes.bool, onActiveChanged: PropTypes.func, inSubmenu: PropTypes.bool, + indicator: customPropTypes.itemShorthand, } static defaultProps = { @@ -181,8 +186,11 @@ class MenuItem extends AutoControlledComponent, MenuIt primary, secondary, active, + vertical, + indicator, disabled, } = this.props + const indicatorWithDefaults = indicator === undefined ? {} : indicator const { menuOpen } = this.state @@ -205,6 +213,13 @@ class MenuItem extends AutoControlledComponent, MenuIt defaultProps: { xSpacing: !!content ? 'after' : 'none' }, })} {content} + {menu && + Indicator.create(indicatorWithDefaults, { + defaultProps: { + direction: vertical ? 'end' : 'bottom', + styles: styles.indicator, + }, + })} ) @@ -219,6 +234,7 @@ class MenuItem extends AutoControlledComponent, MenuIt secondary, styles: styles.menu, submenu: true, + indicator, }, })} diff --git a/src/index.ts b/src/index.ts index eb5489f13c..c86b4b12b6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -127,6 +127,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 // diff --git a/src/themes/teams/components/Accordion/accordionTitleStyles.ts b/src/themes/teams/components/Accordion/accordionTitleStyles.ts index ca642e8ee1..d368fb80d5 100644 --- a/src/themes/teams/components/Accordion/accordionTitleStyles.ts +++ b/src/themes/teams/components/Accordion/accordionTitleStyles.ts @@ -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', + }), + indicator: () => ({ + userSelect: 'none', + }), } export default accordionTitleStyles diff --git a/src/themes/teams/components/Icon/iconStyles.ts b/src/themes/teams/components/Icon/iconStyles.ts index a717a0ebf4..888871e41f 100644 --- a/src/themes/teams/components/Icon/iconStyles.ts +++ b/src/themes/teams/components/Icon/iconStyles.ts @@ -92,11 +92,12 @@ const getIconColor = (colorProp: string, variables: IconVariables) => const iconStyles: ComponentSlotStylesInput = { 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 { @@ -120,6 +121,14 @@ const iconStyles: ComponentSlotStylesInput = { ...((bordered || v.borderColor || circular) && getBorderedStyles(circular, v.borderColor || getIconColor(color, v))), + + ...(rtl && { + transform: `scaleX(-1) rotate(${-1 * rotate}deg)`, + }), + + ...(!rtl && { + transform: `rotate(${rotate}deg)`, + }), } }, diff --git a/src/themes/teams/components/Menu/menuItemStyles.ts b/src/themes/teams/components/Menu/menuItemStyles.ts index a7d5309b8f..d559fe1e5a 100644 --- a/src/themes/teams/components/Menu/menuItemStyles.ts +++ b/src/themes/teams/components/Menu/menuItemStyles.ts @@ -1,4 +1,3 @@ -import { getSideArrow } from '../../utils' import { pxToRem } from '../../../../lib' import { ComponentSlotStyleFunction, ComponentSlotStylesInput, ICSSInJSStyle } from '../../../types' import { MenuVariables } from './menuVariables' @@ -279,8 +278,6 @@ const menuItemStyles: ComponentSlotStylesInput ({ + position: 'relative', + float: 'right', + left: pxToRem(10), + userSelect: 'none', + }), } export default menuItemStyles diff --git a/src/themes/teams/siteVariables.ts b/src/themes/teams/siteVariables.ts index 43c0e2d8a7..96228fbc2e 100644 --- a/src/themes/teams/siteVariables.ts +++ b/src/themes/teams/siteVariables.ts @@ -98,10 +98,3 @@ export const bodyFontSize = '1.4rem' export const bodyBackground = white export const bodyColor = black export const bodyLineHeight = lineHeightBase - -// -// UNICODE CHARACTERS -// -export const arrowRight = '\u25B8' -export const arrowDown = '\u25BE' -export const arrowLeft = '\u25C2' diff --git a/src/themes/teams/utils/index.ts b/src/themes/teams/utils/index.ts deleted file mode 100644 index e818cf91b3..0000000000 --- a/src/themes/teams/utils/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { ThemeInput } from '../../types' - -export const getSideArrow = (theme: ThemeInput) => { - const { rtl, siteVariables } = theme - const { arrowLeft, arrowRight } = siteVariables - return rtl ? arrowLeft : arrowRight -} diff --git a/test/specs/components/Indicator/Indicator-test.ts b/test/specs/components/Indicator/Indicator-test.ts new file mode 100644 index 0000000000..dd21346825 --- /dev/null +++ b/test/specs/components/Indicator/Indicator-test.ts @@ -0,0 +1,7 @@ +import { isConformant } from 'test/specs/commonTests' + +import Indicator from 'src/components/Indicator/Indicator' + +describe('Indicator', () => { + isConformant(Indicator) +})