diff --git a/README.md b/README.md index 16a1a7f..1bc83e4 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,8 @@ Once the package is installed, simply add the reference to the Bootstrap Italia + + ``` diff --git a/docs/components/modal.md b/docs/components/modal.md new file mode 100644 index 0000000..affd455 --- /dev/null +++ b/docs/components/modal.md @@ -0,0 +1,208 @@ +# BitModal + +The `BitModal` component provides a [modal dialog using Bootstrap Italia styles](https://italia.github.io/bootstrap-italia/docs/componenti/modale/). + +## Namespace + +```csharp +BitBlazor.Components +``` + +## Description + +The Modal component renders an accessible dialog that overlays the page. Visibility is controlled entirely through Blazor state using the `IsVisible` / `@bind-IsVisible` pattern — no JavaScript interop is required. + +## Parameters + +| Name | Type | Required | Default | Description | +|------|------|----------|---------|-------------| +| `IsVisible` | `bool` | ✗ | `false` | Controls whether the modal is displayed. Use `@bind-IsVisible` for two-way binding. | +| `IsVisibleChanged` | `EventCallback` | ✗ | - | Callback invoked when the modal requests a visibility change (close). Wired automatically by `@bind-IsVisible`. | +| `BodyContent` | `RenderFragment` | ✓ | - | The main body content of the modal. | +| `Title` | `string?` | ✗ | `null` | Text displayed in the header as an `h2` element. When set, the modal uses `aria-labelledby` for accessibility. | +| `TitleId` | `string?` | ✗ | `null` | Override for the `id` attribute of the title `h2` element. Defaults to `"{modal-id}-title"`. | +| `AriaLabel` | `string?` | ✗ | `null` | Sets the `aria-label` on the modal element. Use when `Title` is not set. | +| `AriaDescribedById` | `string?` | ✗ | `null` | Sets the `aria-describedby` attribute, pointing to a description element inside the modal. | +| `HeaderContent` | `RenderFragment?` | ✗ | `null` | Fully custom header content, replacing the default `Title`-based header. | +| `FooterContent` | `RenderFragment?` | ✗ | `null` | Footer content. When `null`, no footer element is rendered. | +| `ShowCloseButton` | `bool` | ✗ | `true` | Whether to show the close (×) button in the header. | +| `CloseButtonAriaLabel` | `string` | ✗ | `"Chiudi"` | The `aria-label` for the close button. | +| `Size` | `ModalSize` | ✗ | `Default` | The size of the modal dialog. | +| `Position` | `ModalPosition` | ✗ | `Default` | The positioning of the modal dialog. | +| `Type` | `ModalType` | ✗ | `Default` | The visual type variant of the modal. | +| `Backdrop` | `ModalBackdrop` | ✗ | `Default` | Controls backdrop visibility and whether clicking it closes the modal. | +| `ScrollableContent` | `bool` | ✗ | `false` | Enables internal body scrolling, keeping header and footer always visible. | +| `FooterShadow` | `bool` | ✗ | `false` | Adds a top shadow to the footer element (`modal-footer-shadow`). | +| `Fade` | `bool` | ✗ | `true` | Enables the CSS fade animation. Set to `false` for instant show/hide. | +| `OnClose` | `EventCallback` | ✗ | - | Callback invoked just before the modal is closed. | + +## Enumerations + +### ModalSize + +| Value | CSS class on `.modal-dialog` | Description | +|-------|------------------------------|-------------| +| `Default` | *(none)* | Standard size | +| `Small` | `modal-sm` | Small dialog | +| `Large` | `modal-lg` | Large dialog | +| `ExtraLarge` | `modal-xl` | Extra-large dialog | + +### ModalPosition + +| Value | Effect | Description | +|-------|--------|-------------| +| `Default` | *(none)* | Top-center (default Bootstrap Italia) | +| `Centered` | `modal-dialog-centered` on dialog | Vertically centered | +| `Left` | `modal-dialog-left` on dialog + `it-dialog-scrollable` on modal | Full-height, left-aligned | +| `Right` | `modal-dialog-right` on dialog + `it-dialog-scrollable` on modal | Full-height, right-aligned | + +### ModalType + +| Value | CSS class on `.modal` | Description | +|-------|----------------------|-------------| +| `Default` | *(none)* | Standard modal | +| `Alert` | `alert-modal` | Alert modal, used with an icon in the header | +| `LinkList` | `it-dialog-link-list` | Optimised for navigation link lists | +| `Popconfirm` | `popconfirm-modal` | Compact confirmation dialog | + +### ModalBackdrop + +| Value | Description | +|-------|-------------| +| `Default` | Backdrop is shown; clicking it closes the modal | +| `Static` | Backdrop is shown; clicking it does **not** close the modal | +| `None` | No backdrop is rendered | + +## Usage Examples + +### Basic modal with two-way binding + +```razor + + + + +

Descrizione scopo della modale.

+

Contenuto della modale.

+
+ + + + +
+ +@code { + private bool isOpen = false; +} +``` + +### Modal without header + +```razor + + +

Questa modale non ha intestazione.

+
+ + + +
+``` + +### Centered modal with scrollable content + +```razor + + + @* Long content that scrolls inside the modal body *@ + @for (int i = 1; i <= 20; i++) + { +

Paragrafo @i lorem ipsum dolor sit amet.

+ } +
+ + + +
+``` + +### Static backdrop (does not close on click) + +```razor + + +

Devi fare una scelta per continuare.

+
+ + + + +
+``` + +### Alert modal with icon + +```razor + + +
+ + +
+
+ +

Il documento verrà eliminato definitivamente.

+
+ + + + +
+``` + +### Popconfirm modal + +```razor + + +

Questa operazione non può essere annullata.

+
+ + + + +
+``` + +## Accessibility + +- The modal element always carries `role="dialog"` and `aria-modal="true"`. +- The dialog element carries `role="document"`. +- When `Title` is set (and `HeaderContent` is not overriding the header), `aria-labelledby` points automatically to the title `h2` element. +- When there is no `Title`, set `AriaLabel` to provide an accessible name for screen readers. +- Use `AriaDescribedById` to reference a descriptive paragraph inside the `BodyContent` for a richer screen-reader experience. +- The close button always has an `aria-label` (defaults to `"Chiudi"`). Customise it with `CloseButtonAriaLabel`. + +## References + +- [Bootstrap Italia — Finestre modali](https://italia.github.io/bootstrap-italia/docs/componenti/modale/) +- [WAI-ARIA Authoring Practices — Dialog Modal](https://www.w3.org/TR/wai-aria-practices/#dialog_modal) diff --git a/samples/BitBlazor.Sample/BitBlazor.Sample.Client/Pages/Comunicazioni.razor b/samples/BitBlazor.Sample/BitBlazor.Sample.Client/Pages/Comunicazioni.razor new file mode 100644 index 0000000..bf7703c --- /dev/null +++ b/samples/BitBlazor.Sample/BitBlazor.Sample.Client/Pages/Comunicazioni.razor @@ -0,0 +1,160 @@ +@page "/comunicazioni" +@rendermode InteractiveAuto + +@using BitBlazor.Components +@using BitBlazor.Utilities + +Comunicazioni — Comune di Bitopoli + + + +
+

Comunicazioni e Avvisi

+

+ Tutte le comunicazioni istituzionali, avvisi urgenti e notifiche del Comune di Bitopoli. +

+
+ + +
+ Filtra per: + + + + +
+ + +
+ + @foreach (var item in visibleItems) + { + + +
+
+ + @item.Date.ToString("dd MMMM yyyy") +
+ +
+ +

@item.Body

+
+
+
+ } + + @if (!visibleItems.Any()) + { + + + +
+ +

Nessuna comunicazione da visualizzare.
Hai chiuso tutti gli avvisi attivi.

+
+
+
+
+ } +
+ +@if (dismissed.Any()) +{ +
+ @dismissed.Count avvisi chiusi + + Ripristina tutti + +
+} + + +

Archivio comunicazioni

+
+ @foreach (var arch in archive) + { +
+ + +
+ + @arch.Date.ToString("dd/MM/yyyy") +
+ @arch.Title + +

@arch.Summary

+
+ + + Leggi tutto + + +
+
+
+ } +
+ +@code { + private List dismissed = new(); + + private int visibleCount => visibleItems.Count; + + private List allItems = new() + { + new(1, "Urgente", AlertType.Danger, "Allerta meteo — Codice Arancione", + "La Protezione Civile ha emesso un'allerta meteo arancione per il comune di Bitopoli valida dalle 18:00 di oggi alle 06:00 di domani. Si raccomanda la massima prudenza.", + new DateOnly(2026, 3, 14), "PC", Color.Danger, Color.Danger), + new(2, "Lavori", AlertType.Warning, "Interruzione acqua in Via Mazzini", + "Giovedì 19 marzo dalle 08:00 alle 16:00 sarà sospesa l'erogazione idrica in Via Mazzini, Via Garibaldi e vie limitrofe per interventi di manutenzione straordinaria.", + new DateOnly(2026, 3, 13), "AM", Color.Warning, Color.Warning), + new(3, "Informativo", AlertType.Info, "Apertura sportello PagoPA", + "A partire dal 17 marzo 2026 sarà attivo il nuovo sportello PagoPA per il pagamento online di tutti i tributi e le sanzioni comunali, incluse IMU, TARI e infrazioni stradali.", + new DateOnly(2026, 3, 12), "PA", Color.Primary, Color.Primary), + new(4, "Avviso", AlertType.Info, "Consultazione pubblica PNRR", + "È aperta fino al 31 marzo 2026 la consultazione pubblica sul Piano Nazionale di Ripresa e Resilienza — progetti del Comune di Bitopoli. Tutti i cittadini possono partecipare.", + new DateOnly(2026, 3, 10), "PN", Color.Secondary, Color.Secondary), + new(5, "Servizi", AlertType.Success, "Nuovo CIE Point operativo", + "Da oggi è operativo il nuovo punto di rilascio CIE (Carta d'Identità Elettronica) presso la sede municipale. Tempi di attesa ridotti e prenotazione online disponibile.", + new DateOnly(2026, 3, 8), "CI", Color.Success, Color.Success), + }; + + private List visibleItems => + allItems.Where(i => !dismissed.Contains(i.Id)).ToList(); + + private List archive = new() + { + new("Notizie", "Approvato il bilancio preventivo 2026", + "Il Consiglio Comunale ha approvato il bilancio preventivo per l'anno 2026 con 14 voti favorevoli.", + new DateOnly(2026, 2, 28)), + new("Turismo", "Calendario eventi primaverili 2026", + "Pubblicato il programma completo degli eventi culturali e turistici del Comune per la stagione primaverile.", + new DateOnly(2026, 2, 20)), + new("Sociale", "Bando contributi affitto 2026", + "Aperto il bando per la concessione di contributi a fondo perduto per il sostegno al pagamento dei canoni di locazione.", + new DateOnly(2026, 2, 15)), + new("Ambiente", "Piano rifiuti: nuovi orari raccolta", + "A partire dal 1° marzo 2026 variano gli orari e i giorni della raccolta differenziata porta a porta. Consulta il nuovo calendario.", + new DateOnly(2026, 2, 10)), + }; + + private void DismissItem(int id) => dismissed.Add(id); + + private void RestoreAll() => dismissed.Clear(); + + private IReadOnlyList breadcrumbItems = new List + { + new BitBreadcrumbItem { Text = "Home", Link = "/" }, + new BitBreadcrumbItem { Text = "Comunicazioni" } + }; + + private record ComunicazioneItem( + int Id, string Category, AlertType AlertType, string Title, string Body, + DateOnly Date, string AvatarText, Color AvatarColor, Color BadgeColor); + + private record ArchivioItem(string Category, string Title, string Summary, DateOnly Date); +} diff --git a/samples/BitBlazor.Sample/BitBlazor.Sample.Client/Pages/Modulistica.razor b/samples/BitBlazor.Sample/BitBlazor.Sample.Client/Pages/Modulistica.razor new file mode 100644 index 0000000..caf7859 --- /dev/null +++ b/samples/BitBlazor.Sample/BitBlazor.Sample.Client/Pages/Modulistica.razor @@ -0,0 +1,291 @@ +@page "/modulistica" +@rendermode InteractiveAuto + +@using BitBlazor.Components +@using BitBlazor.Utilities +@using BitBlazor.Form + +Modulistica — Comune di Bitopoli + + + +
+

Modulistica Online

+

+ Compila e invia i moduli digitali direttamente dal portale. I dati salvati sono + protetti e trasmessi in modo sicuro agli uffici competenti. +

+
+ +
+ + +
+ + + + + Dati Anagrafici + + + + +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+
+
+
+ + + + + Tipo di Richiesta + +
+ + + Anagrafe e Stato Civile + Tributi e Pagamenti + Edilizia e Territorio + SUAP — Attività Produttive + Servizi Sociali + Ambiente e Mobilità + +
+ +
+ + + + + + +
+ +
+
+ +
+
+ +
+
+ +
+ +
+
+
+
+ + + + + Dettaglio della Richiesta + +
+ +
+ +
+ +
+
+ +
+ +
+ + +
+ +
+
+
+
+
+ + +
+ + + Invia Richiesta + + + Azzera modulo + +
+ + @if (submitted) + { +
+ +

+ Il tuo numero di protocollo è #@protocolNumber. + Riceverai una conferma all'indirizzo @formModel.Email + entro pochi minuti. +

+
+
+ } +
+ + +
+ + + Riepilogo Richiesta + +
+
Richiedente
+
+ @if (!string.IsNullOrWhiteSpace(formModel.Nome) || !string.IsNullOrWhiteSpace(formModel.Cognome)) + { + @formModel.Nome @formModel.Cognome + } + else + { + + } +
+
Area
+
+ @if (!string.IsNullOrWhiteSpace(formModel.AreaServizio)) + { + + } + else + { + + } +
+
Urgenza
+
+ @if (!string.IsNullOrWhiteSpace(formModel.Urgenza)) + { + @formModel.Urgenza + } + else + { + + } +
+
Copie
+
@formModel.NumeroCopie
+
+
+
+
+ + + + Orari Sportello + +
    +
  • + Lunedì – Mercoledì + 09:00 – 13:00 +
  • +
  • + Giovedì + 09:00 – 17:00 +
  • +
  • + Venerdì + 09:00 – 12:00 +
  • +
  • + Sabato – Domenica + Chiuso +
  • +
+
+
+
+
+ +
+ +@code { + private FormModel formModel = new(); + private bool submitted = false; + private string protocolNumber = string.Empty; + + private bool IsFormValid => + !string.IsNullOrWhiteSpace(formModel.Nome) && + !string.IsNullOrWhiteSpace(formModel.Cognome) && + !string.IsNullOrWhiteSpace(formModel.Email) && + formModel.AccettaDichiarazione && + formModel.AccettaPrivacy; + + private string areaLabel => formModel.AreaServizio switch + { + "anagrafe" => "Anagrafe", + "tributi" => "Tributi", + "edilizia" => "Edilizia", + "suap" => "SUAP", + "sociale" => "Sociale", + "ambiente" => "Ambiente", + _ => formModel.AreaServizio ?? string.Empty + }; + + private void HandleValidSubmit() + { + if (!IsFormValid) return; + protocolNumber = $"BIT-{DateTime.Now:yyyyMMdd}-{Random.Shared.Next(1000, 9999)}"; + submitted = true; + } + + private void ResetForm() + { + formModel = new(); + submitted = false; + protocolNumber = string.Empty; + } + + private IReadOnlyList breadcrumbItems = new List + { + new BitBreadcrumbItem { Text = "Home", Link = "/" }, + new BitBreadcrumbItem { Text = "Modulistica" } + }; + + private class FormModel + { + public string? Nome { get; set; } + public string? Cognome { get; set; } + public string? CodiceFiscale { get; set; } + public string? Telefono { get; set; } + public string? Email { get; set; } + public string? AreaServizio { get; set; } + public string? Urgenza { get; set; } = "normale"; + public DateOnly? DataRilascio { get; set; } + public TimeOnly? OraRilascio { get; set; } + public int NumeroCopie { get; set; } = 1; + public string? Descrizione { get; set; } + public bool AccettaDichiarazione { get; set; } + public bool AccettaPrivacy { get; set; } + public bool ConsegnaDigitale { get; set; } = true; + public bool NotificaSMS { get; set; } + } +} diff --git a/samples/BitBlazor.Sample/BitBlazor.Sample.Client/Pages/Sportello.razor b/samples/BitBlazor.Sample/BitBlazor.Sample.Client/Pages/Sportello.razor new file mode 100644 index 0000000..7a022ed --- /dev/null +++ b/samples/BitBlazor.Sample/BitBlazor.Sample.Client/Pages/Sportello.razor @@ -0,0 +1,477 @@ +@page "/sportello" +@rendermode InteractiveAuto + +@using BitBlazor.Components +@using BitBlazor.Utilities + +Sportello Digitale — Comune di Bitopoli + + + +
+

Sportello Digitale

+

+ Prenota un appuntamento, avvia una pratica urgente o contatta direttamente + un operatore. I nostri sportelli digitali sono disponibili 24 ore su 24. +

+
+ + +
+
+ + + +
+
@ticketNumber
+
Il tuo numero
+
+
+
+
+
+
+ + + +
+
@calledNumber
+
Numero chiamato
+
+
+
+
+
+
+ + + +
+
@waitCount
+
In attesa
+
+
+
+
+
+
+ + + +
+
~@waitMinutes min
+
Attesa stimata
+
+
+
+
+
+
+ + +
+
+ + + Prenota Sportello + +

+ Salta la fila! Prenota il tuo slot in uno degli sportelli fisici e + arriva puntuale senza attese. +

+
+ + + + Prenota ora + + +
+
+
+
+ + + Pratica Urgente + +

+ Hai bisogno di un documento con urgenza? Invia una richiesta di + trattazione prioritaria motivata. +

+
+ + + + Richiesta urgente + + +
+
+
+
+ + + Contatta Operatore + +

+ Parla direttamente con un operatore per chiarimenti su pratiche in + corso o per ricevere assistenza. +

+
+ + + + Contatta ora + + +
+
+
+
+ + +

Varianti BitModal

+
+
+ + Modale Base + +
+
+ + Verticalmente centrata + +
+
+ + Modale Alert + +
+
+ + Popconfirm + +
+
+ + Piccola + + + Grande + +
+
+ + ← Sinistra + + + Destra → + +
+
+ + Contenuto lungo + +
+
+ + Backdrop statico + +
+
+ + + + + + +

Seleziona il giorno e lo sportello di tua preferenza per prenotare un appuntamento.

+
+
+ + +
+
+ + +
+
+
+
+ + +
+
+ + +
+
+ + +
+
+
+ + Conferma prenotazione + Annulla + +
+ + + + +
+ +
+

+ Le pratiche urgenti sono soggette a valutazione da parte dell'ufficio competente. + La tua richiesta verrà presa in carico entro 24 ore. +

+

+ Inserisci la motivazione dell'urgenza. La richiesta priva di valida motivazione + verrà respinta automaticamente. +

+
+
+
+ + +
+
+ + + Invia richiesta urgente + + Annulla + +
+ + + + +

+ Scrivi il tuo messaggio. Un operatore ti risponderà all'indirizzo email indicato + entro il prossimo giorno lavorativo. +

+
+ + +
+
+ + +
+
+ + +
+
+ + Invia messaggio + Chiudi + +
+ + + + +
+ + Richiesta urgente registrata con successo! +
+
+ + OK + +
+ + + + +

Questa è una modale base con titolo, corpo e footer. Supporta header/body/footer customizzabili tramite RenderFragment.

+

Clicca fuori o sul pulsante Chiudi per chiuderla.

+
+ + Chiudi + +
+ + + + +

Questa modale è centrata verticalmente nella viewport grazie a Position="ModalPosition.Centered".

+
+ + Chiudi + +
+ + + + +

Variante Type="ModalType.Alert" — usata per messaggi importanti che richiedono attenzione dell'utente come avvisi critici del sistema.

+
+ + Ho capito + +
+ + + + +

Vuoi davvero annullare la pratica #BIT-2026-0042?

+
+ + Sì, annulla + No + +
+ + + +

Variante ModalSize.Small.

+ + OK + +
+ + + +

Variante ModalSize.Large — adatta per contenuti più articolati come tabelle, form complessi o dettagli documento.

+ + Chiudi + +
+ + + + +

Pannello che si apre da sinistra a destra (ModalPosition.Left). Utile per menu contestuali e drawer di navigazione.

+
    +
  • Dettagli pratica
  • +
  • Documenti allegati
  • +
  • Storico modifiche
  • +
  • Note operatore
  • +
+
+ + Chiudi + +
+ + + + +

Pannello che si apre da destra a sinistra (ModalPosition.Right). Adatto per pannelli di dettaglio e configurazione.

+
+ + Chiudi + +
+ + + + +

Art. 1 — Oggetto e finalità
+ Il presente regolamento disciplina le modalità di presentazione, istruttoria e conclusione dei procedimenti edilizi nel territorio del Comune di Bitopoli, ai sensi del D.P.R. 380/2001 e s.m.i.

+

Art. 2 — Definizioni
+ Ai fini del presente regolamento si intende per "intervento edilizio" qualsiasi trasformazione fisica o funzionale del territorio...

+

Art. 3 — Permesso di costruire
+ Sono subordinati a permesso di costruire gli interventi di nuova costruzione, gli interventi di ristrutturazione urbanistica e gli interventi di ristrutturazione edilizia che portino ad un organismo edilizio in tutto o in parte diverso dal precedente...

+

Art. 4 — CILA e SCIA
+ La Comunicazione di Inizio Lavori Asseverata (CILA) è richiesta per interventi di manutenzione straordinaria ove non riguardino parti strutturali...

+

Art. 5 — Contributi di costruzione
+ Il rilascio del permesso di costruire comporta la corresponsione di un contributo commisurato all'incidenza degli oneri di urbanizzazione nonché al costo di costruzione...

+

Art. 6 — Procedimento
+ Il procedimento per il rilascio del permesso di costruire si svolge secondo le disposizioni di cui all'art. 20 del D.P.R. 380/2001...

+

Art. 7 — Sanzioni
+ Chiunque esegua interventi edilizi in assenza del prescritto titolo abilitativo è soggetto alle sanzioni di cui agli artt. 31 e seguenti del D.P.R. 380/2001.

+
+ + + Scarica PDF + + Chiudi + +
+ + + + +

Questo dialogo ha Backdrop="ModalBackdrop.Static": cliccando fuori non si chiude. Devi usare il pulsante per procedere.

+ +

La sessione scadrà tra 4:58. Salva il tuo lavoro.

+
+
+ + Rinnova sessione + Esci + +
+ +@code { + // Dati sportello + private int ticketNumber = 47; + private int calledNumber = 38; + private int waitCount = 9; + private int waitMinutes => waitCount * 5; + + // Visibilità modali + private bool showPrenota; + private bool showUrgente; + private bool showContatta; + private bool showConfirmAlert; + private bool showDefault; + private bool showCentered; + private bool showAlert; + private bool showPopconfirm; + private bool showSmall; + private bool showLarge; + private bool showLeft; + private bool showRight; + private bool showScrollable; + private bool showStatic; + + private void OpenModal(string type) + { + showPrenota = type == "prenota"; + showUrgente = type == "urgente"; + showContatta = type == "contatta"; + showDefault = type == "default"; + showCentered = type == "centered"; + showAlert = type == "alert"; + showPopconfirm = type == "popconfirm"; + showSmall = type == "small"; + showLarge = type == "large"; + showLeft = type == "left"; + showRight = type == "right"; + showScrollable = type == "scrollable"; + showStatic = type == "static"; + } + + private void ConfirmPrenotazione() + { + ticketNumber++; + showPrenota = false; + } + + private IReadOnlyList breadcrumbItems = new List + { + new BitBreadcrumbItem { Text = "Home", Link = "/" }, + new BitBreadcrumbItem { Text = "Sportello Digitale" } + }; +} diff --git a/samples/BitBlazor.Sample/BitBlazor.Sample/Components/App.razor b/samples/BitBlazor.Sample/BitBlazor.Sample/Components/App.razor index d8a0b97..9dcb976 100644 --- a/samples/BitBlazor.Sample/BitBlazor.Sample/Components/App.razor +++ b/samples/BitBlazor.Sample/BitBlazor.Sample/Components/App.razor @@ -7,6 +7,7 @@ + diff --git a/samples/BitBlazor.Sample/BitBlazor.Sample/Components/Layout/MainLayout.razor b/samples/BitBlazor.Sample/BitBlazor.Sample/Components/Layout/MainLayout.razor index 78624f3..47d6566 100644 --- a/samples/BitBlazor.Sample/BitBlazor.Sample/Components/Layout/MainLayout.razor +++ b/samples/BitBlazor.Sample/BitBlazor.Sample/Components/Layout/MainLayout.razor @@ -1,23 +1,43 @@ @inherits LayoutComponentBase -
- - -
-
- About +
+
+ +
-
- @Body -
-
+
+ +
+
+ @Body +
+
+
- An unhandled error has occurred. - Reload + Si è verificato un errore imprevisto. + Ricarica 🗙
diff --git a/samples/BitBlazor.Sample/BitBlazor.Sample/Components/Layout/MainLayout.razor.css b/samples/BitBlazor.Sample/BitBlazor.Sample/Components/Layout/MainLayout.razor.css index 38d1f25..011f50f 100644 --- a/samples/BitBlazor.Sample/BitBlazor.Sample/Components/Layout/MainLayout.razor.css +++ b/samples/BitBlazor.Sample/BitBlazor.Sample/Components/Layout/MainLayout.razor.css @@ -1,17 +1,139 @@ -.page { - position: relative; +/* ========================================= + Portal wrapper & layout + ========================================= */ +.portal-wrapper { display: flex; flex-direction: column; + min-height: 100vh; } -main { +/* ========================================= + Header + ========================================= */ +.portal-header { + background-color: #06c; + color: white; + position: sticky; + top: 0; + z-index: 100; + border-bottom: 3px solid #004d99; + box-shadow: 0 2px 8px rgba(0,0,0,0.2); +} + +.portal-brand-title { + font-size: 1.1rem; + font-weight: 700; + color: white; + line-height: 1.2; +} + +.portal-brand-subtitle { + font-size: 0.72rem; + color: rgba(255, 255, 255, 0.75); + line-height: 1.3; +} + +.portal-logo { + width: 2.6rem; + height: 2.6rem; + background-color: rgba(255, 255, 255, 0.15); + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; +} + +.portal-username { + font-size: 0.82rem; + font-weight: 600; + color: white; + line-height: 1.2; +} + +.portal-userstatus { + font-size: 0.68rem; + color: rgba(255, 255, 255, 0.65); +} + +/* ========================================= + Body: sidebar + main + ========================================= */ +.portal-body { + display: flex; + flex: 1; +} + +.portal-sidebar { + width: 256px; + min-width: 256px; + background-color: #fff; + border-right: 1px solid #e0e0e0; + min-height: calc(100vh - 64px); + flex-shrink: 0; +} + +.portal-main { flex: 1; + background-color: #f4f5f6; + min-width: 0; +} + +.portal-content { + max-width: 1200px; + padding: 2rem 1.5rem; } -.sidebar { - background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%); +/* ========================================= + Utility overrides + ========================================= */ +.stat-icon { + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; } +/* ========================================= + Error UI + ========================================= */ +#blazor-error-ui { + color-scheme: light only; + background: #fff3f3; + bottom: 0; + box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.15); + box-sizing: border-box; + display: none; + left: 0; + padding: 0.6rem 1.25rem 0.7rem 1.25rem; + position: fixed; + width: 100%; + z-index: 1000; + color: #b20000; + border-top: 2px solid #e00; +} + + #blazor-error-ui .dismiss { + cursor: pointer; + position: absolute; + right: 0.75rem; + top: 0.5rem; + } + +/* ========================================= + Responsive + ========================================= */ +@media (max-width: 991.98px) { + .portal-sidebar { + display: none; + } + + .portal-content { + padding: 1rem; + } +} + + .top-row { background-color: #f7f7f7; border-bottom: 1px solid #d6d5d5; diff --git a/samples/BitBlazor.Sample/BitBlazor.Sample/Components/Layout/NavMenu.razor b/samples/BitBlazor.Sample/BitBlazor.Sample/Components/Layout/NavMenu.razor index 1187830..1adcbcc 100644 --- a/samples/BitBlazor.Sample/BitBlazor.Sample/Components/Layout/NavMenu.razor +++ b/samples/BitBlazor.Sample/BitBlazor.Sample/Components/Layout/NavMenu.razor @@ -2,71 +2,89 @@ @inject NavigationManager NavigationManager - - - - - @code { diff --git a/samples/BitBlazor.Sample/BitBlazor.Sample/Components/Layout/NavMenu.razor.css b/samples/BitBlazor.Sample/BitBlazor.Sample/Components/Layout/NavMenu.razor.css index 0145d9d..dfa79aa 100644 --- a/samples/BitBlazor.Sample/BitBlazor.Sample/Components/Layout/NavMenu.razor.css +++ b/samples/BitBlazor.Sample/BitBlazor.Sample/Components/Layout/NavMenu.razor.css @@ -1,18 +1,20 @@ -.navbar-toggler { - appearance: none; +/* ========================================= + Sidebar — Bootstrap Italia overrides + ========================================= */ + +/* Il sidebar-wrapper deve riempire il contenitore laterale */ +.sidebar-wrapper { + height: 100%; + overflow-y: auto; +} + +/* Bottone logout: resetta lo stile + } + + } + + + + @if (FooterContent is not null) + { +
+ @FooterContent +
+ } + + + + +} diff --git a/src/BitBlazor/Components/Modal/BitModal.razor.cs b/src/BitBlazor/Components/Modal/BitModal.razor.cs new file mode 100644 index 0000000..7abeb1e --- /dev/null +++ b/src/BitBlazor/Components/Modal/BitModal.razor.cs @@ -0,0 +1,255 @@ +using BitBlazor.Core; +using BitBlazor.Utilities; +using Microsoft.AspNetCore.Components; + +namespace BitBlazor.Components; + +/// +/// Represents a modal dialog component following Bootstrap Italia styles and accessibility guidelines. +/// +/// +/// The modal visibility is controlled entirely via Blazor state using / +/// — no JavaScript interop is required to show or hide it. +/// +public partial class BitModal : BitComponentBase +{ + private string _autoId = string.Empty; + + /// + /// Gets or sets whether the modal is currently visible. + /// + [Parameter] + public bool IsVisible { get; set; } + + /// + /// Callback invoked when the modal visibility changes, enabling two-way binding via @bind-IsVisible. + /// + [Parameter] + public EventCallback IsVisibleChanged { get; set; } + + /// + /// Gets or sets the main body content of the modal. + /// + [Parameter] + [EditorRequired] + public RenderFragment BodyContent { get; set; } = default!; + + /// + /// Gets or sets an optional title displayed in the modal header as an h2 element. + /// + /// + /// When set, the modal automatically receives an aria-labelledby attribute pointing to the title element. + /// When , use for accessibility. + /// + [Parameter] + public string? Title { get; set; } + + /// + /// Gets or sets an override for the id attribute of the title h2 element. + /// When , an auto-generated ID based on the modal's own ID is used. + /// + [Parameter] + public string? TitleId { get; set; } + + /// + /// Gets or sets the aria-label attribute for the modal. + /// Use this when is not set to ensure accessibility for screen readers. + /// + [Parameter] + public string? AriaLabel { get; set; } + + /// + /// Gets or sets the value of the aria-describedby attribute on the modal element, + /// pointing to the element ID that provides a description of the dialog. + /// + [Parameter] + public string? AriaDescribedById { get; set; } + + /// + /// Gets or sets a fully custom header render fragment that replaces the default title/close-button header. + /// When set, is ignored but still applies. + /// + [Parameter] + public RenderFragment? HeaderContent { get; set; } + + /// + /// Gets or sets the footer content of the modal. + /// When , no footer element is rendered. + /// + [Parameter] + public RenderFragment? FooterContent { get; set; } + + /// + /// Gets or sets whether a close button (×) is shown in the modal header. Defaults to true. + /// + [Parameter] + public bool ShowCloseButton { get; set; } = true; + + /// + /// Gets or sets the aria-label value for the close button, used by screen readers. + /// Defaults to "Chiudi". + /// + [Parameter] + public string CloseButtonAriaLabel { get; set; } = "Chiudi"; + + /// + /// Gets or sets the size variant of the modal. Defaults to . + /// + [Parameter] + public ModalSize Size { get; set; } = ModalSize.Default; + + /// + /// Gets or sets the positioning variant of the modal. Defaults to . + /// + [Parameter] + public ModalPosition Position { get; set; } = ModalPosition.Default; + + /// + /// Gets or sets the visual type variant of the modal. Defaults to . + /// + [Parameter] + public ModalType Type { get; set; } = ModalType.Default; + + /// + /// Gets or sets the backdrop behaviour. Defaults to . + /// + [Parameter] + public ModalBackdrop Backdrop { get; set; } = ModalBackdrop.Default; + + /// + /// Gets or sets whether the modal body content should scroll internally, + /// keeping the header and footer always visible. + /// Applies the it-dialog-scrollable class to the modal element. + /// Defaults to false. + /// + [Parameter] + public bool ScrollableContent { get; set; } + + /// + /// Gets or sets whether the modal footer renders with a top shadow (modal-footer-shadow). + /// Useful for long scrollable content. Defaults to false. + /// + [Parameter] + public bool FooterShadow { get; set; } + + /// + /// Gets or sets whether the modal uses a fade animation when appearing/disappearing. + /// Defaults to true. + /// + [Parameter] + public bool Fade { get; set; } = true; + + /// + /// Callback invoked just before the modal is closed (before is set to false). + /// + [Parameter] + public EventCallback OnClose { get; set; } + + /// Gets the effective element ID, using the auto-generated fallback when is not set. + private string _effectiveId => string.IsNullOrWhiteSpace(Id) ? _autoId : Id!; + + /// Gets the ID used for the title h2 element and aria-labelledby. + private string _titleId => string.IsNullOrWhiteSpace(TitleId) ? $"{_effectiveId}-title" : TitleId!; + + /// + /// Returns true when a header section should be rendered (title, custom header content or close button). + /// + private bool HasHeader => !string.IsNullOrWhiteSpace(Title) || HeaderContent is not null || ShowCloseButton; + + /// + protected override void OnInitialized() + { + _autoId = $"modal-{Guid.NewGuid():N}"[..14]; + base.OnInitialized(); + } + + /// + protected override void SetElementId() + { + AdditionalAttributes["id"] = _effectiveId; + } + + private string ComputeModalCssClasses() + { + var builder = new CssClassBuilder("modal"); + + if (Fade) + { + builder.Add("fade"); + } + + if (IsVisible) + { + builder.Add("show"); + } + + var typeClass = Type switch + { + ModalType.Alert => "alert-modal", + ModalType.LinkList => "it-dialog-link-list", + ModalType.Popconfirm => "popconfirm-modal", + _ => string.Empty, + }; + builder.Add(typeClass); + + if (ScrollableContent || Position == ModalPosition.Left || Position == ModalPosition.Right) + { + builder.Add("it-dialog-scrollable"); + } + + AddCustomCssClass(builder); + + return builder.Build(); + } + + private string ComputeDialogCssClasses() + { + var builder = new CssClassBuilder("modal-dialog"); + + var sizeClass = Size switch + { + ModalSize.Small => "modal-sm", + ModalSize.Large => "modal-lg", + ModalSize.ExtraLarge => "modal-xl", + _ => string.Empty, + }; + builder.Add(sizeClass); + + var positionClass = Position switch + { + ModalPosition.Centered => "modal-dialog-centered", + ModalPosition.Left => "modal-dialog-left", + ModalPosition.Right => "modal-dialog-right", + _ => string.Empty, + }; + builder.Add(positionClass); + + return builder.Build(); + } + + private string ComputeFooterCssClasses() + { + var builder = new CssClassBuilder("modal-footer"); + + if (FooterShadow) + { + builder.Add("modal-footer-shadow"); + } + + return builder.Build(); + } + + private async Task CloseAsync() + { + await OnClose.InvokeAsync(); + await IsVisibleChanged.InvokeAsync(false); + } + + private async Task HandleBackdropClickAsync() + { + if (Backdrop != ModalBackdrop.Static) + { + await CloseAsync(); + } + } +} diff --git a/src/BitBlazor/Components/Modal/ModalBackdrop.cs b/src/BitBlazor/Components/Modal/ModalBackdrop.cs new file mode 100644 index 0000000..f8fba1e --- /dev/null +++ b/src/BitBlazor/Components/Modal/ModalBackdrop.cs @@ -0,0 +1,22 @@ +namespace BitBlazor.Components; + +/// +/// Defines the backdrop behaviour for the component. +/// +public enum ModalBackdrop +{ + /// + /// A backdrop is shown and clicking it closes the modal. + /// + Default, + + /// + /// A backdrop is shown but clicking it does not close the modal. + /// + Static, + + /// + /// No backdrop is shown. + /// + None, +} diff --git a/src/BitBlazor/Components/Modal/ModalPosition.cs b/src/BitBlazor/Components/Modal/ModalPosition.cs new file mode 100644 index 0000000..1a6faa2 --- /dev/null +++ b/src/BitBlazor/Components/Modal/ModalPosition.cs @@ -0,0 +1,29 @@ +namespace BitBlazor.Components; + +/// +/// Defines the positioning variants available for the component. +/// +public enum ModalPosition +{ + /// + /// Default positioning (top-center). + /// + Default, + + /// + /// Vertically centered modal. Applies modal-dialog-centered to the dialog element. + /// + Centered, + + /// + /// Left-aligned, full-height modal. Applies it-dialog-scrollable to the modal element + /// and modal-dialog-left to the dialog element. + /// + Left, + + /// + /// Right-aligned, full-height modal. Applies it-dialog-scrollable to the modal element + /// and modal-dialog-right to the dialog element. + /// + Right, +} diff --git a/src/BitBlazor/Components/Modal/ModalSize.cs b/src/BitBlazor/Components/Modal/ModalSize.cs new file mode 100644 index 0000000..f0c1bf6 --- /dev/null +++ b/src/BitBlazor/Components/Modal/ModalSize.cs @@ -0,0 +1,27 @@ +namespace BitBlazor.Components; + +/// +/// Defines the size variants available for the component. +/// +public enum ModalSize +{ + /// + /// Default size (no extra size class applied). + /// + Default, + + /// + /// Small modal. Applies the modal-sm class to the dialog element. + /// + Small, + + /// + /// Large modal. Applies the modal-lg class to the dialog element. + /// + Large, + + /// + /// Extra-large modal. Applies the modal-xl class to the dialog element. + /// + ExtraLarge, +} diff --git a/src/BitBlazor/Components/Modal/ModalType.cs b/src/BitBlazor/Components/Modal/ModalType.cs new file mode 100644 index 0000000..c033475 --- /dev/null +++ b/src/BitBlazor/Components/Modal/ModalType.cs @@ -0,0 +1,27 @@ +namespace BitBlazor.Components; + +/// +/// Defines the visual type variants available for the component. +/// +public enum ModalType +{ + /// + /// Default modal with no additional type class. + /// + Default, + + /// + /// Alert modal. Applies the alert-modal class, used alongside an icon in the header. + /// + Alert, + + /// + /// Link list modal. Applies the it-dialog-link-list class, optimised for rendering navigation link lists. + /// + LinkList, + + /// + /// Popconfirm modal. Applies the popconfirm-modal class for compact confirmation dialogs. + /// + Popconfirm, +} diff --git a/src/BitBlazor/wwwroot/css/fonts.css b/src/BitBlazor/wwwroot/css/fonts.css new file mode 100644 index 0000000..6768aec --- /dev/null +++ b/src/BitBlazor/wwwroot/css/fonts.css @@ -0,0 +1,268 @@ +/* + * Bootstrap Italia — default fonts + * Include this file in your app to use the official BootstrapItalia typography. + * + * Usage: + * + * + * Font families declared here: + * - Titillium Web (sans-serif — primary UI font) + * - Lora (serif — display/reading) + * - Roboto Mono (monospace — code) + */ + +/* ============================================================ + Titillium Web — 300 (Light) + ============================================================ */ +@font-face { + font-family: 'Titillium Web'; + font-style: normal; + font-weight: 300; + font-display: swap; + src: url('../bootstrap-italia/fonts/Titillium_Web/titillium-web-v10-latin-ext_latin-300.eot'); + src: local(''), + url('../bootstrap-italia/fonts/Titillium_Web/titillium-web-v10-latin-ext_latin-300.eot?#iefix') format('embedded-opentype'), + url('../bootstrap-italia/fonts/Titillium_Web/titillium-web-v10-latin-ext_latin-300.woff2') format('woff2'), + url('../bootstrap-italia/fonts/Titillium_Web/titillium-web-v10-latin-ext_latin-300.woff') format('woff'), + url('../bootstrap-italia/fonts/Titillium_Web/titillium-web-v10-latin-ext_latin-300.ttf') format('truetype'), + url('../bootstrap-italia/fonts/Titillium_Web/titillium-web-v10-latin-ext_latin-300.svg#TitilliumWeb') format('svg'); +} + +/* Titillium Web — 300 Italic */ +@font-face { + font-family: 'Titillium Web'; + font-style: italic; + font-weight: 300; + font-display: swap; + src: url('../bootstrap-italia/fonts/Titillium_Web/titillium-web-v10-latin-ext_latin-300italic.eot'); + src: local(''), + url('../bootstrap-italia/fonts/Titillium_Web/titillium-web-v10-latin-ext_latin-300italic.eot?#iefix') format('embedded-opentype'), + url('../bootstrap-italia/fonts/Titillium_Web/titillium-web-v10-latin-ext_latin-300italic.woff2') format('woff2'), + url('../bootstrap-italia/fonts/Titillium_Web/titillium-web-v10-latin-ext_latin-300italic.woff') format('woff'), + url('../bootstrap-italia/fonts/Titillium_Web/titillium-web-v10-latin-ext_latin-300italic.ttf') format('truetype'), + url('../bootstrap-italia/fonts/Titillium_Web/titillium-web-v10-latin-ext_latin-300italic.svg#TitilliumWeb') format('svg'); +} + +/* ============================================================ + Titillium Web — 400 (Regular) + ============================================================ */ +@font-face { + font-family: 'Titillium Web'; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url('../bootstrap-italia/fonts/Titillium_Web/titillium-web-v10-latin-ext_latin-regular.eot'); + src: local(''), + url('../bootstrap-italia/fonts/Titillium_Web/titillium-web-v10-latin-ext_latin-regular.eot?#iefix') format('embedded-opentype'), + url('../bootstrap-italia/fonts/Titillium_Web/titillium-web-v10-latin-ext_latin-regular.woff2') format('woff2'), + url('../bootstrap-italia/fonts/Titillium_Web/titillium-web-v10-latin-ext_latin-regular.woff') format('woff'), + url('../bootstrap-italia/fonts/Titillium_Web/titillium-web-v10-latin-ext_latin-regular.ttf') format('truetype'), + url('../bootstrap-italia/fonts/Titillium_Web/titillium-web-v10-latin-ext_latin-regular.svg#TitilliumWeb') format('svg'); +} + +/* Titillium Web — 400 Italic */ +@font-face { + font-family: 'Titillium Web'; + font-style: italic; + font-weight: 400; + font-display: swap; + src: url('_content/BitBlazor/bootstrap-italia/fonts/Titillium_Web/titillium-web-v10-latin-ext_latin-italic.eot'); + src: local(''), + url('_content/BitBlazor/bootstrap-italia/fonts/Titillium_Web/titillium-web-v10-latin-ext_latin-italic.eot?#iefix') format('embedded-opentype'), + url('_content/BitBlazor/bootstrap-italia/fonts/Titillium_Web/titillium-web-v10-latin-ext_latin-italic.woff2') format('woff2'), + url('_content/BitBlazor/bootstrap-italia/fonts/Titillium_Web/titillium-web-v10-latin-ext_latin-italic.woff') format('woff'), + url('_content/BitBlazor/bootstrap-italia/fonts/Titillium_Web/titillium-web-v10-latin-ext_latin-italic.ttf') format('truetype'), + url('_content/BitBlazor/bootstrap-italia/fonts/Titillium_Web/titillium-web-v10-latin-ext_latin-italic.svg#TitilliumWeb') format('svg'); +} + +/* ============================================================ + Titillium Web — 600 (SemiBold) + ============================================================ */ +@font-face { + font-family: 'Titillium Web'; + font-style: normal; + font-weight: 600; + font-display: swap; + src: url('_content/BitBlazor/bootstrap-italia/fonts/Titillium_Web/titillium-web-v10-latin-ext_latin-600.eot'); + src: local(''), + url('_content/BitBlazor/bootstrap-italia/fonts/Titillium_Web/titillium-web-v10-latin-ext_latin-600.eot?#iefix') format('embedded-opentype'), + url('_content/BitBlazor/bootstrap-italia/fonts/Titillium_Web/titillium-web-v10-latin-ext_latin-600.woff2') format('woff2'), + url('_content/BitBlazor/bootstrap-italia/fonts/Titillium_Web/titillium-web-v10-latin-ext_latin-600.woff') format('woff'), + url('_content/BitBlazor/bootstrap-italia/fonts/Titillium_Web/titillium-web-v10-latin-ext_latin-600.ttf') format('truetype'), + url('_content/BitBlazor/bootstrap-italia/fonts/Titillium_Web/titillium-web-v10-latin-ext_latin-600.svg#TitilliumWeb') format('svg'); +} + +/* Titillium Web — 600 Italic */ +@font-face { + font-family: 'Titillium Web'; + font-style: italic; + font-weight: 600; + font-display: swap; + src: url('_content/BitBlazor/bootstrap-italia/fonts/Titillium_Web/titillium-web-v10-latin-ext_latin-600italic.eot'); + src: local(''), + url('_content/BitBlazor/bootstrap-italia/fonts/Titillium_Web/titillium-web-v10-latin-ext_latin-600italic.eot?#iefix') format('embedded-opentype'), + url('_content/BitBlazor/bootstrap-italia/fonts/Titillium_Web/titillium-web-v10-latin-ext_latin-600italic.woff2') format('woff2'), + url('_content/BitBlazor/bootstrap-italia/fonts/Titillium_Web/titillium-web-v10-latin-ext_latin-600italic.woff') format('woff'), + url('_content/BitBlazor/bootstrap-italia/fonts/Titillium_Web/titillium-web-v10-latin-ext_latin-600italic.ttf') format('truetype'), + url('_content/BitBlazor/bootstrap-italia/fonts/Titillium_Web/titillium-web-v10-latin-ext_latin-600italic.svg#TitilliumWeb') format('svg'); +} + +/* ============================================================ + Titillium Web — 700 (Bold) + ============================================================ */ +@font-face { + font-family: 'Titillium Web'; + font-style: normal; + font-weight: 700; + font-display: swap; + src: url('_content/BitBlazor/bootstrap-italia/fonts/Titillium_Web/titillium-web-v10-latin-ext_latin-700.eot'); + src: local(''), + url('_content/BitBlazor/bootstrap-italia/fonts/Titillium_Web/titillium-web-v10-latin-ext_latin-700.eot?#iefix') format('embedded-opentype'), + url('_content/BitBlazor/bootstrap-italia/fonts/Titillium_Web/titillium-web-v10-latin-ext_latin-700.woff2') format('woff2'), + url('_content/BitBlazor/bootstrap-italia/fonts/Titillium_Web/titillium-web-v10-latin-ext_latin-700.woff') format('woff'), + url('_content/BitBlazor/bootstrap-italia/fonts/Titillium_Web/titillium-web-v10-latin-ext_latin-700.ttf') format('truetype'), + url('_content/BitBlazor/bootstrap-italia/fonts/Titillium_Web/titillium-web-v10-latin-ext_latin-700.svg#TitilliumWeb') format('svg'); +} + +/* Titillium Web — 700 Italic */ +@font-face { + font-family: 'Titillium Web'; + font-style: italic; + font-weight: 700; + font-display: swap; + src: url('_content/BitBlazor/bootstrap-italia/fonts/Titillium_Web/titillium-web-v10-latin-ext_latin-700italic.eot'); + src: local(''), + url('_content/BitBlazor/bootstrap-italia/fonts/Titillium_Web/titillium-web-v10-latin-ext_latin-700italic.eot?#iefix') format('embedded-opentype'), + url('_content/BitBlazor/bootstrap-italia/fonts/Titillium_Web/titillium-web-v10-latin-ext_latin-700italic.woff2') format('woff2'), + url('_content/BitBlazor/bootstrap-italia/fonts/Titillium_Web/titillium-web-v10-latin-ext_latin-700italic.woff') format('woff'), + url('_content/BitBlazor/bootstrap-italia/fonts/Titillium_Web/titillium-web-v10-latin-ext_latin-700italic.ttf') format('truetype'), + url('_content/BitBlazor/bootstrap-italia/fonts/Titillium_Web/titillium-web-v10-latin-ext_latin-700italic.svg#TitilliumWeb') format('svg'); +} + +/* ============================================================ + Lora — 400 (Regular) + ============================================================ */ +@font-face { + font-family: 'Lora'; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url('_content/BitBlazor/bootstrap-italia/fonts/Lora/lora-v20-latin-ext_latin-regular.eot'); + src: local(''), + url('_content/BitBlazor/bootstrap-italia/fonts/Lora/lora-v20-latin-ext_latin-regular.eot?#iefix') format('embedded-opentype'), + url('_content/BitBlazor/bootstrap-italia/fonts/Lora/lora-v20-latin-ext_latin-regular.woff2') format('woff2'), + url('_content/BitBlazor/bootstrap-italia/fonts/Lora/lora-v20-latin-ext_latin-regular.woff') format('woff'), + url('_content/BitBlazor/bootstrap-italia/fonts/Lora/lora-v20-latin-ext_latin-regular.ttf') format('truetype'), + url('_content/BitBlazor/bootstrap-italia/fonts/Lora/lora-v20-latin-ext_latin-regular.svg#Lora') format('svg'); +} + +/* Lora — 400 Italic */ +@font-face { + font-family: 'Lora'; + font-style: italic; + font-weight: 400; + font-display: swap; + src: url('_content/BitBlazor/bootstrap-italia/fonts/Lora/lora-v20-latin-ext_latin-italic.eot'); + src: local(''), + url('_content/BitBlazor/bootstrap-italia/fonts/Lora/lora-v20-latin-ext_latin-italic.eot?#iefix') format('embedded-opentype'), + url('_content/BitBlazor/bootstrap-italia/fonts/Lora/lora-v20-latin-ext_latin-italic.woff2') format('woff2'), + url('_content/BitBlazor/bootstrap-italia/fonts/Lora/lora-v20-latin-ext_latin-italic.woff') format('woff'), + url('_content/BitBlazor/bootstrap-italia/fonts/Lora/lora-v20-latin-ext_latin-italic.ttf') format('truetype'), + url('_content/BitBlazor/bootstrap-italia/fonts/Lora/lora-v20-latin-ext_latin-italic.svg#Lora') format('svg'); +} + +/* ============================================================ + Lora — 700 (Bold) + ============================================================ */ +@font-face { + font-family: 'Lora'; + font-style: normal; + font-weight: 700; + font-display: swap; + src: url('_content/BitBlazor/bootstrap-italia/fonts/Lora/lora-v20-latin-ext_latin-700.eot'); + src: local(''), + url('_content/BitBlazor/bootstrap-italia/fonts/Lora/lora-v20-latin-ext_latin-700.eot?#iefix') format('embedded-opentype'), + url('_content/BitBlazor/bootstrap-italia/fonts/Lora/lora-v20-latin-ext_latin-700.woff2') format('woff2'), + url('_content/BitBlazor/bootstrap-italia/fonts/Lora/lora-v20-latin-ext_latin-700.woff') format('woff'), + url('_content/BitBlazor/bootstrap-italia/fonts/Lora/lora-v20-latin-ext_latin-700.ttf') format('truetype'), + url('_content/BitBlazor/bootstrap-italia/fonts/Lora/lora-v20-latin-ext_latin-700.svg#Lora') format('svg'); +} + +/* Lora — 700 Italic */ +@font-face { + font-family: 'Lora'; + font-style: italic; + font-weight: 700; + font-display: swap; + src: url('_content/BitBlazor/bootstrap-italia/fonts/Lora/lora-v20-latin-ext_latin-700italic.eot'); + src: local(''), + url('_content/BitBlazor/bootstrap-italia/fonts/Lora/lora-v20-latin-ext_latin-700italic.eot?#iefix') format('embedded-opentype'), + url('_content/BitBlazor/bootstrap-italia/fonts/Lora/lora-v20-latin-ext_latin-700italic.woff2') format('woff2'), + url('_content/BitBlazor/bootstrap-italia/fonts/Lora/lora-v20-latin-ext_latin-700italic.woff') format('woff'), + url('_content/BitBlazor/bootstrap-italia/fonts/Lora/lora-v20-latin-ext_latin-700italic.ttf') format('truetype'), + url('_content/BitBlazor/bootstrap-italia/fonts/Lora/lora-v20-latin-ext_latin-700italic.svg#Lora') format('svg'); +} + +/* ============================================================ + Roboto Mono — 400 (Regular) + ============================================================ */ +@font-face { + font-family: 'Roboto Mono'; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url('_content/BitBlazor/bootstrap-italia/fonts/Roboto_Mono/roboto-mono-v13-latin-ext_latin-regular.eot'); + src: local(''), + url('_content/BitBlazor/bootstrap-italia/fonts/Roboto_Mono/roboto-mono-v13-latin-ext_latin-regular.eot?#iefix') format('embedded-opentype'), + url('_content/BitBlazor/bootstrap-italia/fonts/Roboto_Mono/roboto-mono-v13-latin-ext_latin-regular.woff2') format('woff2'), + url('_content/BitBlazor/bootstrap-italia/fonts/Roboto_Mono/roboto-mono-v13-latin-ext_latin-regular.woff') format('woff'), + url('_content/BitBlazor/bootstrap-italia/fonts/Roboto_Mono/roboto-mono-v13-latin-ext_latin-regular.ttf') format('truetype'), + url('_content/BitBlazor/bootstrap-italia/fonts/Roboto_Mono/roboto-mono-v13-latin-ext_latin-regular.svg#RobotoMono') format('svg'); +} + +/* Roboto Mono — 400 Italic */ +@font-face { + font-family: 'Roboto Mono'; + font-style: italic; + font-weight: 400; + font-display: swap; + src: url('_content/BitBlazor/bootstrap-italia/fonts/Roboto_Mono/roboto-mono-v13-latin-ext_latin-italic.eot'); + src: local(''), + url('_content/BitBlazor/bootstrap-italia/fonts/Roboto_Mono/roboto-mono-v13-latin-ext_latin-italic.eot?#iefix') format('embedded-opentype'), + url('_content/BitBlazor/bootstrap-italia/fonts/Roboto_Mono/roboto-mono-v13-latin-ext_latin-italic.woff2') format('woff2'), + url('_content/BitBlazor/bootstrap-italia/fonts/Roboto_Mono/roboto-mono-v13-latin-ext_latin-italic.woff') format('woff'), + url('_content/BitBlazor/bootstrap-italia/fonts/Roboto_Mono/roboto-mono-v13-latin-ext_latin-italic.ttf') format('truetype'), + url('_content/BitBlazor/bootstrap-italia/fonts/Roboto_Mono/roboto-mono-v13-latin-ext_latin-italic.svg#RobotoMono') format('svg'); +} + +/* ============================================================ + Roboto Mono — 700 (Bold) + ============================================================ */ +@font-face { + font-family: 'Roboto Mono'; + font-style: normal; + font-weight: 700; + font-display: swap; + src: url('_content/BitBlazor/bootstrap-italia/fonts/Roboto_Mono/roboto-mono-v13-latin-ext_latin-700.eot'); + src: local(''), + url('_content/BitBlazor/bootstrap-italia/fonts/Roboto_Mono/roboto-mono-v13-latin-ext_latin-700.eot?#iefix') format('embedded-opentype'), + url('_content/BitBlazor/bootstrap-italia/fonts/Roboto_Mono/roboto-mono-v13-latin-ext_latin-700.woff2') format('woff2'), + url('_content/BitBlazor/bootstrap-italia/fonts/Roboto_Mono/roboto-mono-v13-latin-ext_latin-700.woff') format('woff'), + url('_content/BitBlazor/bootstrap-italia/fonts/Roboto_Mono/roboto-mono-v13-latin-ext_latin-700.ttf') format('truetype'), + url('_content/BitBlazor/bootstrap-italia/fonts/Roboto_Mono/roboto-mono-v13-latin-ext_latin-700.svg#RobotoMono') format('svg'); +} + +/* Roboto Mono — 700 Italic */ +@font-face { + font-family: 'Roboto Mono'; + font-style: italic; + font-weight: 700; + font-display: swap; + src: url('_content/BitBlazor/bootstrap-italia/fonts/Roboto_Mono/roboto-mono-v13-latin-ext_latin-700italic.eot'); + src: local(''), + url('_content/BitBlazor/bootstrap-italia/fonts/Roboto_Mono/roboto-mono-v13-latin-ext_latin-700italic.eot?#iefix') format('embedded-opentype'), + url('_content/BitBlazor/bootstrap-italia/fonts/Roboto_Mono/roboto-mono-v13-latin-ext_latin-700italic.woff2') format('woff2'), + url('_content/BitBlazor/bootstrap-italia/fonts/Roboto_Mono/roboto-mono-v13-latin-ext_latin-700italic.woff') format('woff'), + url('_content/BitBlazor/bootstrap-italia/fonts/Roboto_Mono/roboto-mono-v13-latin-ext_latin-700italic.ttf') format('truetype'), + url('_content/BitBlazor/bootstrap-italia/fonts/Roboto_Mono/roboto-mono-v13-latin-ext_latin-700italic.svg#RobotoMono') format('svg'); +} diff --git a/stories/BitBlazor.Stories/Components/Pages/IFramePage.razor b/stories/BitBlazor.Stories/Components/Pages/IFramePage.razor index a37caf7..ae19aae 100644 --- a/stories/BitBlazor.Stories/Components/Pages/IFramePage.razor +++ b/stories/BitBlazor.Stories/Components/Pages/IFramePage.razor @@ -16,6 +16,7 @@ + diff --git a/stories/BitBlazor.Stories/Components/Stories/Components/BitModal.stories.razor b/stories/BitBlazor.Stories/Components/Stories/Components/BitModal.stories.razor new file mode 100644 index 0000000..fef0f48 --- /dev/null +++ b/stories/BitBlazor.Stories/Components/Stories/Components/BitModal.stories.razor @@ -0,0 +1,538 @@ +@attribute [Stories("Components/BitModal")] + +@using BitBlazor.Components + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +@code { + private ViewModel model = new(); + + class ViewModel + { + public bool DefaultVisible { get; set; } + public bool CloseButtonVisible { get; set; } + public bool SizeSmallVisible { get; set; } + public bool SizeLargeVisible { get; set; } + public bool SizeXLVisible { get; set; } + public bool CenteredVisible { get; set; } + public bool LeftVisible { get; set; } + public bool RightVisible { get; set; } + public bool AlertVisible { get; set; } + public bool PopconfirmBasicVisible { get; set; } + public bool PopconfirmHeaderVisible { get; set; } + public bool NoHeaderVisible { get; set; } + public bool ScrollableVisible { get; set; } + public bool StaticBackdropVisible { get; set; } + public bool NoFadeVisible { get; set; } + } +} diff --git a/tests/BitBlazor.Test/Components/Modal/BitModalTest.Behaviors.cs b/tests/BitBlazor.Test/Components/Modal/BitModalTest.Behaviors.cs new file mode 100644 index 0000000..00bcc26 --- /dev/null +++ b/tests/BitBlazor.Test/Components/Modal/BitModalTest.Behaviors.cs @@ -0,0 +1,83 @@ +using BitBlazor.Components; +using Bunit; +using Microsoft.AspNetCore.Components; + +namespace BitBlazor.Test.Components.Modal; + +public class BitModalTest +{ + private static RenderFragment SimpleBody => + builder => builder.AddContent(0, "Body content"); + + [Fact] + public void BitModal_Should_Invoke_IsVisibleChanged_With_False_When_Close_Button_Is_Clicked() + { + using var ctx = new BunitContext(); + + bool newVisibility = true; + bool isVisibleChangedCalled = false; + + var component = ctx.Render(parameters => parameters + .Add(p => p.IsVisible, true) + .Add(p => p.BodyContent, SimpleBody) + .Add(p => p.IsVisibleChanged, (v) => { isVisibleChangedCalled = true; newVisibility = v; })); + + component.Find("button.btn-close").Click(); + + Assert.True(isVisibleChangedCalled); + Assert.False(newVisibility); + } + + [Fact] + public void BitModal_Should_Invoke_OnClose_When_Close_Button_Is_Clicked() + { + using var ctx = new BunitContext(); + + bool onCloseCalled = false; + + var component = ctx.Render(parameters => parameters + .Add(p => p.IsVisible, true) + .Add(p => p.BodyContent, SimpleBody) + .Add(p => p.OnClose, () => onCloseCalled = true)); + + component.Find("button.btn-close").Click(); + + Assert.True(onCloseCalled); + } + + [Fact] + public void BitModal_Should_Invoke_IsVisibleChanged_With_False_When_Backdrop_Is_Clicked() + { + using var ctx = new BunitContext(); + + bool newVisibility = true; + + var component = ctx.Render(parameters => parameters + .Add(p => p.IsVisible, true) + .Add(p => p.Backdrop, ModalBackdrop.Default) + .Add(p => p.BodyContent, SimpleBody) + .Add(p => p.IsVisibleChanged, (v) => newVisibility = v)); + + component.Find(".modal-backdrop").Click(); + + Assert.False(newVisibility); + } + + [Fact] + public void BitModal_Should_Not_Invoke_IsVisibleChanged_When_Static_Backdrop_Is_Clicked() + { + using var ctx = new BunitContext(); + + bool isVisibleChangedCalled = false; + + var component = ctx.Render(parameters => parameters + .Add(p => p.IsVisible, true) + .Add(p => p.Backdrop, ModalBackdrop.Static) + .Add(p => p.BodyContent, SimpleBody) + .Add(p => p.IsVisibleChanged, (v) => isVisibleChangedCalled = true)); + + component.Find(".modal-backdrop").Click(); + + Assert.False(isVisibleChangedCalled); + } +} diff --git a/tests/BitBlazor.Test/Components/Modal/BitModalTest.Rendering.razor b/tests/BitBlazor.Test/Components/Modal/BitModalTest.Rendering.razor new file mode 100644 index 0000000..18d1669 --- /dev/null +++ b/tests/BitBlazor.Test/Components/Modal/BitModalTest.Rendering.razor @@ -0,0 +1,327 @@ +@inherits BunitContext + +@code { + [Fact] + public void BitModal_Should_Not_Render_When_IsVisible_Is_False() + { + var component = Render( + @ + Content + ); + + Assert.Empty(component.Markup.Trim()); + } + + [Fact] + public void BitModal_Should_Render_When_IsVisible_Is_True() + { + var component = Render( + @ + Content + ); + + Assert.NotNull(component.Find(".modal")); + Assert.NotNull(component.Find(".modal-dialog")); + Assert.NotNull(component.Find(".modal-content")); + Assert.NotNull(component.Find(".modal-body")); + } + + [Fact] + public void BitModal_Should_Apply_Fade_Class_By_Default() + { + var component = Render( + @ + Content + ); + + var modal = component.Find(".modal"); + Assert.Contains("fade", modal.ClassList); + } + + [Fact] + public void BitModal_Should_Not_Apply_Fade_Class_When_Fade_Is_False() + { + var component = Render( + @ + Content + ); + + var modal = component.Find(".modal"); + Assert.DoesNotContain("fade", modal.ClassList); + } + + [Fact] + public void BitModal_Should_Apply_Show_Class_When_IsVisible_Is_True() + { + var component = Render( + @ + Content + ); + + var modal = component.Find(".modal"); + Assert.Contains("show", modal.ClassList); + } + + [Theory] + [InlineData(ModalSize.Small, "modal-sm")] + [InlineData(ModalSize.Large, "modal-lg")] + [InlineData(ModalSize.ExtraLarge, "modal-xl")] + public void BitModal_Should_Apply_Size_Class_To_Dialog_Correctly(ModalSize size, string expectedClass) + { + var component = Render( + @ + Content + ); + + var dialog = component.Find(".modal-dialog"); + Assert.Contains(expectedClass, dialog.ClassList); + } + + [Fact] + public void BitModal_Should_Apply_Centered_Position_Class_To_Dialog() + { + var component = Render( + @ + Content + ); + + var dialog = component.Find(".modal-dialog"); + Assert.Contains("modal-dialog-centered", dialog.ClassList); + } + + [Theory] + [InlineData(ModalPosition.Left, "modal-dialog-left")] + [InlineData(ModalPosition.Right, "modal-dialog-right")] + public void BitModal_Should_Apply_Left_Right_Position_Classes_And_Scrollable(ModalPosition position, string expectedDialogClass) + { + var component = Render( + @ + Content + ); + + var modal = component.Find(".modal"); + var dialog = component.Find(".modal-dialog"); + + Assert.Contains(expectedDialogClass, dialog.ClassList); + Assert.Contains("it-dialog-scrollable", modal.ClassList); + } + + [Theory] + [InlineData(ModalType.Alert, "alert-modal")] + [InlineData(ModalType.LinkList, "it-dialog-link-list")] + [InlineData(ModalType.Popconfirm, "popconfirm-modal")] + public void BitModal_Should_Apply_Type_Class_To_Modal_Correctly(ModalType type, string expectedClass) + { + var component = Render( + @ + Content + ); + + var modal = component.Find(".modal"); + Assert.Contains(expectedClass, modal.ClassList); + } + + [Fact] + public void BitModal_Should_Render_Title_In_Header_H2() + { + var component = Render( + @ + Content + ); + + var title = component.Find("h2.modal-title"); + Assert.Equal("My Modal Title", title.TextContent.Trim()); + } + + [Fact] + public void BitModal_Should_Set_Aria_LabelledBy_Pointing_To_Title_Element() + { + var component = Render( + @ + Content + ); + + var modal = component.Find(".modal"); + Assert.Equal("test-modal-title", modal.GetAttribute("aria-labelledby")); + + var titleEl = component.Find("h2.modal-title"); + Assert.Equal("test-modal-title", titleEl.Id); + } + + [Fact] + public void BitModal_Should_Set_Aria_Label_When_Provided_And_No_Title() + { + var component = Render( + @ + Content + ); + + var modal = component.Find(".modal"); + Assert.Equal("Accessible label", modal.GetAttribute("aria-label")); + Assert.Null(modal.GetAttribute("aria-labelledby")); + } + + [Fact] + public void BitModal_Should_Set_Aria_DescribedBy_When_Provided() + { + var component = Render( + @ + Content + ); + + var modal = component.Find(".modal"); + Assert.Equal("modal-desc", modal.GetAttribute("aria-describedby")); + } + + [Fact] + public void BitModal_Should_Render_Close_Button_By_Default() + { + var component = Render( + @ + Content + ); + + var closeButton = component.Find("button.btn-close"); + Assert.Equal("Chiudi", closeButton.GetAttribute("aria-label")); + } + + [Fact] + public void BitModal_Should_Not_Render_Close_Button_When_ShowCloseButton_Is_False() + { + var component = Render( + @ + Content + ); + + Assert.Throws(() => component.Find("button.btn-close")); + } + + [Fact] + public void BitModal_Should_Use_Custom_Close_Button_Aria_Label() + { + var component = Render( + @ + Content + ); + + var closeButton = component.Find("button.btn-close"); + Assert.Equal("Close dialog", closeButton.GetAttribute("aria-label")); + } + + [Fact] + public void BitModal_Should_Not_Render_Header_When_ShowCloseButton_False_And_No_Title() + { + var component = Render( + @ + Content + ); + + Assert.Throws(() => component.Find(".modal-header")); + } + + [Fact] + public void BitModal_Should_Render_Custom_Header_Content_When_Provided() + { + var component = Render( + @ +

Custom Header

+ Content +
); + + Assert.NotNull(component.Find(".modal-header")); + Assert.Equal("Custom Header", component.Find("h3").TextContent.Trim()); + } + + [Fact] + public void BitModal_Should_Render_Footer_When_FooterContent_Is_Set() + { + var component = Render( + @ + Content + + ); + + Assert.NotNull(component.Find(".modal-footer")); + } + + [Fact] + public void BitModal_Should_Not_Render_Footer_When_FooterContent_Is_Not_Set() + { + var component = Render( + @ + Content + ); + + Assert.Throws(() => component.Find(".modal-footer")); + } + + [Fact] + public void BitModal_Should_Apply_Footer_Shadow_Class_When_FooterShadow_Is_True() + { + var component = Render( + @ + Content + Footer + ); + + var footer = component.Find(".modal-footer"); + Assert.Contains("modal-footer-shadow", footer.ClassList); + } + + [Fact] + public void BitModal_Should_Render_Backdrop_When_Backdrop_Is_Default() + { + var component = Render( + @ + Content + ); + + Assert.NotNull(component.Find(".modal-backdrop")); + } + + [Fact] + public void BitModal_Should_Render_Backdrop_When_Backdrop_Is_Static() + { + var component = Render( + @ + Content + ); + + Assert.NotNull(component.Find(".modal-backdrop")); + } + + [Fact] + public void BitModal_Should_Not_Render_Backdrop_When_Backdrop_Is_None() + { + var component = Render( + @ + Content + ); + + Assert.Throws(() => component.Find(".modal-backdrop")); + } + + [Fact] + public void BitModal_Should_Apply_Scrollable_Class_When_ScrollableContent_Is_True() + { + var component = Render( + @ + Content + ); + + var modal = component.Find(".modal"); + Assert.Contains("it-dialog-scrollable", modal.ClassList); + } + + [Fact] + public void BitModal_Should_Apply_Custom_Css_Class_To_Modal_Element() + { + var component = Render( + @ + Content + ); + + var modal = component.Find(".modal"); + Assert.Contains("my-custom-class", modal.ClassList); + } +}