+
diff --git a/wcfsetup/install/files/acp/templates/__menuPipGui.tpl b/wcfsetup/install/files/acp/templates/__menuPipGui.tpl
index fd583b65320..900d6a41c09 100644
--- a/wcfsetup/install/files/acp/templates/__menuPipGui.tpl
+++ b/wcfsetup/install/files/acp/templates/__menuPipGui.tpl
@@ -4,7 +4,7 @@
{jsphrase name='wcf.acp.pip.menu.boxVisibilityExceptions.visibleEverywhere'}
var boxVisibleEverywhere = elById('boxVisibleEverywhere');
- var boxVisibilityExceptionsLabel = elBySel('#boxVisibilityExceptionsContainer > dt > label');
+ var boxVisibilityExceptionsLabel = elById('boxVisibilityExceptionsLabel');
function updateVisibilityExceptions() {
if (boxVisibleEverywhere.checked) {
diff --git a/wcfsetup/install/files/acp/templates/__noticeCssClassNameExample.tpl b/wcfsetup/install/files/acp/templates/__noticeCssClassNameExample.tpl
index 6e0110394e0..7d15151dc7f 100644
--- a/wcfsetup/install/files/acp/templates/__noticeCssClassNameExample.tpl
+++ b/wcfsetup/install/files/acp/templates/__noticeCssClassNameExample.tpl
@@ -1,32 +1 @@
-
-
- -
- {lang}wcf.acp.notice.example{/lang}
-
-
-
-
+
{lang}wcf.acp.notice.example{/lang}
diff --git a/wcfsetup/install/files/acp/templates/__tagFormSynonym.tpl b/wcfsetup/install/files/acp/templates/__tagFormSynonym.tpl
index e72e83228a7..f6d30b6e3ce 100644
--- a/wcfsetup/install/files/acp/templates/__tagFormSynonym.tpl
+++ b/wcfsetup/install/files/acp/templates/__tagFormSynonym.tpl
@@ -1,6 +1,6 @@
-
-
- -
+
diff --git a/wcfsetup/install/files/acp/update_com.woltlab.wcf_6.3_styleVariables.php b/wcfsetup/install/files/acp/update_com.woltlab.wcf_6.3_styleVariables.php
index 26aa6fd5756..b51acf2eb93 100644
--- a/wcfsetup/install/files/acp/update_com.woltlab.wcf_6.3_styleVariables.php
+++ b/wcfsetup/install/files/acp/update_com.woltlab.wcf_6.3_styleVariables.php
@@ -22,6 +22,7 @@
['wcfFooterLink', 'rgba(38, 113, 166, 1)', 'rgba(29, 155, 209, 1)'],
['wcfFooterLinkActive', 'rgba(22, 81, 124, 1)', 'rgba(64, 179, 228, 1)'],
['wcfFooterText', 'rgba(58, 58, 61, 1)', 'rgba(209, 210, 211, 1)'],
+ ['wcfInputLabel', 'rgba(58, 58, 61, 1)', 'rgba(209, 210, 211, 1)'],
];
// Existing styles keep the footer colors of earlier versions, the new defaults
diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Ckeditor/Keyboard.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Ckeditor/Keyboard.js
index 7897944ca85..746b8462ba6 100644
--- a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Ckeditor/Keyboard.js
+++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Ckeditor/Keyboard.js
@@ -36,7 +36,7 @@ define(["require", "exports", "./Event"], function (require, exports, Event_1) {
if (container === null) {
return;
}
- const formSubmit = container.querySelector(".formSubmit");
+ const formSubmit = container.querySelector(".formSubmit, .formBuilder__submit");
if (formSubmit === null) {
return;
}
diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Quote/List.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Quote/List.js
index b0c8a404ff3..f3db472ef80 100644
--- a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Quote/List.js
+++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Quote/List.js
@@ -145,7 +145,7 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Component/Ckeditor", "
this.#container.prepend(buttons);
}
#formSubmitted() {
- const formSubmit = this.#editor.closest("form").querySelector(".formSubmit");
+ const formSubmit = this.#editor.closest("form").querySelector(".formSubmit, .formBuilder__submit");
(0, Storage_1.getUsedQuotes)(this.#editorId).forEach((uuid) => {
formSubmit.append(Util_1.default.createFragmentFromHtml(``));
});
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)');