joeattardi / picmo

JavaScript emoji picker. Any app, any framework.
https://picmojs.com
MIT License
1.19k stars 118 forks source link

Picmo flashes in top-left corner of browser before closing when triggerElement/referenceElement is hidden or removed #245

Closed MicJarek closed 2 years ago

MicJarek commented 2 years ago

I set up picmo to work with multiple input/textarea controls on a page which can also display a Bootstrap 4 modal that has it's own input/textarea controls. I ended up using an attribute 'data-event-click' in order to not create duplicate listeners. The edit controls are either displayed (using .show()) or dynamically created when the user edits things like comments and/or replies. This is all done through this function:

function getPicmoEmojiInitialize(btnId, ctrlId) {
    const triggerButton = document.getElementById(btnId);

    if (triggerButton.getAttribute('data-event-click') !== 'true') {
        triggerButton.setAttribute('data-event-click', 'true');

        // Create the picker
        const picker = createPopup({
            className: "popupContainer",
            hideOnClickOutside: true,
            hideOnEmojiSelect: true,
            hideOnEscape: true,
            showCloseButton: true,
            position: 'bottom-start'
            //position: 'auto'
        }, {
            // The element that triggers the popup
            triggerElement: triggerButton,

            // The element to position the picker relative to - often this is also the trigger element,
            referenceElement: triggerButton
        });

        // The picker emits an event when an emoji is selected. Do with it as you will!
        picker.addEventListener('emoji:select', event => {
            setTextAtCaret(ctrlId, event.emoji);
        });

        triggerButton.addEventListener('click', event => {
            picker.toggle()
        });
    }
};

Everything works great except when the picker is being displayed and the user either clicks the 'Cancel' button or closes the modal by clicking on the side of the page. In either case, the picker triggerElement/referenceElement control is hidden or removed from DOM and that seems to cause it to briefly flash in the top-right corner before it fades out/closes. Just to be clear, this does not happen when the user clicks the 'Save' button where additional processing is done before the triggerElement/referenceElement control is hidden or removed.

Is this something that can be addressed in picmo internally, and if not, is there a way for me to find/reference the related picker so I can close it myself before hiding or removing the triggerElement/referenceElement control?

Thanks for your input... ☺️

joeattardi commented 2 years ago

Just to make sure I understand correctly - you have a Bootstrap modal, containing a text input, with a button that opens a popup PicMo picker. When you close the modal while the picker is open, the reference element gets removed from the DOM and then the PicMo picker flashes to the top right corner. Is that right?

Sounds like the floating-ui library is getting confused because the reference element its watching suddenly is gone, so it probably freaks out and resets the positioning to x=0, y=0.

I can try to find a fix for this issue, maybe don't update the position if the reference element is gone.

Have you tried explicitly calling close() on the picker before destroying the modal? Actually, if the picker is no longer needed you should probably call destroy() on it. Either way, close and destroy both return a Promise that is resolved once the close animation is complete. You can await that Promise and then close/destroy the modal.

Let me know if that works. Thanks!

MicJarek commented 2 years ago

Yes, that is one of the scenarios. Just to make it more clear, this particular page is quite complex and has multiple input/textarea controls that I show()/hide() in some cases, and others are cloned/removed from DOM as needed. As an example, the comment section on the page allow a user to insert a comment always on top of the list (here I use show() to display a <div> containing the appropriate controls). If the user cancels the insert, I simply hide() the controls, including the button that opens the popup PicMo picker. But if the user adds a 'Reply' to a comment, then I clone() the <div> that contains multiple controls including the picker trigger image. And if the user cancels the 'Reply', I remove() the <div> from DOM. That is all done on the page itself. The Bootstrap modal functionality comes in when the user clicks one of the images, where I display the details in a modal, including a caption for the photo (textarea with picmo trigger button) and a comment/reply tab for the photo that works almost identical to what I just described for the page comments/replies above.

So what this means is that the picker flashes not only when the reference element gets removed from DOM, but also when it is hidden. Likewise, when the picker is displayed in a modal and the user clicks the page to close the modal, the picker also flashes in the top-left corner. I think you're spot-on about the positioning to x=0, y=0 causing this.

I've tried closing the picker myself, but am not sure how to even reference it because of the scope. How do I find the open picker to close() or destroy() it? I tried:

    let picker = $('body').find('div.picmo-picker.picker.light.popupContainer');
    if (picker.length) {
        picker.destroy();
        //  picker.close();
    }

but the picker.destroy(); line ends up in dropdown.js?? Besides, there are a bunch of places where I call the picker so it would be much easier to find a solution within picmo itself.

I hope that makes sense. Thanks Joe... 😊

joeattardi commented 2 years ago

I'm not sure because I haven't seen the code - is it something you can share? When you create the popup picker, createPopup returns a popup picker, which you can call hide on. The snippet you included won't work because that's referencing the DOM element itself.

Are you able to save a reference to the popup picker object returned by createPopup? Then you should be able to call those.

That said, this is probably something I should handle internally in the library as well. Let me see what I can come up with.

I can add an option, maybe, that if the reference element is removed or hidden (i.e. the floating element no longer has an element to position to), the popup can either keep its previous position or, what I think you want to do in this case, just close it. I can try a few ideas and get back to you later in the week probably.

MicJarek commented 2 years ago

As a last resort, I could keep track/save a reference to the picker so it can be closed just before I either hide or remove the picker trigger image from DOM. But because the picker is linked to the trigger image through triggerElement/referenceElement control, the logistics might be an extraneous workaround especially in cases like when the user clicks on the side of the page to close the model. Here I would also have to deal with the Bootstrap modal functionality, etc.

So I think your idea of adding the option to either retain the position or auto-close if element is hidden or removed would be awesome. It would solve ALL my problems at the source and I'm sure others would also benefit from it in the future. Looking forward to seeing what you come up with. Tks... 😊

joeattardi commented 2 years ago

Ok! I just published v5.7.0. I hope this solves your problem! I added a onPositionLost option to the popup and you can tell it whether you want to close the picker, destroy the picker, hold the position, or do nothing (and let it jump to the top left corner).

Relevant docs here: https://picmojs.com/docs/usage/popup-picker#handling-a-loss-of-the-reference-element

This is a new option in the PopupOptions object, and the values are listed here: https://picmojs.com/docs/api/popup-picker/types/position-lost-strategy

Let me know if this solves the issue you're having and I will close the issue. Thanks!

MicJarek commented 2 years ago

That is awesome Joe! Works perfectly, for both pickers on the page and when they are in a Bootstrap modal.

Great job and thanks for adding this nice option/feature... Cheers... 😊