office-ui-fabric-react
Version:
Reusable React components for building experiences for Office 365.
501 lines • 29.1 kB
JavaScript
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