Skip to content

Popups ​

The popup control opens a modal dialog on a native <dialog> element, usually with markup returned by an AJAX handler. It is styled with Bootstrap 6's dialog classes and needs no Bootstrap JavaScript. Popovers open a panel beside an element, and tooltips show a short hint on hover.

Ui::popupButton() renders a button that runs its handler and opens the response in a dialog. The dataSize attribute sets the width, from tiny to giant (every size is listed under Sizes), and requestData sends extra values to the handler, here the size, so the dialog can name it in its title.

blade
{{ Ui::popupButton(label: 'Open a dialog', handler: 'onLoadDialog', primary: true) }}
{{ Ui::popupButton(label: 'Tiny', handler: 'onLoadDialog', secondary: true, requestData: ['size' => 'tiny'], dataSize: 'tiny') }}
{{ Ui::popupButton(label: 'Small', handler: 'onLoadDialog', secondary: true, requestData: ['size' => 'small'], dataSize: 'small') }}
{{ Ui::popupButton(label: 'Large', handler: 'onLoadDialog', secondary: true, requestData: ['size' => 'large'], dataSize: 'large') }}
{{ Ui::popupButton(label: 'Huge', handler: 'onLoadDialog', secondary: true, requestData: ['size' => 'huge'], dataSize: 'huge') }}
{{ Ui::popupButton(label: 'Giant', handler: 'onLoadDialog', secondary: true, requestData: ['size' => 'giant'], dataSize: 'giant') }}

The handler returns the dialog markup, rendered from a view:

php
<?php

namespace App\Controllers\Reference\UiController;

use Illuminate\Support\Sleep;
use Larajax\Classes\AjaxResponse;

/**
 * PopupHandlers holds the AJAX handlers behind the Popups page.
 */
trait PopupHandlers
{
    /**
     * onLoadDialog renders the demo dialog, titled with the size the popup button asked for.
     */
    public function onLoadDialog(): string
    {
        $size = (string) input('size');

        if (ctype_digit($size)) {
            $title = "{$size}px dialog";
        }
        elseif (in_array($size, $this->getDialogSizes(), true)) {
            $title = ucfirst($size).' dialog';
        }
        else {
            $title = 'Default dialog';
        }

        return view('reference.ui._dialog', ['title' => $title])->render();
    }

    /**
     * onSubmitDialog handles the dialog's submit button, which closes the popup when it succeeds.
     */
    public function onSubmitDialog(): AjaxResponse
    {
        Sleep::for(500)->milliseconds();

        return ajax()->flash('success', 'The dialog was submitted.');
    }

    /**
     * getDialogSizes returns the named popup sizes the dialog demo accepts.
     *
     * @return array<int, string>
     */
    protected function getDialogSizes(): array
    {
        return ['tiny', 'small', 'large', 'huge', 'giant', 'adaptive', 'auto', 'fullscreen'];
    }
}
blade
<form data-request="onSubmitDialog" data-request-flash data-popup-load-indicator>
    <div class="dialog-header">
        <h4 class="dialog-title">
            {{ $title }}
        </h4>
        <button type="button" class="btn-close" data-dismiss="popup" aria-label="Close"></button>
    </div>

    <div class="dialog-body">
        <p>
            The handler returned this markup. The button's <code>dataSize</code> sets the width, and Submit hides the dialog behind a loading indicator until its own request succeeds.
        </p>

        <div class="form-field mb-5">
            <label class="form-label" for="uiDialogNote">
                Note
            </label>
            <input type="text" class="form-control" id="uiDialogNote" name="note" placeholder="Anything you like">
        </div>
    </div>

    <div class="dialog-footer">
        <button type="submit" class="btn-solid theme-primary">
            Submit
        </button>
        <button type="button" class="btn-solid theme-secondary" data-dismiss="popup">
            Cancel
        </button>
        {{ Ui::popupButton(
            label: 'Open another',
            handler: 'onLoadDialog',
            class: 'btn-link ms-auto',
            requestData: ['size' => 'small'],
            dataSize: 'small'
        ) }}
    </div>
