diff --git a/com.woltlab.wcf/fileDelete.xml b/com.woltlab.wcf/fileDelete.xml index 6f093ed6895..383ad287ed4 100644 --- a/com.woltlab.wcf/fileDelete.xml +++ b/com.woltlab.wcf/fileDelete.xml @@ -34,6 +34,7 @@ js/WoltLabSuite/Core/Media/Manager/Search.js js/WoltLabSuite/Core/Media/Replace.js js/WoltLabSuite/Core/Media/Upload.js + js/WoltLabSuite/Core/Form/Builder/Field/Controller/Rating.js lib/system/clipboard/action/MediaClipboardAction.class.php lib/system/upload/MediaReplaceUploadFileValidationStrategy.class.php lib/system/upload/MediaUploadFileValidationStrategy.class.php 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..d81f82fec81 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} @@ -29,7 +28,7 @@ {/foreach} {if $container->getNodeById('submitButton')->isAvailable()} -
+
{unsafe:$container->getNodeById('submitButton')->getHtml()}
{/if} 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..8f706c60829 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} @@ -52,7 +51,7 @@ {/foreach} {if !$form->getButtons()|empty} -
+
{foreach from=$form->getButtons() item=button} {if $button->isAvailable()} {unsafe:$button->getHtml()} 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}
+ {if $container->getDescription() !== null} +
{unsafe:$container->getDescription()}
+ {/if} +
{include file='shared_formContainerDependencies'} diff --git a/com.woltlab.wcf/templates/shared_selectFormField.tpl b/com.woltlab.wcf/templates/shared_selectFormField.tpl index 592f647049a..309a7f72b04 100644 --- a/com.woltlab.wcf/templates/shared_selectFormField.tpl +++ b/com.woltlab.wcf/templates/shared_selectFormField.tpl @@ -7,6 +7,7 @@ {elseif $field->isRequired()} required {/if} + {foreach from=$field->getAriaAttributes() key='attributeName' item='attributeValue'} {$attributeName}="{$attributeValue}"{/foreach} > {if !$field->hasDefaultValue()} 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 @@ `)); }); diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Form/Builder/Dialog.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Form/Builder/Dialog.js index 47a2e7912a3..a388cd71f67 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Form/Builder/Dialog.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Form/Builder/Dialog.js @@ -130,7 +130,7 @@ define(["require", "exports", "tslib", "../../Core", "../../Ui/Dialog", "../../A cancelButton.addEventListener("click", () => this._closeDialog()); cancelButton.dataset.hasEventListener = "1"; } - this._additionalSubmitButtons = Array.from(dialogData.content.querySelectorAll(':not(.formSubmit) button[type="submit"]')); + this._additionalSubmitButtons = Array.from(dialogData.content.querySelectorAll('button[type="submit"]:not(:is(.formSubmit, .formBuilder__submit) *)')); this._additionalSubmitButtons.forEach((submit) => { submit.addEventListener("click", () => { // Mark the button that was clicked so that the button data handlers know diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Form/Builder/Field/Controller/FileProcessor.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Form/Builder/Field/Controller/FileProcessor.js index ef42ba17a93..59db29f1c39 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Form/Builder/Field/Controller/FileProcessor.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Form/Builder/Field/Controller/FileProcessor.js @@ -4,7 +4,7 @@ * @license GNU Lesser General Public License * @since 6.1 */ -define(["require", "exports", "tslib", "WoltLabSuite/Core/Language", "WoltLabSuite/Core/Api/Files/DeleteFile", "WoltLabSuite/Core/Dom/Change/Listener", "WoltLabSuite/Core/Component/File/Helper", "WoltLabSuite/Core/Component/File/Upload", "WoltLabSuite/Core/Dom/Util", "WoltLabSuite/Core/Component/Confirmation"], function (require, exports, tslib_1, Language_1, DeleteFile_1, Listener_1, Helper_1, Upload_1, Util_1, Confirmation_1) { +define(["require", "exports", "tslib", "WoltLabSuite/Core/Language", "WoltLabSuite/Core/Api/Files/DeleteFile", "WoltLabSuite/Core/Dom/Change/Listener", "WoltLabSuite/Core/Component/File/Helper", "WoltLabSuite/Core/Component/File/Upload", "WoltLabSuite/Core/Form/Builder/Field/Error", "WoltLabSuite/Core/Component/Confirmation"], function (require, exports, tslib_1, Language_1, DeleteFile_1, Listener_1, Helper_1, Upload_1, Error_1, Confirmation_1) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.FileProcessor = void 0; @@ -110,6 +110,7 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Language", "WoltLabSui replaceButton.addEventListener("click", () => { const oldContext = this.#startReplaceFile(element); (0, Upload_1.clearPreviousErrors)(this.#uploadButton); + (0, Error_1.setFieldError)(this.#fieldId, null); const changeEventListener = () => { this.#fileInput.removeEventListener("cancel", cancelEventListener); // Wait until the upload starts, @@ -143,7 +144,7 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Language", "WoltLabSui else { errorMessage = (0, Helper_1.getErrorMessageFromFile)(element); } - (0, Util_1.innerError)(this.#uploadButton, errorMessage); + (0, Error_1.setFieldError)(this.#fieldId, errorMessage); element.remove(); if (reason instanceof Error) { throw reason; @@ -164,15 +165,11 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Language", "WoltLabSui notifyValueChange(this.#fieldId, this.values); } else { - let container = element; - if (!this.#useBigPreview) { - container = container.parentElement; - } if (result.error.code === "permission_denied") { - (0, Util_1.innerError)(container, (0, Language_1.getPhrase)("wcf.upload.error.delete.permissionDenied"), true); + (0, Error_1.setFieldError)(this.#fieldId, (0, Language_1.getPhrase)("wcf.upload.error.delete.permissionDenied"), true); } else { - (0, Util_1.innerError)(container, result.error.message ?? (0, Language_1.getPhrase)("wcf.upload.error.delete.unknownError")); + (0, Error_1.setFieldError)(this.#fieldId, result.error.message ?? (0, Language_1.getPhrase)("wcf.upload.error.delete.unknownError")); } } }); diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Form/Builder/Field/Controller/MultilineItemList.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Form/Builder/Field/Controller/MultilineItemList.js index 02488cd2ac6..da598f3a42f 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Form/Builder/Field/Controller/MultilineItemList.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Form/Builder/Field/Controller/MultilineItemList.js @@ -6,7 +6,7 @@ * @license GNU Lesser General Public License * @since 6.0 */ -define(["require", "exports", "tslib", "WoltLabSuite/Core/Ui/ItemList/LineBreakSeparatedText", "WoltLabSuite/Core/Language", "WoltLabSuite/Core/Dom/Util"], function (require, exports, tslib_1, UiItemList, Language_1, Util_1) { +define(["require", "exports", "tslib", "WoltLabSuite/Core/Ui/ItemList/LineBreakSeparatedText", "WoltLabSuite/Core/Language", "WoltLabSuite/Core/Dom/Util", "WoltLabSuite/Core/Form/Builder/Field/Error"], function (require, exports, tslib_1, UiItemList, Language_1, Util_1, Error_1) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.MultilineItemListFormField = void 0; @@ -30,6 +30,9 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Ui/ItemList/LineBreakS getItems() { return this.items; } + getFieldId() { + return this.itemList.id.replace(/^lineBreakSeparatedTextOption_/, ""); + } /** * @inheritDoc */ @@ -90,11 +93,11 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Ui/ItemList/LineBreakS } const newValue = input.value.trim(); if (newValue === "") { - Util_1.default.innerError(input.parentElement, (0, Language_1.getPhrase)("wcf.global.form.error.empty")); + (0, Error_1.setFieldError)(this.getFieldId(), (0, Language_1.getPhrase)("wcf.global.form.error.empty")); } else if (!this.items.has(newValue) || newValue === value) { //remove error message - Util_1.default.innerError(input.parentElement, ""); + (0, Error_1.setFieldError)(this.getFieldId(), null); //insert new value button.closest("li").dataset.value = newValue; valueSpan.textContent = newValue; @@ -103,7 +106,7 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Ui/ItemList/LineBreakS endCallback(); } else { - Util_1.default.innerError(input.parentElement, (0, Language_1.getPhrase)("wcf.acp.option.type.lineBreakSeparatedText.error.duplicate", { + (0, Error_1.setFieldError)(this.getFieldId(), (0, Language_1.getPhrase)("wcf.acp.option.type.lineBreakSeparatedText.error.duplicate", { item: newValue, }), true); } diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Form/Builder/Field/Controller/Rating.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Form/Builder/Field/Controller/Rating.js deleted file mode 100644 index 8f8593ea04e..00000000000 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Form/Builder/Field/Controller/Rating.js +++ /dev/null @@ -1,106 +0,0 @@ -/** - * Handles the JavaScript part of the rating form field. - * - * @author Matthias Schmidt - * @copyright 2001-2020 WoltLab GmbH - * @license GNU Lesser General Public License - * @since 5.2 - */ -define(["require", "exports", "tslib", "../../../../Environment"], function (require, exports, tslib_1, Environment) { - "use strict"; - Environment = tslib_1.__importStar(Environment); - class Rating { - _field; - _input; - _ratingElements; - constructor(fieldId, value) { - this._field = document.getElementById(fieldId + "Container"); - if (this._field === null) { - throw new Error("Unknown field with id '" + fieldId + "'"); - } - this._input = document.createElement("input"); - this._input.id = fieldId; - this._input.name = fieldId; - this._input.type = "hidden"; - this._input.value = value; - this._field.appendChild(this._input); - this._ratingElements = new Map(); - const ratingList = this._field.querySelector(".ratingList"); - ratingList.addEventListener("mouseleave", () => this._restoreRating()); - ratingList.querySelectorAll("li").forEach((listItem) => { - if (listItem.classList.contains("ratingMetaButton")) { - listItem.addEventListener("click", (ev) => this._metaButtonClick(ev)); - listItem.addEventListener("mouseenter", () => this._restoreRating()); - } - else { - this._ratingElements.set(listItem.dataset.rating, listItem); - listItem.addEventListener("click", (ev) => this._listItemClick(ev)); - listItem.addEventListener("mouseenter", (ev) => this._listItemMouseEnter(ev)); - listItem.addEventListener("mouseleave", () => this._listItemMouseLeave()); - } - }); - } - /** - * Saves the rating associated with the clicked rating element. - */ - _listItemClick(event) { - const target = event.currentTarget; - this._input.value = target.dataset.rating; - if (Environment.platform() !== "desktop") { - this._restoreRating(); - } - } - /** - * Updates the rating UI when hovering over a rating element. - */ - _listItemMouseEnter(event) { - const target = event.currentTarget; - const currentRating = target.dataset.rating; - this._ratingElements.forEach((ratingElement, rating) => { - const icon = ratingElement.querySelector("fa-icon"); - this._toggleIcon(icon, ~~rating <= ~~currentRating); - }); - } - /** - * Updates the rating UI when leaving a rating element by changing all rating elements - * to their default state. - */ - _listItemMouseLeave() { - this._ratingElements.forEach((ratingElement) => { - const icon = ratingElement.querySelector("fa-icon"); - this._toggleIcon(icon, false); - }); - } - /** - * Handles clicks on meta buttons. - */ - _metaButtonClick(event) { - const target = event.currentTarget; - if (target.dataset.action === "removeRating") { - this._input.value = ""; - this._listItemMouseLeave(); - } - } - /** - * Updates the rating UI by changing the rating elements to the stored rating state. - */ - _restoreRating() { - this._ratingElements.forEach((ratingElement, rating) => { - const icon = ratingElement.querySelector("fa-icon"); - this._toggleIcon(icon, ~~rating <= ~~this._input.value); - }); - } - /** - * Toggles the state of the given icon based on the given state parameter. - */ - _toggleIcon(icon, active = false) { - if (active) { - icon.setIcon("star", true); - } - else { - icon.setIcon("star"); - } - } - } - return Rating; -}); diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Form/Builder/Field/Error.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Form/Builder/Field/Error.js new file mode 100644 index 00000000000..97758204ea6 --- /dev/null +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Form/Builder/Field/Error.js @@ -0,0 +1,108 @@ +/** + * Displays or removes client-side error messages of form builder fields and keeps the + * ARIA attributes of the field in sync. + * + * @author Marcel Werk + * @copyright 2001-2026 WoltLab GmbH + * @license GNU Lesser General Public License + * @since 6.3 + */ +define(["require", "exports"], function (require, exports) { + "use strict"; + Object.defineProperty(exports, "__esModule", { value: true }); + exports.setFieldError = setFieldError; + function getAriaTarget(fieldId, container) { + if (container.getAttribute("role") === "group") { + return container; + } + const label = document.getElementById(fieldId + "Label"); + if (label instanceof HTMLLabelElement && label.htmlFor !== "") { + return document.getElementById(label.htmlFor); + } + return document.getElementById(fieldId); + } + function updateDescribedBy(element, id, add) { + const ids = (element.getAttribute("aria-describedby") ?? "").split(" ").filter((value) => { + return value !== "" && value !== id; + }); + if (add) { + ids.unshift(id); + } + if (ids.length === 0) { + element.removeAttribute("aria-describedby"); + } + else { + element.setAttribute("aria-describedby", ids.join(" ")); + } + } + function updateAriaAttributes(fieldId, container, hasError) { + const target = getAriaTarget(fieldId, container); + if (target === null) { + return; + } + const errorId = fieldId + "Error"; + if (target === container) { + // `aria-invalid` and `aria-errormessage` are not supported by `role="group"` + updateDescribedBy(target, errorId, hasError); + } + else if (hasError) { + target.setAttribute("aria-invalid", "true"); + target.setAttribute("aria-errormessage", errorId); + } + else { + target.removeAttribute("aria-invalid"); + target.removeAttribute("aria-errormessage"); + } + } + function getErrorContainer(fieldId, container) { + const errorId = fieldId + "Error"; + let errorContainer = document.getElementById(errorId); + if (errorContainer !== null) { + return errorContainer; + } + errorContainer = document.createElement("div"); + errorContainer.id = errorId; + errorContainer.classList.add("formField__errors"); + const description = document.getElementById(fieldId + "Description"); + if (description !== null && container.contains(description)) { + description.before(errorContainer); + } + else { + const input = container.querySelector(":scope > .formField__input"); + if (input !== null) { + input.after(errorContainer); + } + else { + container.append(errorContainer); + } + } + return errorContainer; + } + /** + * Displays the given error message for the field with the given id, replacing any existing + * error messages, or removes all error messages if `null` or an empty string is given. + */ + function setFieldError(fieldId, message, isHtml = false) { + const container = document.getElementById(fieldId + "Container"); + if (container === null) { + throw new Error(`Unknown field with id '${fieldId}'.`); + } + if (message === null || message === "") { + document.getElementById(fieldId + "Error")?.remove(); + container.classList.remove("formError"); + updateAriaAttributes(fieldId, container, false); + return; + } + const error = document.createElement("p"); + error.classList.add("formField__error"); + if (isHtml) { + error.innerHTML = message; + } + else { + error.textContent = message; + } + getErrorContainer(fieldId, container).replaceChildren(error); + container.classList.add("formError"); + updateAriaAttributes(fieldId, container, true); + } +}); diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Language/Chooser.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Language/Chooser.js index 02f932711b6..b6af11c8c53 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Language/Chooser.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Language/Chooser.js @@ -24,7 +24,8 @@ define(["require", "exports", "tslib", "../Core", "WoltLabSuite/Core/Language", function initElement(chooserId, element, languageId, languages, callback, allowEmptyValue) { let container; const parent = element.parentElement; - if (parent.nodeName === "DD") { + const isFormFieldInput = parent.nodeName === "DD" || parent.classList.contains("formField__input"); + if (isFormFieldInput) { container = document.createElement("div"); container.className = "dropdown"; // language chooser is the first child so that descriptions and error messages @@ -38,7 +39,7 @@ define(["require", "exports", "tslib", "../Core", "WoltLabSuite/Core/Language", Util_1.default.hide(element); const dropdownToggle = document.createElement("a"); dropdownToggle.className = "dropdownToggle boxFlag box24 inputPrefix"; - if (parent.nodeName === "DD") { + if (isFormFieldInput) { dropdownToggle.classList.add("button"); } container.appendChild(dropdownToggle); diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Acl/List.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Acl/List.js index e517c227e57..d02174e870f 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Acl/List.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Acl/List.js @@ -35,7 +35,7 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Ui/User/Search/Input", Util_1.default.hide(this.#container); this.#container.classList.add("aclContainer"); // insert container elements - const elementContainer = this.#container.querySelector("dd"); + const elementContainer = this.#container.querySelector(":scope > .formField__input") ?? this.#container.querySelector("dd"); this.#aclList = document.createElement("ul"); this.#aclList.classList.add("aclList"); elementContainer.appendChild(this.#aclList); diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Dialog.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Dialog.js index e8e91753843..732e5aff2cb 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Dialog.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Dialog.js @@ -511,7 +511,7 @@ define(["require", "exports", "tslib", "../Core", "../Dom/Change/Listener", "./S return; } const contentContainer = data.content.parentNode; - const formSubmit = data.content.querySelector(".formSubmit"); + const formSubmit = data.content.querySelector(".formSubmit, .formBuilder__submit"); let unavailableHeight = 0; if (formSubmit !== null) { contentContainer.classList.add("dialogForm"); @@ -538,7 +538,7 @@ define(["require", "exports", "tslib", "../Core", "../Dom/Change/Listener", "./S //noinspection JSUnresolvedVariable if (callbackObject !== undefined && typeof callbackObject._dialogSubmit === "function") { const inputFields = data.content.querySelectorAll('input[data-dialog-submit-on-enter="true"]'); - const submitButton = data.content.querySelector('.formSubmit > input[type="submit"], .formSubmit > button[data-type="submit"]'); + const submitButton = data.content.querySelector(':is(.formSubmit, .formBuilder__submit) > :is(input[type="submit"], button[data-type="submit"])'); if (submitButton === null) { // check if there is at least one input field with submit handling, // otherwise we'll assume the dialog has not been populated yet @@ -585,12 +585,12 @@ define(["require", "exports", "tslib", "../Core", "../Dom/Change/Listener", "./S if (inputField.required) { if (inputField.value.trim() === "") { Util_1.default.innerError(inputField, (0, Language_1.getPhrase)("wcf.global.form.error.empty")); - inputField.closest("dl")?.classList.add("formError"); + inputField.closest("dl, .formField")?.classList.add("formError"); isValid = false; } else { Util_1.default.innerError(inputField, false); - inputField.closest("dl")?.classList.remove("formError"); + inputField.closest("dl, .formField")?.classList.remove("formError"); } } }); diff --git a/wcfsetup/install/files/lib/acp/action/LanguageItemEditAction.class.php b/wcfsetup/install/files/lib/acp/action/LanguageItemEditAction.class.php index 00f6463f6ec..722f6ea8f6c 100644 --- a/wcfsetup/install/files/lib/acp/action/LanguageItemEditAction.class.php +++ b/wcfsetup/install/files/lib/acp/action/LanguageItemEditAction.class.php @@ -123,7 +123,6 @@ private function getForm(LanguageItem $languageItem): Psr15DialogForm ->appendChildren([ MultilineTextFormField::create('languageItemValue') ->label('wcf.acp.language.item.value') - ->rows(5) ->immutable((bool)$languageItem->languageItemOriginIsSystem) ]), FormContainer::create('oldValueContainer') @@ -138,7 +137,6 @@ private function getForm(LanguageItem $languageItem): Psr15DialogForm ]) ->appendChildren([ MultilineTextFormField::create('languageItemOldValue') - ->rows(5) ->immutable() ]), FormContainer::create('customValueContainer') @@ -148,7 +146,6 @@ private function getForm(LanguageItem $languageItem): Psr15DialogForm BooleanFormField::create('languageUseCustomValue') ->label('wcf.acp.language.item.useCustomValue'), MultilineTextFormField::create('languageCustomItemValue') - ->rows(5) ->addDependency( NonEmptyFormFieldDependency::create('languageUseCustomValue') ->fieldId('languageUseCustomValue') diff --git a/wcfsetup/install/files/lib/acp/form/ContactOptionAddForm.class.php b/wcfsetup/install/files/lib/acp/form/ContactOptionAddForm.class.php index 6cc2a035a91..0ec29deabe9 100644 --- a/wcfsetup/install/files/lib/acp/form/ContactOptionAddForm.class.php +++ b/wcfsetup/install/files/lib/acp/form/ContactOptionAddForm.class.php @@ -58,7 +58,6 @@ protected function createForm() MultilineTextFormField::create('optionDescription') ->label('wcf.global.description') ->maximumLength(5000) - ->rows(5) ->i18n() ->languageItemPattern('wcf.contact.optionDescription\d+'), ShowOrderFormField::create('showOrder') diff --git a/wcfsetup/install/files/lib/acp/form/NoticeAddForm.class.php b/wcfsetup/install/files/lib/acp/form/NoticeAddForm.class.php index 94dff3f3693..1950d5a0fcf 100644 --- a/wcfsetup/install/files/lib/acp/form/NoticeAddForm.class.php +++ b/wcfsetup/install/files/lib/acp/form/NoticeAddForm.class.php @@ -146,8 +146,12 @@ protected function createForm(): void ->loadValueCallback(static function (Notice $object, IFormField $field) { $field->value($object->isCustom() ? $object->cssClassName : ''); }), - TemplateFormNode::create('cssClassNameExample') - ->templateName('__noticeCssClassNameExample'), + ...\array_map( + static fn(string $type) => TemplateFormNode::create('cssClassNameExample' . \ucfirst($type)) + ->templateName('__noticeCssClassNameExample') + ->variables(['type' => $type]), + Notice::TYPES + ), BooleanFormField::create('isDisabled') ->label('wcf.acp.notice.isDisabled') ->saveValueCallback(static function (NoticeBuilder $builder, IFormField $field) { @@ -181,6 +185,14 @@ protected function finalizeForm(): void ->field($this->form->getFormField('cssClassName')) ->values(['custom']) ); + + foreach (Notice::TYPES as $type) { + $this->form->getNodeById('cssClassNameExample' . \ucfirst($type))->addDependency( + ValueFormFieldDependency::create('cssClassName') + ->field($this->form->getFormField('cssClassName')) + ->values([$type]) + ); + } } /** diff --git a/wcfsetup/install/files/lib/system/form/builder/container/FormContainer.class.php b/wcfsetup/install/files/lib/system/form/builder/container/FormContainer.class.php index 66c4ffc5c38..b4b89568f82 100644 --- a/wcfsetup/install/files/lib/system/form/builder/container/FormContainer.class.php +++ b/wcfsetup/install/files/lib/system/form/builder/container/FormContainer.class.php @@ -43,7 +43,7 @@ class FormContainer implements IFormContainer public function __construct() { - $this->addClass('section'); + $this->addClass('formSection'); } #[\Override] diff --git a/wcfsetup/install/files/lib/system/form/builder/container/TabMenuFormContainer.class.php b/wcfsetup/install/files/lib/system/form/builder/container/TabMenuFormContainer.class.php index ee63b64782f..18751e2145a 100644 --- a/wcfsetup/install/files/lib/system/form/builder/container/TabMenuFormContainer.class.php +++ b/wcfsetup/install/files/lib/system/form/builder/container/TabMenuFormContainer.class.php @@ -26,7 +26,7 @@ class TabMenuFormContainer extends FormContainer implements ITabMenuFormContaine // @phpstan-ignore constructor.missingParentCall public function __construct() { - $this->addClass('section') + $this->addClass('formSection') ->addClass('tabMenuContainer'); } diff --git a/wcfsetup/install/files/lib/system/form/builder/container/wysiwyg/WysiwygSmileyFormContainer.class.php b/wcfsetup/install/files/lib/system/form/builder/container/wysiwyg/WysiwygSmileyFormContainer.class.php index 7250eba1c40..8c17c209fcd 100644 --- a/wcfsetup/install/files/lib/system/form/builder/container/wysiwyg/WysiwygSmileyFormContainer.class.php +++ b/wcfsetup/install/files/lib/system/form/builder/container/wysiwyg/WysiwygSmileyFormContainer.class.php @@ -54,7 +54,7 @@ public function populate() FormContainer::create( $this->getId() . '_smileyCategoryContainer' . $smileyCategory->categoryID ) - ->removeClass('section') + ->removeClass('formSection') ->appendChild( WysiwygSmileyFormNode::create( $this->getId() . '_smileyCategory' . $smileyCategory->categoryID diff --git a/wcfsetup/install/files/lib/system/form/builder/container/wysiwyg/WysiwygTabMenuFormContainer.class.php b/wcfsetup/install/files/lib/system/form/builder/container/wysiwyg/WysiwygTabMenuFormContainer.class.php index f18ebe49b31..3e23c5f007b 100644 --- a/wcfsetup/install/files/lib/system/form/builder/container/wysiwyg/WysiwygTabMenuFormContainer.class.php +++ b/wcfsetup/install/files/lib/system/form/builder/container/wysiwyg/WysiwygTabMenuFormContainer.class.php @@ -23,7 +23,7 @@ class WysiwygTabMenuFormContainer extends TabMenuFormContainer // @phpstan-ignore constructor.missingParentCall public function __construct() { - $this->removeClass('section') + $this->removeClass('formSection') ->removeClass('tabMenuContainer') ->addClass('messageTabMenu'); } diff --git a/wcfsetup/install/files/lib/system/form/builder/field/AbstractFormField.class.php b/wcfsetup/install/files/lib/system/form/builder/field/AbstractFormField.class.php index ba9229c71c4..eb4d3db2afc 100644 --- a/wcfsetup/install/files/lib/system/form/builder/field/AbstractFormField.class.php +++ b/wcfsetup/install/files/lib/system/form/builder/field/AbstractFormField.class.php @@ -139,6 +139,67 @@ public function getHtml() ); } + /** + * Returns the id of the element the label of this field refers to or `null` if this field + * consists of multiple elements and is thus rendered as a group labelled by its label. + * + * @since 6.3 + */ + public function getLabelTarget(): ?string + { + return $this->getPrefixedId(); + } + + /** + * Returns `true` if this field consists of multiple elements and is rendered as a group. + * + * @since 6.3 + */ + public function isGroup(): bool + { + return $this->getLabelTarget() === null; + } + + /** + * Returns the ARIA attributes that link the labelled element of this field (or the group + * element in case of a group) to the field's error messages and description. + * + * @return array + * @since 6.3 + */ + public function getAriaAttributes(): array + { + $attributes = []; + $describedBy = []; + $hasErrors = $this->getValidationErrors() !== []; + + if ($this->isGroup()) { + // `aria-invalid` and `aria-errormessage` are not supported by `role="group"` + if ($hasErrors) { + $describedBy[] = $this->getPrefixedId() . 'Error'; + } + } else { + if ($this->isRequired()) { + $attributes['aria-required'] = 'true'; + } + + if ($hasErrors) { + $attributes['aria-invalid'] = 'true'; + $attributes['aria-errormessage'] = $this->getPrefixedId() . 'Error'; + } + } + + if ($this->getDescription() !== null) { + $describedBy[] = $this->getPrefixedId() . 'Description'; + } + + if ($describedBy !== []) { + $attributes['aria-describedby'] = \implode(' ', $describedBy); + } + + return $attributes; + } + #[\Override] public function getJavaScriptDataHandlerModule() { diff --git a/wcfsetup/install/files/lib/system/form/builder/field/AbstractFormFieldDecorator.class.php b/wcfsetup/install/files/lib/system/form/builder/field/AbstractFormFieldDecorator.class.php index 5aaf4619686..ff4d1bdc0a9 100644 --- a/wcfsetup/install/files/lib/system/form/builder/field/AbstractFormFieldDecorator.class.php +++ b/wcfsetup/install/files/lib/system/form/builder/field/AbstractFormFieldDecorator.class.php @@ -308,6 +308,42 @@ public function getHtml() return $this->field->getHtml(); } + /** + * @see AbstractFormField::getLabelTarget() + * @since 6.3 + */ + public function getLabelTarget(): ?string + { + if ($this->field instanceof AbstractFormField || $this->field instanceof self) { + return $this->field->getLabelTarget(); + } + + return $this->field->getPrefixedId(); + } + + /** + * @see AbstractFormField::isGroup() + * @since 6.3 + */ + public function isGroup(): bool + { + return $this->getLabelTarget() === null; + } + + /** + * @see AbstractFormField::getAriaAttributes() + * @return array + * @since 6.3 + */ + public function getAriaAttributes(): array + { + if ($this->field instanceof AbstractFormField || $this->field instanceof self) { + return $this->field->getAriaAttributes(); + } + + return []; + } + #[\Override] public function getHtmlVariables() { diff --git a/wcfsetup/install/files/lib/system/form/builder/field/BadgeColorFormField.class.php b/wcfsetup/install/files/lib/system/form/builder/field/BadgeColorFormField.class.php index 8418e868883..1f5ee51f809 100644 --- a/wcfsetup/install/files/lib/system/form/builder/field/BadgeColorFormField.class.php +++ b/wcfsetup/install/files/lib/system/form/builder/field/BadgeColorFormField.class.php @@ -47,7 +47,11 @@ final class BadgeColorFormField extends RadioButtonFormField implements IPattern public function __construct() { + parent::__construct(); + + // the colors are laid out by `.labelSelection` $this + ->removeClass('floated') ->addFieldClass('labelSelection__radio') ->defaultLabelText(WCF::getLanguage()->get('wcf.acp.label.defaultValue')) ->options(\array_combine(self::AVAILABLE_CSS_CLASSNAMES, self::AVAILABLE_CSS_CLASSNAMES)) @@ -185,4 +189,10 @@ public function supportsCustomClassName(): bool { return \in_array(BadgeColorFormField::CUSTOM_CSS_CLASSNAME, $this->options, true); } + + #[\Override] + public function getLabelTarget(): ?string + { + return null; + } } diff --git a/wcfsetup/install/files/lib/system/form/builder/field/ColorFormField.class.php b/wcfsetup/install/files/lib/system/form/builder/field/ColorFormField.class.php index 8b5ab411910..4f913b3f7bb 100644 --- a/wcfsetup/install/files/lib/system/form/builder/field/ColorFormField.class.php +++ b/wcfsetup/install/files/lib/system/form/builder/field/ColorFormField.class.php @@ -62,4 +62,25 @@ public function validate() } } } + + #[\Override] + public function getLabelTarget(): ?string + { + if ($this->isImmutable()) { + return null; + } + + return $this->getPrefixedId() . '_colorPickerButton'; + } + + #[\Override] + public function getAriaAttributes(): array + { + $attributes = parent::getAriaAttributes(); + + // `aria-required` is not supported by buttons + unset($attributes['aria-required']); + + return $attributes; + } } diff --git a/wcfsetup/install/files/lib/system/form/builder/field/DateRangeFormField.class.php b/wcfsetup/install/files/lib/system/form/builder/field/DateRangeFormField.class.php index c0f62c8a2d2..aca443eb1ad 100644 --- a/wcfsetup/install/files/lib/system/form/builder/field/DateRangeFormField.class.php +++ b/wcfsetup/install/files/lib/system/form/builder/field/DateRangeFormField.class.php @@ -234,4 +234,10 @@ public function getToValue(): string { return $this->value['to'] ?? ''; } + + #[\Override] + public function getLabelTarget(): ?string + { + return null; + } } diff --git a/wcfsetup/install/files/lib/system/form/builder/field/FileProcessorFormField.class.php b/wcfsetup/install/files/lib/system/form/builder/field/FileProcessorFormField.class.php index 4584b2d682c..56a0bd0ab0a 100644 --- a/wcfsetup/install/files/lib/system/form/builder/field/FileProcessorFormField.class.php +++ b/wcfsetup/install/files/lib/system/form/builder/field/FileProcessorFormField.class.php @@ -456,4 +456,10 @@ public function getThumbnailSize(): ?string { return $this->thumbnailSize; } + + #[\Override] + public function getLabelTarget(): ?string + { + return null; + } } diff --git a/wcfsetup/install/files/lib/system/form/builder/field/IconFormField.class.php b/wcfsetup/install/files/lib/system/form/builder/field/IconFormField.class.php index 40b3f31de1b..d10c5b30815 100644 --- a/wcfsetup/install/files/lib/system/form/builder/field/IconFormField.class.php +++ b/wcfsetup/install/files/lib/system/form/builder/field/IconFormField.class.php @@ -108,4 +108,10 @@ public function getIcon(): ?FontAwesomeIcon return null; } + + #[\Override] + public function getLabelTarget(): ?string + { + return null; + } } diff --git a/wcfsetup/install/files/lib/system/form/builder/field/MultilineItemListFormField.class.php b/wcfsetup/install/files/lib/system/form/builder/field/MultilineItemListFormField.class.php index d5228dd38f5..e2ef9708283 100644 --- a/wcfsetup/install/files/lib/system/form/builder/field/MultilineItemListFormField.class.php +++ b/wcfsetup/install/files/lib/system/form/builder/field/MultilineItemListFormField.class.php @@ -46,4 +46,10 @@ public function isFilterable(): bool { return $this->filterable; } + + #[\Override] + public function getLabelTarget(): ?string + { + return null; + } } diff --git a/wcfsetup/install/files/lib/system/form/builder/field/MultilineTextFormField.class.php b/wcfsetup/install/files/lib/system/form/builder/field/MultilineTextFormField.class.php index 35fd375dc97..3d931c88535 100644 --- a/wcfsetup/install/files/lib/system/form/builder/field/MultilineTextFormField.class.php +++ b/wcfsetup/install/files/lib/system/form/builder/field/MultilineTextFormField.class.php @@ -16,7 +16,7 @@ class MultilineTextFormField extends TextFormField * number of rows of the textarea * @var int */ - protected $rows = 10; + protected $rows = 5; /** * @inheritDoc @@ -38,7 +38,7 @@ protected function getValidAutoCompleteTokens(): array /** * Returns the number of rows of the textarea. If the number of rows has not been - * explicitly set, `10` is returned. + * explicitly set, `5` is returned. * * @return int number of textarea rows */ diff --git a/wcfsetup/install/files/lib/system/form/builder/field/MultipleSelectFormField.class.php b/wcfsetup/install/files/lib/system/form/builder/field/MultipleSelectFormField.class.php index 8045c8d6c1c..055901ce69e 100644 --- a/wcfsetup/install/files/lib/system/form/builder/field/MultipleSelectFormField.class.php +++ b/wcfsetup/install/files/lib/system/form/builder/field/MultipleSelectFormField.class.php @@ -22,4 +22,10 @@ class MultipleSelectFormField extends MultipleSelectionFormField * @inheritDoc */ protected $templateName = 'shared_multipleSelectFormField'; + + #[\Override] + public function getLabelTarget(): string + { + return $this->getPrefixedId(); + } } diff --git a/wcfsetup/install/files/lib/system/form/builder/field/MultipleSelectionFormField.class.php b/wcfsetup/install/files/lib/system/form/builder/field/MultipleSelectionFormField.class.php index ca23d92e87d..27187f2b16a 100644 --- a/wcfsetup/install/files/lib/system/form/builder/field/MultipleSelectionFormField.class.php +++ b/wcfsetup/install/files/lib/system/form/builder/field/MultipleSelectionFormField.class.php @@ -151,4 +151,10 @@ public function ignoreInvalidValues(bool $ignoreInvalidValues = true): self return $this; } + + #[\Override] + public function getLabelTarget(): ?string + { + return null; + } } diff --git a/wcfsetup/install/files/lib/system/form/builder/field/NumericRangeFormField.class.php b/wcfsetup/install/files/lib/system/form/builder/field/NumericRangeFormField.class.php index 131389a4ab7..0be324c49b7 100644 --- a/wcfsetup/install/files/lib/system/form/builder/field/NumericRangeFormField.class.php +++ b/wcfsetup/install/files/lib/system/form/builder/field/NumericRangeFormField.class.php @@ -192,4 +192,10 @@ public function getDefaultStep(): int|string return 'any'; } } + + #[\Override] + public function getLabelTarget(): ?string + { + return null; + } } diff --git a/wcfsetup/install/files/lib/system/form/builder/field/ObjectFilterFormField.class.php b/wcfsetup/install/files/lib/system/form/builder/field/ObjectFilterFormField.class.php index 353c792172f..e87b2abf436 100644 --- a/wcfsetup/install/files/lib/system/form/builder/field/ObjectFilterFormField.class.php +++ b/wcfsetup/install/files/lib/system/form/builder/field/ObjectFilterFormField.class.php @@ -187,4 +187,10 @@ function (array $value) use ($filters): array { )), ); } + + #[\Override] + public function getLabelTarget(): ?string + { + return null; + } } diff --git a/wcfsetup/install/files/lib/system/form/builder/field/RadioButtonFormField.class.php b/wcfsetup/install/files/lib/system/form/builder/field/RadioButtonFormField.class.php index a79e219dcbd..14ad79401ee 100644 --- a/wcfsetup/install/files/lib/system/form/builder/field/RadioButtonFormField.class.php +++ b/wcfsetup/install/files/lib/system/form/builder/field/RadioButtonFormField.class.php @@ -33,6 +33,11 @@ class RadioButtonFormField extends AbstractFormField implements */ protected $templateName = 'shared_radioButtonFormField'; + public function __construct() + { + $this->addClass('floated'); + } + #[\Override] public function readValue() { @@ -69,4 +74,10 @@ public function validate() parent::validate(); } + + #[\Override] + public function getLabelTarget(): ?string + { + return null; + } } diff --git a/wcfsetup/install/files/lib/system/form/builder/field/RatingFormField.class.php b/wcfsetup/install/files/lib/system/form/builder/field/RatingFormField.class.php index abbe18b7a05..8a632d9f52f 100644 --- a/wcfsetup/install/files/lib/system/form/builder/field/RatingFormField.class.php +++ b/wcfsetup/install/files/lib/system/form/builder/field/RatingFormField.class.php @@ -37,7 +37,7 @@ final class RatingFormField extends AbstractFormField implements /** * @inheritDoc */ - protected $javaScriptDataHandlerModule = 'WoltLabSuite/Core/Form/Builder/Field/Value'; + protected $javaScriptDataHandlerModule = 'WoltLabSuite/Core/Form/Builder/Field/RadioButton'; /** * @inheritDoc @@ -119,4 +119,10 @@ public function validate(): void } } } + + #[\Override] + public function getLabelTarget(): ?string + { + return null; + } } diff --git a/wcfsetup/install/files/lib/system/form/builder/field/SelectOptionsFormField.class.php b/wcfsetup/install/files/lib/system/form/builder/field/SelectOptionsFormField.class.php index 4dafb29403b..5b0b42005fa 100644 --- a/wcfsetup/install/files/lib/system/form/builder/field/SelectOptionsFormField.class.php +++ b/wcfsetup/install/files/lib/system/form/builder/field/SelectOptionsFormField.class.php @@ -128,4 +128,10 @@ private function validateKeys(array $keys): void $knownKeys[$key] = true; } } + + #[\Override] + public function getLabelTarget(): ?string + { + return null; + } } diff --git a/wcfsetup/install/files/lib/system/form/builder/field/SingleSelectionFormField.class.php b/wcfsetup/install/files/lib/system/form/builder/field/SingleSelectionFormField.class.php index fb7fd8775e7..2635f1bb454 100644 --- a/wcfsetup/install/files/lib/system/form/builder/field/SingleSelectionFormField.class.php +++ b/wcfsetup/install/files/lib/system/form/builder/field/SingleSelectionFormField.class.php @@ -106,4 +106,10 @@ public function value(mixed $value) return parent::value($value); } + + #[\Override] + public function getLabelTarget(): ?string + { + return $this->isFilterable() ? null : $this->getPrefixedId(); + } } diff --git a/wcfsetup/install/files/lib/system/form/builder/field/TAttributeFormField.class.php b/wcfsetup/install/files/lib/system/form/builder/field/TAttributeFormField.class.php index deddebba72c..4d262080f7d 100644 --- a/wcfsetup/install/files/lib/system/form/builder/field/TAttributeFormField.class.php +++ b/wcfsetup/install/files/lib/system/form/builder/field/TAttributeFormField.class.php @@ -53,10 +53,17 @@ public function getFieldAttribute(string $name) /** * Returns all additional attributes of the actual field element. * + * If the actual field element is the element labelled by the field's label, the ARIA + * attributes linking it to the field's error messages and description are included. + * * @return array */ public function getFieldAttributes(): array { + if ($this instanceof AbstractFormField && $this->getLabelTarget() === $this->getPrefixedId()) { + return \array_merge($this->getAriaAttributes(), $this->fieldAttributes); + } + return $this->fieldAttributes; } diff --git a/wcfsetup/install/files/lib/system/form/builder/field/TimeRangeFormField.class.php b/wcfsetup/install/files/lib/system/form/builder/field/TimeRangeFormField.class.php index 0939aadaaaa..bedacdcbbe1 100644 --- a/wcfsetup/install/files/lib/system/form/builder/field/TimeRangeFormField.class.php +++ b/wcfsetup/install/files/lib/system/form/builder/field/TimeRangeFormField.class.php @@ -158,4 +158,10 @@ public function getToValue(): string { return $this->value['to'] ?? ''; } + + #[\Override] + public function getLabelTarget(): ?string + { + return null; + } } diff --git a/wcfsetup/install/files/lib/system/form/builder/field/UploadFormField.class.php b/wcfsetup/install/files/lib/system/form/builder/field/UploadFormField.class.php index 82e71a60775..8e07e26dacd 100644 --- a/wcfsetup/install/files/lib/system/form/builder/field/UploadFormField.class.php +++ b/wcfsetup/install/files/lib/system/form/builder/field/UploadFormField.class.php @@ -1016,4 +1016,10 @@ public function setAllowedFileExtensions(?array $allowedFileExtensions = null): return $this; } + + #[\Override] + public function getLabelTarget(): ?string + { + return null; + } } diff --git a/wcfsetup/install/files/lib/system/form/builder/field/acl/AclFormField.class.php b/wcfsetup/install/files/lib/system/form/builder/field/acl/AclFormField.class.php index 677c05eaed7..34018309920 100644 --- a/wcfsetup/install/files/lib/system/form/builder/field/acl/AclFormField.class.php +++ b/wcfsetup/install/files/lib/system/form/builder/field/acl/AclFormField.class.php @@ -291,4 +291,10 @@ public function cleanup(): static return $this; } + + #[\Override] + public function getLabelTarget(): ?string + { + return null; + } } diff --git a/wcfsetup/install/files/lib/system/form/builder/field/acl/simple/SimpleAclFormField.class.php b/wcfsetup/install/files/lib/system/form/builder/field/acl/simple/SimpleAclFormField.class.php index 069add8874a..7677dae5e0c 100644 --- a/wcfsetup/install/files/lib/system/form/builder/field/acl/simple/SimpleAclFormField.class.php +++ b/wcfsetup/install/files/lib/system/form/builder/field/acl/simple/SimpleAclFormField.class.php @@ -113,4 +113,10 @@ private function isInverted(): bool { return $this->supportInvertedPermissions && !empty($this->value['invertPermissions']); } + + #[\Override] + public function getLabelTarget(): ?string + { + return null; + } } diff --git a/wcfsetup/install/files/lib/system/form/builder/field/language/ContentLanguageFormField.class.php b/wcfsetup/install/files/lib/system/form/builder/field/language/ContentLanguageFormField.class.php index b7815e1c539..3785ca5ffad 100644 --- a/wcfsetup/install/files/lib/system/form/builder/field/language/ContentLanguageFormField.class.php +++ b/wcfsetup/install/files/lib/system/form/builder/field/language/ContentLanguageFormField.class.php @@ -84,4 +84,10 @@ protected static function getDefaultId(): string { return 'languageID'; } + + #[\Override] + public function getLabelTarget(): ?string + { + return null; + } } diff --git a/wcfsetup/install/files/lib/system/form/builder/field/media/SingleMediaSelectionFormField.class.php b/wcfsetup/install/files/lib/system/form/builder/field/media/SingleMediaSelectionFormField.class.php index 712d374c4d5..0c0cf2a549f 100644 --- a/wcfsetup/install/files/lib/system/form/builder/field/media/SingleMediaSelectionFormField.class.php +++ b/wcfsetup/install/files/lib/system/form/builder/field/media/SingleMediaSelectionFormField.class.php @@ -121,4 +121,10 @@ public function validate(): void $this->addValidationError(new FormFieldValidationError('empty')); } } + + #[\Override] + public function getLabelTarget(): ?string + { + return null; + } } diff --git a/wcfsetup/install/files/lib/system/form/builder/field/poll/PollOptionListFormField.class.php b/wcfsetup/install/files/lib/system/form/builder/field/poll/PollOptionListFormField.class.php index ac05a732a0d..320bb4829ee 100644 --- a/wcfsetup/install/files/lib/system/form/builder/field/poll/PollOptionListFormField.class.php +++ b/wcfsetup/install/files/lib/system/form/builder/field/poll/PollOptionListFormField.class.php @@ -216,4 +216,10 @@ public function getHtmlVariables() ), ]; } + + #[\Override] + public function getLabelTarget(): ?string + { + return null; + } } diff --git a/wcfsetup/install/files/lib/system/form/builder/field/trophy/TrophyBadgeFormField.class.php b/wcfsetup/install/files/lib/system/form/builder/field/trophy/TrophyBadgeFormField.class.php index 4954f1c7ea5..4e6324a8768 100644 --- a/wcfsetup/install/files/lib/system/form/builder/field/trophy/TrophyBadgeFormField.class.php +++ b/wcfsetup/install/files/lib/system/form/builder/field/trophy/TrophyBadgeFormField.class.php @@ -172,4 +172,10 @@ public function getIcon(): FontAwesomeIcon return FontAwesomeIcon::fromString(self::DEFAULT_ICON_NAME); } + + #[\Override] + public function getLabelTarget(): ?string + { + return null; + } } diff --git a/wcfsetup/install/files/lib/system/package/plugin/AbstractOptionPackageInstallationPlugin.class.php b/wcfsetup/install/files/lib/system/package/plugin/AbstractOptionPackageInstallationPlugin.class.php index 0b499bbc076..4367843a79c 100644 --- a/wcfsetup/install/files/lib/system/package/plugin/AbstractOptionPackageInstallationPlugin.class.php +++ b/wcfsetup/install/files/lib/system/package/plugin/AbstractOptionPackageInstallationPlugin.class.php @@ -566,8 +566,7 @@ protected function addFormFields(IFormDocument $form) MultilineTextFormField::create('defaultValue') ->objectProperty('defaultvalue') ->label('wcf.acp.pip.abstractOption.options.defaultValue') - ->description('wcf.acp.pip.abstractOption.options.defaultValue.description') - ->rows(5), + ->description('wcf.acp.pip.abstractOption.options.defaultValue.description'), TextFormField::create('validationPattern') ->objectProperty('validationpattern') @@ -589,8 +588,7 @@ protected function addFormFields(IFormDocument $form) MultilineTextFormField::create('enableOptions') ->objectProperty('enableoptions') ->label('wcf.acp.pip.abstractOption.options.enableOptions') - ->description('wcf.acp.pip.abstractOption.options.enableOptions.description') - ->rows(5), + ->description('wcf.acp.pip.abstractOption.options.enableOptions.description'), IntegerFormField::create('showOrder') ->objectProperty('showorder') diff --git a/wcfsetup/install/files/lib/system/package/plugin/OptionPackageInstallationPlugin.class.php b/wcfsetup/install/files/lib/system/package/plugin/OptionPackageInstallationPlugin.class.php index c8a08580b95..edd3831e1eb 100644 --- a/wcfsetup/install/files/lib/system/package/plugin/OptionPackageInstallationPlugin.class.php +++ b/wcfsetup/install/files/lib/system/package/plugin/OptionPackageInstallationPlugin.class.php @@ -264,7 +264,6 @@ static function (TextFormField $formField) { ->objectProperty('selectoptions') ->label('wcf.acp.pip.abstractOption.options.selectOptions') ->description('wcf.acp.pip.option.abstractOption.selectOptions.description') - ->rows(5) ->addDependency( ValueFormFieldDependency::create('optionType') ->field($optionType) diff --git a/wcfsetup/install/files/lib/system/package/plugin/PagePackageInstallationPlugin.class.php b/wcfsetup/install/files/lib/system/package/plugin/PagePackageInstallationPlugin.class.php index c899e40d754..f5e703e9866 100644 --- a/wcfsetup/install/files/lib/system/package/plugin/PagePackageInstallationPlugin.class.php +++ b/wcfsetup/install/files/lib/system/package/plugin/PagePackageInstallationPlugin.class.php @@ -476,7 +476,6 @@ protected function addFormFields(IFormDocument $form) ->label('wcf.acp.pip.page.pageType') ->description('wcf.acp.pip.page.pageType.description') ->options(\array_combine(Page::$availablePageTypes, Page::$availablePageTypes)) - ->addClass('floated') ->required(), TextFormField::create('name') diff --git a/wcfsetup/install/files/lib/system/package/plugin/UserGroupOptionPackageInstallationPlugin.class.php b/wcfsetup/install/files/lib/system/package/plugin/UserGroupOptionPackageInstallationPlugin.class.php index fa9a04fe8f4..f5612f4faa4 100644 --- a/wcfsetup/install/files/lib/system/package/plugin/UserGroupOptionPackageInstallationPlugin.class.php +++ b/wcfsetup/install/files/lib/system/package/plugin/UserGroupOptionPackageInstallationPlugin.class.php @@ -278,20 +278,17 @@ protected function addFormFields(IFormDocument $form) MultilineTextFormField::create('adminDefaultValue') ->objectProperty('admindefaultvalue') ->label('wcf.acp.pip.userGroupOption.options.adminDefaultValue') - ->description('wcf.acp.pip.userGroupOption.options.adminDefaultValue.description') - ->rows(5), + ->description('wcf.acp.pip.userGroupOption.options.adminDefaultValue.description'), MultilineTextFormField::create('modDefaultValue') ->objectProperty('moddefaultvalue') ->label('wcf.acp.pip.userGroupOption.options.modDefaultValue') - ->description('wcf.acp.pip.userGroupOption.options.modDefaultValue.description') - ->rows(5), + ->description('wcf.acp.pip.userGroupOption.options.modDefaultValue.description'), MultilineTextFormField::create('userDefaultValue') ->objectProperty('userdefaultvalue') ->label('wcf.acp.pip.userGroupOption.options.userDefaultValue') - ->description('wcf.acp.pip.userGroupOption.options.userDefaultValue.description') - ->rows(5), + ->description('wcf.acp.pip.userGroupOption.options.userDefaultValue.description'), BooleanFormField::create('usersOnly') ->objectProperty('usersonly') diff --git a/wcfsetup/install/files/lib/system/package/plugin/UserOptionPackageInstallationPlugin.class.php b/wcfsetup/install/files/lib/system/package/plugin/UserOptionPackageInstallationPlugin.class.php index 28e118d1d94..80c2ad77f1e 100644 --- a/wcfsetup/install/files/lib/system/package/plugin/UserOptionPackageInstallationPlugin.class.php +++ b/wcfsetup/install/files/lib/system/package/plugin/UserOptionPackageInstallationPlugin.class.php @@ -582,8 +582,7 @@ protected function addFormFields(IFormDocument $form) MultilineTextFormField::create('defaultValue') ->objectProperty('defaultvalue') ->label('wcf.acp.pip.abstractOption.options.defaultValue') - ->description('wcf.acp.pip.abstractOption.options.defaultValue.description') - ->rows(5), + ->description('wcf.acp.pip.abstractOption.options.defaultValue.description'), TextFormField::create('validationPattern') ->objectProperty('validationpattern') @@ -596,8 +595,7 @@ protected function addFormFields(IFormDocument $form) MultilineTextFormField::create('selectOptions') ->objectProperty('selectoptions') ->label('wcf.acp.pip.abstractOption.options.selectOptions') - ->description('wcf.acp.pip.abstractOption.options.selectOptions.description') - ->rows(5), + ->description('wcf.acp.pip.abstractOption.options.selectOptions.description'), IntegerFormField::create('showOrder') ->objectProperty('showorder') diff --git a/wcfsetup/install/files/style/bootstrap/mixin/input.scss b/wcfsetup/install/files/style/bootstrap/mixin/input.scss index 4fac3903623..eeee84bd338 100644 --- a/wcfsetup/install/files/style/bootstrap/mixin/input.scss +++ b/wcfsetup/install/files/style/bootstrap/mixin/input.scss @@ -5,7 +5,7 @@ color: var(--wcfInputText); font-weight: 400; outline: none; - padding: 4px 8px; + padding: 8px 12px; // input elements do not inherit font family, size and line-height from body font-family: var(--wcfFontFamily); @@ -19,6 +19,11 @@ color: var(--wcfInputTextActive); } + &:focus-visible { + border-color: var(--wcfInputBorderActive); + box-shadow: 0 0 0 3px rgba(var(--wcfInputBorderActive-rgb) / 0.25); + } + &[disabled], &.disabled { background-color: var(--wcfInputDisabledBackground) !important; diff --git a/wcfsetup/install/files/style/element/dl.scss b/wcfsetup/install/files/style/element/dl.scss index e0f4520536c..d018ea8d28c 100644 --- a/wcfsetup/install/files/style/element/dl.scss +++ b/wcfsetup/install/files/style/element/dl.scss @@ -8,8 +8,8 @@ dl { > dt { color: var(--wcfInputLabel); - display: block; + font-weight: 500; &:not(:empty) { margin-bottom: 5px; diff --git a/wcfsetup/install/files/style/layout/form.scss b/wcfsetup/install/files/style/layout/form.scss index 98349eed134..de93af2408a 100644 --- a/wcfsetup/install/files/style/layout/form.scss +++ b/wcfsetup/install/files/style/layout/form.scss @@ -79,7 +79,7 @@ input[type="text"] { select, textarea { font-size: 16px; - padding: 6px 8px; + padding: 8px 12px; } } @@ -100,6 +100,13 @@ textarea { } } +input[type="checkbox"], +input[type="radio"] { + accent-color: var(--wcfButtonPrimaryBackground); + height: 16px; + width: 16px; +} + input[disabled], textarea[disabled] { -webkit-text-fill-color: var(--wcfInputDisabledText); /* override safari font color change */ @@ -160,6 +167,7 @@ select { > .inputPrefix, > .inputSuffix { align-items: center; + color: var(--wcfContentDimmedText); display: flex; flex: 0 0 auto; } @@ -228,10 +236,6 @@ input { .formError { --color-error: #c00; - dt { - color: var(--color-error) !important; - } - input, select, textarea { @@ -270,7 +274,8 @@ html[data-color-scheme="dark"] .formError { .formFieldRequired, .customOptionRequired { - color: rgba(204, 0, 1, 1) !important; + color: var(--wcfContentDimmedText) !important; + font-weight: 400; } .formFieldRequiredNotice { @@ -279,10 +284,6 @@ html[data-color-scheme="dark"] .formError { @include wcfFontSmall; } -html[data-color-scheme="dark"] :is(.formFieldRequired, .customOptionRequired) { - color: #ff4444 !important; -} - /* password strength estimator */ .inputAddonPasswordStrength { align-items: flex-start; diff --git a/wcfsetup/install/files/style/layout/print.scss b/wcfsetup/install/files/style/layout/print.scss index 17a5cc8a268..c1256a7d83e 100644 --- a/wcfsetup/install/files/style/layout/print.scss +++ b/wcfsetup/install/files/style/layout/print.scss @@ -27,6 +27,7 @@ .statusDisplay, .dialogContainer, .formSubmit, + .formBuilder__submit, .tabMenu > ul > li:not(.active), .showMore, diff --git a/wcfsetup/install/files/style/ui/formBuilder.scss b/wcfsetup/install/files/style/ui/formBuilder.scss new file mode 100644 index 00000000000..ec60ef746a7 --- /dev/null +++ b/wcfsetup/install/files/style/ui/formBuilder.scss @@ -0,0 +1,196 @@ +/* Form builder forms: `.formBuilder > .formSection > .formField` */ +.formBuilder { + display: flex; + flex-direction: column; + row-gap: 20px; +} + +/* Form builder buttons: `.formBuilder > .formBuilder__submit` */ +.formBuilder__submit { + display: flex; + flex-direction: row-reverse; + flex-wrap: wrap; + gap: 10px; + justify-content: center; + + @include screen-xs { + input[type="button"], + input[type="reset"], + input[type="submit"], + .button { + flex: 1 0 auto; + } + } +} + +/* Form builder sections: `.formSection > .formSection__header > .formSection__title + .formSection__description` */ +.formSection { + display: flex; + flex-direction: column; + row-gap: 20px; +} + +.formSection > .tabMenuContent { + display: flex; + flex-direction: column; + row-gap: 20px; +} + +.formSection ~ .formSection:not(.tabMenuContainer) { + border-top: 1px solid var(--wcfContentBorderInner); + padding-top: 20px; +} + +.formSection > .formSection__header { + display: flex; + flex-direction: column; + row-gap: 2px; +} + +.formSection__title { + @include wcfFontHeadline; + + & { + font-weight: 600; + } + + a { + color: var(--wcfContentHeadlineLink); + + &:hover { + color: var(--wcfContentHeadlineLinkActive); + } + } +} + +.formSection__description { + color: var(--wcfContentDimmedText); +} + +.formSection__required { + color: var(--wcfContentDimmedText); + font-weight: 400; +} + +/* Form builder fields: `.formField > .formField__label + .formField__input + .formField__errors + .formField__description` */ +.formField { + display: flex; + flex-direction: column; + row-gap: 5px; + + &.wide > .formField__label { + display: none; + } +} + +/* `HiddenFormField` has no visible output, the wrapper must not take part in the `row-gap` */ +.formField:has(> .formField__input > input[type="hidden"]:only-child) { + display: none; +} + +.formField__label { + color: var(--wcfInputLabel); + font-weight: 500; + + &:empty { + display: none; + } +} + +.formSuccess .formField__label { + color: var(--color-success) !important; +} + +.formGrid .formField__label { + display: none; +} + +/* lists of radio buttons or checkboxes */ +.formField__input:has(> label) { + align-items: flex-start; + display: flex; + flex-direction: column; + row-gap: 5px; +} + +.formField.floated > .formField__input { + column-gap: 10px; + display: flex; + flex-direction: row; + flex-wrap: wrap; +} + +.formField__required { + color: var(--wcfContentDimmedText); + font-weight: 400; +} + +.formField__inlineLabel { + align-items: flex-start; + display: inline-flex; + gap: 8px; +} + +.formField__description { + color: var(--wcfContentDimmedText); + + @include wcfFontSmall; +} + +.formField__errors { + display: flex; + flex-direction: column; + row-gap: 3px; +} + +.formField__error { + color: var(--color-error, #c00); + font-weight: 600; + + @include wcfFontSmall; + + a { + color: inherit; + text-decoration: underline; + } +} + +html[data-color-scheme="dark"] .formField__error { + color: var(--color-error, #ff4747); +} + +/* fields inside a `RowFormFieldContainer` */ +.formField__gridItem { + display: flex; + flex-direction: column; + row-gap: 5px; +} + +/* the tab menu of a `WysiwygFormContainer` is attached to the editor, offsets the `row-gap` of `.formSection` */ +.formSection > .formField + .messageTabMenu { + margin-top: -20px; +} + +/* Form builder forms inside dialogs */ +:is(.dialogContent, .dialog__content) { + .formSection__title { + border-bottom-width: 0; + padding-bottom: 0; + + @include wcfFontDefault; + + & { + font-weight: 600; + } + } + + .formSection > .formSection__header { + border-bottom-width: 0; + padding-bottom: 0; + } + + .formSection ~ .formSection:not(.tabMenuContainer) { + border-top: 1px solid var(--wcfContentBorderInner); + padding-top: 20px; + } +} diff --git a/wcfsetup/install/files/style/ui/rating.scss b/wcfsetup/install/files/style/ui/rating.scss index 0551310d89e..f20be559e5d 100644 --- a/wcfsetup/install/files/style/ui/rating.scss +++ b/wcfsetup/install/files/style/ui/rating.scss @@ -1,15 +1,89 @@ -.ratingList { - font-size: 0; +.ratingField { + align-items: center; + display: inline-flex; + gap: 5px; +} + +.ratingField__stars { + display: inline-flex; +} + +.ratingField__star, +.ratingField__reset { + cursor: pointer; + display: inline-flex; + position: relative; + + &:has(.ratingField__input:disabled) { + cursor: default; + } + + &:has(.ratingField__input:focus-visible) { + border-radius: var(--wcfBorderRadius); + outline: 2px solid var(--wcfInputBorderActive); + outline-offset: 1px; + } +} + +.ratingField__reset { + color: var(--wcfContentDimmedText); + order: 1; + + &:hover { + color: var(--wcfContentText); + } +} - > li { - display: inline-block; +.ratingField__icon--full { + color: #f90; +} + +/* Stars up to and including the selected rating are filled, all following stars are empty. */ +.ratingField__star { + .ratingField__icon--empty { + display: none; } - fa-icon[name="star"][solid] { - color: #f90; + &:has(.ratingField__input:checked) ~ .ratingField__star { + .ratingField__icon--empty { + display: inline-flex; + } + + .ratingField__icon--full { + display: none; + } } +} + +.ratingField__stars:not(:has(.ratingField__input:checked)) .ratingField__star { + .ratingField__icon--empty { + display: inline-flex; + } + + .ratingField__icon--full { + display: none; + } +} + +/* Preview the rating while hovering a star. */ +.ratingField__stars:not(:has(.ratingField__input:disabled)):hover { + .ratingField__star { + .ratingField__icon--empty { + display: none; + } + + .ratingField__icon--full { + display: inline-flex; + } + } + + .ratingField__star:hover ~ .ratingField__star { + .ratingField__icon--empty { + display: inline-flex; + } - .ratingMetaButton { - margin-left: 5px; + .ratingField__icon--full { + display: none; + } } } diff --git a/wcfsetup/setup/db/install.sql b/wcfsetup/setup/db/install.sql index 37ccb63ed7d..2b32f976881 100644 --- a/wcfsetup/setup/db/install.sql +++ b/wcfsetup/setup/db/install.sql @@ -124,7 +124,7 @@ INSERT INTO wcf1_style_variable (variableName, defaultValue, defaultValueDarkMod INSERT INTO wcf1_style_variable (variableName, defaultValue, defaultValueDarkMode) VALUES('wcfInputDisabledBackground', 'rgba(245, 245, 245, 1)', 'rgba(34, 37, 41, 1)'); INSERT INTO wcf1_style_variable (variableName, defaultValue, defaultValueDarkMode) VALUES('wcfInputDisabledBorder', 'rgba(174, 176, 179, 1)', 'rgba(56, 56, 57, 1)'); INSERT INTO wcf1_style_variable (variableName, defaultValue, defaultValueDarkMode) VALUES('wcfInputDisabledText', 'rgba(125, 130, 100, 1)', 'rgba(118, 119, 121, 1)'); -INSERT INTO wcf1_style_variable (variableName, defaultValue, defaultValueDarkMode) VALUES('wcfInputLabel', 'rgba(59, 109, 169, 1)', 'rgba(144, 164, 174, 1)'); +INSERT INTO wcf1_style_variable (variableName, defaultValue, defaultValueDarkMode) VALUES('wcfInputLabel', 'rgba(58, 58, 61, 1)', 'rgba(209, 210, 211, 1)'); INSERT INTO wcf1_style_variable (variableName, defaultValue, defaultValueDarkMode) VALUES('wcfInputText', 'rgba(58, 58, 61, 1)', 'rgba(209, 210, 211, 1)'); INSERT INTO wcf1_style_variable (variableName, defaultValue, defaultValueDarkMode) VALUES('wcfInputTextActive', 'rgba(58, 58, 61, 1)', 'rgba(209, 210, 211, 1)'); INSERT INTO wcf1_style_variable (variableName, defaultValue, defaultValueDarkMode) VALUES('wcfInputPlaceholder', 'rgba(169, 169, 169, 1)', 'rgba(122, 123, 125, 1)');