diff --git a/CHANGELOG.md b/CHANGELOG.md index c17c038..ce49ad8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +### Added +- Components for [modals](https://getbootstrap.com/docs/5.3/components/modal/) and buttons opening a modal + ### Fixed - Move PHPDoc comments outside of `@props` block - Display of disabled navigation items (``) diff --git a/README.md b/README.md index 462dec1..41cb3dd 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,7 @@ for the [Bootstrap 5](https://getbootstrap.com/docs/) frontend framework. - [Error messages](#error-messages) - [Links](#links) - [List groups](#list-groups) + - [Modals](#modals) - [Navigation](#navigation) - [Usage without Laravel](#usage-without-laravel) @@ -444,6 +445,31 @@ Items can be added via ``: `:active="true"` highlights the [active item](https://getbootstrap.com/docs/5.3/components/list-group/#active-items), `:disabled="true"` makes it appear [disabled](https://getbootstrap.com/docs/5.3/components/list-group/#disabled-items). +### Modals +[Modals](https://getbootstrap.com/docs/5.3/components/modal/) can be created via `` with optional slots for title and footer. +Both slots accept additional classes and other attributes. +If you don't want a `

` container for the title, change it via `container="h2"` etc. +```HTML +Open modal + + My modal title + + Test + + +``` +`` supports the following optional attributes: +- `centered` to center the modal vertically (defaults to `false`) +- `fade` for the fade effect when opening the modal (defaults to `true`) +- `fullScreen` to force fullscreen (defaults to `false`, pass `true` to always enforce full screen or `sm` to enforce for sizes below the sm breakpoint etc.), +- `scrollable` to enable a vertical scrollbar for long dialog content (defaults to `false`) +- `staticBackdrop`' to enforce that clicking outside of it does not close the modal (defaults to `false`) +- `closeButton` sets the variant of the close button in the modal footer (defaults to `secondary`, `false` to disable the close button), +- `closeButtonTitle` for the title of the close button (defaults to "Close") + +A `` opens the modal with the ID `my-modal`. +You may pass any additional attributes as known from [``](#buttons). + ### Navigation `` creates a [nav](https://getbootstrap.com/docs/5.3/components/navs-tabs/) container, use `container="ol"` to change the container element from the default `
    ` to `
      `. diff --git a/resources/views/components/modal/button.blade.php b/resources/views/components/modal/button.blade.php new file mode 100644 index 0000000..bcf8c3c --- /dev/null +++ b/resources/views/components/modal/button.blade.php @@ -0,0 +1,13 @@ +@props([ + 'modal', +]) +@php + /** @var string $modal */ + /** @var \Illuminate\View\ComponentAttributeBag $attributes */ +@endphp +{{ $slot }} diff --git a/resources/views/components/modal/index.blade.php b/resources/views/components/modal/index.blade.php new file mode 100644 index 0000000..4b565b1 --- /dev/null +++ b/resources/views/components/modal/index.blade.php @@ -0,0 +1,86 @@ +@props([ + 'id', + 'centered' => false, + 'fade' => true, + 'fullScreen' => false, + 'scrollable' => false, + 'staticBackdrop' => false, + 'closeButton' => true, + 'closeButtonTitle' => 'Close', +]) +@php + /** @var string $id */ + $labelId = $id . 'Label'; + /** @var bool $centered */ + /** @var bool $fade */ + /** @var bool|string $fullScreen */ + /** @var bool $scrollable */ + /** @var bool $staticBackdrop */ + /** @var string|bool $closeButton */ + $closeButton = $closeButton === true ? 'secondary' : $closeButton; + $showCloseButtonInFooter = is_string($closeButton); + /** @var string $closeButtonTitle */ + /** @var \Illuminate\View\ComponentAttributeBag $attributes */ + /** @var ?\Illuminate\View\ComponentSlot $title */ + /** @var ?\Illuminate\View\ComponentSlot $footer */ +@endphp +
      class([ + 'modal', + 'fade' => $fade, + ]) + ->merge([ + 'id' => $id, + 'tabindex' => -1, + 'aria-labelledby' => $id . 'Label', + 'aria-hidden' => 'true', + 'data-bs-backdrop' => $staticBackdrop ? 'static' : null, + 'data-bs-keyboard' => $staticBackdrop ? 'false' : null, + ])}}> +
      $fullScreen === true, + 'modal-fullscreen-' . $fullScreen . '-down' => is_string($fullScreen), + 'modal-dialog-centered' => $centered, + 'modal-dialog-scrollable' => $scrollable, + ])> + +
      +
      diff --git a/tests/Feature/Modal/ModalButtonTest.php b/tests/Feature/Modal/ModalButtonTest.php new file mode 100644 index 0000000..6e469da --- /dev/null +++ b/tests/Feature/Modal/ModalButtonTest.php @@ -0,0 +1,33 @@ +assertBladeRendersToHtml( + '', + sprintf( + 'Open modal', + self::makeVariantAttribute($variant) + ) + ); + } + + public static function variants(): array + { + return [ + ['btn-primary', null], + ...self::makeDataProvider('btn-'), + ]; + } +} diff --git a/tests/Feature/Modal/ModalTest.php b/tests/Feature/Modal/ModalTest.php new file mode 100644 index 0000000..0db3a25 --- /dev/null +++ b/tests/Feature/Modal/ModalTest.php @@ -0,0 +1,174 @@ +assertBladeRendersToHtml( + '', + $this->bladeView('My modal') + ); + } + + public static function modalOptions(): array + { + return [ + ['', 'class="modal fade"', 'modal-dialog'], + [':centered="true"', 'class="modal fade"', 'modal-dialog modal-dialog-centered'], + [':centered="true" :scrollable="true"', 'class="modal fade"', 'modal-dialog modal-dialog-centered modal-dialog-scrollable'], + [':fade="false"', 'class="modal"', 'modal-dialog'], + [':scrollable="true"', 'class="modal fade"', 'modal-dialog modal-dialog-scrollable'], + [':static-backdrop="true"', 'data-bs-backdrop="static" data-bs-keyboard="false" class="modal fade"', 'modal-dialog'], + ]; + } + + /** + * @dataProvider fullScreenOptions + */ + public function testModalRendersFullScreenOptionsCorrectly(bool|string $fullScreen, string $modalDialogClass): void + { + $this->assertBladeRendersToHtml( + '', + $this->bladeView('My modal') + ); + } + + public static function fullScreenOptions(): array + { + return [ + ['', ''], + [':full-screen="false"', ''], + [':full-screen="true"', 'modal-fullscreen'], + ['full-screen="sm"', 'modal-fullscreen-sm-down'], + ['full-screen="md"', 'modal-fullscreen-md-down'], + ['full-screen="lg"', 'modal-fullscreen-lg-down'], + ['full-screen="xl"', 'modal-fullscreen-xl-down'], + ['full-screen="xxl"', 'modal-fullscreen-xxl-down'], + ]; + } + + /** + * @dataProvider closeButtonOptions + */ + public function testModalRendersCloseButtonCorrectly(string $closeButtonAttributes, string $footer): void + { + $this->assertBladeRendersToHtml( + '', + $this->bladeView('My modal') + ); + } + + public static function closeButtonOptions(): array + { + return [ + ['', self::makeFooter('btn-secondary')], + [':close-button="true"', self::makeFooter('btn-secondary')], + [':close-button="false"', ''], + ['close-button="primary"', self::makeFooter('btn-primary')], + ]; + } + + /** + * @dataProvider slots + */ + public function testModalRendersSlotsCorrectly(string $slots, string $header, string $footer): void + { + $this->assertBladeRendersToHtml( + '', + $this->bladeView('My modal' . $slots . '') + ); + } + + public static function slots(): array + { + return [ + [ + 'Test title', + self::makeHeader('

      Test title

      '), + self::makeFooter('btn-secondary'), + ], + [ + 'Test title', + self::makeHeader(''), + self::makeFooter('btn-secondary'), + ], + [ + ' + Submit + ', + self::makeHeader(null), + '', + ], + ]; + } + + private static function makeHeader(?string $title): string + { + return ''; + } + + private static function makeFooter(?string $buttonClass): string + { + if ($buttonClass === null) { + return ''; + } + + return ''; + } +}