</form>

The markup goes straight inside the <dialog>, in three parts: dialog-header with a dialog-title and a close button, dialog-body, and dialog-footer with the actions. A dialog-body with no header or footer can add dialog-no-header or dialog-no-footer for even padding. Any element with data-dismiss="popup" closes the popup when clicked.

Wrapping the parts in a form lets its buttons submit everything in the dialog. A request element with data-popup-load-indicator, here the form with its data-request, hides the popup behind a loading indicator while its request runs, then closes the popup when the request succeeds or shows it again with a shake when it fails.

A popup opened from inside another stacks over it, and the one below steps back until the top one closes, as the dialog's Open another button shows. While a handler's content loads, a backdrop with a loading indicator covers the page.

Sizes from the window and content ​

Besides the fixed widths, a size can follow the window or the content. adaptive fills the window less 100 pixels (the whole width on small screens), auto fits the content, a number sets the width in pixels and fullscreen covers the window. dataAdaptiveHeight stretches any size to the window height, less 100 pixels.

blade
{{ Ui::popupButton(label: 'Adaptive', handler: 'onLoadDialog', secondary: true, requestData: ['size' => 'adaptive'], dataSize: 'adaptive') }}
{{ Ui::popupButton(
    label: 'Adaptive height',
    handler: 'onLoadDialog',
    secondary: true,
    requestData: ['size' => 'large'],
    dataSize: 'large',
    dataAdaptiveHeight: true
) }}
{{ Ui::popupButton(label: 'Fits the content', handler: 'onLoadDialog', secondary: true, requestData: ['size' => 'auto'], dataSize: 'auto') }}
{{ Ui::popupButton(label: '600 pixels', handler: 'onLoadDialog', secondary: true, requestData: ['size' => '600'], dataSize: '600') }}
{{ Ui::popupButton(label: 'Fullscreen', handler: 'onLoadDialog', secondary: true, requestData: ['size' => 'fullscreen'], dataSize: 'fullscreen') }}
SizeWidth
(none)598px.
tiny300px.
small400px.
large750px.
huge900px.
giant982px.
adaptivethe window width less 100px, and the full width on small screens.
autofits the content.
fullscreencovers the window.
a number, such as 600that many pixels.

The handler can also set the size from its response, with a data-popup-size attribute on the first element of the markup or any element inside it. Every size adds a size-* class to the dialog, for your own styles.

Closing ​

Escape closes a popup, and a click on the backdrop shakes it, so a stray click does not lose what the visitor typed. dataKeyboard: 'false' makes Escape shake it too, and dataBackdrop: true lets a backdrop click close it. The close button and any other data-dismiss="popup" element always close it.

blade
{{ Ui::popupButton(
    label: 'Escape does not close',
    handler: 'onLoadDialog',
    secondary: true,
    dataKeyboard: 'false'
) }}
{{ Ui::popupButton(
    label: 'Backdrop click closes',
    handler: 'onLoadDialog',
    secondary: true,
    dataBackdrop: true
) }}

To close a popup from JavaScript, dispatch a bubbling popup:close event on the popup or any element inside it, such as new CustomEvent('popup:close', { bubbles: true }).

data-control="popup" works on any element, which then opens the popup when clicked. This is what Ui::popupButton() renders. data-handler names the handler, and data-request-data sends extra values with it, written as key: value pairs. Every option has a data attribute, listed under Popup options.

blade
<a
    href="javascript:;"
    data-control="popup"
    data-handler="onLoadDialog"
    data-request-data="size: 'small'"
    data-size="small">
    Open a small dialog from a link
</a>

jax.popup.open() takes the same options as the attributes, in camel case, such as handler, extraData and size. content places markup in the dialog without a request, here copied from a <template> already on the page.

blade
<button type="button" class="btn-solid theme-secondary" id="uiTemplatePopupButton">
    Open from a template
</button>

