Skip to content

Commit

Permalink
Add components for modals and buttons opening a modal (#17)
Browse files Browse the repository at this point in the history
* Add components for modals and buttons opening a modal

* Fix PHPDoc comments and formatting
  • Loading branch information
patrickrobrecht authored Mar 21, 2024
1 parent 4114892 commit 779ef41
Show file tree
Hide file tree
Showing 6 changed files with 335 additions and 0 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 (`<x-bs::nav.item :disabled="true">`)
Expand Down
26 changes: 26 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -444,6 +445,31 @@ Items can be added via `<x-bs::list.item>`:
`: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 `<x-bs::modal>` with optional slots for title and footer.
Both slots accept additional classes and other attributes.
If you don't want a `<h1>` container for the title, change it via `container="h2"` etc.
```HTML
<x-bs::modal.button modal="my-modal">Open modal</x-bs::modal.button>
<x-bs::modal id="my-modal">
<x-slot:title>My modal title</x-slot:title>
<x-slot:footer>
<x-bs::button>Test</x-bs::button>
</x-slot:footer>
</x-bs::modal>
```
`<x-bs::modal>` 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 `<x-bs::modal.button modal="my-modal">` opens the modal with the ID `my-modal`.
You may pass any additional attributes as known from [`<x-bs::button>`](#buttons).
### Navigation
`<x-bs::nav>` creates a [nav](https://getbootstrap.com/docs/5.3/components/navs-tabs/) container, use `container="ol"` to change the container element from the default `<ul>` to `<ol>`.
Expand Down
13 changes: 13 additions & 0 deletions resources/views/components/modal/button.blade.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
@props([
'modal',
])
@php
/** @var string $modal */
/** @var \Illuminate\View\ComponentAttributeBag $attributes */
@endphp
<x-bs::button :attributes="$attributes
->merge([
'type' => 'button',
'data-bs-toggle' => 'modal',
'data-bs-target' => '#' . $modal,
])">{{ $slot }}</x-bs::button>
86 changes: 86 additions & 0 deletions resources/views/components/modal/index.blade.php
Original file line number Diff line number Diff line change
@@ -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
<div {{ $attributes
->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,
])}}>
<div @class([
'modal-dialog',
'modal-fullscreen' => $fullScreen === true,
'modal-fullscreen-' . $fullScreen . '-down' => is_string($fullScreen),
'modal-dialog-centered' => $centered,
'modal-dialog-scrollable' => $scrollable,
])>
<div class="modal-content">
<div class="modal-header">
@if(isset($title) && $title instanceof \Illuminate\View\ComponentSlot)
@php
$container = $title->attributes->get('container', 'h1');
@endphp
<{{ $container }} {{ $title->attributes
->except([
'container',
])
->class([
'modal-title',
])
->merge([
'id' => $labelId,
]) }}>{{ $title }}</{{ $container }}>
@endif
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="{{ $closeButtonTitle }}"></button>
</div>
<div class="modal-body">{{ $slot }}</div>
@if($showCloseButtonInFooter || (isset($footer) && $footer instanceof \Illuminate\View\ComponentSlot))
@php
$footer = $footer ?? new \Illuminate\View\ComponentSlot();
@endphp
<div {{ $footer->attributes
->class([
'modal-footer',
]) }}>
@if($showCloseButtonInFooter)
<x-bs::button :variant="$closeButton" {{ $attributes
->merge([
'type' => 'button',
'data-bs-dismiss' => 'modal',
]) }}>{{ $closeButtonTitle }}</x-bs::button>
@endif{{--
--}}{{ $footer }}</div>
@endif
</div>
</div>
</div>
33 changes: 33 additions & 0 deletions tests/Feature/Modal/ModalButtonTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php

namespace Feature\Modal;

use Portavice\Bladestrap\Tests\Feature\ComponentTestCase;
use Portavice\Bladestrap\Tests\Traits\TestsVariants;

class ModalButtonTest extends ComponentTestCase
{
use TestsVariants;

/**
* @dataProvider variants
*/
public function testModalButtonRendersCorrectly(string $buttonClass, ?string $variant): void
{
$this->assertBladeRendersToHtml(
'<button class="btn ' . $buttonClass . '" type="button" data-bs-toggle="modal" data-bs-target="#my-modal">Open modal</button>',
sprintf(
'<x-bs::modal.button %s modal="my-modal">Open modal</x-bs::modal.button>',
self::makeVariantAttribute($variant)
)
);
}

public static function variants(): array
{
return [
['btn-primary', null],
...self::makeDataProvider('btn-'),
];
}
}
174 changes: 174 additions & 0 deletions tests/Feature/Modal/ModalTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
<?php

namespace Feature\Modal;

use Portavice\Bladestrap\Tests\Feature\ComponentTestCase;

class ModalTest extends ComponentTestCase
{
/**
* @dataProvider modalOptions
*/
public function testModalRendersOptionsCorrectly(string $blade, string $modalAttributes, string $modalDialogClasses): void
{
$this->assertBladeRendersToHtml(
'<div id="exampleModal" tabindex="-1" aria-labelledby="exampleModalLabel" aria-hidden="true" ' . $modalAttributes . '>
<div class="' . $modalDialogClasses . '">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">My modal</div>
<div class="modal-footer">
<button class="btn btn-secondary" type="button" data-bs-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>',
$this->bladeView('<x-bs::modal id="exampleModal" ' . $blade .'>My modal</x-bs::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(
'<div id="exampleModal" tabindex="-1" aria-labelledby="exampleModalLabel" aria-hidden="true" class="modal fade">
<div class="modal-dialog ' . $modalDialogClass . '">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">My modal</div>
<div class="modal-footer">
<button class="btn btn-secondary" type="button" data-bs-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>',
$this->bladeView('<x-bs::modal id="exampleModal" ' . $fullScreen .'>My modal</x-bs::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(
'<div id="exampleModal" tabindex="-1" aria-labelledby="exampleModalLabel" aria-hidden="true" class="modal fade">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">My modal</div>'
. ($footer ? "\n" . $footer : '') . '
</div>
</div>
</div>',
$this->bladeView('<x-bs::modal id="exampleModal" ' . $closeButtonAttributes . '>My modal</x-bs::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(
'<div id="exampleModal" tabindex="-1" aria-labelledby="exampleModalLabel" aria-hidden="true" class="modal fade">
<div class="modal-dialog">
<div class="modal-content">
' . $header . '
<div class="modal-body">My modal</div>
' . $footer . '
</div>
</div>
</div>',
$this->bladeView('<x-bs::modal id="exampleModal">My modal' . $slots . '</x-bs::modal>')
);
}

public static function slots(): array
{
return [
[
'<x-slot:title class="fs-5">Test title</x-slot>',
self::makeHeader('<h1 id="exampleModalLabel" class="modal-title fs-5">Test title</h1>'),
self::makeFooter('btn-secondary'),
],
[
'<x-slot:title container="h2" class="text-primary">Test title</x-slot>',
self::makeHeader('<h2 id="exampleModalLabel" class="modal-title text-primary">Test title</h2>'),
self::makeFooter('btn-secondary'),
],
[
'<x-slot:footer>
<x-bs::button variant="primary">Submit</x-bs:button>
</x-slot>',
self::makeHeader(null),
'<div class="modal-footer">
<button class="btn btn-secondary" type="button" data-bs-dismiss="modal">Close</button>
<button class="btn btn-primary">Submit</button></div>',
],
];
}

private static function makeHeader(?string $title): string
{
return '<div class="modal-header">'
. ($title ? "\n" . $title : '') . '
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>';
}

private static function makeFooter(?string $buttonClass): string
{
if ($buttonClass === null) {
return '';
}

return '<div class="modal-footer">
<button class="btn ' . $buttonClass .'" type="button" data-bs-dismiss="modal">Close</button>
</div>';
}
}

0 comments on commit 779ef41

Please sign in to comment.