From f624d118c897117bb973329e42b2f9375aec9a40 Mon Sep 17 00:00:00 2001 From: Marcel Werk Date: Wed, 4 Nov 2026 11:32:43 +0100 Subject: [PATCH 01/12] Replace dl-based form builder markup with accessible formField structure and native radio rating --- .../__multifactorTotpDevicesContainer.tpl | 15 +- .../__multifactorTotpNewDeviceContainer.tpl | 15 +- .../templates/shared_colorFormField.tpl | 1 + com.woltlab.wcf/templates/shared_form.tpl | 7 +- .../templates/shared_formContainer.tpl | 15 +- .../templates/shared_formField.tpl | 29 ++- .../templates/shared_formFieldDescription.tpl | 2 +- .../templates/shared_formFieldError.tpl | 2 +- .../templates/shared_formFieldErrors.tpl | 10 +- .../templates/shared_inlineLabelFormField.tpl | 22 +-- .../shared_multipleSelectFormField.tpl | 1 + .../shared_numericRangeFormField.tpl | 2 + .../templates/shared_ratingFormField.tpl | 62 +++--- .../shared_rowFormFieldContainer.tpl | 25 +-- .../templates/shared_selectFormField.tpl | 1 + .../shared_singleSelectionFormField.tpl | 1 + .../shared_suffixFormFieldContainer.tpl | 39 ++-- .../templates/shared_userFormField.tpl | 1 + .../shared_wysiwygAttachmentFormField.tpl | 11 +- .../Builder/Field/Controller/FileProcessor.ts | 14 +- .../Field/Controller/MultilineItemList.ts | 13 +- .../Form/Builder/Field/Controller/Rating.ts | 122 ------------ .../Core/Form/Builder/Field/Error.ts | 113 +++++++++++ ts/WoltLabSuite/Core/Language/Chooser.ts | 5 +- ts/WoltLabSuite/Core/Ui/Dialog.ts | 4 +- .../files/acp/templates/__boxPipGui.tpl | 2 +- ...__devtoolsProjectInstructionsFormField.tpl | 18 +- .../files/acp/templates/__menuPipGui.tpl | 2 +- .../Builder/Field/Controller/FileProcessor.js | 13 +- .../Field/Controller/MultilineItemList.js | 11 +- .../Form/Builder/Field/Controller/Rating.js | 106 ---------- .../Core/Form/Builder/Field/Error.js | 108 +++++++++++ .../js/WoltLabSuite/Core/Language/Chooser.js | 5 +- .../files/js/WoltLabSuite/Core/Ui/Dialog.js | 4 +- .../builder/container/FormContainer.class.php | 2 +- .../container/TabMenuFormContainer.class.php | 2 +- .../WysiwygSmileyFormContainer.class.php | 2 +- .../WysiwygTabMenuFormContainer.class.php | 2 +- .../builder/field/AbstractFormField.class.php | 61 ++++++ .../AbstractFormFieldDecorator.class.php | 36 ++++ .../field/BadgeColorFormField.class.php | 6 + .../builder/field/ColorFormField.class.php | 10 + .../field/DateRangeFormField.class.php | 6 + .../field/FileProcessorFormField.class.php | 6 + .../builder/field/IconFormField.class.php | 6 + .../MultilineItemListFormField.class.php | 6 + .../MultipleSelectionFormField.class.php | 6 + .../field/NumericRangeFormField.class.php | 6 + .../field/ObjectFilterFormField.class.php | 6 + .../field/RadioButtonFormField.class.php | 6 + .../builder/field/RatingFormField.class.php | 8 +- .../field/SelectOptionsFormField.class.php | 6 + .../field/SingleSelectionFormField.class.php | 6 + .../field/TAttributeFormField.class.php | 7 + .../field/TimeRangeFormField.class.php | 6 + .../builder/field/UploadFormField.class.php | 6 + .../builder/field/acl/AclFormField.class.php | 6 + .../acl/simple/SimpleAclFormField.class.php | 6 + .../ContentLanguageFormField.class.php | 6 + .../SingleMediaSelectionFormField.class.php | 6 + .../poll/PollOptionListFormField.class.php | 6 + .../trophy/TrophyBadgeFormField.class.php | 6 + .../install/files/style/ui/formBuilder.scss | 181 ++++++++++++++++++ wcfsetup/install/files/style/ui/rating.scss | 90 ++++++++- 64 files changed, 906 insertions(+), 410 deletions(-) delete mode 100644 ts/WoltLabSuite/Core/Form/Builder/Field/Controller/Rating.ts create mode 100644 ts/WoltLabSuite/Core/Form/Builder/Field/Error.ts delete mode 100644 wcfsetup/install/files/js/WoltLabSuite/Core/Form/Builder/Field/Controller/Rating.js create mode 100644 wcfsetup/install/files/js/WoltLabSuite/Core/Form/Builder/Field/Error.js create mode 100644 wcfsetup/install/files/style/ui/formBuilder.scss diff --git a/com.woltlab.wcf/templates/__multifactorTotpDevicesContainer.tpl b/com.woltlab.wcf/templates/__multifactorTotpDevicesContainer.tpl index 32a0c1f6e63..274404eff4e 100644 --- a/com.woltlab.wcf/templates/__multifactorTotpDevicesContainer.tpl +++ b/com.woltlab.wcf/templates/__multifactorTotpDevicesContainer.tpl @@ -1,17 +1,16 @@
getClasses()|empty} class="{implode from=$container->getClasses() item='class' glue=' '}{$class}{/implode}"{/if}{* *}{foreach from=$container->getAttributes() key='attributeName' item='attributeValue'} {$attributeName}="{$attributeValue}"{/foreach}{* + *}{if $container->getLabel() !== null} aria-labelledby="{$container->getPrefixedId()}Title"{/if}{* *}{if !$container->checkDependencies()} style="display: none;"{/if}{* *}> {if $container->getLabel() !== null} - {if $container->getDescription() !== null} -
-

