Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Prevent network request when pressing escape to close modal #695

Merged
merged 1 commit into from
Mar 1, 2021

Conversation

freekmurze
Copy link
Contributor

This PR contains a micro-optimisation, but a nice one.

Right now, if you close the modal with esc a network request is being made.

By using defer, no network request will be made when hitting escape. I've confirmed this improved behaviour in one of my projects.

Livewire docs on defer are here: https://laravel-livewire.com/docs/2.x/alpine-js#extracting-blade-components

@freekmurze freekmurze changed the title Prevent network request when hitting escape to close modal Prevent network request when pressing escape to close modal Feb 28, 2021
@taylorotwell taylorotwell merged commit 2c82841 into laravel:2.x Mar 1, 2021
@Loots-it
Copy link
Contributor

Loots-it commented May 7, 2021

Wouldn't it be an idea to open and close the modal without any network requests using alpine?

https://laravel-livewire.com/docs/2.x/alpine-js

I did this in my project by adding an outer div to the modal view which only has the x-data and x-init of the original modal view where all the 'this' references are replaced by 'this.childNodes[1]'. Then I added one div child to this div where the user of the modal can add a 'handler' to open the modal, this handler only needs '@click="show = true"' to work.

So basically this (this still uses an older jetstream version):

@props(['id', 'maxWidth'])

@php
$id = $id ?? md5($attributes->wire('model'));

switch ($maxWidth ?? '2xl') {
    case 'sm':
        $maxWidth = 'sm:max-w-sm';
        break;
    case 'md':
        $maxWidth = 'sm:max-w-md';
        break;
    case 'lg':
        $maxWidth = 'sm:max-w-lg';
        break;
    case 'xl':
        $maxWidth = 'sm:max-w-xl';
        break;
    case '2xl':
    default:
        $maxWidth = 'sm:max-w-2xl';
        break;
}
@endphp

<div
    x-data="{
        show: false,
        focusables() {
            // All focusable element types...
            let selector = 'a, button, input:not([type=\'hidden\']), textarea, select, details, [tabindex]:not([tabindex=\'-1\'])'
            return [...$el.querySelectorAll(selector)]
                // All non-disabled elements...
                .filter(el => ! el.hasAttribute('disabled'))
        },
        firstFocusable() { return this.childNodes[1].focusables()[0] },
        lastFocusable() { return this.childNodes[1].focusables().slice(-1)[0] },
        nextFocusable() { return this.childNodes[1].focusables()[this.childNodes[1].nextFocusableIndex()] || this.childNodes[1].firstFocusable() },
        prevFocusable() { return this.childNodes[1].focusables()[this.childNodes[1].prevFocusableIndex()] || this.childNodes[1].lastFocusable() },
        nextFocusableIndex() { return (this.childNodes[1].focusables().indexOf(document.activeElement) + 1) % (this.childNodes[1].focusables().length + 1) },
        prevFocusableIndex() { return Math.max(0, this.childNodes[1].focusables().indexOf(document.activeElement)) -1 },
    }"
    x-init="$watch('show', value => {
        if (value) {
            document.body.classList.add('overflow-y-hidden');
        } else {
            document.body.classList.remove('overflow-y-hidden');
        }
    });"
    id="parent-{{ $id }}"
>

    <div>
        {{ $handler }}
    </div>


    <div
        x-on:close.stop="show = false"
        x-on:keydown.escape.window="show = false"
        x-on:keydown.tab.prevent="$event.shiftKey || nextFocusable().focus()"
        x-on:keydown.shift.tab.prevent="prevFocusable().focus()"
        x-show="show"
        style="display: none;"
        class="fixed top-0 inset-x-0 px-4 pt-6 z-50 sm:px-0 sm:flex sm:items-top sm:justify-center"
        id="{{ $id }}"
    >
        <div x-show="show" class="fixed inset-0 transform transition-all" x-on:click="show = false" x-transition:enter="ease-out duration-200"
             x-transition:enter-start="opacity-0"
             x-transition:enter-end="opacity-100"
             x-transition:leave="ease-in duration-150"
             x-transition:leave-start="opacity-100"
             x-transition:leave-end="opacity-0">
            <div class="absolute inset-0 bg-gray-500 opacity-75"></div>
        </div>

        <div x-show="show" class="mb-6 bg-white rounded-lg overflow-hidden shadow-xl transform transition-all sm:w-full {{ $maxWidth }} sm:mx-auto"
             x-transition:enter="ease-out duration-200"
             x-transition:enter-start="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
             x-transition:enter-end="opacity-100 translate-y-0 sm:scale-100"
             x-transition:leave="ease-in duration-150"
             x-transition:leave-start="opacity-100 translate-y-0 sm:scale-100"
             x-transition:leave-end="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95">
            {{ $slot }}
        </div>
    </div>
</div>

I don't think this will work with all models, because some models actually need some server side rendering. But for my use case, it makes sense to have a pure js modal that I can reuse.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants