true-perfect-code
Version: 1.1.94

P11ModalNative Component

The P11ModalNative component is an adaptive, high-end dialog system. Unlike standard modals, it automatically adjusts its visual structure—including title alignment, close buttons, and action positions—to match the look and feel of Windows, macOS, iOS, or Android. This ensures a seamless, 'native-like' experience for users across different platforms within a Blazor environment.
Mobile Back-Button & Swipe Support

To prevent users from accidentally leaving the page when they use the Android hardware back-button or the iOS back-swipe while a modal is open, use the following combination:

  • UseHistoryBack="true": Adds a virtual entry to the browser history. This 'traps' the first back-navigation so it only affects the modal, not the entire application.
  • CloseTrigger: Connect this to your global Navigation Service. When Capacitor detects a native back-event, it triggers this action, allowing the modal to close gracefully and clean up the history entry automatically.
Adaptive UI
Automatically repositions header elements (like the macOS 'Traffic Lights' or iOS 'Done' buttons) based on the target device setting.
Desktop & Mobile Pro
Supports desktop features like 'Maximize' and 'Sticky Headers' alongside mobile-first interactions and full-screen modes.
Pro Tip: Use the NativeDevice parameter to switch layouts. For accessibility, the component maintains strict ARIA standards, ensuring that even with custom native layouts, screen readers correctly identify titles and controls.


Modal native Examples

This section demonstrates the usage of the P11ModalNative component with various configurations and highlights its key features.

Windows / Web
macOS
Mobile Varianten


Implementation Details

<div class="container mt-5">
    <div class="p-4 border rounded bg-white shadow-sm">

        <div class="row g-4">
            @* --- WINDOWS / WEB --- *@
            <div class="col-md-4">
                <h6 class="fw-bold text-uppercase text-muted small mb-3 border-bottom pb-1">Windows / Web</h6>
                <div class="d-grid gap-2">
                    <button class="btn btn-sm btn-outline-secondary text-start" @onclick='(() => Open(NativeDevice.WEB, "Info Dialog", false, false))'>Standard (Kein Scroll/Max)</button>
                    <button class="btn btn-sm btn-outline-secondary text-start" @onclick='(() => Open(NativeDevice.WEB, "Dokument", false, true))'>Standard + Scroll</button>
                    <button class="btn btn-sm btn-outline-secondary text-start" @onclick='(() => Open(NativeDevice.WINDOWS, "Editor", true, true))'>Maximize + Scroll</button>
                </div>
            </div>

            @* --- MAC --- *@
            <div class="col-md-4">
                <h6 class="fw-bold text-uppercase text-muted small mb-3 border-bottom pb-1">macOS</h6>
                <div class="d-grid gap-2">
                    <button class="btn btn-sm btn-outline-secondary text-start" @onclick='(() => Open(NativeDevice.MAC, "Finder Info", false, false))'>macOS Info (Nur Rot)</button>
                    <button class="btn btn-sm btn-outline-secondary text-start" @onclick='(() => Open(NativeDevice.MAC, "System Log", true, true))'>macOS Full (Rot/Grün + Scroll)</button>
                </div>
            </div>

            @* --- MOBILE VARIANTEN --- *@
            <div class="col-md-4">
                <h6 class="fw-bold text-uppercase text-muted small mb-3 border-bottom pb-1">Mobile Varianten</h6>
                <div class="d-grid gap-2">
                    @* iOS Triple *@
                    <button class="btn btn-sm btn-primary text-start opacity-75" @onclick='(() => OpenMobile(NativeDevice.IPHONE, "Settings", false, false))'>iOS: Nur Links (Clean)</button>
                    <button class="btn btn-sm btn-primary text-start" @onclick='(() => OpenMobile(NativeDevice.IPHONE, "New Contact", true, false))'>iOS: Links + Rechts (Clean)</button>
                    <button class="btn btn-sm btn-primary text-start fw-bold" @onclick='(() => OpenMobile(NativeDevice.IPHONE, "Mail Edit", true, true))'>iOS: Buttons + Linie</button>

                    @* Android Triple *@
                    <div class="mt-2"></div>
                    <button class="btn btn-sm btn-success text-start opacity-75" @onclick='(() => OpenMobile(NativeDevice.ANDROID, "Profile", false, false))'>Android: Nur Links (Clean)</button>
                    <button class="btn btn-sm btn-success text-start" @onclick='(() => OpenMobile(NativeDevice.ANDROID, "Settings", true, false))'>Android: Links + Rechts (Clean)</button>
                    <button class="btn btn-sm btn-success text-start fw-bold" @onclick='(() => OpenMobile(NativeDevice.ANDROID, "System Info", true, true, "bg-light"))'>Android: Buttons + Linie + Gray</button>
                </div>
            </div>
        </div>
    </div>
</div>