<template id="uiTemplatePopup">
    <div class="dialog-header">
        <h4 class="dialog-title">
            From a template
        </h4>
        <button type="button" class="btn-close" data-dismiss="popup" aria-label="Close"></button>
    </div>
    <div class="dialog-body dialog-no-footer">
        <p class="mb-0">
            This content was already on the page, so no request ran.
        </p>
    </div>
</template>

<script>
    document.getElementById('uiTemplatePopupButton').addEventListener('click', () => {
        jax.popup.open({
            content: document.getElementById('uiTemplatePopup').innerHTML,
            size: 'small'
        });
    });
</script>

jax.popup is the Popup class, also exported by the library's index.js for code that imports it. Its static methods:

MethodDescription
open(options)opens a popup with no trigger element and returns it.
fromElement(element, options)opens the popup for a trigger element, or reloads it when it is open. Given a method name in place of the options, it calls that method on the popup: fromElement(element, 'lock', true).
getInstance(element)returns the popup for its trigger, its <dialog> or the first element of its content, or null.
bindToPopups(container, vars)adds the variables as hidden inputs to the form of any popup opened from inside the container, and from popups opened from that popup.
focusFirstInput(container)focuses the first form-control input, unless an element has autofocus.

A popup instance has show(), hide(), reload(), toggle(), lock(true) to stop it closing, setLoading(true) and setShake().

Popover ​

data-control="popover" opens a panel next to the element when it is clicked, and a second click closes it. Only one popover is open at a time. data-content-from copies the content from an element on the page, such as a <template>, data-placement picks the side, data-width fixes the width in pixels, and data-modal dims the page behind it.

blade
<button type="button" class="btn-solid theme-secondary" data-control="popover" data-content-from="#uiShippingPopover" data-placement="top">
    Top
</button>
<button type="button" class="btn-solid theme-secondary" data-control="popover" data-content-from="#uiShippingPopover" data-placement="bottom">
    Bottom
</button>
<button type="button" class="btn-solid theme-secondary" data-control="popover" data-content-from="#uiShippingPopover" data-placement="left">
    Left
</button>
<button type="button" class="btn-solid theme-secondary" data-control="popover" data-content-from="#uiShippingPopover" data-placement="right">
    Right
</button>
<button type="button" class="btn-solid theme-primary" data-control="popover" data-content-from="#uiShippingPopover" data-modal="true" data-width="320">
    Modal, 320px wide
</button>

<template id="uiShippingPopover">
    <div class="popover-head">
        <h3>
            Shipping estimate
        </h3>
        <p>
            From the main warehouse
        </p>
        <button type="button" class="btn-close" data-dismiss="popover" aria-label="Close"></button>
    </div>
    <div class="popover-body">
        Standard delivery takes 3 to 5 business days.
    </div>
</template>

The content can use a popover-head, with a heading, a line of text and a close button, and a popover-body. Any element with data-dismiss="popover" inside it closes the popover, and so does a click outside it or the Escape key. When the preferred side has no room, the popover moves to another side that fits.

OptionDescription
placementtop, bottom, left, right or center, the middle of the window. Default: bottom.
fallbackPlacementthe side used when no side fits. Default: bottom.
contentthe content as HTML, without a template.
contentFroma selector of an element whose HTML becomes the content.
widththe width in pixels. Default: the width of the content.
modaldims the page behind the popover. Default: false.
highlightModalTargetraises the trigger above the dimmed page. Default: false.
closeOnPageClickcloses the popover on a click outside it. Default: true.
closeOnEsccloses the popover on Escape. Default: true.
containerClassextra classes for the popover element.
offsethow far the popover is shifted along the trigger, in pixels. Default: 15.
offsetXthe offset for the top and bottom sides, in place of offset.
offsetYthe offset for the left and right sides, in place of offset.
containeran element or selector to mount the popover in. Default: the enclosing <dialog>, or else the body.
useAnimationfades the popover in and out. Default: false.
onClosea function called after the popover is removed, from JavaScript only.
onCheckDocumentClickTargeta function given the target of a click outside the popover, which keeps it open by returning true, from JavaScript only.

