Passed
Pull Request — master (#53)
by Wilmer
07:05 queued 04:41
created

Dropdown   F

Complexity

Total Complexity 79

Size/Duplication

Total Lines 822
Duplicated Lines 0 %

Test Coverage

Coverage 72.92%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 272
c 1
b 0
f 0
dl 0
loc 822
ccs 210
cts 288
cp 0.7292
rs 2.08
wmc 79

44 Methods

Rating   Name   Duplication   Size   Complexity  
A renderItemContainer() 0 19 4
A dividerClass() 0 6 1
A toggleAttributes() 0 6 1
A renderItemLink() 0 21 3
A itemClass() 0 6 1
A splitButtonSpanClass() 0 6 1
B renderItem() 0 64 9
A containerTag() 0 5 1
A splitButtonAttributes() 0 5 1
A renderLabel() 0 23 5
A renderToggleLink() 0 3 1
A itemContainerTag() 0 6 1
A itemContainerAttributes() 0 6 1
A container() 0 6 1
A headerClass() 0 6 1
A disabledClass() 0 6 1
A dividerTag() 0 6 1
A renderItems() 0 13 2
A run() 0 17 3
A renderToggleButton() 0 3 1
A toggleType() 0 6 1
A normalizeItems() 0 43 4
A renderDropdown() 0 17 1
A itemContainer() 0 6 1
A renderDivider() 0 21 4
A renderToggleSplit() 0 6 1
A renderToggle() 0 14 3
A splitButtonClass() 0 6 1
A dividerAttributes() 0 6 1
A containerAttributes() 0 6 1
A renderToggleSplitButton() 0 3 1
A itemsContainerAttributes() 0 6 1
A itemsContainerClass() 0 6 1
A headerTag() 0 6 1
A toggleClass() 0 6 1
A renderItemContent() 0 12 1
A items() 0 6 1
A containerClass() 0 6 1
A itemTag() 0 6 1
A itemContainerClass() 0 6 1
A label() 0 22 5
A activeClass() 0 6 1
A attributes() 0 6 1
A renderHeader() 0 19 4

How to fix   Complexity   

Complex Class

Complex classes like Dropdown often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Dropdown, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Yii\Widgets;
6
7
use InvalidArgumentException;
8
use Yiisoft\Definitions\Exception\CircularReferenceException;
9
use Yiisoft\Definitions\Exception\InvalidConfigException;
10
use Yiisoft\Definitions\Exception\NotInstantiableException;
11
use Yiisoft\Factory\NotFoundException;
12
use Yiisoft\Html\Html;
13
use Yiisoft\Html\Tag\A;
14
use Yiisoft\Html\Tag\I;
15
use Yiisoft\Html\Tag\Button;
16
use Yiisoft\Html\Tag\Span;
17
use Yiisoft\Widget\Widget;
18
19
use function gettype;
20
21
final class Dropdown extends Widget
22
{
23
    private string $activeClass = 'active';
24
    private array $attributes = [];
25
    private bool $container = true;
26
    private array $containerAttributes = [];
27
    private string $containerClass = '';
28
    private string $containerTag = 'div';
29
    private string $disabledClass = 'disabled';
30
    private array $dividerAttributes = [];
31
    private string $dividerClass = 'dropdown-divider';
32
    private string $dividerTag = 'hr';
33
    private string $headerClass = '';
34
    private string $headerTag = 'span';
35
    private string $itemClass = '';
36
    private string $itemTag = 'a';
37
    private bool $itemContainer = true;
38
    private array $itemContainerAttributes = [];
39
    private string $itemContainerTag = 'li';
40
    private array $items = [];
41
    private array $itemsContainerAttributes = [];
42
    private string $itemsContainerTag = 'ul';
43
    private array $splitButtonAttributes = [];
44
    private array $splitButtonSpanAttributes = [];
45
    private array $toggleAttributes = [];
46
    private string $toggleType = 'button';
47
48
    /**
49
     * Returns a new instance with the specified active class.
50
     *
51
     * @param string $value The active class.
52
     *
53
     * @return self
54
     */
55 1
    public function activeClass(string $value): self
56
    {
57 1
        $new = clone $this;
58 1
        $new->activeClass = $value;
59
60 1
        return $new;
61
    }
62
63
    /**
64
     * Returns a new instance with the HTML attributes. The following special options are recognized.
65
     *
66
     * @param array $values Attribute values indexed by attribute names.
67
     *
68
     * @return static
69
     */
70 4
    public function attributes(array $values): static
71
    {
72 4
        $new = clone $this;
73 4
        $new->attributes = $values;
74
75 4
        return $new;
76
    }
77
78
    /**
79
     * Returns a new instance with the specified if the container is enabled, or not. Default is true.
80
     *
81
     * @param bool $value The container enabled.
82
     *
83
     * @return self
84
     */
85 4
    public function container(bool $value): self
86
    {
87 4
        $new = clone $this;
88 4
        $new->container = $value;
89
90 4
        return $new;
91
    }
92
93
    /**
94
     * Returns a new instance with the specified container HTML attributes.
95
     *
96
     * @param array $values Attribute values indexed by attribute names.
97
     *
98
     * @return self
99
     */
100
    public function containerAttributes(array $values): self
101
    {
102
        $new = clone $this;
103
        $new->containerAttributes = $values;
104
105
        return $new;
106
    }
107
108
    /**
109
     * Returns a new instance with the specified container class.
110
     *
111
     * @param string $value The container class.
112
     *
113
     * @return self
114
     */
115
    public function containerClass(string $value): self
116
    {
117
        $new = clone $this;
118
        $new->containerClass = $value;
119
120
        return $new;
121
    }
122
123
    /**
124
     * Returns a new instance with the specified container tag.
125
     *
126
     * @param string $value The container tag.
127
     *
128
     * @return self
129
     */
130
    public function containerTag(string $value): self
131
    {
132
        $new = clone $this;
133
        $new->containerTag = $value;
134
        return $new;
135
    }
136
137
    /**
138
     * Returns a new instance with the specified disabled class.
139
     *
140
     * @param string $value The disabled class.
141
     *
142
     * @return self
143
     */
144
    public function disabledClass(string $value): self
145
    {
146
        $new = clone $this;
147
        $new->disabledClass = $value;
148
149
        return $new;
150
    }
151
152
    /**
153
     * Returns a new instance with the specified divider HTML attributes.
154
     *
155
     * @param array $values Attribute values indexed by attribute names.
156
     *
157
     * @return self
158
     */
159 4
    public function dividerAttributes(array $values): self
160
    {
161 4
        $new = clone $this;
162 4
        $new->dividerAttributes = $values;
163
164 4
        return $new;
165
    }
166
167
    /**
168
     * Returns a new instance with the specified divider class.
169
     *
170
     * @param string $value The divider class.
171
     *
172
     * @return self
173
     */
174 4
    public function dividerClass(string $value): self
175
    {
176 4
        $new = clone $this;
177 4
        $new->dividerClass = $value;
178
179 4
        return $new;
180
    }
181
182
    /**
183
     * Returns a new instance with the specified divider tag.
184
     *
185
     * @param string $value The divider tag.
186
     *
187
     * @return self
188
     */
189
    public function dividerTag(string $value): self
190
    {
191
        $new = clone $this;
192
        $new->dividerTag = $value;
193
194
        return $new;
195
    }
196
197
    /**
198
     * Returns a new instance with the specified header class.
199
     *
200
     * @param string $value The header class.
201
     *
202
     * @return self
203
     */
204 4
    public function headerClass(string $value): self
205
    {
206 4
        $new = clone $this;
207 4
        $new->headerClass = $value;
208
209 4
        return $new;
210
    }
211
212
    /**
213
     * Returns a new instance with the specified header tag.
214
     *
215
     * @param string $value The header tag.
216
     *
217
     * @return self
218
     */
219 4
    public function headerTag(string $value): self
220
    {
221 4
        $new = clone $this;
222 4
        $new->headerTag = $value;
223
224 4
        return $new;
225
    }
226
227
    /**
228
     * Returns a new instance with the specified item class.
229
     *
230
     * @param string $value The item class.
231
     *
232
     * @return self
233
     */
234 4
    public function itemClass(string $value): self
235
    {
236 4
        $new = clone $this;
237 4
        $new->itemClass = $value;
238
239 4
        return $new;
240
    }
241
242
    /**
243
     * Returns a new instance with the specified item container, if false, the item container will not be rendered.
244
     *
245
     * @param bool $value The item container.
246
     *
247
     * @return self
248
     */
249
    public function itemContainer(bool $value): self
250
    {
251
        $new = clone $this;
252
        $new->itemContainer = $value;
253
254
        return $new;
255
    }
256
257
    /**
258
     * Returns a new instance with the specified item container HTML attributes.
259
     *
260
     * @param array $values Attribute values indexed by attribute names.
261
     *
262
     * @return self
263
     */
264 4
    public function itemContainerAttributes(array $values): self
265
    {
266 4
        $new = clone $this;
267 4
        $new->itemContainerAttributes = $values;
268
269 4
        return $new;
270
    }
271
272
    /**
273
     * Returns a new instance with the specified item container class.
274
     *
275
     * @param string $value The item container class.
276
     *
277
     * @return self
278
     */
279
    public function itemContainerClass(string $value): self
280
    {
281
        $new = clone $this;
282
        Html::addCssClass($new->itemContainerAttributes, $value);
283
284
        return $new;
285
    }
286
287
    /**
288
     * Returns a new instance with the specified item container tag.
289
     *
290
     * @param string $value The item container tag.
291
     *
292
     * @return self
293
     */
294 4
    public function itemContainerTag(string $value): self
295
    {
296 4
        $new = clone $this;
297 4
        $new->itemContainerTag = $value;
298
299 4
        return $new;
300
    }
301
302
    /**
303
     * Returns a new instance with the specified item tag.
304
     *
305
     * @param string $value The item tag.
306
     *
307
     * @return self
308
     */
309 4
    public function itemTag(string $value): self
310
    {
311 4
        $new = clone $this;
312 4
        $new->itemTag = $value;
313
314 4
        return $new;
315
    }
316
317
    /**
318
     * List of menu items in the dropdown. Each array element can be either an HTML string, or an array representing a
319
     * single menu with the following structure:
320
     *
321
     * - label: string, required, the nav item label.
322
     * - active: bool, whether the item should be on active state or not.
323
     * - disabled: bool, whether the item should be on disabled state or not. For default `disabled` is false.
324
     * - enclose: bool, whether the item should be enclosed by a `<li>` tag or not. For default `enclose` is true.
325
     * - encodeLabel: bool, whether the label should be HTML encoded or not. For default `encodeLabel` is true.
326
     * - headerAttributes: array, HTML attributes to be rendered in the item header.
327
     * - link: string, the item's href. Defaults to "#". For default `link` is "#".
328
     * - linkAttributes: array, the HTML attributes of the item's link. For default `linkAttributes` is `[]`.
329
     * - icon: string, the item's icon. For default `icon` is ``.
330
     * - iconAttributes: array, the HTML attributes of the item's icon. For default `iconAttributes` is `[]`.
331
     * - visible: bool, optional, whether this menu item is visible. Defaults to true.
332
     * - items: array, optional, the submenu items. The structure is the same as this property.
333
     *   Note that Bootstrap doesn't support dropdown submenu. You have to add your own CSS styles to support it.
334
     * - itemsAttributes: array, optional, the HTML attributes for sub-menu.
335
     *
336
     * To insert dropdown divider use `-`.
337
     *
338
     * @param array $value
339
     *
340
     * @return static
341
     */
342 5
    public function items(array $value): self
343
    {
344 5
        $new = clone $this;
345 5
        $new->items = $value;
346
347 5
        return $new;
348
    }
349
350
    /**
351
     * Returns a new instance with the specified items container HTML attributes.
352
     *
353
     * @param array $values Attribute values indexed by attribute names.
354
     *
355
     * @return self
356
     */
357 4
    public function itemsContainerAttributes(array $values): self
358
    {
359 4
        $new = clone $this;
360 4
        $new->itemsContainerAttributes = $values;
361
362 4
        return $new;
363
    }
364
365
    /**
366
     * Returns a new instance with the specified item container class.
367
     *
368
     * @param string $value The item container class.
369
     *
370
     * @return self
371
     */
372 1
    public function itemsContainerClass(string $value): self
373
    {
374 1
        $new = clone $this;
375 1
        Html::addCssClass($new->itemsContainerAttributes, $value);
376
377 1
        return $new;
378
    }
379
380
    /**
381
     * Returns a new instance with the specified split button attributes.
382
     *
383
     * @param array $values Attribute values indexed by attribute names.
384
     *
385
     * @return self
386
     */
387
    public function splitButtonAttributes(array $values): self
388
    {
389
        $new = clone $this;
390
        $new->splitButtonAttributes = $values;
391
        return $new;
392
    }
393
394
    /**
395
     * Returns a new instance with the specified split button class.
396
     *
397
     * @param string $value The split button class.
398
     *
399
     * @return self
400
     */
401
    public function splitButtonClass(string $value): self
402
    {
403
        $new = clone $this;
404
        Html::addCssClass($new->splitButtonAttributes, $value);
405
406
        return $new;
407
    }
408
409
    /**
410
     * Returns a new instance with the specified split button span class.
411
     *
412
     * @param string $value The split button span class.
413
     *
414
     * @return self
415
     */
416
    public function splitButtonSpanClass(string $value): self
417
    {
418
        $new = clone $this;
419
        Html::addCssClass($new->splitButtonSpanAttributes, $value);
420
421
        return $new;
422
    }
423
424
    /**
425
     * Returns a new instance with the specified toggle HTML attributes.
426
     *
427
     * @param array $values Attribute values indexed by attribute names.
428
     *
429
     * @return self
430
     */
431 4
    public function toggleAttributes(array $values): self
432
    {
433 4
        $new = clone $this;
434 4
        $new->toggleAttributes = $values;
435
436 4
        return $new;
437
    }
438
439
    /**
440
     * Returns a new instance with the specified toggle class.
441
     *
442
     * @param string $value The toggle class.
443
     *
444
     * @return self
445
     */
446 1
    public function toggleClass(string $value): self
447
    {
448 1
        $new = clone $this;
449 1
        Html::addCssClass($new->toggleAttributes, $value);
450
451 1
        return $new;
452
    }
453
454
    /**
455
     * Returns a new instance with the specified toggle type, if `button` the toggle will be a button, otherwise a
456
     * `a` tag will be used.
457
     *
458
     * @param string $value The toggle tag.
459
     *
460
     * @return self
461
     */
462 4
    public function toggleType(string $value): self
463
    {
464 4
        $new = clone $this;
465 4
        $new->toggleType = $value;
466
467 4
        return $new;
468
    }
469
470
    /**
471
     * @throws CircularReferenceException|InvalidConfigException|NotFoundException|NotInstantiableException
472
     */
473 5
    protected function run(): string
474
    {
475 5
        $containerAttributes = $this->containerAttributes;
476 5
        $items = $this->normalizeItems($this->items);
477 5
        $items = $this->renderItems($items) . PHP_EOL;
478
479 5
        if ($this->containerClass !== '') {
480
            Html::addCssClass($containerAttributes, $this->containerClass);
481
        }
482
483 5
        if ($this->containerTag === '') {
484
            throw new InvalidArgumentException('Tag name must be a string and cannot be empty.');
485
        }
486
487 5
        return match ($this->container) {
488 1
            true => Html::normalTag($this->containerTag, $items, $containerAttributes)->encode(false)->render(),
489 5
            false => $items,
490
        };
491
    }
492
493 5
    private function label(array $item): string
494
    {
495 5
        if (!isset($item['label'])) {
496
            throw new InvalidArgumentException('The "label" option is required.');
497
        }
498
499 5
        if (!is_string($item['label'])) {
500
            throw new InvalidArgumentException('The "label" option must be a string.');
501
        }
502
503 5
        if ($item['label'] === '') {
504
            throw new InvalidArgumentException('The "label" cannot be an empty string.');
505
        }
506
507
        /** @var bool */
508 5
        $encodeLabels = $item['encodeLabel'] ?? true;
509
510 5
        if ($encodeLabels) {
511 5
            return htmlspecialchars($item['label'], ENT_QUOTES, 'UTF-8', false);
512
        }
513
514
        return $item['label'];
515
    }
516
517 5
    private function normalizeItems(array $items): array
518
    {
519
        /**
520
         * @psalm-var array[] $items
521
         * @psalm-suppress RedundantConditionGivenDocblockType
522
         */
523 5
        foreach ($items as $i => $child) {
524 5
            if (is_array($child)) {
525 5
                $items[$i]['label'] = $this->label($child);
526
                /** @var bool */
527 5
                $items[$i]['active'] = $child['active'] ?? false;
528
                /** @var bool */
529 5
                $items[$i]['disabled'] = $child['disabled'] ?? false;
530
                /** @var bool */
531 5
                $items[$i]['enclose'] = $child['enclose'] ?? true;
532
                /** @var array */
533 5
                $items[$i]['headerAttributes'] = $child['headerAttributes'] ?? [];
534
                /** @var string */
535 5
                $items[$i]['link'] = $child['link'] ?? '/';
536
                /** @var array */
537 5
                $items[$i]['linkAttributes'] = $child['linkAttributes'] ?? [];
538
                /** @var string */
539 5
                $items[$i]['icon'] = $child['icon'] ?? '';
540
                /** @var array */
541 5
                $items[$i]['iconAttributes'] = $child['iconAttributes'] ?? [];
542
                /** @var string */
543 5
                $items[$i]['iconClass'] = $child['iconClass'] ?? '';
544
                /** @var array */
545 5
                $items[$i]['iconContainerAttributes'] = $child['iconContainerAttributes'] ?? [];
546
                /** @var bool */
547 5
                $items[$i]['visible'] = $child['visible'] ?? true;
548
                /** @var array */
549 5
                $dropdown = $child['items'] ?? [];
550
                /** @var array */
551 5
                $items[$i]['itemsAttributes'] = $child['itemsAttributes'] ?? [];
552
553 5
                if ($dropdown !== []) {
554 4
                    $items[$i]['items'] = $this->normalizeItems($dropdown);
555
                }
556
            }
557
        }
558
559 5
        return $items;
560
    }
561
562 5
    private function renderDivider(): string
563
    {
564 5
        $dividerAttributes = $this->dividerAttributes;
565
566 5
        if ($this->itemContainerTag === '') {
567
            throw new InvalidArgumentException('Tag name must be a string and cannot be empty.');
568
        }
569
570 5
        if ($this->dividerClass !== '') {
571 5
            Html::addCssClass($dividerAttributes, $this->dividerClass);
572
        }
573
574 5
        if ($this->dividerTag === '') {
575
            throw new InvalidArgumentException('Tag name must be a string and cannot be empty.');
576
        }
577
578 5
        return Html::normalTag(
579 5
            $this->itemContainerTag,
580 5
            Html::tag($this->dividerTag, '', $dividerAttributes),
581 5
            $this->itemContainerAttributes,
582 5
        )->encode(false)->render();
583
    }
584
585
    /**
586
     * @throws CircularReferenceException|InvalidConfigException|NotFoundException|NotInstantiableException
587
     */
588 4
    private function renderDropdown(array $items, array $itemsAttributes = []): string
589
    {
590 4
        return self::widget()
591 4
            ->attributes($itemsAttributes)
0 ignored issues
show
Bug introduced by
The method attributes() does not exist on Yiisoft\Widget\Widget. It seems like you code against a sub-type of said class. However, the method does not exist in Yiisoft\Yii\Widgets\ContentDecorator or Yiisoft\Yii\Widgets\FragmentCache or Yiisoft\Yii\Widgets\Block. Are you sure you never get one of those? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

591
            ->/** @scrutinizer ignore-call */ attributes($itemsAttributes)
Loading history...
592 4
            ->container(false)
593 4
            ->dividerAttributes($this->dividerAttributes)
594 4
            ->headerClass($this->headerClass)
595 4
            ->headerTag($this->headerTag)
596 4
            ->itemClass($this->itemClass)
597 4
            ->itemContainerAttributes($this->itemContainerAttributes)
598 4
            ->itemContainerTag($this->itemContainerTag)
599 4
            ->itemTag($this->itemTag)
600 4
            ->items($items)
601 4
            ->itemsContainerAttributes($this->itemsContainerAttributes)
602 4
            ->toggleAttributes($this->toggleAttributes)
603 4
            ->toggleType($this->toggleType)
604 4
            ->render();
605
    }
606
607
    private function renderHeader(string $label, array $headerAttributes = []): string
608
    {
609
        if ($this->headerClass !== '') {
610
            Html::addCssClass($headerAttributes, $this->headerClass);
611
        }
612
613
        if ($this->itemContainerTag === '') {
614
            throw new InvalidArgumentException('Tag name must be a string and cannot be empty.');
615
        }
616
617
        if ($this->headerTag === '') {
618
            throw new InvalidArgumentException('Tag name must be a string and cannot be empty.');
619
        }
620
621
        return Html::normalTag(
622
            $this->itemContainerTag,
623
            Html::normalTag($this->headerTag, $label, $headerAttributes),
624
            $this->itemContainerAttributes,
625
        )->encode(false)->render();
626
    }
627
628
    /**
629
     * @param array $item The item to be rendered.
630
     *
631
     * @throws CircularReferenceException|InvalidConfigException|NotFoundException|NotInstantiableException
632
     */
633 5
    private function renderItem(array $item): string
634
    {
635
        /** @var bool */
636 5
        $enclose = $item['enclose'] ?? true;
637
        /** @var array */
638 5
        $headerAttributes = $item['headerAttributes'];
639
        /** @var array */
640 5
        $items = $item['items'] ?? [];
641
        /** @var array */
642 5
        $itemsAttributes = $item['itemsAttributes'] ?? [];
643
        /**
644
         * @var string $item['label']
645
         * @var string $item['icon']
646
         * @var string $item['iconClass']
647
         * @var array $item['iconAttributes']
648
         * @var array $item['iconContainerAttributes']
649
         */
650 5
        $label = $this->renderLabel(
651 5
            $item['label'],
652 5
            $item['icon'],
653 5
            $item['iconClass'],
654 5
            $item['iconAttributes'],
655 5
            $item['iconContainerAttributes'],
656
        );
657
        /** @var array */
658 5
        $lines = [];
659
        /** @var string */
660 5
        $link = $item['link'];
661
        /** @var array */
662 5
        $linkAttributes = $item['linkAttributes'];
663
        /** @var array */
664 5
        $toggleAttributes = $item['toggleAttributes'] ?? [];
665
666 5
        if ($this->itemClass !== '') {
667 1
            Html::addCssClass($linkAttributes, $this->itemClass);
668
        }
669
670 5
        if ($item['active']) {
671 1
            $linkAttributes['aria-current'] = 'true';
672 1
            Html::addCssClass($linkAttributes, [$this->activeClass]);
673
        }
674
675 5
        if ($item['disabled']) {
676 1
            Html::addCssClass($linkAttributes, $this->disabledClass);
677
        }
678
679 5
        if ($items === []) {
680 5
            $lines[] = $this->renderItemContent($label, $link, $enclose, $linkAttributes, $headerAttributes);
681
        } else {
682 4
            $itemContainer = $this->renderItemContainer($items, $itemsAttributes);
683 4
            $toggle = $this->renderToggle($label, $link, $toggleAttributes);
684 4
            $toggleSplitButton = $this->renderToggleSplitButton($label);
685
686 4
            if ($this->toggleType === 'split' && !str_contains($this->containerClass, 'dropstart')) {
687
                $lines[] = $toggleSplitButton . PHP_EOL . $toggle . PHP_EOL . $itemContainer;
688 4
            } elseif ($this->toggleType === 'split' && str_contains($this->containerClass, 'dropstart')) {
689
                $lines[] = $toggle . PHP_EOL . $itemContainer . PHP_EOL . $toggleSplitButton;
690
            } else {
691 4
                $lines[] = $toggle . PHP_EOL . $itemContainer;
692
            }
693
        }
694
695
        /** @psalm-var string[] $lines */
696 5
        return implode(PHP_EOL, $lines);
697
    }
698
699
    /**
700
     * @throws CircularReferenceException|InvalidConfigException|NotFoundException|NotInstantiableException
701
     */
702 4
    private function renderItemContainer(array $items, array $itemsContainerAttributes = []): string
703
    {
704 4
        if ($itemsContainerAttributes === []) {
705 4
            $itemsContainerAttributes = $this->itemsContainerAttributes;
706
        }
707
708 4
        if (isset($this->attributes['id'])) {
709
            $itemsContainerAttributes['aria-labelledby'] = $this->attributes['id'];
710
        }
711
712 4
        if ($this->itemsContainerTag === '') {
713
            throw new InvalidArgumentException('Tag name must be a string and cannot be empty.');
714
        }
715
716 4
        return Html::normalTag(
717 4
            $this->itemsContainerTag,
718 4
            $this->renderDropdown($items, $itemsContainerAttributes),
719
            $itemsContainerAttributes,
720 4
        )->encode(false)->render();
721
    }
722
723 5
    private function renderItemContent(
724
        string $label,
725
        string $link,
726
        bool $enclose,
727
        array $linkAttributes = [],
728
        array $headerAttributes = []
729
    ): string {
730
        return match (true) {
731 5
            $label === '-' => $this->renderDivider(),
732 5
            $enclose === false => $label,
733 5
            $link === '' => $this->renderHeader($label, $headerAttributes),
734 5
            default => $this->renderItemLink($label, $link, $linkAttributes),
735
        };
736
    }
737
738
    /**
739
     * @throws CircularReferenceException|InvalidConfigException|NotFoundException|NotInstantiableException
740
     */
741 5
    private function renderItems(array $items = []): string
742
    {
743 5
        $lines = [];
744
745
        /** @var array|string $item */
746 5
        foreach ($items as $item) {
747 5
            $lines[] = match (gettype($item)) {
748 5
                'array' => $this->renderItem($item),
749 5
                'string' => $this->renderDivider(),
750
            };
751
        }
752
753 5
        return PHP_EOL . implode(PHP_EOL, $lines);
754
    }
755
756 5
    private function renderLabel(
757
        string $label,
758
        string $icon,
759
        string $iconClass,
760
        array $iconAttributes = [],
761
        array $iconContainerAttributes = []
762
    ): string {
763 5
        $html = '';
764
765 5
        if ($iconClass !== '') {
766
            Html::addCssClass($iconAttributes, $iconClass);
767
        }
768
769 5
        if ($icon !== '' || $iconClass !== '') {
770
            $i = I::tag()->addAttributes($iconAttributes)->content($icon)->encode(false)->render();
771
            $html = Span::tag()->addAttributes($iconContainerAttributes)->content($i)->encode(false)->render();
772
        }
773
774 5
        if ($label !== '') {
775 5
            $html .= $label;
776
        }
777
778 5
        return $html;
779
    }
780
781 5
    private function renderItemLink(
782
        string $label,
783
        string $link,
784
        array $linkAttributes = []
785
    ): string {
786 5
        $itemContainerAttributes = $this->itemContainerAttributes;
787 5
        $linkAttributes['href'] = $link;
788
789 5
        if ($this->itemTag === '') {
790
            throw new InvalidArgumentException('Tag name must be a string and cannot be empty.');
791
        }
792
793 5
        $a = Html::normalTag($this->itemTag, $label, $linkAttributes)->encode(false);
794
795 5
        if ($this->itemContainerTag === '') {
796
            throw new InvalidArgumentException('Tag name must be a string and cannot be empty.');
797
        }
798
799 5
        return match ($this->itemContainer) {
800 5
            true => Html::normalTag($this->itemContainerTag, $a, $itemContainerAttributes)->encode(false)->render(),
801 5
            default => $a->render(),
802
        };
803
    }
804
805 4
    private function renderToggle(string $label, string $link, array $toggleAttributes = []): string
806
    {
807 4
        if ($toggleAttributes === []) {
808 4
            $toggleAttributes = $this->toggleAttributes;
809
        }
810
811 4
        if (isset($this->attributes['id'])) {
812
            $toggleAttributes['id'] = $this->attributes['id'];
813
        }
814
815 4
        return match ($this->toggleType) {
816 4
            'link' => $this->renderToggleLink($label, $link, $toggleAttributes),
817
            'split' => $this->renderToggleSplit($label, $toggleAttributes),
818 4
            default => $this->renderToggleButton($label, $toggleAttributes),
819
        };
820
    }
821
822
    private function renderToggleButton(string $label, array $toggleAttributes = []): string
823
    {
824
        return Button::tag()->addAttributes($toggleAttributes)->content($label)->render();
825
    }
826
827 4
    private function renderToggleLink(string $label, string $link, array $toggleAttributes = []): string
828
    {
829 4
        return A::tag()->addAttributes($toggleAttributes)->content($label)->href($link)->render();
830
    }
831
832
    private function renderToggleSplit(string $label, array $toggleAttributes = []): string
833
    {
834
        return Button::tag()
835
            ->addAttributes($toggleAttributes)
836
            ->content(Span::tag()->addAttributes($this->splitButtonSpanAttributes)->content($label))
837
            ->render();
838
    }
839
840 4
    private function renderToggleSplitButton(string $label): string
841
    {
842 4
        return Button::tag()->addAttributes($this->splitButtonAttributes)->content($label)->render();
843
    }
844
}
845