@* Die Komponente *@
<P11ModalNative @bind-Visible="_isVisible"
                NativeDevice="_currentDevice"
                AllowMaximize="_allowMaximize"
                Position="@(_shouldScroll ? ModalPosition.Scrollable : ModalPosition.Default)"
                Title="@_modalTitle"
                ShowHeaderSeparator="_showSeparator"
                CssClassHeader="@_headerClass"
                CloseButtonText="@_btnText"
                CloseButtonIconCss="@_btnIcon">

    <HeaderAction>
        @if (_showSaveButton)
        {
            @if (_currentDevice == NativeDevice.IPHONE)
            {
                <button class="btn btn-link p-0 text-decoration-none fw-bold" style="color: #007aff; font-size: 1rem;" @onclick="Save">Fertig</button>
            }
            else if (_currentDevice == NativeDevice.ANDROID)
            {
                <button class="btn btn-link text-dark p-1" @onclick="Save"><i class="bi bi-check2 fs-4"></i></button>
            }
        }
    </HeaderAction>

    <BodyContent>
        <div class="p-3">
            <p class="text-muted small">Modus: <strong>@_currentDevice</strong></p>
            @if (_shouldScroll)
            {
                <div style="height: 1000px; background: linear-gradient(180deg, #f8f9fa 0%, #dee2e6 100%); padding: 20px;" class="rounded border shadow-sm">
                    <p class="fw-bold">Scrollbarer Inhalt aktiv.</p>
                </div>
            }
            else
            {
                <div class="alert alert-secondary border-0 rounded-3">Kompakter Inhalt ohne Scrollen.</div>
            }
        </div>
    </BodyContent>

    <FooterContent>
        <div class="d-flex justify-content-end gap-2 w-100">
            @if (_currentDevice == NativeDevice.MAC)
            {
                @* macOS Buttons mit BS 5.3 Utilities (rounded-2 = 6px radius) *@
                <button class="btn btn-sm btn-outline-secondary rounded-2 px-3 border-secondary-subtle text-dark" style="font-size: 0.85rem; min-width: 80px;" @onclick="Close">Abbrechen</button>
                <button class="btn btn-sm btn-primary rounded-2 px-3 shadow-sm" style="font-size: 0.85rem; min-width: 80px;" @onclick="Save">Speichern</button>
            }
            else if (_currentDevice == NativeDevice.WEB || _currentDevice == NativeDevice.WINDOWS)
            {
                <button class="btn btn-secondary" @onclick="Close">Abbrechen</button>
                <button class="btn btn-primary px-4" @onclick="Save">OK</button>
            }
        </div>
    </FooterContent>
</P11ModalNative>
@code {
    private bool _isVisible;
    private NativeDevice _currentDevice;
    private bool _allowMaximize;
    private bool _shouldScroll;
    private bool _showSeparator;
    private bool _showSaveButton;
    private string _modalTitle = "";
    private string? _btnText;
    private string? _btnIcon;
    private string? _headerClass;

    private void Open(NativeDevice dev, string title, bool max, bool scroll)
    {
        Reset();
        _currentDevice = dev;
        _modalTitle = title;
        _allowMaximize = max;
        _shouldScroll = scroll;
        _isVisible = true;
    }

    private void OpenMobile(NativeDevice dev, string title, bool showSave, bool separator, string? bgColorClass = null)
    {
        Reset();
        _currentDevice = dev;
        _modalTitle = title;
        _showSaveButton = showSave;
        _showSeparator = separator;
        _shouldScroll = true; // Mobile fast immer mit Scroll
        _headerClass = bgColorClass;
        _isVisible = true;

        if (dev == NativeDevice.IPHONE)
        {
            _btnText = "Zurück";
            _btnIcon = "bi bi-chevron-left";
        }
    }

    private void Reset()
    {
        _allowMaximize = false;
        _shouldScroll = false;
        _showSeparator = true;
        _showSaveButton = false;
        _btnText = null;
        _btnIcon = null;
        _headerClass = null;
    }

    private void Save() => _isVisible = false;
    private void Close() => _isVisible = false;
}


Component API

Parameter Type Default Description
Visible bool false Controls the visibility of the modal. Bind with @bind-Visible.
NativeDevice NativeDevice NativeDevice.WEB Sets the visual style and header behavior (WEB, WINDOWS, MAC, IPHONE, ANDROID).
AllowMaximize bool false Enables the maximize/zoom button for Desktop modes (Windows/macOS).
Content & Layout
Title string? null The title displayed in the header. Position adapts to the NativeDevice.
HeaderContent RenderFragment - Additional custom content for the header area.
HeaderAction RenderFragment - Slot for primary actions (e.g., 'Save' or 'Done' buttons) positioned according to OS standards.
BodyContent RenderFragment - The main content of the modal.
FooterContent RenderFragment - Content for the modal's footer (usually Action-Buttons).
Visuals & UI
ShowHeaderSeparator bool true If true, a border-bottom is shown under the header. Often disabled for clean iOS looks.
Size Size Size.Medium The Bootstrap size of the modal dialog.
Position ModalPosition Default Positioning logic. Use 'Scrollable' to keep the header/footer sticky while content scrolls.
IsFullScreen bool false Forces the modal to take up the entire viewport.
CloseButtonText string? null Custom text for the close/back button (e.g., 'Cancel' or 'Back').
CloseButtonIconCss string? null Icon class (e.g., 'bi bi-chevron-left') for the native close button.
Behavior & A11y
ShowCloseButton bool true Determines if any close control (X, Dots, or Back-Button) is rendered.
RestoreFocusId string? null ID of the element to refocus after closing.
PreventScroll bool true Locks the background page scrolling while modal is active.
UseHistoryBack bool false Creates a virtual browser history entry when opened. Enables closing via hardware back-button/swipe without leaving the page.
CloseTrigger Action? null External trigger (Action) to close the modal, e.g., from a global Capacitor back-button listener.
AriaLabel string? null Accessibility label for screen readers.
Custom Styling
CssClassContent string? null CSS class for the .modal-content container.
CssClassHeader string? null CSS class for the .modal-header (useful for background colors).
Events
VisibleChanged EventCallback<bool> - Fires when visibility changes (supports @bind-Visible).
OnClose EventCallback - Invoked when the modal is closed by any means.
An unhandled error has occurred. Reload 🗙