diff --git a/CHANGELOG.md b/CHANGELOG.md index 050deba557..666b98e217 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3079,6 +3079,7 @@ possible breaking changes to any direct/custom use of AG Grid APIs and props wit ### 🎁 New Features +* New `SplitButton` component added to button component collection. * GridModel `groupSortFn` now accepts `null` to turn off sorting of group rows. * `DockViewModel` now supports optional `width`, `height` and `collapsedWidth` configs. * The `appMenuButton.extraItems` prop now accepts `MenuItem` configs (as before) but also React diff --git a/desktop/cmp/button/Button.scss b/desktop/cmp/button/Button.scss index c74eabab8a..5620a8ea0c 100644 --- a/desktop/cmp/button/Button.scss +++ b/desktop/cmp/button/Button.scss @@ -61,12 +61,12 @@ border-radius: 0; &.xh-button--standard, &.xh-button--outlined { - &:first-child { + &:first-child:not(.xh-split-button__trigger-right) { border-bottom-left-radius: var(--xh-button-border-radius-px); border-top-left-radius: var(--xh-button-border-radius-px); } - &:last-child { + &:last-child:not(.xh-split-button__trigger-left) { border-bottom-right-radius: var(--xh-button-border-radius-px); border-top-right-radius: var(--xh-button-border-radius-px); } diff --git a/desktop/cmp/button/SplitButton.scss b/desktop/cmp/button/SplitButton.scss new file mode 100644 index 0000000000..8fbe227091 --- /dev/null +++ b/desktop/cmp/button/SplitButton.scss @@ -0,0 +1,6 @@ +.xh-split-button { + &__trigger { + width: 22px; + min-width: 22px; + } +} \ No newline at end of file diff --git a/desktop/cmp/button/SplitButton.ts b/desktop/cmp/button/SplitButton.ts new file mode 100644 index 0000000000..6fff6a73db --- /dev/null +++ b/desktop/cmp/button/SplitButton.ts @@ -0,0 +1,110 @@ +import {hoistCmp, MenuItemLike} from '@xh/hoist/core'; +import {button, buttonGroup, ButtonProps} from '@xh/hoist/desktop/cmp/button'; +import {Icon} from '@xh/hoist/icon'; +import {menu, menuItem, popover} from '@xh/hoist/kit/blueprint'; +import {wait} from '@xh/hoist/promise'; +import classNames from 'classnames'; +import {castArray} from 'lodash'; +import './SplitButton.scss'; + +/** + * A split button combines a primary action button with an integrated menu of secondary actions. + */ + +export interface SplitButtonProps extends ButtonProps { + menuItems: MenuItemLike[]; + menuSide?: 'left' | 'right'; +} +export const [SplitButton, splitButton] = hoistCmp.withFactory({ + displayName: 'SplitButton', + model: false, + memo: false, + observer: false, + + render({ + menuItems = [], + menuSide = 'right', + disabled, + intent, + minimal = false, + className, + ...rest + }) { + menuItems = castArray(menuItems); + const noMenu = !menuItems.length; + + return buttonGroup({ + className: classNames(className, 'xh-split-button'), + items: [ + menuTriggerButton({ + omit: noMenu || menuSide == 'right', + menuItems, + menuSide, + className, + disabled, + intent, + minimal + }), + primaryButton({disabled, intent, minimal, ...rest}), + menuTriggerButton({ + omit: noMenu || menuSide == 'left', + menuItems, + menuSide, + className, + disabled, + intent, + minimal + }) + ] + }); + } +}); + +const primaryButton = hoistCmp.factory({ + displayName: 'SplitButtonPrimary', + model: false, + memo: false, + observer: false, + + render(props) { + return button({ + className: 'xh-split-button__primary', + ...props + }); + } +}); + +const menuTriggerButton = hoistCmp.factory({ + displayName: 'SplitButtonTrigger', + model: false, + memo: false, + observer: false, + + render({menuItems, menuSide, className, disabled, intent, minimal}) { + return popover({ + position: `bottom-${menuSide}`, + minimal: true, + disabled, + target: button({ + icon: Icon.chevronDown({prefix: 'fas'}), + className: `xh-split-button__trigger-${menuSide}`, + disabled, + intent, + minimal + }), + content: menu({ + className: classNames(className, 'xh-split-button__menu'), + items: menuItems.map((item, idx) => { + const {actionFn, ...rest} = item; + + return menuItem({ + key: idx, + className: classNames(item.className, 'xh-split-button__menu-item'), + onClick: actionFn ? () => wait().then(actionFn) : null, // do async to allow menu to close + ...rest + }); + }) + }) + }); + } +}); diff --git a/desktop/cmp/button/index.ts b/desktop/cmp/button/index.ts index 8b0f2a3ffe..d9bd0d72ea 100644 --- a/desktop/cmp/button/index.ts +++ b/desktop/cmp/button/index.ts @@ -13,4 +13,5 @@ export * from './ModalToggleButton'; export * from './OptionsButton'; export * from './RefreshButton'; export * from './RestoreDefaultsButton'; +export * from './SplitButton'; export * from './ThemeToggleButton';