diff --git a/CHANGELOG.md b/CHANGELOG.md index c14f4dd052..78bb7ce161 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -43,6 +43,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm - Add `color`, `inverted` and `renderContent` props and `content` slot to `Segment` component @Bugaa92 ([#389](https://github.com/stardust-ui/react/pull/389)) - Add focus trap behavior to `Popup` @kuzhelov ([#457](https://github.com/stardust-ui/react/pull/457)) - Export `Ref` component and add `handleRef` util @layershifter ([#459](https://github.com/stardust-ui/react/pull/459)) +- Add `wrapper` slot to `MenuItem` @miroslavstastny ([#323](https://github.com/stardust-ui/react/pull/323)) ### Documentation - Add all missing component descriptions and improve those existing @levithomason ([#400](https://github.com/stardust-ui/react/pull/400)) diff --git a/src/components/Menu/MenuItem.tsx b/src/components/Menu/MenuItem.tsx index 7d4950f884..7f32950e16 100644 --- a/src/components/Menu/MenuItem.tsx +++ b/src/components/Menu/MenuItem.tsx @@ -5,6 +5,7 @@ import * as React from 'react' import { childrenExist, createShorthandFactory, customPropTypes, UIComponent } from '../../lib' import Icon from '../Icon/Icon' +import Slot from '../Slot/Slot' import { menuItemBehavior } from '../../lib/accessibility' import { Accessibility, AccessibilityActionHandlers } from '../../lib/accessibility/types' import IsFromKeyboard from '../../lib/isFromKeyboard' @@ -81,6 +82,15 @@ export interface MenuItemProps */ renderIcon?: ShorthandRenderFunction + /** + * A custom render function the wrapper slot. + * + * @param {React.ReactType} Component - The computed component for this slot. + * @param {object} props - The computed props for this slot. + * @param {ReactNode|ReactNodeArray} children - The computed children for this slot. + */ + renderWrapper?: ShorthandRenderFunction + /** The menu item can have secondary type. */ secondary?: boolean @@ -89,6 +99,9 @@ export interface MenuItemProps /** A vertical menu displays elements vertically. */ vertical?: boolean + + /** Shorthand for the wrapper component. */ + wrapper?: ShorthandValue } export interface MenuItemState { @@ -123,46 +136,56 @@ class MenuItem extends UIComponent, MenuItemState> { underlined: PropTypes.bool, vertical: PropTypes.bool, renderIcon: PropTypes.func, + wrapper: PropTypes.oneOfType([PropTypes.node, PropTypes.object]), + renderWrapper: PropTypes.func, } static defaultProps = { - as: 'li', + as: 'a', accessibility: menuItemBehavior as Accessibility, + wrapper: { as: 'li' }, } state = IsFromKeyboard.initial renderComponent({ ElementType, classes, accessibility, rest }) { - const { children, content, icon, renderIcon } = this.props + const { children, content, icon, renderIcon, renderWrapper, wrapper } = this.props - return ( + const menuItemInner = childrenExist(children) ? ( + children + ) : ( - {childrenExist(children) ? ( - children - ) : ( - - {icon && - Icon.create(this.props.icon, { - defaultProps: { xSpacing: !!content ? 'after' : 'none' }, - render: renderIcon, - })} - {content} - - )} + {icon && + Icon.create(this.props.icon, { + defaultProps: { xSpacing: !!content ? 'after' : 'none' }, + render: renderIcon, + })} + {content} ) + + if (wrapper) { + return Slot.create(wrapper, { + defaultProps: { + className: cx('ui-menu__item__wrapper', classes.wrapper), + ...accessibility.attributes.root, + ...accessibility.keyHandlers.root, + }, + render: renderWrapper, + overrideProps: () => ({ + children: menuItemInner, + }), + }) + } + return menuItemInner } protected actionHandlers: AccessibilityActionHandlers = { diff --git a/src/themes/teams/components/Menu/menuItemStyles.ts b/src/themes/teams/components/Menu/menuItemStyles.ts index 64fa9c1da1..2f2263e12e 100644 --- a/src/themes/teams/components/Menu/menuItemStyles.ts +++ b/src/themes/teams/components/Menu/menuItemStyles.ts @@ -120,7 +120,7 @@ const pointingBeak: ComponentSlotStyleFunction = { - root: ({ props, variables: v, theme }): ICSSInJSStyle => { + wrapper: ({ props, variables: v, theme }): ICSSInJSStyle => { const { active, isFromKeyboard, pills, pointing, secondary, underlined, vertical } = props return { @@ -186,7 +186,7 @@ const menuItemStyles: ComponentSlotStylesInput { + root: ({ props, variables: v }): ICSSInJSStyle => { const { active, iconOnly, isFromKeyboard, pointing, primary, underlined, vertical } = props return { diff --git a/test/specs/commonTests/handlesAccessibility.tsx b/test/specs/commonTests/handlesAccessibility.tsx index 44ca8d25e3..32c321fe89 100644 --- a/test/specs/commonTests/handlesAccessibility.tsx +++ b/test/specs/commonTests/handlesAccessibility.tsx @@ -37,12 +37,22 @@ const TestBehavior: Accessibility = (props: any) => ({ * @param {string} [options.partSelector=''] Selector to scope the test to a part * @param {FocusZoneDefinition} [options.focusZoneDefinition={}] FocusZone definition */ -export default (Component, options: any = {}) => { +export default ( + Component, + options: { + requiredProps?: any + defaultRootRole?: string + partSelector?: string + focusZoneDefinition?: any + usesWrapperSlot?: boolean + } = {}, +) => { const { requiredProps = {}, defaultRootRole, partSelector = '', focusZoneDefinition = {}, + usesWrapperSlot = false, } = options test('gets default accessibility when no override used', () => { @@ -73,20 +83,24 @@ export default (Component, options: any = {}) => { test('gets correct role when overrides role', () => { const testRole = 'test-role' - const rendered = mountWithProviderAndGetComponent( - Component, - , + const element = usesWrapperSlot ? ( + + ) : ( + ) + const rendered = mountWithProviderAndGetComponent(Component, element) const role = getRenderedAttribute(rendered, 'role', partSelector) expect(role).toBe(testRole) }) test('gets correct role when overrides both accessibility and role', () => { const testRole = 'test-role' - const rendered = mountWithProviderAndGetComponent( - Component, - , + const element = usesWrapperSlot ? ( + + ) : ( + ) + const rendered = mountWithProviderAndGetComponent(Component, element) const role = getRenderedAttribute(rendered, 'role', partSelector) expect(role).toBe(testRole) }) diff --git a/test/specs/commonTests/isConformant.tsx b/test/specs/commonTests/isConformant.tsx index 0f1a4058da..599bab7576 100644 --- a/test/specs/commonTests/isConformant.tsx +++ b/test/specs/commonTests/isConformant.tsx @@ -22,6 +22,7 @@ export interface Conformant { requiredProps?: object exportedAtTopLevel?: boolean rendersPortal?: boolean + usesWrapperSlot?: boolean } /** @@ -32,6 +33,7 @@ export interface Conformant { * @param {boolean} [options.exportedAtTopLevel=false] Is this component exported as top level API? * @param {boolean} [options.rendersPortal=false] Does this component render a Portal powered component? * @param {Object} [options.requiredProps={}] Props required to render Component without errors or warnings. + * @param {boolean} [options.usesWrapperSlot=false] This component uses wrapper slot to wrap the 'meaningful' element. */ export default (Component, options: Conformant = {}) => { const { @@ -39,6 +41,7 @@ export default (Component, options: Conformant = {}) => { exportedAtTopLevel = true, requiredProps = {}, rendersPortal = false, + usesWrapperSlot = false, } = options const { throwError } = helpers('isConformant', Component) @@ -58,6 +61,14 @@ export default (Component, options: Conformant = {}) => { component = component.childAt(0) // skip the additional wrap
of the FocusZone } } + + if (usesWrapperSlot) { + component = component + .childAt(0) + .childAt(0) + .childAt(0) + } + return component } @@ -396,7 +407,7 @@ export default (Component, options: Conformant = {}) => { .find('[className]') .hostNodes() .filterWhere(c => !c.prop(FOCUSZONE_WRAP_ATTRIBUTE)) // filter out FocusZone wrap
- .first() + .at(usesWrapperSlot ? 1 : 0) .prop('className') return classes } diff --git a/test/specs/components/Menu/MenuItem-test.tsx b/test/specs/components/Menu/MenuItem-test.tsx index 94ff4d3dab..ac72b14ee6 100644 --- a/test/specs/components/Menu/MenuItem-test.tsx +++ b/test/specs/components/Menu/MenuItem-test.tsx @@ -10,12 +10,13 @@ describe('MenuItem', () => { eventTargets: { onClick: 'a', }, + usesWrapperSlot: true, }) it('content renders as `li > a`', () => { - const menuItem = mountWithProviderAndGetComponent(MenuItem, ).find( - '.ui-menu__item', - ) + const menuItem = mountWithProviderAndGetComponent(MenuItem, ) + .find('.ui-menu__item__wrapper') + .hostNodes() expect(menuItem.is('li')).toBe(true) expect(menuItem.childAt(0).is('a')).toBe(true) @@ -24,13 +25,16 @@ describe('MenuItem', () => { it('children render directly inside `li`', () => { const menuItem = mountWithProviderAndGetComponent(MenuItem, Home) + .find('.ui-menu__item__wrapper') + .hostNodes() - expect(menuItem.find('.ui-menu__item').is('li')).toBe(true) + expect(menuItem.is('li')).toBe(true) + expect(menuItem.childAt(0).exists()).toBe(false) expect(menuItem.text()).toBe('Home') }) describe('accessibility', () => { - handlesAccessibility(MenuItem, { defaultRootRole: 'presentation' }) + handlesAccessibility(MenuItem, { defaultRootRole: 'presentation', usesWrapperSlot: true }) handlesAccessibility(MenuItem, { defaultRootRole: 'menuitem', partSelector: 'a' }) describe('as a default MenuItem', () => {