UNPKG

office-ui-fabric-react

Version:

Reusable React components for building experiences for Office 365.

501 lines • 29.1 kB
import * as tslib_1 from "tslib"; import * as React from 'react'; import { BaseComponent, anchorProperties, assign, buttonProperties, getId, getNativeProps, createRef, css, mergeAriaAttributeValues, portalContainsElement } from '../../Utilities'; import { Icon } from '../../Icon'; import { ContextualMenu } from '../../ContextualMenu'; import { getBaseButtonClassNames } from './BaseButton.classNames'; import { getClassNames as getBaseSplitButtonClassNames } from './SplitButton/SplitButton.classNames'; import { KeytipData } from '../../KeytipData'; var TouchIdleDelay = 500; /* ms */ var BaseButton = /** @class */ (function (_super) { tslib_1.__extends(BaseButton, _super); function BaseButton(props, rootClassName) { var _this = _super.call(this, props) || this; _this._buttonElement = createRef(); _this._splitButtonContainer = createRef(); _this._onRenderIcon = function (buttonProps, defaultRender) { var iconProps = _this.props.iconProps; if (iconProps) { var className = iconProps.className, rest = tslib_1.__rest(iconProps, ["className"]); return React.createElement(Icon, tslib_1.__assign({ className: css(_this._classNames.icon, className) }, rest)); } return null; }; _this._onRenderTextContents = function () { var _a = _this.props, text = _a.text, children = _a.children, _b = _a.secondaryText, secondaryText = _b === void 0 ? _this.props.description : _b, _c = _a.onRenderText, onRenderText = _c === void 0 ? _this._onRenderText : _c, _d = _a.onRenderDescription, onRenderDescription = _d === void 0 ? _this._onRenderDescription : _d; if (text || typeof children === 'string' || secondaryText) { return (React.createElement("div", { className: _this._classNames.textContainer }, onRenderText(_this.props, _this._onRenderText), onRenderDescription(_this.props, _this._onRenderDescription))); } return [onRenderText(_this.props, _this._onRenderText), onRenderDescription(_this.props, _this._onRenderDescription)]; }; _this._onRenderText = function () { var text = _this.props.text; var children = _this.props.children; // For backwards compat, we should continue to take in the text content from children. if (text === undefined && typeof children === 'string') { text = children; } if (_this._hasText()) { return (React.createElement("div", { key: _this._labelId, className: _this._classNames.label, id: _this._labelId }, text)); } return null; }; _this._onRenderChildren = function () { var children = _this.props.children; // If children is just a string, either it or the text will be rendered via onRenderLabel // If children is another component, it will be rendered after text if (typeof children === 'string') { return null; } return children; }; _this._onRenderDescription = function (props) { var _a = props.secondaryText, secondaryText = _a === void 0 ? _this.props.description : _a; // ms-Button-description is only shown when the button type is compound. // In other cases it will not be displayed. return secondaryText ? (React.createElement("div", { key: _this._descriptionId, className: _this._classNames.description, id: _this._descriptionId }, secondaryText)) : null; }; _this._onRenderAriaDescription = function () { var ariaDescription = _this.props.ariaDescription; // If ariaDescription is given, descriptionId will be assigned to ariaDescriptionSpan, // otherwise it will be assigned to descriptionSpan. return ariaDescription ? (React.createElement("span", { className: _this._classNames.screenReaderText, id: _this._ariaDescriptionId }, ariaDescription)) : null; }; _this._onRenderMenuIcon = function (props) { var menuIconProps = _this.props.menuIconProps; return React.createElement(Icon, tslib_1.__assign({ iconName: "ChevronDown" }, menuIconProps, { className: _this._classNames.menuIcon })); }; _this._onRenderMenu = function (menuProps) { var _a = menuProps.onDismiss, onDismiss = _a === void 0 ? _this._dismissMenu : _a; var MenuType = _this.props.menuAs || ContextualMenu; // the accessible menu label (accessible name) has a relationship to the button. // If the menu props do not specify an explicit value for aria-label or aria-labelledBy, // AND the button has text, we'll set the menu aria-labelledBy to the text element id. if (!menuProps.ariaLabel && !menuProps.labelElementId && _this._hasText()) { menuProps = tslib_1.__assign({}, menuProps, { labelElementId: _this._labelId }); } return (React.createElement(MenuType, tslib_1.__assign({ id: _this._labelId + '-menu', directionalHint: 4 /* bottomLeftEdge */ }, menuProps, { shouldFocusOnContainer: _this.state.menuProps ? _this.state.menuProps.shouldFocusOnContainer : undefined, shouldFocusOnMount: _this.state.menuProps ? _this.state.menuProps.shouldFocusOnMount : undefined, className: css('ms-BaseButton-menuhost', menuProps.className), target: _this._isSplitButton ? _this._splitButtonContainer.current : _this._buttonElement.current, onDismiss: onDismiss }))); }; _this._dismissMenu = function () { var menuProps = null; if (_this.props.persistMenu && _this.state.menuProps) { menuProps = _this.state.menuProps; menuProps.hidden = true; } _this.setState({ menuProps: menuProps }); }; _this._openMenu = function (shouldFocusOnContainer, shouldFocusOnMount) { if (shouldFocusOnMount === void 0) { shouldFocusOnMount = true; } if (_this.props.menuProps) { var menuProps = tslib_1.__assign({}, _this.props.menuProps, { shouldFocusOnContainer: shouldFocusOnContainer, shouldFocusOnMount: shouldFocusOnMount }); if (_this.props.persistMenu) { menuProps.hidden = false; } _this.setState({ menuProps: menuProps }); } }; _this._onToggleMenu = function (shouldFocusOnContainer) { var currentMenuProps = _this.state.menuProps; var shouldFocusOnMount = true; if (_this.props.menuProps && _this.props.menuProps.shouldFocusOnMount === false) { shouldFocusOnMount = false; } if (_this.props.persistMenu) { currentMenuProps && currentMenuProps.hidden ? _this._openMenu(shouldFocusOnContainer, shouldFocusOnMount) : _this._dismissMenu(); } else { currentMenuProps ? _this._dismissMenu() : _this._openMenu(shouldFocusOnContainer, shouldFocusOnMount); } }; _this._onSplitContainerFocusCapture = function (ev) { var container = _this._splitButtonContainer.current; // If the target is coming from the portal we do not need to set focus on the container. if (!container || (ev.target && portalContainsElement(ev.target, container))) { return; } // We should never be able to focus the individual buttons in a split button. Focus // should always remain on the container. container.focus(); }; _this._onSplitButtonPrimaryClick = function (ev) { if (_this._isExpanded) { _this._dismissMenu(); } if (!_this._processingTouch && _this.props.onClick) { _this.props.onClick(ev); } else if (_this._processingTouch) { _this._onMenuClick(ev); } }; _this._onKeyDown = function (ev) { // explicity cancelling event so click won't fire after this if (_this.props.disabled && (ev.which === 13 /* enter */ || ev.which === 32 /* space */)) { ev.preventDefault(); ev.stopPropagation(); } else if (!_this.props.disabled) { if (_this.props.menuProps) { _this._onMenuKeyDown(ev); } else if (_this.props.onKeyDown !== undefined) { _this.props.onKeyDown(ev); // not cancelling event because it's not disabled } } }; _this._onKeyUp = function (ev) { if (!_this.props.disabled && _this.props.onKeyUp !== undefined) { _this.props.onKeyUp(ev); // not cancelling event because it's not disabled } }; _this._onKeyPress = function (ev) { if (!_this.props.disabled && _this.props.onKeyPress !== undefined) { _this.props.onKeyPress(ev); // not cancelling event because it's not disabled } }; _this._onMouseUp = function (ev) { if (!_this.props.disabled && _this.props.onMouseUp !== undefined) { _this.props.onMouseUp(ev); // not cancelling event because it's not disabled } }; _this._onMouseDown = function (ev) { if (!_this.props.disabled && _this.props.onMouseDown !== undefined) { _this.props.onMouseDown(ev); // not cancelling event because it's not disabled } }; _this._onClick = function (ev) { if (!_this.props.disabled) { if (_this.props.menuProps) { _this._onMenuClick(ev); } else if (_this.props.onClick !== undefined) { _this.props.onClick(ev); // not cancelling event because it's not disabled } } }; _this._onSplitButtonContainerKeyDown = function (ev) { if (ev.which === 13 /* enter */) { if (_this._buttonElement.current) { _this._buttonElement.current.click(); ev.preventDefault(); ev.stopPropagation(); } } else { _this._onMenuKeyDown(ev); } }; _this._onMenuKeyDown = function (ev) { if (_this.props.disabled) { return; } if (_this.props.onKeyDown) { _this.props.onKeyDown(ev); } if (!ev.defaultPrevented && _this._isValidMenuOpenKey(ev)) { var onMenuClick = _this.props.onMenuClick; if (onMenuClick) { onMenuClick(ev, _this); } _this._onToggleMenu(false); ev.preventDefault(); ev.stopPropagation(); } }; _this._onTouchStart = function () { if (_this._isSplitButton && _this._splitButtonContainer.value && !('onpointerdown' in _this._splitButtonContainer.value)) { _this._handleTouchAndPointerEvent(); } }; _this._onMenuClick = function (ev) { var onMenuClick = _this.props.onMenuClick; if (onMenuClick) { onMenuClick(ev, _this); } if (!ev.defaultPrevented) { // When Edge + Narrator are used together (regardless of if the button is in a form or not), pressing // "Enter" fires this method and not _onMenuKeyDown. Checking ev.nativeEvent.detail differentiates // between a real click event and a keypress event. var shouldFocusOnContainer = ev.nativeEvent.detail !== 0; _this._onToggleMenu(shouldFocusOnContainer); ev.preventDefault(); ev.stopPropagation(); } }; _this._warnConditionallyRequiredProps(['menuProps', 'onClick'], 'split', _this.props.split); _this._warnDeprecations({ rootProps: undefined, description: 'secondaryText', toggled: 'checked' }); _this._labelId = getId(); _this._descriptionId = getId(); _this._ariaDescriptionId = getId(); var menuProps = null; if (props.persistMenu && props.menuProps) { menuProps = props.menuProps; menuProps.hidden = true; } _this.state = { menuProps: menuProps }; return _this; } Object.defineProperty(BaseButton.prototype, "_isSplitButton", { get: function () { return !!this.props.menuProps && !!this.props.onClick && this.props.split === true; }, enumerable: true, configurable: true }); Object.defineProperty(BaseButton.prototype, "_isExpanded", { get: function () { if (this.props.persistMenu) { return !this.state.menuProps.hidden; } return !!this.state.menuProps; }, enumerable: true, configurable: true }); BaseButton.prototype.render = function () { var _a = this.props, ariaDescription = _a.ariaDescription, ariaLabel = _a.ariaLabel, ariaHidden = _a.ariaHidden, className = _a.className, disabled = _a.disabled, allowDisabledFocus = _a.allowDisabledFocus, primaryDisabled = _a.primaryDisabled, _b = _a.secondaryText, secondaryText = _b === void 0 ? this.props.description : _b, href = _a.href, iconProps = _a.iconProps, menuIconProps = _a.menuIconProps, styles = _a.styles, checked = _a.checked, variantClassName = _a.variantClassName, theme = _a.theme, toggle = _a.toggle, getClassNames = _a.getClassNames; var menuProps = this.state.menuProps; // Button is disabled if the whole button (in case of splitbutton is disabled) or if the primary action is disabled var isPrimaryButtonDisabled = disabled || primaryDisabled; this._classNames = getClassNames ? getClassNames(theme, className, variantClassName, iconProps && iconProps.className, menuIconProps && menuIconProps.className, isPrimaryButtonDisabled, checked, !!menuProps, this.props.split, !!allowDisabledFocus) : getBaseButtonClassNames(theme, styles, className, variantClassName, iconProps && iconProps.className, menuIconProps && menuIconProps.className, isPrimaryButtonDisabled, checked, !!menuProps, this.props.split); var _c = this, _ariaDescriptionId = _c._ariaDescriptionId, _labelId = _c._labelId, _descriptionId = _c._descriptionId; // Anchor tag cannot be disabled hence in disabled state rendering // anchor button as normal button var renderAsAnchor = !isPrimaryButtonDisabled && !!href; var tag = renderAsAnchor ? 'a' : 'button'; var nativeProps = getNativeProps(assign(renderAsAnchor ? {} : { type: 'button' }, this.props.rootProps, this.props), renderAsAnchor ? anchorProperties : buttonProperties, [ 'disabled' // let disabled buttons be focused and styled as disabled. ]); // Check for ariaLabel passed in via Button props, and fall back to aria-label passed in via native props var resolvedAriaLabel = ariaLabel || nativeProps['aria-label']; // Check for ariaDescription, secondaryText or aria-describedby in the native props to determine source of aria-describedby // otherwise default to undefined so property does not appear in output. var ariaDescribedBy = undefined; if (ariaDescription) { ariaDescribedBy = _ariaDescriptionId; } else if (secondaryText) { ariaDescribedBy = _descriptionId; } else if (nativeProps['aria-describedby']) { ariaDescribedBy = nativeProps['aria-describedby']; } // If an explicit ariaLabel is given, use that as the label and we're done. // If an explicit aria-labelledby is given, use that and we're done. // If any kind of description is given (which will end up as an aria-describedby attribute), // set the labelledby element. Otherwise, the button is labeled implicitly by the descendent // text on the button (if it exists). Never set both aria-label and aria-labelledby. var ariaLabelledBy = undefined; if (!resolvedAriaLabel) { if (nativeProps['aria-labelledby']) { ariaLabelledBy = nativeProps['aria-labelledby']; } else if (ariaDescribedBy) { ariaLabelledBy = this._hasText() ? _labelId : undefined; } } var dataIsFocusable = this.props['data-is-focusable'] === false || (disabled && !allowDisabledFocus) || this._isSplitButton ? false : true; var buttonProps = assign(nativeProps, { className: this._classNames.root, ref: this._buttonElement, disabled: isPrimaryButtonDisabled && !allowDisabledFocus, onKeyDown: this._onKeyDown, onKeyPress: this._onKeyPress, onKeyUp: this._onKeyUp, onMouseDown: this._onMouseDown, onMouseUp: this._onMouseUp, onClick: this._onClick, 'aria-label': resolvedAriaLabel, 'aria-labelledby': ariaLabelledBy, 'aria-describedby': ariaDescribedBy, 'aria-disabled': isPrimaryButtonDisabled, 'data-is-focusable': dataIsFocusable, 'aria-pressed': toggle ? !!checked : undefined // aria-pressed attribute should only be present for toggle buttons }); if (ariaHidden) { buttonProps['aria-hidden'] = true; } if (this._isSplitButton) { return this._onRenderSplitButtonContent(tag, buttonProps); } else if (this.props.menuProps) { assign(buttonProps, { 'aria-expanded': this._isExpanded, 'aria-owns': this.state.menuProps ? this._labelId + '-menu' : null, 'aria-haspopup': true }); } return this._onRenderContent(tag, buttonProps); }; BaseButton.prototype.componentDidMount = function () { // For split buttons, touching anywhere in the button should drop the dropdown, which should contain the primary action. // This gives more hit target space for touch environments. We're setting the onpointerdown here, because React // does not support Pointer events yet. if (this._isSplitButton && this._splitButtonContainer.value && 'onpointerdown' in this._splitButtonContainer.value) { this._events.on(this._splitButtonContainer.value, 'pointerdown', this._onPointerDown, true); } }; BaseButton.prototype.componentDidUpdate = function (prevProps, prevState) { // If Button's menu was closed, run onAfterMenuDismiss if (this.props.onAfterMenuDismiss && prevState.menuProps && !this.state.menuProps) { this.props.onAfterMenuDismiss(); } }; BaseButton.prototype.focus = function () { if (this._isSplitButton && this._splitButtonContainer.current) { this._splitButtonContainer.current.focus(); } else if (this._buttonElement.current) { this._buttonElement.current.focus(); } }; BaseButton.prototype.dismissMenu = function () { this._dismissMenu(); }; BaseButton.prototype.openMenu = function (shouldFocusOnContainer, shouldFocusOnMount) { this._openMenu(shouldFocusOnContainer, shouldFocusOnMount); }; BaseButton.prototype._onRenderContent = function (tag, buttonProps) { var _this = this; var props = this.props; var Tag = tag; var menuIconProps = props.menuIconProps, menuProps = props.menuProps, _a = props.onRenderIcon, onRenderIcon = _a === void 0 ? this._onRenderIcon : _a, _b = props.onRenderAriaDescription, onRenderAriaDescription = _b === void 0 ? this._onRenderAriaDescription : _b, _c = props.onRenderChildren, onRenderChildren = _c === void 0 ? this._onRenderChildren : _c, _d = props.onRenderMenu, onRenderMenu = _d === void 0 ? this._onRenderMenu : _d, _e = props.onRenderMenuIcon, onRenderMenuIcon = _e === void 0 ? this._onRenderMenuIcon : _e, disabled = props.disabled; var keytipProps = props.keytipProps; if (keytipProps && menuProps) { keytipProps = tslib_1.__assign({}, keytipProps, { hasMenu: true }); } var Content = ( // If we're making a split button, we won't put the keytip here React.createElement(KeytipData, { keytipProps: !this._isSplitButton ? keytipProps : undefined, ariaDescribedBy: buttonProps['aria-describedby'], disabled: disabled }, function (keytipAttributes) { return (React.createElement(Tag, tslib_1.__assign({}, buttonProps, keytipAttributes), React.createElement("div", { className: _this._classNames.flexContainer }, onRenderIcon(props, _this._onRenderIcon), _this._onRenderTextContents(), onRenderAriaDescription(props, _this._onRenderAriaDescription), onRenderChildren(props, _this._onRenderChildren), !_this._isSplitButton && (menuProps || menuIconProps || _this.props.onRenderMenuIcon) && onRenderMenuIcon(_this.props, _this._onRenderMenuIcon), _this.state.menuProps && !_this.state.menuProps.doNotLayer && onRenderMenu(menuProps, _this._onRenderMenu)))); })); if (menuProps && menuProps.doNotLayer) { return (React.createElement("div", { style: { display: 'inline-block' } }, Content, this.state.menuProps && onRenderMenu(menuProps, this._onRenderMenu))); } return Content; }; BaseButton.prototype._hasText = function () { // _onRenderTextContents and _onRenderText do not perform the same checks. Below is parity with what _onRenderText used to have // before the refactor that introduced this function. _onRenderTextContents does not require props.text to be undefined in order // for props.children to be used as a fallback. Purely a code maintainability/reuse issue, but logged as Issue #4979 return this.props.text !== null && (this.props.text !== undefined || typeof this.props.children === 'string'); }; BaseButton.prototype._onRenderSplitButtonContent = function (tag, buttonProps) { var _this = this; var _a = this.props, _b = _a.styles, styles = _b === void 0 ? {} : _b, disabled = _a.disabled, allowDisabledFocus = _a.allowDisabledFocus, checked = _a.checked, getSplitButtonClassNames = _a.getSplitButtonClassNames, primaryDisabled = _a.primaryDisabled, menuProps = _a.menuProps, toggle = _a.toggle; var keytipProps = this.props.keytipProps; var classNames = getSplitButtonClassNames ? getSplitButtonClassNames(!!disabled, !!this.state.menuProps, !!checked, !!allowDisabledFocus) : styles && getBaseSplitButtonClassNames(styles, !!disabled, !!this.state.menuProps, !!checked); assign(buttonProps, { onClick: undefined, tabIndex: -1, 'data-is-focusable': false }); var ariaDescribedBy = buttonProps.ariaDescription; if (keytipProps && menuProps) { keytipProps = tslib_1.__assign({}, keytipProps, { hasMenu: true }); } var containerProps = getNativeProps(buttonProps, [], ['disabled']); return (React.createElement(KeytipData, { keytipProps: keytipProps, disabled: disabled }, function (keytipAttributes) { return (React.createElement("div", tslib_1.__assign({}, containerProps, { "data-ktp-target": keytipAttributes['data-ktp-target'], role: 'button', "aria-disabled": disabled, "aria-haspopup": true, "aria-expanded": _this._isExpanded, "aria-pressed": toggle ? !!checked : undefined, "aria-describedby": mergeAriaAttributeValues(ariaDescribedBy, keytipAttributes['aria-describedby']), className: classNames && classNames.splitButtonContainer, onKeyDown: _this._onSplitButtonContainerKeyDown, onTouchStart: _this._onTouchStart, ref: _this._splitButtonContainer, "data-is-focusable": true, onClick: !disabled && !primaryDisabled ? _this._onSplitButtonPrimaryClick : undefined, tabIndex: !disabled || allowDisabledFocus ? 0 : undefined, "aria-roledescription": buttonProps['aria-roledescription'], onFocusCapture: _this._onSplitContainerFocusCapture }), React.createElement("span", { style: { display: 'flex' } }, _this._onRenderContent(tag, buttonProps), _this._onRenderSplitButtonMenuButton(classNames, keytipAttributes), _this._onRenderSplitButtonDivider(classNames)))); })); }; BaseButton.prototype._onRenderSplitButtonDivider = function (classNames) { if (classNames && classNames.divider) { return React.createElement("span", { className: classNames.divider }); } return null; }; BaseButton.prototype._onRenderSplitButtonMenuButton = function (classNames, keytipAttributes) { var _a = this.props, allowDisabledFocus = _a.allowDisabledFocus, checked = _a.checked, disabled = _a.disabled; var menuIconProps = this.props.menuIconProps; var splitButtonAriaLabel = this.props.splitButtonAriaLabel; if (menuIconProps === undefined) { menuIconProps = { iconName: 'ChevronDown' }; } var splitButtonProps = { styles: classNames, checked: checked, disabled: disabled, allowDisabledFocus: allowDisabledFocus, onClick: this._onMenuClick, menuProps: undefined, iconProps: tslib_1.__assign({}, menuIconProps, { className: this._classNames.menuIcon }), ariaLabel: splitButtonAriaLabel, 'aria-haspopup': true, 'aria-expanded': this._isExpanded, 'data-is-focusable': false }; // Add data-ktp-execute-target to the split button if the keytip is defined return (React.createElement(BaseButton, tslib_1.__assign({}, splitButtonProps, { "data-ktp-execute-target": keytipAttributes['data-ktp-execute-target'], onMouseDown: this._onMouseDown, tabIndex: -1 }))); }; BaseButton.prototype._onPointerDown = function (ev) { if (ev.pointerType === 'touch') { this._handleTouchAndPointerEvent(); ev.preventDefault(); ev.stopImmediatePropagation(); } }; BaseButton.prototype._handleTouchAndPointerEvent = function () { var _this = this; // If we already have an existing timeeout from a previous touch and pointer event // cancel that timeout so we can set a nwe one. if (this._lastTouchTimeoutId !== undefined) { this._async.clearTimeout(this._lastTouchTimeoutId); this._lastTouchTimeoutId = undefined; } this._processingTouch = true; this._lastTouchTimeoutId = this._async.setTimeout(function () { _this._processingTouch = false; _this._lastTouchTimeoutId = undefined; }, TouchIdleDelay); }; /** * Returns if the user hits a valid keyboard key to open the menu * @param ev - the keyboard event * @returns True if user clicks on custom trigger key if enabled or alt + down arrow if not. False otherwise. */ BaseButton.prototype._isValidMenuOpenKey = function (ev) { if (this.props.menuTriggerKeyCode) { return ev.which === this.props.menuTriggerKeyCode; } else if (this.props.menuProps) { return ev.which === 40 /* down */ && (ev.altKey || ev.metaKey); } // Note: When enter is pressed, we will let the event continue to propagate // to trigger the onClick event on the button return false; }; BaseButton.defaultProps = { baseClassName: 'ms-Button', styles: {}, split: false }; return BaseButton; }(BaseComponent)); export { BaseButton }; //# sourceMappingURL=BaseButton.js.map