import { Accessibility, splitButtonBehavior, SplitButtonBehaviorProps } from '@fluentui/accessibility'; import * as customPropTypes from '@fluentui/react-proptypes'; import * as PropTypes from 'prop-types'; import * as React from 'react'; import * as _ from 'lodash'; import { ALIGNMENTS, POSITIONS } from '../../utils/positioner'; import { ComponentEventHandler, ShorthandValue, ShorthandCollection, FluentComponentStaticProps } from '../../types'; import { UIComponentProps, ChildrenComponentProps, ContentComponentProps, isFromKeyboard as checkIsFromKeyboard, commonPropTypes, SizeValue, createShorthand, createShorthandFactory, } from '../../utils'; import { SplitButtonToggle, SplitButtonToggleProps } from './SplitButtonToggle'; import { Button, ButtonProps } from '../Button/Button'; import { MenuButton, MenuButtonProps } from '../MenuButton/MenuButton'; import { MenuProps } from '../Menu/Menu'; import { MenuItemProps } from '../Menu/MenuItem'; import { PopupProps } from '../Popup/Popup'; import { Ref } from '@fluentui/react-component-ref'; import { PositioningProps } from '../../utils/positioner/types'; import { ComponentWithAs, useTelemetry, useAutoControlled, useAccessibility, getElementType, useFluentContext, useUnhandledProps, useStyles, } from '@fluentui/react-bindings'; export interface SplitButtonProps extends UIComponentProps, ChildrenComponentProps, ContentComponentProps, PositioningProps { /** * Accessibility behavior if overridden by the user. */ accessibility?: Accessibility; /** Shorthand for the main button. */ button?: ShorthandValue; /** Initial value for 'open'. */ defaultOpen?: boolean; /** A split button can be disabled. */ disabled?: boolean; /** Shorthand for the menu. */ menu?: ShorthandValue | ShorthandCollection; /** * Called after user's click on the main button. * * @param event - React's original SyntheticEvent. * @param data - All props. */ onMainButtonClick?: ComponentEventHandler; /** * Called after user's click on a menu item. * * @param event - React's original SyntheticEvent. * @param data - All props. */ onMenuItemClick?: ComponentEventHandler; /** * Event for request to change 'open' value. * @param event - React's original SyntheticEvent. * @param data - All props and proposed value. */ onOpenChange?: ComponentEventHandler; /** Defines whether menu is displayed. */ open?: boolean; /** A split button can be formatted to show different levels of emphasis. */ primary?: boolean; /** A split button can be formatted to show different levels of emphasis. */ secondary?: boolean; /** A split button can be sized */ size?: SizeValue; /** Shorthand for the toggle button. */ toggleButton?: ShorthandValue; } export const splitButtonClassName = 'ui-splitbutton'; export type SplitButtonStylesProps = Required> & { isFromKeyboard: boolean }; /** * A SplitButton enables users to take one of several related actions, one being dominant and rest being displayed in a menu. */ export const SplitButton: ComponentWithAs<'div', SplitButtonProps> & FluentComponentStaticProps & { Toggle: typeof SplitButtonToggle; } = props => { const context = useFluentContext(); const { setStart, setEnd } = useTelemetry(SplitButton.displayName, context.telemetry); setStart(); const { button, disabled, menu, primary, secondary, toggleButton, size, position, align, flipBoundary, overflowBoundary, popperRef, positionFixed, offset, unstable_pinned, className, design, styles, accessibility, variables, } = props; const targetRef = React.useRef(); const ElementType = getElementType(props); const unhandledProps = useUnhandledProps(SplitButton.handledProps, props); const [open, setOpen] = useAutoControlled({ defaultValue: props.defaultOpen, value: props.open, initialValue: false, }); const [isFromKeyboard, setIsFromKeyboard] = React.useState(false); const getA11yProps = useAccessibility(accessibility, { debugName: SplitButton.displayName, mapPropsToBehavior: () => ({ open, }), rtl: context.rtl, }); const { classes, styles: resolvedStyles } = useStyles(SplitButton.displayName, { className: splitButtonClassName, mapPropsToStyles: () => ({ isFromKeyboard, size, }), mapPropsToInlineStyles: () => ({ className, design, styles, variables, }), rtl: context.rtl, }); const handleMenuButtonOverrides = (predefinedProps: MenuButtonProps) => ({ onMenuItemClick: (e: React.SyntheticEvent, menuItemProps: MenuItemProps) => { setOpen(false); _.invoke(props, 'onOpenChange', e, { ...props, open: false }); _.invoke(predefinedProps, 'onMenuItemClick', e, menuItemProps); _.invoke(props, 'onMenuItemClick', e, menuItemProps); }, onOpenChange: (e: React.SyntheticEvent, popupProps: PopupProps) => { e.stopPropagation(); setOpen(popupProps.open); _.invoke(props, 'onOpenChange', e, { ...props, open: popupProps.open }); }, }); const handleMenuButtonTriggerOverrides = (predefinedProps: ButtonProps) => ({ onClick: (e: React.SyntheticEvent, buttonProps: ButtonProps) => { _.invoke(predefinedProps, 'onClick', e, buttonProps); _.invoke(props, 'onMainButtonClick', e, buttonProps); }, onFocus: (e: React.SyntheticEvent, buttonProps: ButtonProps) => { _.invoke(predefinedProps, 'onFocus', e, buttonProps); setIsFromKeyboard(checkIsFromKeyboard()); }, }); const element = ( {MenuButton.create( {}, { defaultProps: () => getA11yProps('menuButton', { menu, on: [], open, trigger: createShorthand(Button, button, { defaultProps: () => ({ styles: resolvedStyles.menuButton, primary, secondary, disabled, }), overrideProps: handleMenuButtonTriggerOverrides, }), target: targetRef.current, position, align, flipBoundary, overflowBoundary, popperRef, positionFixed, offset, unstable_pinned, }), overrideProps: handleMenuButtonOverrides, }, )} {SplitButtonToggle.create(toggleButton, { defaultProps: () => getA11yProps('toggleButton', { disabled, primary, secondary, size, }), overrideProps: (predefinedProps: ButtonProps) => ({ onClick: (e: React.SyntheticEvent, buttonProps: ButtonProps) => { _.invoke(predefinedProps, 'onClick', e, buttonProps); setOpen(isOpen => { const open = !isOpen; _.invoke(props, 'onOpenChange', e, { ...props, open }); return open; }); }, }), })} ); setEnd(); return element; }; SplitButton.displayName = 'SplitButton'; SplitButton.Toggle = SplitButtonToggle; SplitButton.propTypes = { ...commonPropTypes.createCommon({ content: false, }), button: customPropTypes.itemShorthand, defaultOpen: PropTypes.bool, menu: PropTypes.oneOfType([ customPropTypes.itemShorthand, PropTypes.arrayOf(customPropTypes.itemShorthandWithoutJSX), ]), onMainButtonClick: PropTypes.func, onMenuItemClick: PropTypes.func, onOpenChange: PropTypes.func, open: PropTypes.bool, size: customPropTypes.size, popperRef: customPropTypes.ref, primary: customPropTypes.every([customPropTypes.disallow(['secondary']), PropTypes.bool]), secondary: customPropTypes.every([customPropTypes.disallow(['primary']), PropTypes.bool]), toggleButton: customPropTypes.itemShorthand, position: PropTypes.oneOf(POSITIONS), align: PropTypes.oneOf(ALIGNMENTS), flipBoundary: PropTypes.oneOfType([ PropTypes.object as PropTypes.Requireable, PropTypes.arrayOf(PropTypes.object) as PropTypes.Requireable, PropTypes.oneOf<'clippingParents' | 'window' | 'scrollParent'>(['clippingParents', 'window', 'scrollParent']), ]), overflowBoundary: PropTypes.oneOfType([ PropTypes.object as PropTypes.Requireable, PropTypes.arrayOf(PropTypes.object) as PropTypes.Requireable, PropTypes.oneOf<'clippingParents' | 'window' | 'scrollParent'>(['clippingParents', 'window', 'scrollParent']), ]), positionFixed: PropTypes.bool, offset: PropTypes.oneOfType([ PropTypes.func, PropTypes.arrayOf(PropTypes.number) as PropTypes.Requireable<[number, number]>, ]), unstable_pinned: PropTypes.bool, }; SplitButton.defaultProps = { accessibility: splitButtonBehavior, toggleButton: {}, position: 'below', align: 'start', }; SplitButton.handledProps = Object.keys(SplitButton.propTypes) as any; SplitButton.create = createShorthandFactory({ Component: SplitButton, });