{unsafe:$container->getLabel()}{if $container->markAsRequired()} *{/if}

-

{unsafe:$container->getDescription()}

-
- {else} -

{unsafe:$container->getLabel()}{if $container->markAsRequired()} *{/if}

- {/if} +
+

{unsafe:$container->getLabel()}{if $container->markAsRequired()} *{/if}

+ {if $container->getDescription() !== null} +

{unsafe:$container->getDescription()}

+ {/if} +
{/if}
diff --git a/com.woltlab.wcf/templates/__multifactorTotpNewDeviceContainer.tpl b/com.woltlab.wcf/templates/__multifactorTotpNewDeviceContainer.tpl index f333f97f146..dccf62639f7 100644 --- a/com.woltlab.wcf/templates/__multifactorTotpNewDeviceContainer.tpl +++ b/com.woltlab.wcf/templates/__multifactorTotpNewDeviceContainer.tpl @@ -1,17 +1,16 @@
getClasses()|empty} class="{implode from=$container->getClasses() item='class' glue=' '}{$class}{/implode}"{/if}{* *}{foreach from=$container->getAttributes() key='attributeName' item='attributeValue'} {$attributeName}="{$attributeValue}"{/foreach}{* + *}{if $container->getLabel() !== null} aria-labelledby="{$container->getPrefixedId()}Title"{/if}{* *}{if !$container->checkDependencies()} style="display: none;"{/if}{* *}> {if $container->getLabel() !== null} - {if $container->getDescription() !== null} -
-

{unsafe:$container->getLabel()}{if $container->markAsRequired()} *{/if}

-

{unsafe:$container->getDescription()}

-
- {else} -

{unsafe:$container->getLabel()}{if $container->markAsRequired()} *{/if}

- {/if} +
+

{unsafe:$container->getLabel()}{if $container->markAsRequired()} *{/if}

+ {if $container->getDescription() !== null} +

{unsafe:$container->getDescription()}

