Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions asset/css/callout.less
Comment thread
nilmerg marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// Layout
.callout {
display: flex;
column-gap: 1em;
justify-content: start;

&.callout-fit-content {
width: fit-content;
}

i.icon::before {
margin-right: 0;
}

p {
margin: 0;
}

.callout-title {
margin-bottom: .5em;
}

.callout-text {
display: flex;
flex-direction: column;
}
}

// Style
.callout {
padding: .5em 1em;
border: 1px solid var(--callout-color);
background-color: var(--callout-bg-color);
border-radius: .25em;

i.icon {
color: var(--callout-color);
font-size: 1.5em;
}

&.callout-type-info {
--callout-color: @callout-info-color;
--callout-bg-color: @callout-info-bg;
}

&.callout-type-success {
--callout-color: @callout-success-color;
--callout-bg-color: @callout-success-bg;
}

&.callout-type-warning {
--callout-color: @callout-warning-color;
--callout-bg-color: @callout-warning-bg;
}

&.callout-type-error {
--callout-color: @callout-error-color;
--callout-bg-color: @callout-error-bg;
}
}
10 changes: 10 additions & 0 deletions asset/css/variables.less
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,16 @@
@schedule-element-fields-disabled-selected-bg: @base-gray-light;
@schedule-element-keyboard-note-bg: @base-gray-light;

@callout-success-color: @state-ok;
@callout-info-color: #008fe8;
@callout-warning-color: @state-warning;
@callout-error-color: @state-critical;

@callout-success-bg: fade(@callout-success-color, 10%);
@callout-info-bg: fade(@callout-info-color, 10%);
@callout-warning-bg: fade(@callout-warning-color, 10%);
@callout-error-bg: fade(@callout-error-color, 10%);

@empty-state-color: @base-gray-semilight;
@empty-state-bar-bg: @base-gray-lighter;

Expand Down
31 changes: 31 additions & 0 deletions src/Common/CalloutType.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

namespace ipl\Web\Common;

use ipl\Web\Widget\Icon;

/**
* An enum containing all possible callout types for the {@see \ipl\Web\Widget\Callout} widget
*/
enum CalloutType: string
{
case Info = 'callout-type-info';
case Success = 'callout-type-success';
case Warning = 'callout-type-warning';
case Error = 'callout-type-error';

/**
* Get the icon element for use in the callout
*
* @return Icon
*/
public function getIcon(): Icon
{
return new Icon(match ($this) {
self::Info => 'circle-info',
self::Success => 'circle-check',
self::Warning => 'warning',
self::Error => 'circle-xmark',
});
}
}
79 changes: 79 additions & 0 deletions src/Widget/Callout.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
<?php

namespace ipl\Web\Widget;

use ipl\Html\Attributes;
use ipl\Html\BaseHtmlElement;
use ipl\Html\HtmlElement;
use ipl\Html\Text;
use ipl\Html\ValidHtml;
use ipl\Web\Common\CalloutType;

