/**
* Button.tsx
*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT license.
*
* Web-specific implementation of the cross-platform Button abstraction.
*/
import * as PropTypes from 'prop-types';
import * as React from 'react';
import AppConfig from '../common/AppConfig';
import { FocusArbitratorProvider } from '../common/utils/AutoFocusHelper';
import { Button as ButtonBase, Types } from '../common/Interfaces';
import Timers from '../common/utils/Timers';
import AccessibilityUtil from './AccessibilityUtil';
import { applyFocusableComponentMixin } from './utils/FocusManager';
import Styles from './Styles';
import UserInterface from './UserInterface';
const _styles = {
defaultButton: {
position: 'relative',
display: 'flex',
flexDirection: 'column',
flexGrow: 0,
flexShrink: 0,
overflow: 'hidden',
alignItems: 'stretch',
justifyContent: 'center',
appRegion: 'no-drag',
backgroundColor: 'transparent',
borderColor: 'transparent',
textAlign: 'left',
borderWidth: '0'
}
};
const _longPressTime = 1000;
const _defaultAccessibilityTrait = Types.AccessibilityTrait.Button;
export interface ButtonContext {
hasRxButtonAscendant?: boolean;
focusArbitrator?: FocusArbitratorProvider;
}
export class Button extends ButtonBase {
static contextTypes = {
hasRxButtonAscendant: PropTypes.bool,
focusArbitrator: PropTypes.object
};
context!: ButtonContext;
static childContextTypes = {
hasRxButtonAscendant: PropTypes.bool
};
private _mountedButton: HTMLButtonElement | null = null;
private _lastMouseDownEvent: Types.SyntheticEvent | undefined;
private _ignoreTouchEnd = false;
private _ignoreClick = false;
private _longPressTimer: number | undefined;
private _isMouseOver = false;
private _isFocusedWithKeyboard = false;
private _isHoverStarted = false;
constructor(props: Types.ButtonProps, context?: ButtonContext) {
super(props, context);
if (context && context.hasRxButtonAscendant) {
if (AppConfig.isDevelopmentMode()) {
console.warn('Button components should not be embedded. Some APIs, e.g. Accessibility, will not work.');
}
}
}
getChildContext(): ButtonContext {
return { hasRxButtonAscendant: true };
}
render() {
const ariaRole = AccessibilityUtil.accessibilityTraitToString(this.props.accessibilityTraits,
_defaultAccessibilityTrait);
const ariaSelected = AccessibilityUtil.accessibilityTraitToAriaSelected(this.props.accessibilityTraits);
const ariaChecked = AccessibilityUtil.accessibilityTraitToAriaChecked(this.props.accessibilityTraits);
const isAriaHidden = AccessibilityUtil.isHidden(this.props.importantForAccessibility);
const ariaHasPopup = AccessibilityUtil.accessibilityTraitToAriaHasPopup(this.props.accessibilityTraits);
// NOTE: We use tabIndex=0 to support focus.
return (
);
}
componentDidMount() {
if (this.props.autoFocus) {
this.requestFocus();
}
}
requestFocus() {
FocusArbitratorProvider.requestFocus(
this,
() => this.focus(),
() => this._mountedButton !== null
);
}
focus() {
if (this._mountedButton) {
this._mountedButton.focus();
}
}
blur() {
if (this._mountedButton) {
this._mountedButton.blur();
}
}
private _onMount = (ref: HTMLButtonElement | null) => {
this._mountedButton = ref;
}
protected onClick = (e: Types.MouseEvent) => {
if (this._ignoreClick) {
e.stopPropagation();
this._ignoreClick = false;
} else if (!this.props.disabled && this.props.onPress) {
this.props.onPress(e);
}
}
private _getStyles(): Types.ButtonStyleRuleSet {
const buttonStyleMutations: Types.ButtonStyle = {};
const buttonStyles = Styles.combine(this.props.style) as any;
// Specify default style for padding only if padding is not already specified
if (buttonStyles && buttonStyles.padding === undefined &&
buttonStyles.paddingRight === undefined && buttonStyles.paddingLeft === undefined &&
buttonStyles.paddingBottom === undefined && buttonStyles.paddingTop === undefined &&
buttonStyles.paddingHorizontal === undefined && buttonStyles.paddingVertical === undefined) {
buttonStyleMutations.padding = 0;
}
if (this.props.disabled) {
buttonStyleMutations.opacity = this.props.disabledOpacity !== undefined ? this.props.disabledOpacity : 0.5;
}
// Default to 'pointer' cursor for enabled buttons unless otherwise specified.
if (!buttonStyles || !buttonStyles.cursor) {
buttonStyleMutations.cursor = this.props.disabled ? 'default' : 'pointer';
}
return Styles.combine([_styles.defaultButton, buttonStyles, buttonStyleMutations]);
}
private _onContextMenu = (e: React.MouseEvent) => {
if (this.props.onContextMenu) {
this.props.onContextMenu(e);
}
}
private _onMouseDown = (e: React.SyntheticEvent) => {
if (this.props.disabled) {
return;
}
this._isMouseOver = true;
if (this.props.onPressIn) {
this.props.onPressIn(e);
}
if (this.props.onLongPress) {
this._lastMouseDownEvent = e;
e.persist();
// In the unlikely event we get 2 mouse down events, clear existing timer
if (this._longPressTimer) {
Timers.clearTimeout(this._longPressTimer);
}
this._longPressTimer = Timers.setTimeout(() => {
this._longPressTimer = undefined;
if (this.props.onLongPress) {
// lastMouseDownEvent can never be undefined at this point
this.props.onLongPress(this._lastMouseDownEvent!);
if ('touches' in e) {
this._ignoreTouchEnd = true;
} else {
this._ignoreClick = true;
}
}
}, this.props.delayLongPress || _longPressTime);
}
}
private _onTouchMove = (e: React.SyntheticEvent) => {
const buttonRect = (e.target as HTMLButtonElement).getBoundingClientRect();
const wasMouseOver = this._isMouseOver;
const isMouseOver =
e.nativeEvent.touches[0].clientX > buttonRect.left &&
e.nativeEvent.touches[0].clientX < buttonRect.right &&
e.nativeEvent.touches[0].clientY > buttonRect.top &&
e.nativeEvent.touches[0].clientY < buttonRect.bottom;
// Touch has left the button, cancel the longpress handler.
if (wasMouseOver && !isMouseOver) {
if (this._longPressTimer) {
clearTimeout(this._longPressTimer);
}
if (this.props.onHoverEnd) {
this.props.onHoverEnd(e);
}
}
this._isMouseOver = isMouseOver;
}
private _onMouseUp = (e: Types.SyntheticEvent | Types.TouchEvent) => {
if (!this.props.disabled && this.props.onPressOut) {
this.props.onPressOut(e);
}
if (this._longPressTimer) {
Timers.clearTimeout(this._longPressTimer);
}
}
/**
* Case where onPressOut is not triggered and the bubbling is canceled:
* 1- Long press > release
*
* Cases where onPressOut is triggered:
* 2- Long press > leave button > release touch
* 3- Tap
*
* All other cases: onPressOut is not triggered and the bubbling is NOT canceled:
*/
private _onTouchEnd = (e: Types.SyntheticEvent | Types.TouchEvent) => {
if (this._isMouseOver && this._ignoreTouchEnd) {
/* 1 */
e.stopPropagation();
} else if (
/* 2 */
!this._isMouseOver && this._ignoreTouchEnd ||
/* 3 */
this._isMouseOver && !this._ignoreTouchEnd
) {
if ('touches' in e) {
// Stop the to event sequence to prevent trigger button.onMouseDown
e.preventDefault();
if (this.props.onPress) {
this.props.onPress(e);
}
}
if (this.props.onPressOut) {
this.props.onPressOut(e);
}
}
this._ignoreTouchEnd = false;
if (this._longPressTimer) {
Timers.clearTimeout(this._longPressTimer);
}
}
private _onMouseEnter = (e: Types.SyntheticEvent) => {
this._isMouseOver = true;
this._onHoverStart(e);
}
private _onMouseLeave = (e: Types.SyntheticEvent) => {
this._isMouseOver = false;
this._onHoverEnd(e);
// The mouse is still down. A long press may be just happened. Re-enable the next click.
this._ignoreClick = false;
// Cancel longpress if mouse has left.
if (this._longPressTimer) {
Timers.clearTimeout(this._longPressTimer);
}
}
// When we get focus on an element, show the hover effect on the element.
// This ensures that users using keyboard also get the similar experience as mouse users for accessibility.
private _onFocus = (e: Types.FocusEvent) => {
this._isFocusedWithKeyboard = UserInterface.isNavigatingWithKeyboard();
this._onHoverStart(e);
if (this.props.onFocus) {
this.props.onFocus(e);
}
}
private _onBlur = (e: Types.FocusEvent) => {
this._isFocusedWithKeyboard = false;
this._onHoverEnd(e);
if (this.props.onBlur) {
this.props.onBlur(e);
}
}
private _onHoverStart = (e: Types.SyntheticEvent) => {
if (!this._isHoverStarted && (this._isMouseOver || this._isFocusedWithKeyboard)) {
this._isHoverStarted = true;
if (this.props.onHoverStart) {
this.props.onHoverStart(e);
}
}
}
private _onHoverEnd = (e: Types.SyntheticEvent) => {
if (this._isHoverStarted && !this._isMouseOver && !this._isFocusedWithKeyboard) {
this._isHoverStarted = false;
if (this.props.onHoverEnd) {
this.props.onHoverEnd(e);
}
}
}
}
applyFocusableComponentMixin(Button);
export default Button;