+ {/if} +
{/if} {lang}wcf.user.security.multifactor.totp.newDevice.description{/lang} diff --git a/com.woltlab.wcf/templates/shared_colorFormField.tpl b/com.woltlab.wcf/templates/shared_colorFormField.tpl index 18db9a18fcf..7cf0a9116c5 100644 --- a/com.woltlab.wcf/templates/shared_colorFormField.tpl +++ b/com.woltlab.wcf/templates/shared_colorFormField.tpl @@ -9,6 +9,7 @@ id="{$field->getPrefixedId()}_colorPickerButton" title="{lang}wcf.style.colorPicker.button.changeColor{/lang}" data-store="{$field->getPrefixedId()}" + {foreach from=$field->getAriaAttributes() key='attributeName' item='attributeValue'} {$attributeName}="{$attributeValue}"{/foreach} > getValue()} style="background-color: {$field->getValue()}"{/if}> diff --git a/com.woltlab.wcf/templates/shared_form.tpl b/com.woltlab.wcf/templates/shared_form.tpl index f6988e3b09d..1a30872d351 100644 --- a/com.woltlab.wcf/templates/shared_form.tpl +++ b/com.woltlab.wcf/templates/shared_form.tpl @@ -33,15 +33,14 @@ {/if} {if $form->isAjax()} -
getClasses()|empty} class="{implode from=$form->getClasses() item='class' glue=' '}{$class}{/implode}"{/if}{* +
getAttributes() key='attributeName' item='attributeValue'} {$attributeName}="{$attributeValue}"{/foreach}{* *}> {else}
getClasses()|empty} class="{implode from=$form->getClasses() item='class' glue=' '}{$class}{/implode}"{/if}{* + *}id="{$form->getId()}" {* + *}class="formBuilder{foreach from=$form->getClasses() item='class'} {$class}{/foreach}"{* *}{foreach from=$form->getAttributes() key='attributeName' item='attributeValue'} {$attributeName}="{$attributeValue}"{/foreach}{* *}> {/if} diff --git a/com.woltlab.wcf/templates/shared_formContainer.tpl b/com.woltlab.wcf/templates/shared_formContainer.tpl index bec0d15161e..70cdfaa7a94 100644 --- a/com.woltlab.wcf/templates/shared_formContainer.tpl +++ b/com.woltlab.wcf/templates/shared_formContainer.tpl @@ -1,17 +1,16 @@
getClasses()|empty} class="{implode from=$container->getClasses() item='class' glue=' '}{$class}{/implode}"{/if}{* *}{foreach from=$container->getAttributes() key='attributeName' item='attributeValue'} {$attributeName}="{$attributeValue}"{/foreach}{* + *}{if $container->getLabel() !== null} aria-labelledby="{$container->getPrefixedId()}Title"{/if}{* *}{if !$container->checkDependencies()} style="display: none;"{/if}{* *}> {if $container->getLabel() !== null} - {if $container->getDescription() !== null} -
-

{unsafe:$container->getLabel()}{if $container->markAsRequired()} *{/if}

-

{unsafe:$container->getDescription()}

-
- {else} -

{unsafe:$container->getLabel()}{if $container->markAsRequired()} *{/if}

- {/if} +
+

{unsafe:$container->getLabel()}{if $container->markAsRequired()} *{/if}

+ {if $container->getDescription() !== null} +

{unsafe:$container->getDescription()}

+ {/if} +
{/if} {include file='shared_formContainerChildren'} diff --git a/com.woltlab.wcf/templates/shared_formField.tpl b/com.woltlab.wcf/templates/shared_formField.tpl index a71293f7992..591f0b2f5e2 100644 --- a/com.woltlab.wcf/templates/shared_formField.tpl +++ b/com.woltlab.wcf/templates/shared_formField.tpl @@ -1,15 +1,24 @@ -
getClasses()|empty} class="{implode from=$field->getClasses() item='class' glue=' '}{$class}{/implode}"{/if}{* +
getAttributes() key='attributeName' item='attributeValue'} {$attributeName}="{$attributeValue}"{/foreach}{* + *}{if $field->isGroup()} role="group"{if $field->getLabel() !== null} aria-labelledby="{$field->getPrefixedId()}Label"{/if}{foreach from=$field->getAriaAttributes() key='attributeName' item='attributeValue'} {$attributeName}="{$attributeValue}"{/foreach}{/if}{* *}{if !$field->checkDependencies()} style="display: none;"{/if}{* *}> -
{if $field->getLabel() !== null}{if $field->isRequired() && $form->marksRequiredFields()} *{/if}{/if}
-
+
{* + *}{if $field->getLabel() !== null}{* + *}{if $field->isGroup()}{* + *}{unsafe:$field->getLabel()}{* + *}{else}{* + *}{* + *}{/if}{* + *}{if $field->isRequired() && $form->marksRequiredFields()} isGroup()} aria-hidden="true"{/if}>*{/if}{* + *}{/if}{* + *}
+
{unsafe:$field->getFieldHtml()} +
- {include file='shared_formFieldErrors'} - {include file='shared_formFieldDescription'} - {include file='shared_formFieldDependencies'} - {include file='shared_formFieldDataHandler'} -
-
+ {include file='shared_formFieldErrors'} + {include file='shared_formFieldDescription'} + {include file='shared_formFieldDependencies'} + {include file='shared_formFieldDataHandler'} +
diff --git a/com.woltlab.wcf/templates/shared_formFieldDescription.tpl b/com.woltlab.wcf/templates/shared_formFieldDescription.tpl index a6262351b7c..6543fdefe55 100644 --- a/com.woltlab.wcf/templates/shared_formFieldDescription.tpl +++ b/com.woltlab.wcf/templates/shared_formFieldDescription.tpl @@ -1,3 +1,3 @@ {if $field->getDescription() !== null} - {unsafe:$field->getDescription()} +
{unsafe:$field->getDescription()}
{/if} diff --git a/com.woltlab.wcf/templates/shared_formFieldError.tpl b/com.woltlab.wcf/templates/shared_formFieldError.tpl index e73edbfb29b..a0acef20b64 100644 --- a/com.woltlab.wcf/templates/shared_formFieldError.tpl +++ b/com.woltlab.wcf/templates/shared_formFieldError.tpl @@ -1 +1 @@ -{unsafe:$error->getMessage()} +

