import _ from 'lodash';
import '../../chipper/js/browser/initialize-globals.js';
import '../../assert/js/assert.js';
import '../../globals.js';
// Copyright 2022-2024, University of Colorado Boulder
/**
 * Popupable trait
 *
 * @author Jonathan Olson <jonathan.olson@colorado.edu>
 * @author Sam Reid (PhET Interactive Simulations)
 * @author Andrea Lin (PhET Interactive Simulations)
 * @author Chris Malley (PixelZoom, Inc.)
 */
import BooleanProperty from '../../axon/js/BooleanProperty.js';
import ScreenView from '../../joist/js/ScreenView.js';
import gracefulBind from '../../phet-core/js/gracefulBind.js';
import optionize from '../../phet-core/js/optionize.js';
import { FocusManager, Node } from '../../scenery/js/imports.js';
import sun from './sun.js';
const Popupable = (Type, optionsArgPosition) => {
    return class extends Type {
        // Support the same signature as the type we mix into.  However, we also have our own options, which we assume
        // are passed in the last arg.
        constructor(...args) {
            super(...args);
            const providedOptions = (args[optionsArgPosition] || {});
            // `config` is required for Popupable, to work well with ...args but all fields of the config are optional
            const requiredConfig = args[args.length - 1];
            assert && assert(requiredConfig !== undefined, 'config object is required for Popupable.');
            const showPopup = gracefulBind('phet.joist.sim.showPopup');
            const hidePopup = gracefulBind('phet.joist.sim.hidePopup');
            const options = optionize()({
                showPopup: showPopup,
                hidePopup: hidePopup,
                isModal: true,
                layoutBounds: null,
                focusOnShowNode: null,
                focusOnHideNode: null,
                disableModals: _.get(window, 'phet.chipper.queryParameters.disableModals') || false
            }, providedOptions);
            // see https://github.com/phetsims/joist/issues/293
            assert && assert(options.isModal, 'Non-modal popups not currently supported');
            this.layoutBounds = options.layoutBounds;
            this._focusOnShowNode = options.focusOnShowNode;
            this.disableModals = options.disableModals;
            this.isModal = options.isModal;
            this._focusOnHideNode = options.focusOnHideNode;
            this._nodeToFocusOnHide = null;
            this.popupParent = new PopupParentNode(this, {
                show: this.show.bind(this),
                hide: this.hide.bind(this),
                layout: this.layout.bind(this)
            });
            this.isShowingProperty = new BooleanProperty(false, {
                tandem: options.tandem?.createTandem('isShowingProperty'),
                phetioReadOnly: true,
                phetioFeatured: true
            });
            this.isShowingProperty.lazyLink(isShowing => {
                if (isShowing) {
                    options.showPopup(this.popupParent, options.isModal);
                }
                else {
                    options.hidePopup(this.popupParent, options.isModal);
                }
            });
        }
        layout(bounds) {
            if (this.layoutBounds) {
                this.popupParent.matrix = ScreenView.getLayoutMatrix(this.layoutBounds, bounds);
            }
        }
        // Provide a chance of not showing, see disableModals
        // @mixin-protected - made public for use in the mixin only
        shouldShowPopup() {
            const optOut = this.isModal && this.disableModals;
            return !optOut;
        }
        show() {
            if (!this.shouldShowPopup()) {
                return;
            }
            // save a reference before setting isShowingProperty because listeners on the isShowingProperty may modify or
            // clear focus from FocusManager.pdomFocusedNode.
            this._nodeToFocusOnHide = this._focusOnHideNode || FocusManager.pdomFocusedNode;
            this.isShowingProperty.value = true;
            // after it is shown, move focus to the focusOnShownNode, presumably moving focus into the Popupable content
            if (this._focusOnShowNode && this._focusOnShowNode.focusable) {
                this._focusOnShowNode.focus();
            }
        }
        /**
         * Hide the popup. If you create a new popup next time you show(), be sure to dispose this popup instead.
         */
        hide() {
            this.interruptSubtreeInput();
            this.isShowingProperty.value = false;
            // return focus to the Node that had focus when the Popupable was opened (or the focusOnHideNode if provided)
            if (this._nodeToFocusOnHide && this._nodeToFocusOnHide.focusable) {
                this._nodeToFocusOnHide.focus();
            }
        }
        // @mixin-protected - made public for use in the mixin only
        get focusOnHideNode() {
            return this._focusOnHideNode;
        }
        /**
         * Releases references
         */
        dispose() {
            this.hide();
            this.isShowingProperty.dispose();
            super.dispose();
        }
    };
};
class PopupParentNode extends Node {
    constructor(popupableNode, providedOptions) {
        const options = optionize()({
            children: [popupableNode]
        }, providedOptions);
        super(options);
        this.show = options.show;
        this.hide = options.hide;
        this.layout = options.layout;
    }
}
sun.register('Popupable', Popupable);
export default Popupable;
//# sourceMappingURL=Popupable.js.map