Skip to content

Commit abc4ef0

Browse files
committed
Add Callout widget
Introduces a Callout widget that renders an information box with a bordered, tinted background and icon determined by its CalloutType (Info, Success, Warning, Error). Supports an optional title and a fit-content sizing mode. Includes LESS variables for theming, and unit tests.
1 parent 93675dc commit abc4ef0

5 files changed

Lines changed: 304 additions & 0 deletions

File tree

asset/css/callout.less

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
// Layout
2+
.callout {
3+
display: flex;
4+
column-gap: 1em;
5+
justify-content: start;
6+
7+
&.callout-fit-content {
8+
width: fit-content;
9+
}
10+
11+
i.icon::before {
12+
margin-right: 0;
13+
}
14+
15+
p {
16+
margin: 0;
17+
}
18+
19+
.callout-title {
20+
margin-bottom: .5em;
21+
}
22+
23+
.callout-text {
24+
display: flex;
25+
flex-direction: column;
26+
}
27+
}
28+
29+
// Style
30+
.callout {
31+
padding: .5em 1em;
32+
border: 1px solid var(--callout-color);
33+
background-color: var(--callout-bg-color);
34+
border-radius: .25em;
35+
36+
i.icon {
37+
color: var(--callout-color);
38+
font-size: 1.5em;
39+
}
40+
41+
&.callout-type-info {
42+
--callout-color: @callout-info-color;
43+
--callout-bg-color: @callout-info-bg;
44+
}
45+
46+
&.callout-type-success {
47+
--callout-color: @callout-success-color;
48+
--callout-bg-color: @callout-success-bg;
49+
}
50+
51+
&.callout-type-warning {
52+
--callout-color: @callout-warning-color;
53+
--callout-bg-color: @callout-warning-bg;
54+
}
55+
56+
&.callout-type-error {
57+
--callout-color: @callout-error-color;
58+
--callout-bg-color: @callout-error-bg;
59+
}
60+
}

asset/css/variables.less

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,16 @@
130130
@schedule-element-fields-disabled-selected-bg: @base-gray-light;
131131
@schedule-element-keyboard-note-bg: @base-gray-light;
132132

133+
@callout-success-color: @state-ok;
134+
@callout-info-color: #008fe8;
135+
@callout-warning-color: @state-warning;
136+
@callout-error-color: @state-critical;
137+
138+
@callout-success-bg: fade(@callout-success-color, 10%);
139+
@callout-info-bg: fade(@callout-info-color, 10%);
140+
@callout-warning-bg: fade(@callout-warning-color, 10%);
141+
@callout-error-bg: fade(@callout-error-color, 10%);
142+
133143
@empty-state-color: @base-gray-semilight;
134144
@empty-state-bar-bg: @base-gray-lighter;
135145

src/Common/CalloutType.php

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?php
2+
3+
namespace ipl\Web\Common;
4+
5+
use ipl\Web\Widget\Icon;
6+
7+
/**
8+
* An enum containing all possible callout types for the {@see \ipl\Web\Widget\Callout} widget
9+
*/
10+
enum CalloutType: string
11+
{
12+
case Info = 'callout-type-info';
13+
case Success = 'callout-type-success';
14+
case Warning = 'callout-type-warning';
15+
case Error = 'callout-type-error';
16+
17+
/**
18+
* Get the icon element for use in the callout
19+
*
20+
* @return Icon
21+
*/
22+
public function getIcon(): Icon
23+
{
24+
return new Icon(match ($this) {
25+
self::Info => 'circle-info',
26+
self::Success => 'circle-check',
27+
self::Warning => 'warning',
28+
self::Error => 'circle-xmark',
29+
});
30+
}
31+
}

src/Widget/Callout.php

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
<?php
2+
3+
namespace ipl\Web\Widget;
4+
5+
use ipl\Html\Attributes;
6+
use ipl\Html\BaseHtmlElement;
7+
use ipl\Html\HtmlElement;
8+
use ipl\Html\Text;
9+
use ipl\Html\ValidHtml;
10+
use ipl\Web\Common\CalloutType;
11+
12+
/**
13+
* Information box with a type specific color and icon
14+
*
15+
* The type controls both the color scheme and the icon. An optional title
16+
* is displayed above the content.
17+
*/
18+
class Callout extends BaseHtmlElement
19+
{
20+
/** @var string Class name for fit content callouts */
21+
protected const CLASS_FIT_CONTENT = 'callout-fit-content';
22+
23+
protected $tag = 'div';
24+
25+
protected $defaultAttributes = ['class' => 'callout'];
26+
27+
/**
28+
* Create a new callout
29+
*
30+
* The $type parameter determines the color and icon of the callout.
31+
*
32+
* @param CalloutType $type The type of the callout
33+
* @param ValidHtml|string $content The content of the callout
34+
* @param ?string $title An optional title, displayed above the content
35+
*/
36+
public function __construct(
37+
protected CalloutType $type,
38+
protected ValidHtml|string $content,
39+
protected ?string $title = null
40+
) {
41+
$this->addAttributes(Attributes::create(['class' => $type->value]));
42+
}
43+
44+
protected function assemble(): void
45+
{
46+
$this->addHtml($this->type->getIcon());
47+
48+
$this->addHtml(HtmlElement::create(
49+
'div',
50+
['class' => 'callout-text'],
51+
[
52+
$this->title === null || trim($this->title) === ''
53+
? null
54+
: HtmlElement::create('strong', ['class' => 'callout-title'], Text::create($this->title)),
55+
is_string($this->content) ? Text::create($this->content) : $this->content,
56+
],
57+
));
58+
}
59+
60+
/**
61+
* Set the callout width to 100% of its parent container
62+
*
63+
* Callouts are by default sized to fill their parent container.
64+
*
65+
* @param bool $isFitContent Whether the callout size should be dependent on its content
66+
*
67+
* @return $this
68+
*/
69+
public function setFitContent(bool $isFitContent = true): static
70+
{
71+
if ($isFitContent) {
72+
$this->addAttributes(Attributes::create(['class' => static::CLASS_FIT_CONTENT]));
73+
} else {
74+
$this->removeAttribute('class', static::CLASS_FIT_CONTENT);
75+
}
76+
77+
return $this;
78+
}
79+
}