/**
* Information box with a type specific color and icon
*
* The type controls both the color scheme and the icon. An optional title
* is displayed above the content.
*/
Comment thread
TheSyscall marked this conversation as resolved.
class Callout extends BaseHtmlElement
Comment thread
jrauh01 marked this conversation as resolved.
{
/** @var string Class name for fit content callouts */
protected const CLASS_FIT_CONTENT = 'callout-fit-content';

protected $tag = 'div';

protected $defaultAttributes = ['class' => 'callout'];

/**
* Create a new callout
*
* The $type parameter determines the color and icon of the callout.
*
* @param CalloutType $type The type of the callout
* @param ValidHtml|string $content The content of the callout
* @param ?string $title An optional title, displayed above the content
*/
public function __construct(
protected CalloutType $type,
protected ValidHtml|string $content,
protected ?string $title = null
) {
$this->addAttributes(Attributes::create(['class' => $type->value]));
}

protected function assemble(): void
{
$this->addHtml($this->type->getIcon());

$this->addHtml(HtmlElement::create(
'div',
['class' => 'callout-text'],
[
$this->title === null || trim($this->title) === ''
? null
: HtmlElement::create('strong', ['class' => 'callout-title'], Text::create($this->title)),
is_string($this->content) ? Text::create($this->content) : $this->content,
],
));
}

/**
* Set the callout width to 100% of its parent container
*
* Callouts are by default sized to fill their parent container.
*
* @param bool $isFitContent Whether the callout size should be dependent on its content
*
* @return $this
*/
public function setFitContent(bool $isFitContent = true): static
{
if ($isFitContent) {
$this->addAttributes(Attributes::create(['class' => static::CLASS_FIT_CONTENT]));
} else {
$this->removeAttribute('class', static::CLASS_FIT_CONTENT);
}

return $this;
}
}
124 changes: 124 additions & 0 deletions tests/Widget/CalloutTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
<?php

namespace ipl\Tests\Web\Widget;

use ipl\Html\Html;
use ipl\Html\Test\TestCase;
use ipl\Web\Common\CalloutType;
use ipl\Web\Widget\Callout;

class CalloutTest extends TestCase
{
public function testCalloutWithoutTitle(): void
{
$callout = new Callout(CalloutType::Info, 'Content');

$html = <<<'HTML'
<div class="callout callout-type-info">
<i class="icon fa-circle-info fa"></i>
<div class="callout-text">
Content
</div>
</div>
HTML;

$this->assertHtml($html, $callout);
}

public function testCalloutWithTitle(): void
{
$callout = new Callout(CalloutType::Info, 'Content', 'Title');

$html = <<<'HTML'
<div class="callout callout-type-info">
<i class="icon fa-circle-info fa"></i>
<div class="callout-text">
<strong class="callout-title">Title</strong>
Content
</div>
</div>
HTML;

$this->assertHtml($html, $callout);
}

public function testCalloutFalsyTitle(): void
{
$callout = new Callout(CalloutType::Warning, 'Content', '0');

$html = <<<'HTML'
<div class="callout callout-type-warning">
<i class="icon fa-warning fa"></i>
<div class="callout-text">
<strong class="callout-title">0</strong>
Content
</div>
</div>
HTML;
$this->assertHtml($html, $callout);
}

public function testCalloutEmptyTitle(): void
{
$callout = new Callout(CalloutType::Error, 'Content', '');

$html = <<<'HTML'
<div class="callout callout-type-error">
<i class="icon fa-circle-xmark fa"></i>
<div class="callout-text">
Content
</div>
</div>
HTML;
$this->assertHtml($html, $callout);
}

public function testCalloutValidHtmlContent(): void
{
$callout = new Callout(
CalloutType::Success,
Html::tag('p', ['class' => 'test-class'], 'This is a Test'),
'Test Title',
);

$html = <<<'HTML'
<div class="callout callout-type-success">
<i class="icon fa-circle-check fa"></i>
<div class="callout-text">
<strong class="callout-title">Test Title</strong>
<p class="test-class">This is a Test</p>
</div>
</div>
HTML;

$this->assertHtml($html, $callout);
}

public function testCalloutFitContent(): void
{
$callout = (new Callout(CalloutType::Error, 'Content'))
->setFitContent(true);

$html = <<<'HTML'
<div class="callout callout-type-error callout-fit-content">
<i class="icon fa-circle-xmark fa"></i>
<div class="callout-text">
Content
</div>
</div>
HTML;
$this->assertHtml($html, $callout);

$callout->setFitContent(false);

$html2 = <<<'HTML'
<div class="callout callout-type-error">
<i class="icon fa-circle-xmark fa"></i>
<div class="callout-text">
Content
</div>
</div>
HTML;
$this->assertHtml($html2, $callout);
}
}
Loading