{unsafe:$error->getMessage()}

diff --git a/com.woltlab.wcf/templates/shared_formFieldErrors.tpl b/com.woltlab.wcf/templates/shared_formFieldErrors.tpl index d6cf3b29ed8..421c9e97c0b 100644 --- a/com.woltlab.wcf/templates/shared_formFieldErrors.tpl +++ b/com.woltlab.wcf/templates/shared_formFieldErrors.tpl @@ -1,3 +1,7 @@ -{foreach from=$field->getValidationErrors() item='validationError'} - {unsafe:$validationError->getHtml()} -{/foreach} +{if !$field->getValidationErrors()|empty} +
+ {foreach from=$field->getValidationErrors() item='validationError'} + {unsafe:$validationError->getHtml()} + {/foreach} +
+{/if} diff --git a/com.woltlab.wcf/templates/shared_inlineLabelFormField.tpl b/com.woltlab.wcf/templates/shared_inlineLabelFormField.tpl index c4f7b10cce4..8662a72b889 100644 --- a/com.woltlab.wcf/templates/shared_inlineLabelFormField.tpl +++ b/com.woltlab.wcf/templates/shared_inlineLabelFormField.tpl @@ -1,18 +1,16 @@ -
getClasses()|empty} class="{implode from=$field->getClasses() item='class' glue=' '}{$class}{/implode}"{/if}{* +
getAttributes() key='attributeName' item='attributeValue'} {$attributeName}="{$attributeValue}"{/foreach}{* *}{if !$field->checkDependencies()} style="display: none;"{/if}{* *}> -
-
-
-
+ {include file='shared_formFieldErrors'} + {include file='shared_formFieldDescription'} + {include file='shared_formFieldDependencies'} + {include file='shared_formFieldDataHandler'} + diff --git a/com.woltlab.wcf/templates/shared_multipleSelectFormField.tpl b/com.woltlab.wcf/templates/shared_multipleSelectFormField.tpl index 1765d7744fa..857edafe24c 100644 --- a/com.woltlab.wcf/templates/shared_multipleSelectFormField.tpl +++ b/com.woltlab.wcf/templates/shared_multipleSelectFormField.tpl @@ -4,6 +4,7 @@ {if !$field->getFieldClasses()|empty} class="{implode from=$field->getFieldClasses() item='class' glue=' '}{$class}{/implode}"{/if} {if $field->isRequired()} required{/if} multiple + {foreach from=$field->getAriaAttributes() key='attributeName' item='attributeValue'} {$attributeName}="{$attributeValue}"{/foreach} > {foreach from=$field->getNestedOptions() item=__fieldNestedOption} diff --git a/com.woltlab.wcf/templates/shared_singleSelectionFormField.tpl b/com.woltlab.wcf/templates/shared_singleSelectionFormField.tpl index 732485e1cbb..8bdf56fe5ca 100644 --- a/com.woltlab.wcf/templates/shared_singleSelectionFormField.tpl +++ b/com.woltlab.wcf/templates/shared_singleSelectionFormField.tpl @@ -31,6 +31,7 @@