diff --git a/application/applicationextension/common/ApplicationPopupMenuItem.php b/application/applicationextension/common/ApplicationPopupMenuItem.php index 17972cf60e..f0ecaccbe6 100644 --- a/application/applicationextension/common/ApplicationPopupMenuItem.php +++ b/application/applicationextension/common/ApplicationPopupMenuItem.php @@ -1,5 +1,9 @@ + * @since 3.3.0 + */ + protected function GetMenuItemAdditionalData(): array + { + $aAdditionalData = []; + + $aDataAttributes = $this->GetDataAttributes()->GetAttributes(); + if (!empty($aDataAttributes)) { + $aAdditionalData['data_attributes'] = $aDataAttributes; + } + + $aAriaAttributes = $this->GetAriaAttributes()->GetAttributes(); + if (!empty($aAriaAttributes)) { + $aAdditionalData['aria_attributes'] = $aAriaAttributes; + } + + $oGrouping = $this->GetGrouping(); + if ($oGrouping !== null) { + $aAdditionalData['grouping_uid'] = $oGrouping->GetUID(); + } + + return $aAdditionalData; + } + /** @ignore */ public function GetLinkedScripts() { return []; } + + /** + * @api + * @since 3.3.0 + */ + public function GetAriaAttributes(): AriaAttributes + { + return $this->oAriaAttributes ??= new AriaAttributes(); + } + + /** + * @api + * @since 3.3.0 + */ + public function GetDataAttributes(): DataAttributes + { + return $this->oDataAttributes ??= new DataAttributes(); + } + + /** + * @api + * @since 3.3.0 + */ + public function GetGrouping(): ?Grouping + { + return $this->oGrouping; + } + + /** + * @param Grouping|null $oGrouping + * @return static + * @api + * @since 3.3.0 + */ + public function SetGrouping(?Grouping $oGrouping): static + { + $this->oGrouping = $oGrouping; + + return $this; + } } diff --git a/application/applicationextension/common/JSPopupMenuItem.php b/application/applicationextension/common/JSPopupMenuItem.php index 7e1bea2443..b3308e53b2 100644 --- a/application/applicationextension/common/JSPopupMenuItem.php +++ b/application/applicationextension/common/JSPopupMenuItem.php @@ -40,7 +40,7 @@ class JSPopupMenuItem extends ApplicationPopupMenuItem public function GetMenuItem() { // Note: the semicolumn is a must here! - return [ + $aData = [ 'label' => $this->GetLabel(), 'onclick' => $this->GetJsCode().'; return false;', 'url' => $this->GetUrl(), @@ -48,6 +48,8 @@ class JSPopupMenuItem extends ApplicationPopupMenuItem 'icon_class' => $this->sIconClass, 'tooltip' => $this->sTooltip, ]; + + return array_merge($aData, $this->GetMenuItemAdditionalData()); } /** @ignore */ diff --git a/application/applicationextension/common/SeparatorPopupMenuItem.php b/application/applicationextension/common/SeparatorPopupMenuItem.php index 3be00a0f78..f1cf27c9a1 100644 --- a/application/applicationextension/common/SeparatorPopupMenuItem.php +++ b/application/applicationextension/common/SeparatorPopupMenuItem.php @@ -24,6 +24,12 @@ class SeparatorPopupMenuItem extends ApplicationPopupMenuItem /** @ignore */ public function GetMenuItem() { - return ['label' => '', 'url' => '', 'css_classes' => $this->aCssClasses]; + $aData = [ + 'label' => '', + 'url' => '', + 'css_classes' => $this->aCssClasses, + ]; + + return array_merge($aData, $this->GetMenuItemAdditionalData()); } } diff --git a/application/applicationextension/common/URLPopupMenuItem.php b/application/applicationextension/common/URLPopupMenuItem.php index adf737f56b..4276627fda 100644 --- a/application/applicationextension/common/URLPopupMenuItem.php +++ b/application/applicationextension/common/URLPopupMenuItem.php @@ -35,13 +35,15 @@ class URLPopupMenuItem extends ApplicationPopupMenuItem /** @ignore */ public function GetMenuItem() { - return ['label' => $this->GetLabel(), + $aData = ['label' => $this->GetLabel(), 'url' => $this->GetUrl(), 'target' => $this->GetTarget(), 'css_classes' => $this->aCssClasses, 'icon_class' => $this->sIconClass, 'tooltip' => $this->sTooltip, ]; + + return array_merge($aData, $this->GetMenuItemAdditionalData()); } /** @ignore */ diff --git a/application/applicationextension/common/iAriaAttributesProvider.php b/application/applicationextension/common/iAriaAttributesProvider.php new file mode 100644 index 0000000000..8f7298706b --- /dev/null +++ b/application/applicationextension/common/iAriaAttributesProvider.php @@ -0,0 +1,15 @@ + Attribute name => value, ready to be escaped and printed */ + public function GetHtmlAttributes(): array; +} diff --git a/application/displayblock.class.inc.php b/application/displayblock.class.inc.php index b7940b90dc..e4da37dfac 100644 --- a/application/displayblock.class.inc.php +++ b/application/displayblock.class.inc.php @@ -2292,6 +2292,17 @@ class MenuBlock extends DisplayBlock } // ResourceId should not be sanitized $oActionButton->AddDataAttribute('resource-id', $sActionId); + if (isset($aAction['data_attributes']) && is_array($aAction['data_attributes'])) { + foreach ($aAction['data_attributes'] as $sName => $sValue) { + $sDataAttributeName = $sName; + $oActionButton->AddDataAttribute($sDataAttributeName, (string) $sValue); + } + } + if (isset($aAction['aria_attributes']) && is_array($aAction['aria_attributes'])) { + foreach ($aAction['aria_attributes'] as $sName => $sValue) { + $oActionButton->AddAriaAttribute((string) $sName, (string) $sValue); + } + } $oActionButton->AddCSSClasses(['ibo-action-button', 'ibo-regular-action-button']); if (empty($sLabel)) { if (empty($aAction['tooltip'])) { diff --git a/lib/composer/autoload_classmap.php b/lib/composer/autoload_classmap.php index b54be5396b..253375f5f3 100644 --- a/lib/composer/autoload_classmap.php +++ b/lib/composer/autoload_classmap.php @@ -160,6 +160,9 @@ return array( 'Combodo\\iTop\\Application\\UI\\Base\\Common\\Action\\tActionCommon' => $baseDir . '/sources/Application/UI/Base/Common/Action/tActionCommon.php', 'Combodo\\iTop\\Application\\UI\\Base\\Common\\Action\\tActionJs' => $baseDir . '/sources/Application/UI/Base/Common/Action/tActionJs.php', 'Combodo\\iTop\\Application\\UI\\Base\\Common\\Action\\tActionURL' => $baseDir . '/sources/Application/UI/Base/Common/Action/tActionURL.php', + 'Combodo\\iTop\\Application\\UI\\Base\\Common\\Metadata\\AriaAttributes' => $baseDir . '/sources/Application/UI/Base/Common/Metadata/AriaAttributes.php', + 'Combodo\\iTop\\Application\\UI\\Base\\Common\\Metadata\\DataAttributes' => $baseDir . '/sources/Application/UI/Base/Common/Metadata/DataAttributes.php', + 'Combodo\\iTop\\Application\\UI\\Base\\Common\\Metadata\\Grouping' => $baseDir . '/sources/Application/UI/Base/Common/Metadata/Grouping.php', 'Combodo\\iTop\\Application\\UI\\Base\\Component\\Alert\\Alert' => $baseDir . '/sources/Application/UI/Base/Component/Alert/Alert.php', 'Combodo\\iTop\\Application\\UI\\Base\\Component\\Alert\\AlertUIBlockFactory' => $baseDir . '/sources/Application/UI/Base/Component/Alert/AlertUIBlockFactory.php', 'Combodo\\iTop\\Application\\UI\\Base\\Component\\Badge\\Badge' => $baseDir . '/sources/Application/UI/Base/Component/Badge/Badge.php', @@ -225,8 +228,6 @@ return array( 'Combodo\\iTop\\Application\\UI\\Base\\Component\\Input\\tInputLabel' => $baseDir . '/sources/Application/UI/Base/Component/Input/tInputLabel.php', 'Combodo\\iTop\\Application\\UI\\Base\\Component\\MedallionIcon\\MedallionIcon' => $baseDir . '/sources/Application/UI/Base/Component/MedallionIcon/MedallionIcon.php', 'Combodo\\iTop\\Application\\UI\\Base\\Component\\Modal\\DoNotShowAgainOptionBlock' => $baseDir . '/sources/Application/UI/Base/Component/Modal/DoNotShowAgainOptionBlock.php', - 'Combodo\\iTop\\Application\\UI\\Base\\Component\\OverflowLine\\OverflowLine' => $baseDir . '/sources/Application/UI/Base/Component/OverflowLine/OverflowLine.php', - 'Combodo\\iTop\\Application\\UI\\Base\\Component\\OverflowLine\\OverflowLineUIBlockFactory' => $baseDir . '/sources/Application/UI/Base/Component/OverflowLine/OverflowLineUIBlockFactory.php', 'Combodo\\iTop\\Application\\UI\\Base\\Component\\Panel\\Panel' => $baseDir . '/sources/Application/UI/Base/Component/Panel/Panel.php', 'Combodo\\iTop\\Application\\UI\\Base\\Component\\Panel\\PanelUIBlockFactory' => $baseDir . '/sources/Application/UI/Base/Component/Panel/PanelUIBlockFactory.php', 'Combodo\\iTop\\Application\\UI\\Base\\Component\\Pill\\Pill' => $baseDir . '/sources/Application/UI/Base/Component/Pill/Pill.php', @@ -3967,6 +3968,7 @@ return array( 'cmdbAbstractObject' => $baseDir . '/application/cmdbabstract.class.inc.php', 'cmdbDataGenerator' => $baseDir . '/core/data.generator.class.inc.php', 'iApplicationUIExtension' => $baseDir . '/application/applicationextension/backoffice/iApplicationUIExtension.php', + 'iAriaAttributesProvider' => $baseDir . '/application/applicationextension/common/iAriaAttributesProvider.php', 'iBackgroundProcess' => $baseDir . '/core/backgroundprocess.inc.php', 'iBackofficeDictEntriesExtension' => $baseDir . '/application/applicationextension/backoffice/iBackofficeDictEntriesExtension.php', 'iBackofficeDictEntriesPrefixesExtension' => $baseDir . '/application/applicationextension/backoffice/iBackofficeDictEntriesPrefixesExtension.php', @@ -3982,8 +3984,11 @@ return array( 'iCMDBChangeOp' => $baseDir . '/core/cmdbchangeop.class.inc.php', 'iDBObjectSetIterator' => $baseDir . '/core/dbobjectiterator.php', 'iDBObjectURLMaker' => $baseDir . '/application/applicationcontext.class.inc.php', + 'iDataAttributesProvider' => $baseDir . '/application/applicationextension/common/iDataAttributesProvider.php', 'iDisplay' => $baseDir . '/core/dbobject.class.php', 'iFieldRendererMappingsExtension' => $baseDir . '/application/applicationextension/backoffice/iFieldRendererMappingsExtension.php', + 'iGroupingProvider' => $baseDir . '/application/applicationextension/common/iGroupingProvider.php', + 'iHtmlAttributesSource' => $baseDir . '/application/applicationextension/common/iHtmlAttributesSource.php', 'iKPILoggerExtension' => $baseDir . '/application/applicationextension/iKPILoggerExtension.php', 'iLogFileNameBuilder' => $baseDir . '/core/log.class.inc.php', 'iLoginExtension' => $baseDir . '/application/applicationextension/login/iLoginExtension.php', @@ -4028,5 +4033,5 @@ return array( 'privUITransactionFile' => $baseDir . '/application/transaction.class.inc.php', 'privUITransactionSession' => $baseDir . '/application/transaction.class.inc.php', 'utils' => $baseDir . '/application/utils.inc.php', - '�' => $vendorDir . '/symfony/cache/Traits/ValueWrapper.php', + '©' => $vendorDir . '/symfony/cache/Traits/ValueWrapper.php', ); diff --git a/lib/composer/autoload_static.php b/lib/composer/autoload_static.php index bd53a856b3..eb7bd78b88 100644 --- a/lib/composer/autoload_static.php +++ b/lib/composer/autoload_static.php @@ -561,6 +561,9 @@ class ComposerStaticInitfc0e9e9dea11dcbb6272414776c30685 'Combodo\\iTop\\Application\\UI\\Base\\Common\\Action\\tActionCommon' => __DIR__ . '/../..' . '/sources/Application/UI/Base/Common/Action/tActionCommon.php', 'Combodo\\iTop\\Application\\UI\\Base\\Common\\Action\\tActionJs' => __DIR__ . '/../..' . '/sources/Application/UI/Base/Common/Action/tActionJs.php', 'Combodo\\iTop\\Application\\UI\\Base\\Common\\Action\\tActionURL' => __DIR__ . '/../..' . '/sources/Application/UI/Base/Common/Action/tActionURL.php', + 'Combodo\\iTop\\Application\\UI\\Base\\Common\\Metadata\\AriaAttributes' => __DIR__ . '/../..' . '/sources/Application/UI/Base/Common/Metadata/AriaAttributes.php', + 'Combodo\\iTop\\Application\\UI\\Base\\Common\\Metadata\\DataAttributes' => __DIR__ . '/../..' . '/sources/Application/UI/Base/Common/Metadata/DataAttributes.php', + 'Combodo\\iTop\\Application\\UI\\Base\\Common\\Metadata\\Grouping' => __DIR__ . '/../..' . '/sources/Application/UI/Base/Common/Metadata/Grouping.php', 'Combodo\\iTop\\Application\\UI\\Base\\Component\\Alert\\Alert' => __DIR__ . '/../..' . '/sources/Application/UI/Base/Component/Alert/Alert.php', 'Combodo\\iTop\\Application\\UI\\Base\\Component\\Alert\\AlertUIBlockFactory' => __DIR__ . '/../..' . '/sources/Application/UI/Base/Component/Alert/AlertUIBlockFactory.php', 'Combodo\\iTop\\Application\\UI\\Base\\Component\\Badge\\Badge' => __DIR__ . '/../..' . '/sources/Application/UI/Base/Component/Badge/Badge.php', @@ -626,8 +629,6 @@ class ComposerStaticInitfc0e9e9dea11dcbb6272414776c30685 'Combodo\\iTop\\Application\\UI\\Base\\Component\\Input\\tInputLabel' => __DIR__ . '/../..' . '/sources/Application/UI/Base/Component/Input/tInputLabel.php', 'Combodo\\iTop\\Application\\UI\\Base\\Component\\MedallionIcon\\MedallionIcon' => __DIR__ . '/../..' . '/sources/Application/UI/Base/Component/MedallionIcon/MedallionIcon.php', 'Combodo\\iTop\\Application\\UI\\Base\\Component\\Modal\\DoNotShowAgainOptionBlock' => __DIR__ . '/../..' . '/sources/Application/UI/Base/Component/Modal/DoNotShowAgainOptionBlock.php', - 'Combodo\\iTop\\Application\\UI\\Base\\Component\\OverflowLine\\OverflowLine' => __DIR__ . '/../..' . '/sources/Application/UI/Base/Component/OverflowLine/OverflowLine.php', - 'Combodo\\iTop\\Application\\UI\\Base\\Component\\OverflowLine\\OverflowLineUIBlockFactory' => __DIR__ . '/../..' . '/sources/Application/UI/Base/Component/OverflowLine/OverflowLineUIBlockFactory.php', 'Combodo\\iTop\\Application\\UI\\Base\\Component\\Panel\\Panel' => __DIR__ . '/../..' . '/sources/Application/UI/Base/Component/Panel/Panel.php', 'Combodo\\iTop\\Application\\UI\\Base\\Component\\Panel\\PanelUIBlockFactory' => __DIR__ . '/../..' . '/sources/Application/UI/Base/Component/Panel/PanelUIBlockFactory.php', 'Combodo\\iTop\\Application\\UI\\Base\\Component\\Pill\\Pill' => __DIR__ . '/../..' . '/sources/Application/UI/Base/Component/Pill/Pill.php', @@ -4368,6 +4369,7 @@ class ComposerStaticInitfc0e9e9dea11dcbb6272414776c30685 'cmdbAbstractObject' => __DIR__ . '/../..' . '/application/cmdbabstract.class.inc.php', 'cmdbDataGenerator' => __DIR__ . '/../..' . '/core/data.generator.class.inc.php', 'iApplicationUIExtension' => __DIR__ . '/../..' . '/application/applicationextension/backoffice/iApplicationUIExtension.php', + 'iAriaAttributesProvider' => __DIR__ . '/../..' . '/application/applicationextension/common/iAriaAttributesProvider.php', 'iBackgroundProcess' => __DIR__ . '/../..' . '/core/backgroundprocess.inc.php', 'iBackofficeDictEntriesExtension' => __DIR__ . '/../..' . '/application/applicationextension/backoffice/iBackofficeDictEntriesExtension.php', 'iBackofficeDictEntriesPrefixesExtension' => __DIR__ . '/../..' . '/application/applicationextension/backoffice/iBackofficeDictEntriesPrefixesExtension.php', @@ -4383,8 +4385,11 @@ class ComposerStaticInitfc0e9e9dea11dcbb6272414776c30685 'iCMDBChangeOp' => __DIR__ . '/../..' . '/core/cmdbchangeop.class.inc.php', 'iDBObjectSetIterator' => __DIR__ . '/../..' . '/core/dbobjectiterator.php', 'iDBObjectURLMaker' => __DIR__ . '/../..' . '/application/applicationcontext.class.inc.php', + 'iDataAttributesProvider' => __DIR__ . '/../..' . '/application/applicationextension/common/iDataAttributesProvider.php', 'iDisplay' => __DIR__ . '/../..' . '/core/dbobject.class.php', 'iFieldRendererMappingsExtension' => __DIR__ . '/../..' . '/application/applicationextension/backoffice/iFieldRendererMappingsExtension.php', + 'iGroupingProvider' => __DIR__ . '/../..' . '/application/applicationextension/common/iGroupingProvider.php', + 'iHtmlAttributesSource' => __DIR__ . '/../..' . '/application/applicationextension/common/iHtmlAttributesSource.php', 'iKPILoggerExtension' => __DIR__ . '/../..' . '/application/applicationextension/iKPILoggerExtension.php', 'iLogFileNameBuilder' => __DIR__ . '/../..' . '/core/log.class.inc.php', 'iLoginExtension' => __DIR__ . '/../..' . '/application/applicationextension/login/iLoginExtension.php', @@ -4429,7 +4434,7 @@ class ComposerStaticInitfc0e9e9dea11dcbb6272414776c30685 'privUITransactionFile' => __DIR__ . '/../..' . '/application/transaction.class.inc.php', 'privUITransactionSession' => __DIR__ . '/../..' . '/application/transaction.class.inc.php', 'utils' => __DIR__ . '/../..' . '/application/utils.inc.php', - '�' => __DIR__ . '/..' . '/symfony/cache/Traits/ValueWrapper.php', + '©' => __DIR__ . '/..' . '/symfony/cache/Traits/ValueWrapper.php', ); public static function getInitializer(ClassLoader $loader) diff --git a/sources/Application/UI/Base/Common/Metadata/AriaAttributes.php b/sources/Application/UI/Base/Common/Metadata/AriaAttributes.php new file mode 100644 index 0000000000..6c7e31c641 --- /dev/null +++ b/sources/Application/UI/Base/Common/Metadata/AriaAttributes.php @@ -0,0 +1,65 @@ +sLabel = $sLabel; + + return $this; + } + + public function GetLabel(): ?string + { + return $this->sLabel; + } + + /** + * Set several attributes at once, by ARIA attribute name ("label", "description", ...) + * + * @param array $aAttributes + * @since 3.3.0 + */ + public function SetMultiple(array $aAttributes): self + { + foreach ($aAttributes as $sName => $sValue) { + $sMethod = 'Set'.$sName; + if ($sMethod !== 'SetMultiple' && method_exists($this, $sMethod)) { + $this->$sMethod($sValue); + } + } + + return $this; + } + + public function GetAttributes(): array + { + $aAttributes = []; + if ($this->sLabel !== null) { + $aAttributes['label'] = $this->sLabel; + } + + return $aAttributes; + } + + public function GetHtmlAttributes(): array + { + $aAttributes = []; + if ($this->sLabel !== null) { + $aAttributes['aria-label'] = $this->sLabel; + } + + return $aAttributes; + } +} diff --git a/sources/Application/UI/Base/Common/Metadata/DataAttributes.php b/sources/Application/UI/Base/Common/Metadata/DataAttributes.php new file mode 100644 index 0000000000..095b8d44b3 --- /dev/null +++ b/sources/Application/UI/Base/Common/Metadata/DataAttributes.php @@ -0,0 +1,82 @@ + */ + private array $aValues = []; + + public function Set(string $sName, string $sValue): self + { + $this->aValues[static::NormalizeName($sName)] = $sValue; + + return $this; + } + + /** @param array $aValues */ + public function SetMultiple(array $aValues): self + { + foreach ($aValues as $sName => $sValue) { + $this->Set($sName, $sValue); + } + + return $this; + } + + public function Get(string $sName): ?string + { + return $this->aValues[static::NormalizeName($sName)] ?? null; + } + + public function Has(string $sName): bool + { + return array_key_exists(static::NormalizeName($sName), $this->aValues); + } + + public function Remove(string $sName): self + { + unset($this->aValues[static::NormalizeName($sName)]); + + return $this; + } + + /** @return array Names without the "data-" prefix */ + public function GetAttributes(): array + { + return $this->aValues; + } + + public function GetHtmlAttributes(): array + { + $aAttributes = []; + foreach ($this->aValues as $sName => $sValue) { + $aAttributes['data-'.$sName] = $sValue; + } + + return $aAttributes; + } + + /** @throws \InvalidArgumentException */ + private static function NormalizeName(string $sName): string + { + $sNormalized = strtolower(trim($sName)); + if (str_starts_with($sNormalized, 'data-')) { + $sNormalized = substr($sNormalized, 5); + } + if (preg_match(static::NAME_PATTERN, $sNormalized) !== 1) { + throw new \InvalidArgumentException(sprintf('Invalid data attribute name "%s"', $sName)); + } + + return $sNormalized; + } +} diff --git a/sources/Application/UI/Base/Common/Metadata/Grouping.php b/sources/Application/UI/Base/Common/Metadata/Grouping.php new file mode 100644 index 0000000000..ad6ff28e72 --- /dev/null +++ b/sources/Application/UI/Base/Common/Metadata/Grouping.php @@ -0,0 +1,24 @@ +sUID = $sUID; + } + + public function GetUID(): string + { + return $this->sUID; + } +} diff --git a/sources/Application/UI/Base/Component/Button/Button.php b/sources/Application/UI/Base/Component/Button/Button.php index 7921aa5664..ac814ae271 100644 --- a/sources/Application/UI/Base/Component/Button/Button.php +++ b/sources/Application/UI/Base/Component/Button/Button.php @@ -21,6 +21,7 @@ namespace Combodo\iTop\Application\UI\Base\Component\Button; use Combodo\iTop\Application\UI\Base\UIBlock; +use utils; /** * Class Button @@ -271,4 +272,20 @@ class Button extends UIBlock return $this; } + + public function GetAriaAttributes(): array + { + $aDefaultValues = []; + + // Default aria-label is the tooltip + if (utils::IsNotNullOrEmptyString($this->sTooltip)) { + $aDefaultValues['label'] = $this->sTooltip; + } + return array_merge($aDefaultValues, parent::GetAriaAttributes()); + } + + public function HasAriaAttributes(): bool + { + return !empty($this->GetAriaAttributes()); + } } diff --git a/sources/Application/UI/Base/Component/Button/ButtonUIBlockFactory.php b/sources/Application/UI/Base/Component/Button/ButtonUIBlockFactory.php index e688bb8752..37a9de0295 100644 --- a/sources/Application/UI/Base/Component/Button/ButtonUIBlockFactory.php +++ b/sources/Application/UI/Base/Component/Button/ButtonUIBlockFactory.php @@ -407,6 +407,8 @@ class ButtonUIBlockFactory extends AbstractUIBlockFactory } $oButton->SetActionType($sActionType); $oButton->SetColor($sColor); + $oButton->SetAriaAttributes($oPopupItem->GetAriaAttributes()->GetAttributes()); + $oButton->SetDataAttributes($oPopupItem->GetDataAttributes()->GetAttributes()); return $oButton; } elseif ($oPopupItem instanceof URLPopupMenuItem) { @@ -424,6 +426,8 @@ class ButtonUIBlockFactory extends AbstractUIBlockFactory $oButton->SetActionType($sActionType); $oButton->SetColor($sColor); + $oButton->SetAriaAttributes($oPopupItem->GetAriaAttributes()->GetAttributes()); + $oButton->SetDataAttributes($oPopupItem->GetDataAttributes()->GetAttributes()); return $oButton; } elseif ($oPopupItem instanceof SeparatorPopupMenuItem) { @@ -460,6 +464,8 @@ class ButtonUIBlockFactory extends AbstractUIBlockFactory if (utils::IsNotNullOrEmptyString($oPopupItem->GetTooltip())) { $oButton->SetTooltip($oPopupItem->GetTooltip()); } + $oButton->SetAriaAttributes($oPopupItem->GetAriaAttributes()->GetAttributes()); + $oButton->SetDataAttributes($oPopupItem->GetDataAttributes()->GetAttributes()); $oButton->SetOnClickJsCode($oPopupItem->GetJsCode()); @@ -486,6 +492,8 @@ class ButtonUIBlockFactory extends AbstractUIBlockFactory if (utils::IsNotNullOrEmptyString($oPopupItem->GetTarget())) { $oButton->SetTarget($oPopupItem->GetTarget()); } + $oButton->SetAriaAttributes($oPopupItem->GetAriaAttributes()->GetAttributes()); + $oButton->SetDataAttributes($oPopupItem->GetDataAttributes()->GetAttributes()); return $oButton; } elseif ($oPopupItem instanceof SeparatorPopupMenuItem) { diff --git a/sources/Application/UI/Base/Component/PopoverMenu/PopoverMenuItem/PopoverMenuItem.php b/sources/Application/UI/Base/Component/PopoverMenu/PopoverMenuItem/PopoverMenuItem.php index 9e07716b21..3d0a6f0813 100644 --- a/sources/Application/UI/Base/Component/PopoverMenu/PopoverMenuItem/PopoverMenuItem.php +++ b/sources/Application/UI/Base/Component/PopoverMenu/PopoverMenuItem/PopoverMenuItem.php @@ -159,4 +159,29 @@ class PopoverMenuItem extends UIBlock return $this->oPopupMenuItem->GetUID(); } + public function GetDataAttributes(): array + { + // Merge attributes from the embedded popup item and from the UI block itself + return array_merge($this->oPopupMenuItem->GetDataAttributes()->GetAttributes(), parent::GetDataAttributes()); + } + + public function HasDataAttributes(): bool + { + return !empty($this->GetDataAttributes()); + } + + public function GetAriaAttributes(): array + { + return array_merge($this->oPopupMenuItem->GetAriaAttributes()->GetAttributes(), parent::GetAriaAttributes()); + } + + public function HasAriaAttribute(string $sName): bool + { + return array_key_exists($sName, $this->GetAriaAttributes()); + } + + public function HasAriaAttributes(): bool + { + return !empty($this->GetAriaAttributes()); + } } diff --git a/sources/Application/UI/Base/Component/PopoverMenu/PopoverMenuItem/PopoverMenuItemFactory.php b/sources/Application/UI/Base/Component/PopoverMenu/PopoverMenuItem/PopoverMenuItemFactory.php index aea86c2ade..445c6dc530 100644 --- a/sources/Application/UI/Base/Component/PopoverMenu/PopoverMenuItem/PopoverMenuItemFactory.php +++ b/sources/Application/UI/Base/Component/PopoverMenu/PopoverMenuItem/PopoverMenuItemFactory.php @@ -88,6 +88,8 @@ class PopoverMenuItemFactory 'label' => $aActionData['label'], 'icon_class' => isset($aActionData['icon_class']) ? $aActionData['icon_class'] : '', 'tooltip' => isset($aActionData['tooltip']) ? $aActionData['tooltip'] : '', + 'data_attributes' => isset($aActionData['data_attributes']) && is_array($aActionData['data_attributes']) ? $aActionData['data_attributes'] : [], + 'aria_attributes' => isset($aActionData['aria_attributes']) && is_array($aActionData['aria_attributes']) ? $aActionData['aria_attributes'] : [], ]; // Avoid meaningless tooltips which are identical to the label @@ -128,6 +130,12 @@ class PopoverMenuItemFactory if (!empty($aRefactoredItem['tooltip'])) { $oPopoverMenuItem->SetTooltip($aRefactoredItem['tooltip']); } + if (!empty($aRefactoredItem['data_attributes'])) { + $oPopoverMenuItem->SetDataAttributes($aRefactoredItem['data_attributes']); + } + if (!empty($aRefactoredItem['aria_attributes'])) { + $oPopoverMenuItem->SetAriaAttributes($aRefactoredItem['aria_attributes']); + } return $oPopoverMenuItem; } diff --git a/sources/Application/UI/Base/Layout/ActivityPanel/ActivityPanelAction/ActivityPanelAction.php b/sources/Application/UI/Base/Layout/ActivityPanel/ActivityPanelAction/ActivityPanelAction.php index ad5670c857..c11952e10b 100644 --- a/sources/Application/UI/Base/Layout/ActivityPanel/ActivityPanelAction/ActivityPanelAction.php +++ b/sources/Application/UI/Base/Layout/ActivityPanel/ActivityPanelAction/ActivityPanelAction.php @@ -23,6 +23,7 @@ namespace Combodo\iTop\Application\UI\Base\Layout\ActivityPanel\ActivityPanelAct use Combodo\iTop\Application\UI\Base\Common\Action\tActionCommon; use Combodo\iTop\Application\UI\Base\Component\PopoverMenu\PopoverMenu; use Combodo\iTop\Application\UI\Base\UIBlock; +use utils; /** * Class ActivityPanelAction @@ -93,6 +94,22 @@ abstract class ActivityPanelAction extends UIBlock return $this->oPopoverMenu !== null; } + public function GetAriaAttributes(): array + { + $aDefaultValues = []; + // Default value for aria-label is the tooltip + if (utils::IsNotNullOrEmptyString($this->sTooltip)) { + $aDefaultValues['label'] = $this->sTooltip; + } + + return array_merge($aDefaultValues, parent::GetAriaAttributes()); + } + + public function HasAriaAttributes(): bool + { + return !empty($this->GetAriaAttributes()); + } + public function GetSubBlocks(): array { if (false === $this->HasPopoverMenu()) { diff --git a/sources/Application/UI/Base/Layout/ActivityPanel/ActivityPanelAction/ActivityPanelActionFactory.php b/sources/Application/UI/Base/Layout/ActivityPanel/ActivityPanelAction/ActivityPanelActionFactory.php index 0193c79e94..1bba93529e 100644 --- a/sources/Application/UI/Base/Layout/ActivityPanel/ActivityPanelAction/ActivityPanelActionFactory.php +++ b/sources/Application/UI/Base/Layout/ActivityPanel/ActivityPanelAction/ActivityPanelActionFactory.php @@ -49,12 +49,12 @@ class ActivityPanelActionFactory $aActionsByUid = []; foreach ($aPopupItems as $sSectionId => $aSectionData) { - $sSectionLabel = (string) $sSectionId; + $sSectionLabel = (string)$sSectionId; $aSectionPopupItems = $aSectionData; if (is_array($aSectionData) && array_key_exists('items', $aSectionData)) { $aSectionPopupItems = $aSectionData['items']; if (array_key_exists('label', $aSectionData)) { - $sSectionLabel = (string) $aSectionData['label']; + $sSectionLabel = (string)$aSectionData['label']; } } @@ -99,8 +99,8 @@ class ActivityPanelActionFactory $oPopupItemForPopoverMenuItem->SetIconClass(''); $oPopoverMenuItem = PopoverMenuItemFactory::MakeFromApplicationPopupMenuItem($oPopupItemForPopoverMenuItem); - $oPopoverMenuItem->AddDataAttribute('caselog-attribute-code', (string) $sSectionId); - $aActionsByUid[$sUid]->GetPopoverMenu()->AddItem((string) $sSectionId, $oPopoverMenuItem); + $oPopoverMenuItem->AddDataAttribute('caselog-attribute-code', (string)$sSectionId); + $aActionsByUid[$sUid]->GetPopoverMenu()->AddItem((string)$sSectionId, $oPopoverMenuItem); } } @@ -119,6 +119,8 @@ class ActivityPanelActionFactory $oAction->SetJsCode($oPopupItem->GetJsCode()); $oAction->SetUrl($oPopupItem->GetUrl()); $oAction->SetIncludeJSFiles($oPopupItem->GetLinkedScripts()); + $oAction->SetAriaAttributes($oPopupItem->GetAriaAttributes()->GetAttributes()); + $oAction->SetDataAttributes($oPopupItem->GetDataAttributes()->GetAttributes()); return $oAction; } elseif ($oPopupItem instanceof URLPopupMenuItem) { @@ -130,6 +132,8 @@ class ActivityPanelActionFactory ); $oAction->SetUrl($oPopupItem->GetUrl()); $oAction->SetTarget($oPopupItem->GetTarget()); + $oAction->SetAriaAttributes($oPopupItem->GetAriaAttributes()->GetAttributes()); + $oAction->SetDataAttributes($oPopupItem->GetDataAttributes()->GetAttributes()); return $oAction; } elseif ($oPopupItem instanceof SeparatorPopupMenuItem) { @@ -140,13 +144,4 @@ class ActivityPanelActionFactory return null; } - - protected static function ClonePopupItemWithLabel(ApplicationPopupMenuItem $oPopupItem, string $sLabel): ApplicationPopupMenuItem - { - $oClonedPopupItem = clone $oPopupItem; - $oClonedPopupItem->SetLabel($sLabel); - - return $oClonedPopupItem; - } - } diff --git a/sources/Application/UI/Base/Layout/TopBar/TopBarQuickAction/TopBarQuickAction.php b/sources/Application/UI/Base/Layout/TopBar/TopBarQuickAction/TopBarQuickAction.php index 03fed69618..b16c8101ee 100644 --- a/sources/Application/UI/Base/Layout/TopBar/TopBarQuickAction/TopBarQuickAction.php +++ b/sources/Application/UI/Base/Layout/TopBar/TopBarQuickAction/TopBarQuickAction.php @@ -21,6 +21,7 @@ namespace Combodo\iTop\Application\UI\Base\Layout\TopBar\TopBarAction; use Combodo\iTop\Application\UI\Base\UIBlock; +use utils; /** * Class TopBarAction @@ -96,4 +97,19 @@ abstract class TopBarQuickAction extends UIBlock return $this; } + public function GetAriaAttributes(): array + { + $aDefaultValues = []; + // Default value for aria-label is the tooltip + if (utils::IsNotNullOrEmptyString($this->sTooltip)) { + $aDefaultValues['label'] = $this->sTooltip; + } + + return array_merge($aDefaultValues, parent::GetAriaAttributes()); + } + + public function HasAriaAttributes(): bool + { + return !empty($this->GetAriaAttributes()); + } } diff --git a/sources/Application/UI/Base/Layout/TopBar/TopBarQuickAction/TopBarQuickActionFactory.php b/sources/Application/UI/Base/Layout/TopBar/TopBarQuickAction/TopBarQuickActionFactory.php index 76f5eb764c..be941e39b9 100644 --- a/sources/Application/UI/Base/Layout/TopBar/TopBarQuickAction/TopBarQuickActionFactory.php +++ b/sources/Application/UI/Base/Layout/TopBar/TopBarQuickAction/TopBarQuickActionFactory.php @@ -44,8 +44,9 @@ class TopBarQuickActionFactory ); $oTopBarAction->SetJsCode($oApplicationPopupItem->GetJsCode()); $oTopBarAction->SetUrl($oApplicationPopupItem->GetUrl()); - $oTopBarAction->SetIncludeJSFiles($oApplicationPopupItem->GetLinkedScripts()); + $oTopBarAction->SetAriaAttributes($oApplicationPopupItem->GetAriaAttributes()->GetAttributes()); + $oTopBarAction->SetDataAttributes($oApplicationPopupItem->GetDataAttributes()->GetAttributes()); return $oTopBarAction; } elseif ($oApplicationPopupItem instanceof URLPopupMenuItem) { $oTopBarAction = new TopBarQuickActionURL( @@ -56,10 +57,14 @@ class TopBarQuickActionFactory ); $oTopBarAction->SetUrl($oApplicationPopupItem->GetUrl()); $oTopBarAction->SetTarget($oApplicationPopupItem->GetTarget()); + $oTopBarAction->SetAriaAttributes($oApplicationPopupItem->GetAriaAttributes()->GetAttributes()); + $oTopBarAction->SetDataAttributes($oApplicationPopupItem->GetDataAttributes()->GetAttributes()); return $oTopBarAction; } elseif ($oApplicationPopupItem instanceof SeparatorPopupMenuItem) { $oTopBarAction = new TopBarQuickActionSeparator($oApplicationPopupItem->GetUID()); $oTopBarAction->SetCSSClasses($oApplicationPopupItem->GetCssClasses()); + $oTopBarAction->SetAriaAttributes($oApplicationPopupItem->GetAriaAttributes()->GetAttributes()); + $oTopBarAction->SetDataAttributes($oApplicationPopupItem->GetDataAttributes()->GetAttributes()); return $oTopBarAction; } diff --git a/sources/Application/UI/Base/UIBlock.php b/sources/Application/UI/Base/UIBlock.php index 1197ad3cc1..f8f7a7d594 100644 --- a/sources/Application/UI/Base/UIBlock.php +++ b/sources/Application/UI/Base/UIBlock.php @@ -145,6 +145,8 @@ abstract class UIBlock implements iUIBlock protected $aCssFilesRelPath = []; /** @var array Array => which will be output as HTML data-xxx attributes (eg. data-="") */ protected $aDataAttributes = []; + /** @var array Array => which will be output as HTML aria-xxx attributes (eg. aria-="") */ + protected $aAriaAttributes = []; /** @var bool Whether the current block is shown or hidden */ protected $bIsHidden; /** @var array Additional CSS classes to put on the block */ @@ -622,6 +624,67 @@ abstract class UIBlock implements iUIBlock return !empty($this->aDataAttributes); } + /** + * @return array + * @see static::$aAriaAttributes + * @since 3.3.0 + */ + public function GetAriaAttributes(): array + { + return $this->aAriaAttributes; + } + + /** + * @param array $aAriaAttributes Array of aria attributes in the format ['name' => 'value'] + * + * @return $this + * @see static::$aAriaAttributes + * @since 3.3.0 + */ + public function SetAriaAttributes(array $aAriaAttributes) + { + $this->aAriaAttributes = $aAriaAttributes; + + return $this; + } + + /** + * @param string $sName + * @param string $sValue + * + * @return $this + * @see static::$aAriaAttributes + * @since 3.3.0 + */ + public function AddAriaAttribute(string $sName, string $sValue) + { + $this->aAriaAttributes[$sName] = $sValue; + + return $this; + } + + /** + * @param string $sName Name of the aria attribute + * + * @return bool True if $sName is already defined (even as a null value) in the UIBlock aria attributes, false otherwise + * @see static::$aAriaAttributes + * @since 3.3.0 + */ + public function HasAriaAttribute(string $sName): bool + { + return array_key_exists($sName, $this->aAriaAttributes); + } + + /** + * @return bool + * @see static::$aAriaAttributes + * @since 3.3.0 + */ + public function HasAriaAttributes(): bool + { + return !empty($this->aAriaAttributes); + } + /** * @return bool * @see static::$bIsHidden diff --git a/templates/base/components/button/buttonjs.html.twig b/templates/base/components/button/buttonjs.html.twig index 14523a63e6..54ee1ec138 100644 --- a/templates/base/components/button/buttonjs.html.twig +++ b/templates/base/components/button/buttonjs.html.twig @@ -1,4 +1,3 @@ -{% set sAriaLabel = oUIBlock.GetLabel() is not empty ? oUIBlock.GetLabel() : (oUIBlock.GetTooltip() is not empty ? oUIBlock.GetTooltip() : '') %}