tests/Widget/CalloutTest.php

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
<?php
2+
3+
namespace ipl\Tests\Web\Widget;
4+
5+
use ipl\Html\Html;
6+
use ipl\Html\Test\TestCase;
7+
use ipl\Web\Common\CalloutType;
8+
use ipl\Web\Widget\Callout;
9+
10+
class CalloutTest extends TestCase
11+
{
12+
public function testCalloutWithoutTitle(): void
13+
{
14+
$callout = new Callout(CalloutType::Info, 'Content');
15+
16+
$html = <<<'HTML'
17+
<div class="callout callout-type-info">
18+
<i class="icon fa-circle-info fa"></i>
19+
<div class="callout-text">
20+
Content
21+
</div>
22+
</div>
23+
HTML;
24+
25+
$this->assertHtml($html, $callout);
26+
}
27+
28+
public function testCalloutWithTitle(): void
29+
{
30+
$callout = new Callout(CalloutType::Info, 'Content', 'Title');
31+
32+
$html = <<<'HTML'
33+
<div class="callout callout-type-info">
34+
<i class="icon fa-circle-info fa"></i>
35+
<div class="callout-text">
36+
<strong class="callout-title">Title</strong>
37+
Content
38+
</div>
39+
</div>
40+
HTML;
41+
42+
$this->assertHtml($html, $callout);
43+
}
44+
45+
public function testCalloutFalsyTitle(): void
46+
{
47+
$callout = new Callout(CalloutType::Warning, 'Content', '0');
48+
49+
$html = <<<'HTML'
50+
<div class="callout callout-type-warning">
51+
<i class="icon fa-warning fa"></i>
52+
<div class="callout-text">
53+
<strong class="callout-title">0</strong>
54+
Content
55+
</div>
56+
</div>
57+
HTML;
58+
$this->assertHtml($html, $callout);
59+
}
60+
61+
public function testCalloutEmptyTitle(): void
62+
{
63+
$callout = new Callout(CalloutType::Error, 'Content', '');
64+
65+
$html = <<<'HTML'
66+
<div class="callout callout-type-error">
67+
<i class="icon fa-circle-xmark fa"></i>
68+
<div class="callout-text">
69+
Content
70+
</div>
71+
</div>
72+
HTML;
73+
$this->assertHtml($html, $callout);
74+
}
75+
76+
public function testCalloutValidHtmlContent(): void
77+
{
78+
$callout = new Callout(
79+
CalloutType::Success,
80+
Html::tag('p', ['class' => 'test-class'], 'This is a Test'),
81+
'Test Title',
82+
);
83+
84+
$html = <<<'HTML'
85+
<div class="callout callout-type-success">
86+
<i class="icon fa-circle-check fa"></i>
87+
<div class="callout-text">
88+
<strong class="callout-title">Test Title</strong>
89+
<p class="test-class">This is a Test</p>
90+
</div>
91+
</div>
92+
HTML;
93+
94+
$this->assertHtml($html, $callout);
95+
}
96+
97+
public function testCalloutFitContent(): void
98+
{
99+
$callout = (new Callout(CalloutType::Error, 'Content'))
100+
->setFitContent(true);
101+
102+
$html = <<<'HTML'
103+
<div class="callout callout-type-error callout-fit-content">
104+
<i class="icon fa-circle-xmark fa"></i>
105+
<div class="callout-text">
106+
Content
107+
</div>
108+
</div>
109+
HTML;
110+
$this->assertHtml($html, $callout);
111+
112+
$callout->setFitContent(false);
113+
114+
$html2 = <<<'HTML'
115+
<div class="callout callout-type-error">
116+
<i class="icon fa-circle-xmark fa"></i>
117+
<div class="callout-text">
118+
Content
119+
</div>
120+
</div>
121+
HTML;
122+
$this->assertHtml($html2, $callout);
123+
}
124+
}

0 commit comments

Comments
 (0)