UNPKG

baseui

Version:

A React Component library implementing the Base design language

460 lines (453 loc) • 17.9 kB
"use strict"; 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 */