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
10 changes: 10 additions & 0 deletions .idea/phpunit.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions .idea/symfony2.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

40 changes: 40 additions & 0 deletions docs/actions-and-cells.md
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,46 @@ The host application must load the icon CSS if it wants icons to appear.

More details are available in [`icons.md`](icons.md).

## Action icon rendering

Actions can define an optional icon CSS class:

```php
$definition->addRowAction(
name: 'view',
route: 'app_user_show',
label: 'View',
icon: 'bi bi-eye',
routeParameters: [
'id' => 'e.id',
],
);
```

Icons render before the label by default.

To render the icon after the label:

```php
use Zhortein\DatatableBundle\Enum\ActionIconPosition;

$definition->addRowAction(
name: 'view',
route: 'app_user_show',
label: 'View',
icon: 'bi bi-arrow-right',
iconPosition: ActionIconPosition::After,
routeParameters: [
'id' => 'e.id',
],
);
```

The bundle does not require a specific icon library.

The host application must load the relevant icon CSS.


## Typed cell templates

The renderer supports type-specific cell templates.
Expand Down
21 changes: 20 additions & 1 deletion docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -1111,7 +1111,26 @@ The bundle does not require Bootstrap Icons, FontAwesome, Symfony UX Icons or an

Action icons render as decorative spans with `aria-hidden="true"`, while the action label remains visible and accessible.

The strategy is documented in `docs/icons.md`.
The strategy is documented in [`icons.md`](icons.md).

### Action icon rendering options

Actions can render optional CSS-class based icons.

The current strategy remains dependency-free:

- no mandatory Bootstrap Icons dependency;
- no mandatory FontAwesome dependency;
- no SVG icon provider yet.

Action icons render as decorative spans with `aria-hidden="true"`.

Supported icon positions:

- before label;
- after label.

Labels remain visible to preserve accessibility.

### Cell template reference

Expand Down
8 changes: 8 additions & 0 deletions src/Definition/ActionDefinition.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

namespace Zhortein\DatatableBundle\Definition;

use Zhortein\DatatableBundle\Enum\ActionIconPosition;

