diff --git a/dependencies.md b/dependencies.md
index 4e4527bf..87fbaa22 100644
--- a/dependencies.md
+++ b/dependencies.md
@@ -1,6 +1,6 @@
-# Dependencies of `io.spine.chords:spine-chords-client:2.0.0-SNAPSHOT.88`
+# Dependencies of `io.spine.chords:spine-chords-client:2.0.0-SNAPSHOT.89`
## Runtime
1. **Group** : cafe.adriel.voyager. **Name** : voyager-core. **Version** : 1.0.1.**No license information found**
@@ -1104,12 +1104,12 @@
The dependencies distributed under several licenses, are used according their commercial-use-friendly license.
-This report was generated on **Tue Jul 14 20:02:53 EEST 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE).
+This report was generated on **Thu Jul 16 14:46:06 EEST 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE).
-# Dependencies of `io.spine.chords:spine-chords-codegen-tests:2.0.0-SNAPSHOT.88`
+# Dependencies of `io.spine.chords:spine-chords-codegen-tests:2.0.0-SNAPSHOT.89`
## Runtime
1. **Group** : com.google.code.findbugs. **Name** : jsr305. **Version** : 3.0.2.
@@ -1899,12 +1899,12 @@ This report was generated on **Tue Jul 14 20:02:53 EEST 2026** using [Gradle-Lic
The dependencies distributed under several licenses, are used according their commercial-use-friendly license.
-This report was generated on **Tue Jul 14 20:02:54 EEST 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE).
+This report was generated on **Thu Jul 16 14:46:07 EEST 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE).
-# Dependencies of `io.spine.chords:spine-chords-core:2.0.0-SNAPSHOT.88`
+# Dependencies of `io.spine.chords:spine-chords-core:2.0.0-SNAPSHOT.89`
## Runtime
1. **Group** : cafe.adriel.voyager. **Name** : voyager-core. **Version** : 1.0.1.
@@ -2938,12 +2938,12 @@ This report was generated on **Tue Jul 14 20:02:54 EEST 2026** using [Gradle-Lic
The dependencies distributed under several licenses, are used according their commercial-use-friendly license.
-This report was generated on **Tue Jul 14 20:02:55 EEST 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE).
+This report was generated on **Thu Jul 16 14:46:08 EEST 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE).
-# Dependencies of `io.spine.chords:spine-chords-proto:2.0.0-SNAPSHOT.88`
+# Dependencies of `io.spine.chords:spine-chords-proto:2.0.0-SNAPSHOT.89`
## Runtime
1. **Group** : cafe.adriel.voyager. **Name** : voyager-core. **Version** : 1.0.1.**No license information found**
@@ -3976,12 +3976,12 @@ This report was generated on **Tue Jul 14 20:02:55 EEST 2026** using [Gradle-Lic
The dependencies distributed under several licenses, are used according their commercial-use-friendly license.
-This report was generated on **Tue Jul 14 20:02:56 EEST 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE).
+This report was generated on **Thu Jul 16 14:46:09 EEST 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE).
-# Dependencies of `io.spine.chords:spine-chords-proto-values:2.0.0-SNAPSHOT.88`
+# Dependencies of `io.spine.chords:spine-chords-proto-values:2.0.0-SNAPSHOT.89`
## Runtime
1. **Group** : com.google.code.findbugs. **Name** : jsr305. **Version** : 3.0.2.
@@ -4775,12 +4775,12 @@ This report was generated on **Tue Jul 14 20:02:56 EEST 2026** using [Gradle-Lic
The dependencies distributed under several licenses, are used according their commercial-use-friendly license.
-This report was generated on **Tue Jul 14 20:02:57 EEST 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE).
+This report was generated on **Thu Jul 16 14:46:10 EEST 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE).
-# Dependencies of `io.spine.chords:spine-chords-runtime:2.0.0-SNAPSHOT.88`
+# Dependencies of `io.spine.chords:spine-chords-runtime:2.0.0-SNAPSHOT.89`
## Runtime
1. **Group** : com.google.code.findbugs. **Name** : jsr305. **Version** : 3.0.2.
@@ -5544,4 +5544,4 @@ This report was generated on **Tue Jul 14 20:02:57 EEST 2026** using [Gradle-Lic
The dependencies distributed under several licenses, are used according their commercial-use-friendly license.
-This report was generated on **Tue Jul 14 20:02:57 EEST 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE).
\ No newline at end of file
+This report was generated on **Thu Jul 16 14:46:11 EEST 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE).
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
index 9e6a9c17..9f8dc7bd 100644
--- a/pom.xml
+++ b/pom.xml
@@ -10,7 +10,7 @@ all modules and does not describe the project structure per-subproject.
-->
io.spine.chords
Chords
-2.0.0-SNAPSHOT.88
+2.0.0-SNAPSHOT.89
2015
diff --git a/proto/src/main/kotlin/io/spine/chords/proto/form/MessageForm.kt b/proto/src/main/kotlin/io/spine/chords/proto/form/MessageForm.kt
index 7c076fb9..0c452026 100644
--- a/proto/src/main/kotlin/io/spine/chords/proto/form/MessageForm.kt
+++ b/proto/src/main/kotlin/io/spine/chords/proto/form/MessageForm.kt
@@ -1544,17 +1544,17 @@ public open class MessageForm : InputComponent(), InputContext {
* @param updateValidationErrors Specifies whether a failure to create
* a message should be accompanied by displaying the respective
* validation errors.
- * @param focusInvalidField Specifies whether the first field that has
+ * @param focusInvalidPart Specifies whether the first field that has
* caused a validation failure (if any) should receive a focus.
*/
private fun updateMessage(
updateValidationErrors: Boolean = true,
- focusInvalidField: Boolean = true
+ focusInvalidPart: Boolean = true
) {
if (updateValidationErrors) {
clearValidationDisplay()
fields.values.forEach {
- it.editor?.updateValidationDisplay(focusInvalidField)
+ it.editor?.updateValidationDisplay(focusInvalidPart)
}
}
@@ -1575,8 +1575,8 @@ public open class MessageForm : InputComponent(), InputContext {
} catch (e: ValidationException) {
if (updateValidationErrors) {
showConstraintViolations(e.constraintViolations)
- if (focusInvalidField) {
- focus()
+ if (focusInvalidPart) {
+ focusInvalidField()
}
recoveringFromManualValidationErrors = true
}
@@ -1595,8 +1595,8 @@ public open class MessageForm : InputComponent(), InputContext {
}
valid.value = !nestedErrorsPresent
value.value = if (nestedErrorsPresent) {
- if (focusInvalidField) {
- focus()
+ if (focusInvalidPart) {
+ focusInvalidField()
}
null
} else {
@@ -1711,13 +1711,71 @@ public open class MessageForm : InputComponent(), InputContext {
}
}
+ /**
+ * Specifies that the focus request that this form is about to receive was
+ * made in order to focus the field that has caused a validation failure.
+ *
+ * Focus requests are dispatched asynchronously (see [focusRequest]), and
+ * a nested form receives them via its [focus] method. This flag is how
+ * a form that focuses an invalid field whose editor is a nested form tells
+ * that form to focus its invalid field as well, instead of its first one.
+ *
+ * The flag is cleared as soon as it is read in [focus].
+ *
+ * @see focusInvalidField
+ */
+ private var focusInvalidFieldRequested = false
+
/**
* Focuses the form.
*
- * More precisely, it focuses the first field, or the first invalid field
- * (if there's at least one invalid field).
+ * More precisely, the form's first field is focused, unless this form has
+ * been asked to focus the field that has caused a validation failure, in
+ * which case [focusInvalidField] is invoked.
+ *
+ * Focusing the first field is what is expected when the form is displayed
+ * to the user for the first time (e.g., when a dialog containing the form
+ * is opened), so the user can start filling the form in from its
+ * first field.
+ *
+ * Note that a field being empty doesn't make it invalid in this context:
+ * all fields of a form that has just been displayed are typically empty,
+ * and some of them can be reported as invalid merely because of this. Use
+ * [focusInvalidField] to focus the field whose entry has actually caused
+ * a validation failure.
+ *
+ * @see focusInvalidField
*/
override fun focus() {
+ if (focusInvalidFieldRequested) {
+ focusInvalidFieldRequested = false
+ focusInvalidField()
+ return
+ }
+ val firstField = fields.values.firstOrNull()
+
+ // If the first field is a oneof field, make sure that the
+ // selected oneof option actually gets the focus by default.
+ val fieldToFocus = firstField?.formOneof?.selectedFormField ?: firstField
+
+ fieldToFocus?.focusEditor()
+ }
+
+ /**
+ * Focuses the first field that has caused a validation failure, and falls
+ * back to focusing the form's first field if there's no such field.
+ *
+ * This is the focusing that is expected once the form's validation has
+ * been triggered, so the user can fix the entry that has caused it
+ * to fail.
+ *
+ * If the field to be focused is edited by a nested form, that form is
+ * asked to focus its invalid field as well, so that the invalid entry is
+ * focused however deeply it is nested.
+ *
+ * @see focus
+ */
+ protected fun focusInvalidField() {
val fieldToFocus =
fields.values.firstOrNull { field ->
!field.valueValid.value ||
@@ -1732,15 +1790,14 @@ public open class MessageForm : InputComponent(), InputContext {
}
formOneof?.selectedFormField
?: formOneof?.fields?.values?.firstOrNull()
- } ?: run {
- val firstField = fields.values.firstOrNull()
-
- // If the first field is a oneof field, make sure that the
- // selected oneof option actually gets the focus by default.
- firstField?.formOneof?.selectedFormField ?: firstField
}
- fieldToFocus?.focusEditor()
+ if (fieldToFocus == null) {
+ focus()
+ return
+ }
+ (fieldToFocus.editor as? MessageForm<*>)?.focusInvalidFieldRequested = true
+ fieldToFocus.focusEditor()
}
}
diff --git a/version.gradle.kts b/version.gradle.kts
index 18bbc932..1d6450f8 100644
--- a/version.gradle.kts
+++ b/version.gradle.kts
@@ -27,4 +27,4 @@
/**
* The version of all Chords libraries.
*/
-val chordsVersion: String by extra("2.0.0-SNAPSHOT.88")
+val chordsVersion: String by extra("2.0.0-SNAPSHOT.89")