Set each option as a data attribute in kebab case, such as data-content-from or data-close-on-page-click="false". The Popover class, exported by the library's index.js, takes the same options from JavaScript: new Popover(trigger, options).show(), with setContent(html) and hide().

The popover dispatches popover:showing before it opens and popover:hiding before it closes, where preventDefault() cancels either, then popover:show and popover:hide, all on the trigger. Dispatch popover:close on any element inside it to close it.

Tooltip ​

Any element with data-tooltip-text shows a tooltip after 300 milliseconds of hovering, below it unless data-tooltip-position="top". It hides when the pointer leaves, on any click or key press, and before an AJAX update. The control listens on the document, so elements added later need no setup.

data-tooltip-hotkey adds key labels after the text, separated by commas. This is how buttons with a hotkey show their shortcut, since they set all three attributes.

blade
<button
    type="button"
    class="btn-solid theme-secondary"
    data-tooltip-text="Shown below by default">
    Below
</button>

<button
    type="button"
    class="btn-solid theme-secondary"
    data-tooltip-text="Shown above"
    data-tooltip-position="top">
    Above
</button>

<button
    type="button"
    class="btn-solid theme-secondary"
    data-tooltip-text="Copy the link"
    data-tooltip-hotkey="⌃C, ⌘C"
    data-tooltip-position="top">
    With hotkey labels
</button>
AttributeDescription
data-tooltip-textthe tooltip text.
data-tooltip-hotkeykey labels shown after the text, separated by commas, such as ⌃S, ⌘S.
data-tooltip-positiontop places the tooltip above the element. Default: below.

jax.tooltips.clear() hides the tooltip and cancels one that is waiting to show. Inside a popup, the tooltip mounts in the dialog so it shows above the backdrop. The tooltips on form fields and list columns are a different one: Bootstrap's own tooltip, with data-bs-toggle="tooltip".

Set these as data attributes on a data-control="popup" element, as attributes of Ui::popupButton(), or as options of jax.popup.open().

AttributeOptionDescription
data-handlerhandlerthe AJAX handler that returns the dialog markup.
data-ajaxajaxa URL to load the dialog markup from, in place of a handler.
(none)contentthe dialog markup as an HTML string, an element or a function, with no request.
data-extra-dataextraDataextra request data, as key: value pairs or an object. The trigger's data-request-data is merged in.
data-sizesizethe width, from the sizes table.
data-adaptive-heightadaptiveHeightstretches the popup to the window height. Default: false.
data-keyboardkeyboardfalse makes Escape shake the popup in place of closing it. Default: true.
data-backdropbackdropwhat a backdrop click does: static shakes the popup, true closes it and false ignores the click. Default: static.

Inside the dialog:

AttributeDescription
data-dismiss="popup"closes the popup when clicked.
data-popup-load-indicatoron an element with data-request, hides the popup behind a loading indicator while that request runs, closes it on success and shakes it on failure.
data-popup-sizeon the first element of the markup, or any element in it, sets the popup size.

The popup dispatches these on its trigger and on the first element of its content. event.detail holds popup, the instance, trigger, and modal, the <dialog> element.

EventDescription
popup:showthe content was placed and the popup is opening. Also dispatched on the document.
popup:shownthe opening transition finished.
popup:hidethe popup is closing.
popup:hiddenthe popup closed and was removed.
popup:completethe handler's content loaded.
popup:errorthe handler's request failed.
popup:toggletoggle() was called.
popup:downloadthe handler responded with a file download.

Arguments ​

ArgumentDescription
labelthe button text, escaped.
handlerthe AJAX handler that returns the dialog markup, rendered as data-handler.
requestDataan array of extra values to send to the handler, rendered as data-request-data.
iconicon classes, rendered before the label.
primaryprimary styling, btn-solid theme-primary. Default: false.
secondarysecondary styling, btn-solid theme-secondary. Default: false.
dangerdanger styling, btn-solid theme-danger. Default: false.
hotkeya keyboard shortcut, or an array of them, that opens the popup.

Any other argument becomes an attribute, as on buttons, so the popup options go in as dataSize, dataKeyboard, dataBackdrop and dataAdaptiveHeight.