final readonly class ActionDefinition
{
/**
Expand All @@ -15,6 +17,7 @@ public function __construct(
private string $route,
private ?string $label = null,
private ?string $icon = null,
private ActionIconPosition $iconPosition = ActionIconPosition::Before,
private string $httpMethod = 'GET',
private ?string $confirmationMessage = null,
private ?string $className = null,
Expand Down Expand Up @@ -43,6 +46,11 @@ public function getIcon(): ?string
return $this->icon;
}

public function getIconPosition(): ActionIconPosition
{
return $this->iconPosition;
}

public function getHttpMethod(): string
{
return $this->httpMethod;
Expand Down
5 changes: 5 additions & 0 deletions src/Definition/DatatableDefinition.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace Zhortein\DatatableBundle\Definition;

use Zhortein\DatatableBundle\Enum\ActionIconPosition;
use Zhortein\DatatableBundle\Enum\FilterOperator;
use Zhortein\DatatableBundle\Enum\FilterType;
use Zhortein\DatatableBundle\Enum\JoinType;
Expand Down Expand Up @@ -198,6 +199,7 @@ public function addRowAction(
string $route,
?string $label = null,
?string $icon = null,
ActionIconPosition $iconPosition = ActionIconPosition::Before,
string $httpMethod = 'GET',
?string $confirmationMessage = null,
?string $className = null,
Expand All @@ -209,6 +211,7 @@ public function addRowAction(
route: $route,
label: $label,
icon: $icon,
iconPosition: $iconPosition,
httpMethod: $httpMethod,
confirmationMessage: $confirmationMessage,
className: $className,
Expand Down Expand Up @@ -236,6 +239,7 @@ public function addGlobalAction(
string $route,
?string $label = null,
?string $icon = null,
ActionIconPosition $iconPosition = ActionIconPosition::Before,
string $httpMethod = 'GET',
?string $confirmationMessage = null,
?string $className = null,
Expand All @@ -247,6 +251,7 @@ public function addGlobalAction(
route: $route,
label: $label,
icon: $icon,
iconPosition: $iconPosition,
httpMethod: $httpMethod,
confirmationMessage: $confirmationMessage,
className: $className,
Expand Down
20 changes: 20 additions & 0 deletions src/Enum/ActionIconPosition.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

declare(strict_types=1);

namespace Zhortein\DatatableBundle\Enum;

enum ActionIconPosition: string
{
case Before = 'before';
case After = 'after';

public static function fromNullableString(?string $position): self
{
if (null === $position || '' === trim($position)) {
return self::Before;
}

return self::tryFrom(strtolower(trim($position))) ?? self::Before;
}
}
3 changes: 2 additions & 1 deletion src/Renderer/DatatableRenderer.php
Original file line number Diff line number Diff line change
Expand Up @@ -278,7 +278,7 @@ private function isActionVisible(ActionDefinition $action, DatatableDefinition $
}

/**
* @return array{name: string, label: string|null, icon: string|null, url: string, httpMethod: string, confirmationMessage: string|null, csrfToken: string|null, className: string|null, attributes: array<string, string>}
* @return array{name: string, label: string|null, icon: string|null, iconPosition: string, url: string, httpMethod: string, confirmationMessage: string|null, csrfToken: string|null, className: string|null, attributes: array<string, string>}
*/
private function normalizeAction(ActionDefinition $action, string $url): array
{
Expand All @@ -288,6 +288,7 @@ private function normalizeAction(ActionDefinition $action, string $url): array
'name' => $action->getName(),
'label' => $action->getLabel(),
'icon' => $action->getIcon(),
'iconPosition' => $action->getIconPosition()->value,
'url' => $url,
'httpMethod' => $httpMethod,
'confirmationMessage' => $action->getConfirmationMessage(),
Expand Down
24 changes: 18 additions & 6 deletions templates/bootstrap/_action.html.twig
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
{% set has_icon = action.icon is not null and action.icon is not empty %}
{% set icon_position = action.iconPosition ?? 'before' %}
{% set label = action.label ?? action.name %}

{% if action.httpMethod == 'GET' %}
<a
href="{{ action.url }}"
Expand All @@ -11,11 +15,15 @@
{{ attribute_name }}="{{ attribute_value }}"
{% endfor %}
>
{% if action.icon is not null %}
<span class="{{ action.icon }}" aria-hidden="true"></span>
{% if has_icon and icon_position == 'before' %}
<span class="{{ action.icon }} me-1" aria-hidden="true"></span>
{% endif %}

{{ action.label ?? action.name }}
<span>{{ label }}</span>

{% if has_icon and icon_position == 'after' %}
<span class="{{ action.icon }} ms-1" aria-hidden="true"></span>
{% endif %}
</a>
{% else %}
<form
Expand All @@ -40,11 +48,15 @@
{{ attribute_name }}="{{ attribute_value }}"
{% endfor %}
>
{% if action.icon is not null %}
<span class="{{ action.icon }}" aria-hidden="true"></span>
{% if has_icon and icon_position == 'before' %}
<span class="{{ action.icon }} me-1" aria-hidden="true"></span>
{% endif %}

{{ action.label ?? action.name }}
<span>{{ label }}</span>

{% if has_icon and icon_position == 'after' %}
<span class="{{ action.icon }} ms-1" aria-hidden="true"></span>
{% endif %}
</button>
</form>
{% endif %}
25 changes: 25 additions & 0 deletions tests/Unit/Enum/ActionIconPositionTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php

declare(strict_types=1);

namespace Zhortein\DatatableBundle\Tests\Unit\Enum;

use PHPUnit\Framework\TestCase;
use Zhortein\DatatableBundle\Enum\ActionIconPosition;

final class ActionIconPositionTest extends TestCase
{
public function test_it_creates_icon_position_from_valid_string(): void
{
self::assertSame(ActionIconPosition::Before, ActionIconPosition::fromNullableString('before'));
self::assertSame(ActionIconPosition::After, ActionIconPosition::fromNullableString('after'));
self::assertSame(ActionIconPosition::After, ActionIconPosition::fromNullableString(' AFTER '));
}

public function test_it_falls_back_to_before_for_null_empty_or_unknown_value(): void
{
self::assertSame(ActionIconPosition::Before, ActionIconPosition::fromNullableString(null));
self::assertSame(ActionIconPosition::Before, ActionIconPosition::fromNullableString(''));
self::assertSame(ActionIconPosition::Before, ActionIconPosition::fromNullableString('unknown'));
}
}
Loading