baseui
Version:
A React Component library implementing the Base design language
460 lines (453 loc) • 17.9 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var React = _interopRequireWildcard(require("react"));
var _reactFocusLock = _interopRequireWildcard(require("react-focus-lock"));
var _overrides = require("../helpers/overrides");
var _constants = require("./constants");
var _layer = require("../layer");
var _styledComponents = require("./styled-components");
var _utils = require("./utils");
var _defaultProps = _interopRequireDefault(require("./default-props"));
var _reactUid = require("react-uid");
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); }
function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && Object.prototype.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; }
function _extends() { _extends = Object.assign ? Object.assign.bind() : function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); }
function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == typeof i ? i : String(i); }
function _toPrimitive(t, r) { if ("object" != typeof t || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != typeof i) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); } /*
Copyright (c) Uber Technologies, Inc.
This source code is licensed under the MIT license found in the
LICENSE file in the root directory of this source tree.
*/
class PopoverInner extends React.Component {
constructor(...args) {
super(...args);
/* eslint-disable react/sort-comp */
_defineProperty(this, "animateInTimer", void 0);
_defineProperty(this, "animateOutTimer", void 0);
_defineProperty(this, "animateOutCompleteTimer", void 0);
_defineProperty(this, "onMouseEnterTimer", void 0);
_defineProperty(this, "onMouseLeaveTimer", void 0);
_defineProperty(this, "anchorRef", /*#__PURE__*/React.createRef());
_defineProperty(this, "popperRef", /*#__PURE__*/React.createRef());
_defineProperty(this, "arrowRef", /*#__PURE__*/React.createRef());
/* eslint-enable react/sort-comp */
/**
* Yes our "Stateless" popover still has state. This is private state that
* customers shouldn't have to manage themselves, such as positioning and
* other internal flags for managing animation states.
*/
// @ts-ignore
_defineProperty(this, "state", this.getDefaultState(this.props));
_defineProperty(this, "animateIn", () => {
if (this.props.isOpen) {
this.setState({
isAnimating: true
});
}
});
_defineProperty(this, "animateOut", () => {
if (!this.props.isOpen) {
this.setState({
isAnimating: true
});
// Remove the popover from the DOM after animation finishes
this.animateOutCompleteTimer = setTimeout(() => {
this.setState({
isAnimating: false,
// Reset to ideal placement specified in props
// @ts-ignore
placement: this.props.placement
});
}, this.props.animateOutTime || _constants.ANIMATE_OUT_TIME);
}
});
_defineProperty(this, "onAnchorClick", e => {
if (this.props.onClick) {
this.props.onClick(e);
}
});
_defineProperty(this, "onAnchorMouseEnter", e => {
// First clear any existing close timers, this ensures that the user can
// move their mouse from the popover back to anchor without it hiding
if (this.onMouseLeaveTimer) {
clearTimeout(this.onMouseLeaveTimer);
}
this.triggerOnMouseEnterWithDelay(e);
});
_defineProperty(this, "onAnchorMouseLeave", e => {
// Clear any existing open timer, otherwise popover could be stuck open
if (this.onMouseEnterTimer) {
clearTimeout(this.onMouseEnterTimer);
}
this.triggerOnMouseLeaveWithDelay(e);
});
_defineProperty(this, "onPopoverMouseEnter", () => {
if (this.onMouseLeaveTimer) {
clearTimeout(this.onMouseLeaveTimer);
}
});
// eslint-disable-next-line @typescript-eslint/no-explicit-any
_defineProperty(this, "onPopoverMouseLeave", e => {
this.triggerOnMouseLeaveWithDelay(e);
});
_defineProperty(this, "onPopperUpdate", (normalizedOffsets, data) => {
const placement = (0, _utils.fromPopperPlacement)(data.placement) || _constants.PLACEMENT.top;
this.setState({
// @ts-ignore
arrowOffset: normalizedOffsets.arrow,
popoverOffset: normalizedOffsets.popper,
placement
});
// Now that element has been positioned, we can animate it in
this.animateInTimer = setTimeout(this.animateIn, _constants.ANIMATE_IN_TIME);
return data;
});
_defineProperty(this, "triggerOnMouseLeave", e => {
if (this.props.onMouseLeave) {
this.props.onMouseLeave(e);
}
});
_defineProperty(this, "triggerOnMouseEnter", e => {
if (this.props.onMouseEnter) {
this.props.onMouseEnter(e);
}
});
_defineProperty(this, "onDocumentClick", evt => {
const target = evt.composedPath ? evt.composedPath()[0] : evt.target;
const popper = this.popperRef.current;
const anchor = this.anchorRef.current;
// Ignore document click if it came from popover or anchor
if (!popper || popper === target || target instanceof Node && popper.contains(target)) {
return;
}
if (!anchor || anchor === target || target instanceof Node && anchor.contains(target)) {
return;
}
if (this.props.onClickOutside) {
this.props.onClickOutside(evt);
}
});
}
componentDidMount() {
this.setState({
isMounted: true
});
}
componentDidUpdate(prevProps, prevState) {
this.init(prevProps, prevState);
if (this.props.accessibilityType !== _constants.ACCESSIBILITY_TYPE.tooltip && this.props.autoFocus && !this.state.autoFocusAfterPositioning && this.popperRef.current !== null && this.popperRef.current.getBoundingClientRect().top > 0) {
this.setState({
autoFocusAfterPositioning: true
});
}
if (process.env.NODE_ENV !== "production") {
if (!this.anchorRef.current) {
// eslint-disable-next-line no-console
console.warn(`[baseui][Popover] ref has not been passed to the Popper's anchor element.
See how to pass the ref to an anchor element in the Popover example
https://baseweb.design/components/popover/#anchor-ref-handling-example`);
}
}
}
init(prevProps, prevState) {
if (this.props.isOpen !== prevProps.isOpen || this.state.isMounted !== prevState.isMounted || this.state.isLayerMounted !== prevState.isLayerMounted) {
// Transition from closed to open.
if (this.props.isOpen && this.state.isLayerMounted) {
// Clear any existing timers (like previous animateOutCompleteTimer)
this.clearTimers();
return;
}
// Transition from open to closed.
if (!this.props.isOpen && prevProps.isOpen) {
this.animateOutTimer = setTimeout(this.animateOut, 20);
return;
}
}
}
componentWillUnmount() {
this.clearTimers();
}
getDefaultState(props) {
return {
isAnimating: false,
arrowOffset: {
left: 0,
top: 0
},
popoverOffset: {
left: 0,
top: 0
},
placement: props.placement,
isMounted: false,
isLayerMounted: false,
autoFocusAfterPositioning: false
};
}
clearTimers() {
[this.animateInTimer, this.animateOutTimer, this.animateOutCompleteTimer, this.onMouseEnterTimer, this.onMouseLeaveTimer].forEach(timerId => {
if (timerId) {
clearTimeout(timerId);
}
});
}
triggerOnMouseLeaveWithDelay(e) {
const {
onMouseLeaveDelay
} = this.props;
if (onMouseLeaveDelay) {
this.onMouseLeaveTimer = setTimeout(() => this.triggerOnMouseLeave(e), onMouseLeaveDelay);
return;
}
this.triggerOnMouseLeave(e);
}
triggerOnMouseEnterWithDelay(e) {
const {
onMouseEnterDelay
} = this.props;
if (onMouseEnterDelay) {
this.onMouseEnterTimer = setTimeout(() => this.triggerOnMouseEnter(e), onMouseEnterDelay);
return;
}
this.triggerOnMouseEnter(e);
}
isClickTrigger() {
return this.props.triggerType === _constants.TRIGGER_TYPE.click;
}
isHoverTrigger() {
return this.props.triggerType === _constants.TRIGGER_TYPE.hover;
}
isAccessibilityTypeMenu() {
return this.props.accessibilityType === _constants.ACCESSIBILITY_TYPE.menu;
}
isAccessibilityTypeTooltip() {
return this.props.accessibilityType === _constants.ACCESSIBILITY_TYPE.tooltip;
}
getAnchorIdAttr() {
const popoverId = this.getPopoverIdAttr();
return popoverId ? `${popoverId}__anchor` : null;
}
getPopoverIdAttr() {
return this.props.id || null;
}
getAnchorProps() {
const {
isOpen
} = this.props;
const anchorProps = {
ref: this.anchorRef
};
const popoverId = this.getPopoverIdAttr();
if (this.isAccessibilityTypeMenu()) {
const relationAttr = this.isClickTrigger() ? 'aria-controls' : 'aria-owns';
anchorProps[relationAttr] = isOpen ? popoverId : null;
anchorProps['aria-haspopup'] = true;
anchorProps['aria-expanded'] = Boolean(isOpen);
} else if (this.isAccessibilityTypeTooltip()) {
anchorProps.id = this.getAnchorIdAttr();
anchorProps['aria-describedby'] = isOpen ? popoverId : null;
}
if (this.isHoverTrigger()) {
anchorProps.onMouseEnter = this.onAnchorMouseEnter;
anchorProps.onMouseLeave = this.onAnchorMouseLeave;
// Make it focusable too
anchorProps.onBlur = this.props.onBlur;
anchorProps.onFocus = this.props.onFocus;
} else {
anchorProps.onClick = this.onAnchorClick;
// Make it focusable too
if (this.props.onBlur) {
anchorProps.onBlur = this.props.onBlur;
}
if (this.props.onFocus) {
anchorProps.onFocus = this.props.onFocus;
}
}
return anchorProps;
}
getPopoverBodyProps() {
const bodyProps = {};
const popoverId = this.getPopoverIdAttr();
if (this.isAccessibilityTypeMenu()) {
// @ts-ignore
bodyProps.id = popoverId;
} else if (this.isAccessibilityTypeTooltip()) {
// @ts-ignore
bodyProps.id = popoverId;
bodyProps.role = 'tooltip';
}
if (this.isHoverTrigger()) {
bodyProps.onMouseEnter = this.onPopoverMouseEnter;
bodyProps.onMouseLeave = this.onPopoverMouseLeave;
}
return bodyProps;
}
getSharedProps() {
const {
isOpen,
showArrow,
popoverMargin = _constants.POPOVER_MARGIN
} = this.props;
const {
isAnimating,
arrowOffset,
popoverOffset,
placement
} = this.state;
return {
$showArrow: !!showArrow,
$arrowOffset: arrowOffset,
$popoverOffset: popoverOffset,
// @ts-ignore
$placement: placement,
$isAnimating: isAnimating,
$animationDuration: this.props.animateOutTime || _constants.ANIMATE_OUT_TIME,
$isOpen: isOpen,
$popoverMargin: popoverMargin,
$isHoverTrigger: this.isHoverTrigger()
};
}
getAnchorFromChildren() {
const {
children
} = this.props;
const childArray = React.Children.toArray(children);
if (childArray.length !== 1) {
// eslint-disable-next-line no-console
console.error(`[baseui] Exactly 1 child must be passed to Popover/Tooltip, found ${childArray.length} children`);
}
return childArray[0];
}
renderAnchor() {
const anchor = this.getAnchorFromChildren();
if (!anchor) {
return null;
}
const isValidElement = /*#__PURE__*/React.isValidElement(anchor);
const anchorProps = this.getAnchorProps();
if (typeof anchor === 'object' && isValidElement) {
return /*#__PURE__*/React.cloneElement(anchor, anchorProps);
}
return (
/*#__PURE__*/
// @ts-ignore
React.createElement("span", _extends({
key: "popover-anchor"
}, anchorProps), anchor)
);
}
renderPopover(renderedContent) {
const {
showArrow,
overrides = {}
} = this.props;
const {
Arrow: ArrowOverride,
Body: BodyOverride,
Inner: InnerOverride
} = overrides;
const Arrow = (0, _overrides.getOverride)(ArrowOverride) || _styledComponents.Arrow;
const Body = (0, _overrides.getOverride)(BodyOverride) || _styledComponents.Body;
const Inner = (0, _overrides.getOverride)(InnerOverride) || _styledComponents.Inner;
const sharedProps = this.getSharedProps();
const bodyProps = this.getPopoverBodyProps();
return /*#__PURE__*/React.createElement(Body, _extends({
key: "popover-body",
ref: this.popperRef,
"data-baseweb": this.props['data-baseweb'] || 'popover'
}, bodyProps, sharedProps, (0, _overrides.getOverrideProps)(BodyOverride)), showArrow ? /*#__PURE__*/React.createElement(Arrow, _extends({
key: "popover-arrow",
ref: this.arrowRef
}, sharedProps, (0, _overrides.getOverrideProps)(ArrowOverride))) : null, /*#__PURE__*/React.createElement(Inner, _extends({}, sharedProps, (0, _overrides.getOverrideProps)(InnerOverride)), renderedContent));
}
renderContent() {
const {
content
} = this.props;
return typeof content === 'function' ? content() : content;
}
render() {
const mountedAndOpen = this.state.isMounted && (this.props.isOpen || this.state.isAnimating);
const rendered = [this.renderAnchor()];
const renderedContent = mountedAndOpen || this.props.renderAll ? this.renderContent() : null;
const defaultPopperOptions = {
modifiers: {
preventOverflow: {
enabled: !this.props.ignoreBoundary,
padding: 0
}
}
};
// Only render popover on the browser (portals aren't supported server-side)
if (renderedContent) {
if (mountedAndOpen) {
rendered.push( /*#__PURE__*/React.createElement(_layer.Layer, {
key: "new-layer",
mountNode: this.props.mountNode,
onEscape: this.props.onEsc,
onDocumentClick: this.isHoverTrigger() ? undefined : this.onDocumentClick,
isHoverLayer: this.isHoverTrigger(),
onMount: () => this.setState({
isLayerMounted: true
}),
onUnmount: () => this.setState({
isLayerMounted: false
})
}, /*#__PURE__*/React.createElement(_layer.TetherBehavior, {
anchorRef: this.anchorRef.current,
arrowRef: this.arrowRef.current,
popperRef: this.popperRef.current
// Remove the `ignoreBoundary` prop in the next major version
// and have it replaced with the TetherBehavior props overrides
,
popperOptions: {
...defaultPopperOptions,
...this.props.popperOptions
},
onPopperUpdate: this.onPopperUpdate,
placement: this.state.placement
}, this.props.focusLock && this.props.accessibilityType !== _constants.ACCESSIBILITY_TYPE.tooltip ? /*#__PURE__*/React.createElement(_reactFocusLock.default, {
disabled: !this.props.focusLock,
noFocusGuards: false
// see popover-focus-loop.scenario.js for why hover cannot return focus
,
returnFocus: !this.isHoverTrigger() && this.props.returnFocus,
autoFocus: this.state.autoFocusAfterPositioning
// Allow focus to escape when UI is within an iframe
,
crossFrame: false,
focusOptions: this.props.focusOptions
}, this.renderPopover(renderedContent)) : /*#__PURE__*/React.createElement(_reactFocusLock.MoveFocusInside, {
disabled: !this.props.autoFocus || !this.state.autoFocusAfterPositioning
}, this.renderPopover(renderedContent)))));
} else {
rendered.push( /*#__PURE__*/React.createElement(_styledComponents.Hidden, {
key: "hidden-layer"
}, renderedContent));
}
}
return rendered;
}
}
// Remove when Popover is converted to a functional component.
_defineProperty(PopoverInner, "defaultProps", _defaultProps.default);
const Popover = props => {
const {
innerRef
} = props;
const gID = (0, _reactUid.useUID)();
return /*#__PURE__*/React.createElement(PopoverInner, _extends({
id: props.id || gID
// @ts-expect-error
,
ref: innerRef
}, props));
};
Popover.defaultProps = _defaultProps.default;
var _default = exports.default = Popover;
/* eslint-enable react/no-find-dom-node */