From 472d6923e32360c318f2e978823d8d5e74f1ecd0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikl=C3=B3s=20Fazekas?= Date: Thu, 16 Jul 2026 10:25:50 +0200 Subject: [PATCH] feat(ios): expose editor-authored accessibility semantics to VoiceOver Adds an optional semantics view prop (Semantics.Off/On/Automatic, default Off) that maps to RiveUIView.semantics on the experimental iOS backend, so accessibility semantics authored in the Rive editor (roles, labels, selection state) are exposed to VoiceOver. Semantics-only prop changes apply to the live RiveUIView without rebuilding artboard/state machine. Semantics exist in the new Rive runtime only, so the legacy iOS backend accepts the prop and ignores it; Android (both backends) does the same until upstream rive-android ships semantics support. The Semantics example page uses tabtest.riv from rive-runtime's semantic test assets (three tabs with roles and single-selection state). Verified on an iPhone 17 / iOS 26.5 simulator: with semantics On the tabs appear as accessibility buttons (Child / Parent / All, with the selected trait); switching to Off removes them. Known upstream quirk: re-enabling semantics on a live view (Off -> On) does not repopulate elements until the view is remounted - the SDK restarts its semantics manager but no full tree diff is re-delivered. --- README.md | 1 + .../com/margelo/nitro/rive/HybridRiveView.kt | 4 + .../com/margelo/nitro/rive/HybridRiveView.kt | 4 + example/assets/rive/tabtest.riv | Bin 0 -> 1389 bytes example/src/demos/SemanticsExample.tsx | 122 ++++++++++++++++++ ios/legacy/HybridRiveView.swift | 3 + ios/new/HybridRiveView.swift | 11 ++ ios/new/RiveReactNativeView.swift | 8 ++ .../android/c++/JHybridRiveViewSpec.cpp | 13 ++ .../android/c++/JHybridRiveViewSpec.hpp | 2 + nitrogen/generated/android/c++/JSemantics.hpp | 61 +++++++++ .../c++/views/JHybridRiveViewStateUpdater.cpp | 4 + .../margelo/nitro/rive/HybridRiveViewSpec.kt | 6 + .../com/margelo/nitro/rive/Semantics.kt | 24 ++++ .../generated/ios/RNRive-Swift-Cxx-Bridge.hpp | 18 +++ .../ios/RNRive-Swift-Cxx-Umbrella.hpp | 3 + .../ios/c++/HybridRiveViewSpecSwift.hpp | 10 ++ .../ios/c++/views/HybridRiveViewComponent.mm | 5 + .../ios/swift/HybridRiveViewSpec.swift | 1 + .../ios/swift/HybridRiveViewSpec_cxx.swift | 17 +++ nitrogen/generated/ios/swift/Semantics.swift | 44 +++++++ .../shared/c++/HybridRiveViewSpec.cpp | 2 + .../shared/c++/HybridRiveViewSpec.hpp | 5 + nitrogen/generated/shared/c++/Semantics.hpp | 63 +++++++++ .../c++/views/HybridRiveViewComponent.cpp | 11 ++ .../c++/views/HybridRiveViewComponent.hpp | 2 + .../generated/shared/json/RiveViewConfig.json | 1 + src/core/Semantics.ts | 19 +++ src/index.tsx | 1 + src/specs/RiveView.nitro.ts | 11 ++ 30 files changed, 476 insertions(+) create mode 100644 example/assets/rive/tabtest.riv create mode 100644 example/src/demos/SemanticsExample.tsx create mode 100644 nitrogen/generated/android/c++/JSemantics.hpp create mode 100644 nitrogen/generated/android/kotlin/com/margelo/nitro/rive/Semantics.kt create mode 100644 nitrogen/generated/ios/swift/Semantics.swift create mode 100644 nitrogen/generated/shared/c++/Semantics.hpp create mode 100644 src/core/Semantics.ts diff --git a/README.md b/README.md index 7a367f9f..a89ec04c 100644 --- a/README.md +++ b/README.md @@ -235,6 +235,7 @@ The following table compares feature availability with the [previous Rive React | `useRiveFile()` hook | ✅ | Convenient hook to load a Rive file | | `RiveView` error handling | ✅ | Error handler for failed view operations | | `source` .riv file loading | ✅ | Conveniently load .riv files from JS source | +| Accessibility semantics | ⚠️ | Editor-authored semantics → VoiceOver (iOS; Android in progress) | | Animation selection | ❌ | Animation playback not planned, use state machines | | Renderer options | ❌ | Single renderer option available (Rive) | diff --git a/android/src/legacy/java/com/margelo/nitro/rive/HybridRiveView.kt b/android/src/legacy/java/com/margelo/nitro/rive/HybridRiveView.kt index 318ad099..b3953629 100644 --- a/android/src/legacy/java/com/margelo/nitro/rive/HybridRiveView.kt +++ b/android/src/legacy/java/com/margelo/nitro/rive/HybridRiveView.kt @@ -89,6 +89,10 @@ class HybridRiveView(val context: ThemedReactContext) : HybridRiveViewSpec() { override var alignment: Alignment? = null override var fit: Fit? = null override var layoutScaleFactor: Double? = null + + // Accepted for API parity; semantics are only available in the new Rive + // runtime, and Android support is pending upstream (iOS-only for now). + override var semantics: Semantics? = null override var dataBind: Variant_HybridViewModelInstanceSpec_DataBindMode_DataBindByName? = null set(value) { if (field != value) { diff --git a/android/src/new/java/com/margelo/nitro/rive/HybridRiveView.kt b/android/src/new/java/com/margelo/nitro/rive/HybridRiveView.kt index 518c2871..7cc08ce6 100644 --- a/android/src/new/java/com/margelo/nitro/rive/HybridRiveView.kt +++ b/android/src/new/java/com/margelo/nitro/rive/HybridRiveView.kt @@ -96,6 +96,10 @@ class HybridRiveView(val context: ThemedReactContext) : HybridRiveViewSpec() { override var alignment: Alignment? = null override var fit: Fit? = null override var layoutScaleFactor: Double? = null + + // Accepted for API parity; semantics support is pending in the upstream + // rive-android runtime (iOS-only for now). + override var semantics: Semantics? = null override var dataBind: Variant_HybridViewModelInstanceSpec_DataBindMode_DataBindByName? = null set(value) { if (field != value) { diff --git a/example/assets/rive/tabtest.riv b/example/assets/rive/tabtest.riv new file mode 100644 index 0000000000000000000000000000000000000000..59de6a336fc98b25081f2d4ffaf9b972e009b73f GIT binary patch literal 1389 zcmb7EUr1A76hG&F=kspnQp}R|qC|te$`A=Eo2v&QK{BM*{P?zW&hPg--}l}7q_Opp4)Fmk zO0&44Z&8enXpKf>TBK=`)@evi(?0dk4(`x6#q~{EqA?nlDe9vM{7a*>s_%(zn$x!_ zfeD(S1F=l~v_Xs1OG%oiDcU6{2LR;IIHDZtDdVtlL^;$mz){UX9F;IBrpVS}1dXm}}{4?+6Cqn;4_w)(+!MQ)j>k*1&iEU1Ee@zi1A3zBIy- zu4!hS5R(LCYYe@L_(N?5j0*w@qXgre#90I#;LyjG4t;x~d?^4@``z8NA&l_L$gfVjscn?uLGXyY;?h(vrkG zi0gzKgqwt0gxkdXjJt&Ugek%_Aq?B>JWJS;J-l~2}&2)0)Zr;OJ;l6qf$JeY zGdJ4lv01qZL{48ln=Z=z0g%aXMTu3<{Kxm!>7iVed9o*EthN&t@|2c^bE3zdPRJ>H zRdwbollOee@EM`(Jm<8|&7Yg6wyv}#;*S{BP5!p_j*wAZ19t{df9&rv!o0rso*gh~ zP!IRP`vD352>31FmssR)OpMg0U`;?$3`tGpp>Q5m9t-C|2?0ESw{j5G%n5YBJ2|Hp s!Dq%Y_##>Ph+o01_!41Oe_fa@*1;sueo+?|=*nJ~1j>t=v + {error.message} + + ); + } + + return ( + + + With semantics On (or Automatic + VoiceOver running), the tabs below are + exposed to VoiceOver as selectable accessibility elements. + + + {MODES.map(({ label, value }) => ( + setSemantics(value)} + > + + {label} + + + ))} + + {riveFile && ( + + )} + + ); +} + +SemanticsExample.metadata = { + name: 'Semantics (VoiceOver)', + description: + 'Editor-authored accessibility semantics exposed to VoiceOver (iOS new backend)', + order: 3, +} satisfies Metadata; + +const styles = StyleSheet.create({ + container: { + flex: 1, + padding: 16, + backgroundColor: '#fff', + }, + subtitle: { + fontSize: 14, + color: '#666', + textAlign: 'center', + marginBottom: 12, + }, + modeRow: { + flexDirection: 'row', + justifyContent: 'center', + gap: 8, + marginBottom: 16, + }, + modeButton: { + paddingVertical: 8, + paddingHorizontal: 16, + borderRadius: 8, + borderWidth: 1, + borderColor: '#ccc', + }, + modeOn: { + backgroundColor: '#222', + borderColor: '#222', + }, + modeLabel: { + fontSize: 14, + color: '#333', + }, + modeLabelOn: { + color: '#fff', + }, + rive: { + flex: 1, + }, + errorText: { + color: 'red', + }, +}); diff --git a/ios/legacy/HybridRiveView.swift b/ios/legacy/HybridRiveView.swift index ad82ffbe..8ae0a9bc 100644 --- a/ios/legacy/HybridRiveView.swift +++ b/ios/legacy/HybridRiveView.swift @@ -106,6 +106,9 @@ class HybridRiveView: HybridRiveViewSpec { var alignment: Alignment? var fit: Fit? var layoutScaleFactor: Double? + // Accepted for API parity; semantics are only available in the new Rive + // runtime (the experimental backend). + var semantics: Semantics? var onError: (RiveError) -> Void = { _ in } func awaitViewReady() throws -> Promise { diff --git a/ios/new/HybridRiveView.swift b/ios/new/HybridRiveView.swift index 157e4d49..b63496e5 100644 --- a/ios/new/HybridRiveView.swift +++ b/ios/new/HybridRiveView.swift @@ -98,6 +98,7 @@ class HybridRiveView: HybridRiveViewSpec { var alignment: Alignment? var fit: Fit? var layoutScaleFactor: Double? + var semantics: Semantics? var onError: (RiveError) -> Void = { _ in } func awaitViewReady() throws -> Promise { @@ -211,6 +212,7 @@ class HybridRiveView: HybridRiveViewSpec { autoPlay: autoPlay ?? DefaultConfiguration.autoPlay, file: riveFile, fit: toRiveFit(fit, alignment: alignment, layoutScaleFactor: layoutScaleFactor), + semantics: toRiveSemantics(semantics), bindData: try dataBind.toBindData() ) @@ -258,6 +260,15 @@ class HybridRiveView: HybridRiveViewSpec { } } + private func toRiveSemantics(_ semantics: Semantics?) -> RiveRuntime.Semantics { + switch semantics { + case .off: return .off + case .on: return .on + case .automatic: return .automatic + case nil: return RiveUIView.Constants.Defaults.semantics + } + } + private func toRiveAlignment(_ alignment: Alignment?) -> RiveRuntime.Alignment? { guard let alignment = alignment else { return nil } diff --git a/ios/new/RiveReactNativeView.swift b/ios/new/RiveReactNativeView.swift index 598db1fb..da1fc47b 100644 --- a/ios/new/RiveReactNativeView.swift +++ b/ios/new/RiveReactNativeView.swift @@ -15,6 +15,7 @@ struct ViewConfiguration { let autoPlay: Bool let file: File let fit: RiveRuntime.Fit + let semantics: RiveRuntime.Semantics let bindData: BindData } @@ -27,6 +28,9 @@ class RiveReactNativeView: UIView { private var isViewReady = false private var configTask: Task? private var isPaused = false + private var semantics: RiveRuntime.Semantics = RiveUIView.Constants.Defaults.semantics { + didSet { riveUIView?.semantics = semantics } + } var autoPlay: Bool = true /// Configure failures are reported here (wired to the onError prop). @@ -52,6 +56,8 @@ class RiveReactNativeView: UIView { func configure(_ config: ViewConfiguration, dataBindingChanged: Bool = false, reload: Bool = false, initialUpdate: Bool = false) { dispatchPrecondition(condition: .onQueue(.main)) + semantics = config.semantics + if reload { cleanup() } @@ -208,8 +214,10 @@ class RiveReactNativeView: UIView { // not found") from the old MTKView after removeFromSuperview. existing.rive = rive existing.isPaused = isPaused + existing.semantics = semantics } else { let uiView = RiveUIView(rive: rive, isPaused: isPaused) + uiView.semantics = semantics uiView.translatesAutoresizingMaskIntoConstraints = false addSubview(uiView) NSLayoutConstraint.activate([ diff --git a/nitrogen/generated/android/c++/JHybridRiveViewSpec.cpp b/nitrogen/generated/android/c++/JHybridRiveViewSpec.cpp index e19696a1..28b00f64 100644 --- a/nitrogen/generated/android/c++/JHybridRiveViewSpec.cpp +++ b/nitrogen/generated/android/c++/JHybridRiveViewSpec.cpp @@ -13,6 +13,8 @@ namespace margelo::nitro::rive { class HybridRiveFileSpec; } namespace margelo::nitro::rive { enum class Alignment; } // Forward declaration of `Fit` to properly resolve imports. namespace margelo::nitro::rive { enum class Fit; } +// Forward declaration of `Semantics` to properly resolve imports. +namespace margelo::nitro::rive { enum class Semantics; } // Forward declaration of `HybridViewModelInstanceSpec` to properly resolve imports. namespace margelo::nitro::rive { class HybridViewModelInstanceSpec; } // Forward declaration of `DataBindMode` to properly resolve imports. @@ -37,6 +39,8 @@ namespace margelo::nitro::rive { enum class RiveEventType; } #include "JAlignment.hpp" #include "Fit.hpp" #include "JFit.hpp" +#include "Semantics.hpp" +#include "JSemantics.hpp" #include "HybridViewModelInstanceSpec.hpp" #include "DataBindMode.hpp" #include "DataBindByName.hpp" @@ -156,6 +160,15 @@ namespace margelo::nitro::rive { static const auto method = _javaPart->javaClassStatic()->getMethod /* layoutScaleFactor */)>("setLayoutScaleFactor"); method(_javaPart, layoutScaleFactor.has_value() ? jni::JDouble::valueOf(layoutScaleFactor.value()) : nullptr); } + std::optional JHybridRiveViewSpec::getSemantics() { + static const auto method = _javaPart->javaClassStatic()->getMethod()>("getSemantics"); + auto __result = method(_javaPart); + return __result != nullptr ? std::make_optional(__result->toCpp()) : std::nullopt; + } + void JHybridRiveViewSpec::setSemantics(std::optional semantics) { + static const auto method = _javaPart->javaClassStatic()->getMethod /* semantics */)>("setSemantics"); + method(_javaPart, semantics.has_value() ? JSemantics::fromCpp(semantics.value()) : nullptr); + } std::optional, DataBindMode, DataBindByName>> JHybridRiveViewSpec::getDataBind() { static const auto method = _javaPart->javaClassStatic()->getMethod()>("getDataBind"); auto __result = method(_javaPart); diff --git a/nitrogen/generated/android/c++/JHybridRiveViewSpec.hpp b/nitrogen/generated/android/c++/JHybridRiveViewSpec.hpp index 817d66a8..89190c98 100644 --- a/nitrogen/generated/android/c++/JHybridRiveViewSpec.hpp +++ b/nitrogen/generated/android/c++/JHybridRiveViewSpec.hpp @@ -64,6 +64,8 @@ namespace margelo::nitro::rive { void setFit(std::optional fit) override; std::optional getLayoutScaleFactor() override; void setLayoutScaleFactor(std::optional layoutScaleFactor) override; + std::optional getSemantics() override; + void setSemantics(std::optional semantics) override; std::optional, DataBindMode, DataBindByName>> getDataBind() override; void setDataBind(const std::optional, DataBindMode, DataBindByName>>& dataBind) override; std::function getOnError() override; diff --git a/nitrogen/generated/android/c++/JSemantics.hpp b/nitrogen/generated/android/c++/JSemantics.hpp new file mode 100644 index 00000000..6ea66093 --- /dev/null +++ b/nitrogen/generated/android/c++/JSemantics.hpp @@ -0,0 +1,61 @@ +/// +/// JSemantics.hpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © Marc Rousavy @ Margelo +/// + +#pragma once + +#include +#include "Semantics.hpp" + +namespace margelo::nitro::rive { + + using namespace facebook; + + /** + * The C++ JNI bridge between the C++ enum "Semantics" and the the Kotlin enum "Semantics". + */ + struct JSemantics final: public jni::JavaClass { + public: + static auto constexpr kJavaDescriptor = "Lcom/margelo/nitro/rive/Semantics;"; + + public: + /** + * Convert this Java/Kotlin-based enum to the C++ enum Semantics. + */ + [[maybe_unused]] + [[nodiscard]] + Semantics toCpp() const { + static const auto clazz = javaClassStatic(); + static const auto fieldOrdinal = clazz->getField("value"); + int ordinal = this->getFieldValue(fieldOrdinal); + return static_cast(ordinal); + } + + public: + /** + * Create a Java/Kotlin-based enum with the given C++ enum's value. + */ + [[maybe_unused]] + static jni::alias_ref fromCpp(Semantics value) { + static const auto clazz = javaClassStatic(); + switch (value) { + case Semantics::OFF: + static const auto fieldOFF = clazz->getStaticField("OFF"); + return clazz->getStaticFieldValue(fieldOFF); + case Semantics::ON: + static const auto fieldON = clazz->getStaticField("ON"); + return clazz->getStaticFieldValue(fieldON); + case Semantics::AUTOMATIC: + static const auto fieldAUTOMATIC = clazz->getStaticField("AUTOMATIC"); + return clazz->getStaticFieldValue(fieldAUTOMATIC); + default: + std::string stringValue = std::to_string(static_cast(value)); + throw std::invalid_argument("Invalid enum value (" + stringValue + "!"); + } + } + }; + +} // namespace margelo::nitro::rive diff --git a/nitrogen/generated/android/c++/views/JHybridRiveViewStateUpdater.cpp b/nitrogen/generated/android/c++/views/JHybridRiveViewStateUpdater.cpp index f2f3cdf9..4bef850c 100644 --- a/nitrogen/generated/android/c++/views/JHybridRiveViewStateUpdater.cpp +++ b/nitrogen/generated/android/c++/views/JHybridRiveViewStateUpdater.cpp @@ -65,6 +65,10 @@ void JHybridRiveViewStateUpdater::updateViewProps(jni::alias_ref /* hybridView->setLayoutScaleFactor(props->layoutScaleFactor.value); props->layoutScaleFactor.isDirty = false; } + if (props->semantics.isDirty) { + hybridView->setSemantics(props->semantics.value); + props->semantics.isDirty = false; + } if (props->dataBind.isDirty) { hybridView->setDataBind(props->dataBind.value); props->dataBind.isDirty = false; diff --git a/nitrogen/generated/android/kotlin/com/margelo/nitro/rive/HybridRiveViewSpec.kt b/nitrogen/generated/android/kotlin/com/margelo/nitro/rive/HybridRiveViewSpec.kt index b211c235..43700d52 100644 --- a/nitrogen/generated/android/kotlin/com/margelo/nitro/rive/HybridRiveViewSpec.kt +++ b/nitrogen/generated/android/kotlin/com/margelo/nitro/rive/HybridRiveViewSpec.kt @@ -69,6 +69,12 @@ abstract class HybridRiveViewSpec: HybridView() { @set:Keep abstract var layoutScaleFactor: Double? + @get:DoNotStrip + @get:Keep + @set:DoNotStrip + @set:Keep + abstract var semantics: Semantics? + @get:DoNotStrip @get:Keep @set:DoNotStrip diff --git a/nitrogen/generated/android/kotlin/com/margelo/nitro/rive/Semantics.kt b/nitrogen/generated/android/kotlin/com/margelo/nitro/rive/Semantics.kt new file mode 100644 index 00000000..8a3f22d7 --- /dev/null +++ b/nitrogen/generated/android/kotlin/com/margelo/nitro/rive/Semantics.kt @@ -0,0 +1,24 @@ +/// +/// Semantics.kt +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © Marc Rousavy @ Margelo +/// + +package com.margelo.nitro.rive + +import androidx.annotation.Keep +import com.facebook.proguard.annotations.DoNotStrip + +/** + * Represents the JavaScript enum/union "Semantics". + */ +@DoNotStrip +@Keep +enum class Semantics(@DoNotStrip @Keep val value: Int) { + OFF(0), + ON(1), + AUTOMATIC(2); + + companion object +} diff --git a/nitrogen/generated/ios/RNRive-Swift-Cxx-Bridge.hpp b/nitrogen/generated/ios/RNRive-Swift-Cxx-Bridge.hpp index 3be060dd..792ec88d 100644 --- a/nitrogen/generated/ios/RNRive-Swift-Cxx-Bridge.hpp +++ b/nitrogen/generated/ios/RNRive-Swift-Cxx-Bridge.hpp @@ -78,6 +78,8 @@ namespace margelo::nitro::rive { enum class RiveErrorType; } namespace margelo::nitro::rive { struct RiveError; } // Forward declaration of `RiveEventType` to properly resolve imports. namespace margelo::nitro::rive { enum class RiveEventType; } +// Forward declaration of `Semantics` to properly resolve imports. +namespace margelo::nitro::rive { enum class Semantics; } // Forward declaration of `UnifiedRiveEvent` to properly resolve imports. namespace margelo::nitro::rive { struct UnifiedRiveEvent; } // Forward declaration of `ViewModelPropertyInfo` to properly resolve imports. @@ -167,6 +169,7 @@ namespace RNRive { class HybridViewModelTriggerPropertySpec_cxx; } #include "RiveError.hpp" #include "RiveErrorType.hpp" #include "RiveEventType.hpp" +#include "Semantics.hpp" #include "UnifiedRiveEvent.hpp" #include "ViewModelPropertyInfo.hpp" #include "ViewModelPropertyType.hpp" @@ -955,6 +958,21 @@ namespace margelo::nitro::rive::bridge::swift { return optional.value(); } + // pragma MARK: std::optional + /** + * Specialized version of `std::optional`. + */ + using std__optional_Semantics_ = std::optional; + inline std::optional create_std__optional_Semantics_(const Semantics& value) noexcept { + return std::optional(value); + } + inline bool has_value_std__optional_Semantics_(const std::optional& optional) noexcept { + return optional.has_value(); + } + inline Semantics get_std__optional_Semantics_(const std::optional& optional) noexcept { + return optional.value(); + } + // pragma MARK: std::shared_ptr /** * Specialized version of `std::shared_ptr`. diff --git a/nitrogen/generated/ios/RNRive-Swift-Cxx-Umbrella.hpp b/nitrogen/generated/ios/RNRive-Swift-Cxx-Umbrella.hpp index cad39b99..02505b5e 100644 --- a/nitrogen/generated/ios/RNRive-Swift-Cxx-Umbrella.hpp +++ b/nitrogen/generated/ios/RNRive-Swift-Cxx-Umbrella.hpp @@ -80,6 +80,8 @@ namespace margelo::nitro::rive { enum class RiveErrorType; } namespace margelo::nitro::rive { struct RiveError; } // Forward declaration of `RiveEventType` to properly resolve imports. namespace margelo::nitro::rive { enum class RiveEventType; } +// Forward declaration of `Semantics` to properly resolve imports. +namespace margelo::nitro::rive { enum class Semantics; } // Forward declaration of `UnifiedRiveEvent` to properly resolve imports. namespace margelo::nitro::rive { struct UnifiedRiveEvent; } // Forward declaration of `ViewModelPropertyInfo` to properly resolve imports. @@ -124,6 +126,7 @@ namespace margelo::nitro::rive { enum class ViewModelPropertyType; } #include "RiveError.hpp" #include "RiveErrorType.hpp" #include "RiveEventType.hpp" +#include "Semantics.hpp" #include "UnifiedRiveEvent.hpp" #include "ViewModelPropertyInfo.hpp" #include "ViewModelPropertyType.hpp" diff --git a/nitrogen/generated/ios/c++/HybridRiveViewSpecSwift.hpp b/nitrogen/generated/ios/c++/HybridRiveViewSpecSwift.hpp index b4f184a0..e2119b22 100644 --- a/nitrogen/generated/ios/c++/HybridRiveViewSpecSwift.hpp +++ b/nitrogen/generated/ios/c++/HybridRiveViewSpecSwift.hpp @@ -18,6 +18,8 @@ namespace margelo::nitro::rive { class HybridRiveFileSpec; } namespace margelo::nitro::rive { enum class Alignment; } // Forward declaration of `Fit` to properly resolve imports. namespace margelo::nitro::rive { enum class Fit; } +// Forward declaration of `Semantics` to properly resolve imports. +namespace margelo::nitro::rive { enum class Semantics; } // Forward declaration of `HybridViewModelInstanceSpec` to properly resolve imports. namespace margelo::nitro::rive { class HybridViewModelInstanceSpec; } // Forward declaration of `DataBindMode` to properly resolve imports. @@ -39,6 +41,7 @@ namespace margelo::nitro::rive { enum class RiveEventType; } #include "HybridRiveFileSpec.hpp" #include "Alignment.hpp" #include "Fit.hpp" +#include "Semantics.hpp" #include "HybridViewModelInstanceSpec.hpp" #include "DataBindMode.hpp" #include "DataBindByName.hpp" @@ -146,6 +149,13 @@ namespace margelo::nitro::rive { inline void setLayoutScaleFactor(std::optional layoutScaleFactor) noexcept override { _swiftPart.setLayoutScaleFactor(layoutScaleFactor); } + inline std::optional getSemantics() noexcept override { + auto __result = _swiftPart.getSemantics(); + return __result; + } + inline void setSemantics(std::optional semantics) noexcept override { + _swiftPart.setSemantics(semantics); + } inline std::optional, DataBindMode, DataBindByName>> getDataBind() noexcept override { auto __result = _swiftPart.getDataBind(); return __result; diff --git a/nitrogen/generated/ios/c++/views/HybridRiveViewComponent.mm b/nitrogen/generated/ios/c++/views/HybridRiveViewComponent.mm index b8b5bba5..d449bd22 100644 --- a/nitrogen/generated/ios/c++/views/HybridRiveViewComponent.mm +++ b/nitrogen/generated/ios/c++/views/HybridRiveViewComponent.mm @@ -107,6 +107,11 @@ - (void) updateProps:(const std::shared_ptr&)props swiftPart.setLayoutScaleFactor(newViewProps.layoutScaleFactor.value); newViewProps.layoutScaleFactor.isDirty = false; } + // semantics: optional + if (newViewProps.semantics.isDirty) { + swiftPart.setSemantics(newViewProps.semantics.value); + newViewProps.semantics.isDirty = false; + } // dataBind: optional if (newViewProps.dataBind.isDirty) { swiftPart.setDataBind(newViewProps.dataBind.value); diff --git a/nitrogen/generated/ios/swift/HybridRiveViewSpec.swift b/nitrogen/generated/ios/swift/HybridRiveViewSpec.swift index 42d644fc..e217f1a2 100644 --- a/nitrogen/generated/ios/swift/HybridRiveViewSpec.swift +++ b/nitrogen/generated/ios/swift/HybridRiveViewSpec.swift @@ -17,6 +17,7 @@ public protocol HybridRiveViewSpec_protocol: HybridObject, HybridView { var alignment: Alignment? { get set } var fit: Fit? { get set } var layoutScaleFactor: Double? { get set } + var semantics: Semantics? { get set } var dataBind: Variant__any_HybridViewModelInstanceSpec__DataBindMode_DataBindByName? { get set } var onError: (_ error: RiveError) -> Void { get set } diff --git a/nitrogen/generated/ios/swift/HybridRiveViewSpec_cxx.swift b/nitrogen/generated/ios/swift/HybridRiveViewSpec_cxx.swift index 3901ed53..95e15c7f 100644 --- a/nitrogen/generated/ios/swift/HybridRiveViewSpec_cxx.swift +++ b/nitrogen/generated/ios/swift/HybridRiveViewSpec_cxx.swift @@ -269,6 +269,23 @@ open class HybridRiveViewSpec_cxx { } } + public final var semantics: bridge.std__optional_Semantics_ { + @inline(__always) + get { + return { () -> bridge.std__optional_Semantics_ in + if let __unwrappedValue = self.__implementation.semantics { + return bridge.create_std__optional_Semantics_(__unwrappedValue) + } else { + return .init() + } + }() + } + @inline(__always) + set { + self.__implementation.semantics = newValue.has_value() ? newValue.pointee : nil + } + } + public final var dataBind: bridge.std__optional_std__variant_std__shared_ptr_HybridViewModelInstanceSpec___DataBindMode__DataBindByName__ { @inline(__always) get { diff --git a/nitrogen/generated/ios/swift/Semantics.swift b/nitrogen/generated/ios/swift/Semantics.swift new file mode 100644 index 00000000..501888e6 --- /dev/null +++ b/nitrogen/generated/ios/swift/Semantics.swift @@ -0,0 +1,44 @@ +/// +/// Semantics.swift +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © Marc Rousavy @ Margelo +/// + +/** + * Represents the JS enum `Semantics`, backed by a C++ enum. + */ +public typealias Semantics = margelo.nitro.rive.Semantics + +public extension Semantics { + /** + * Get a Semantics for the given String value, or + * return `nil` if the given value was invalid/unknown. + */ + init?(fromString string: String) { + switch string { + case "Off": + self = .off + case "On": + self = .on + case "Automatic": + self = .automatic + default: + return nil + } + } + + /** + * Get the String value this Semantics represents. + */ + var stringValue: String { + switch self { + case .off: + return "Off" + case .on: + return "On" + case .automatic: + return "Automatic" + } + } +} diff --git a/nitrogen/generated/shared/c++/HybridRiveViewSpec.cpp b/nitrogen/generated/shared/c++/HybridRiveViewSpec.cpp index 5d1ee0fb..2ef5d7ba 100644 --- a/nitrogen/generated/shared/c++/HybridRiveViewSpec.cpp +++ b/nitrogen/generated/shared/c++/HybridRiveViewSpec.cpp @@ -28,6 +28,8 @@ namespace margelo::nitro::rive { prototype.registerHybridSetter("fit", &HybridRiveViewSpec::setFit); prototype.registerHybridGetter("layoutScaleFactor", &HybridRiveViewSpec::getLayoutScaleFactor); prototype.registerHybridSetter("layoutScaleFactor", &HybridRiveViewSpec::setLayoutScaleFactor); + prototype.registerHybridGetter("semantics", &HybridRiveViewSpec::getSemantics); + prototype.registerHybridSetter("semantics", &HybridRiveViewSpec::setSemantics); prototype.registerHybridGetter("dataBind", &HybridRiveViewSpec::getDataBind); prototype.registerHybridSetter("dataBind", &HybridRiveViewSpec::setDataBind); prototype.registerHybridGetter("onError", &HybridRiveViewSpec::getOnError); diff --git a/nitrogen/generated/shared/c++/HybridRiveViewSpec.hpp b/nitrogen/generated/shared/c++/HybridRiveViewSpec.hpp index 1ec43b23..c015acab 100644 --- a/nitrogen/generated/shared/c++/HybridRiveViewSpec.hpp +++ b/nitrogen/generated/shared/c++/HybridRiveViewSpec.hpp @@ -19,6 +19,8 @@ namespace margelo::nitro::rive { class HybridRiveFileSpec; } namespace margelo::nitro::rive { enum class Alignment; } // Forward declaration of `Fit` to properly resolve imports. namespace margelo::nitro::rive { enum class Fit; } +// Forward declaration of `Semantics` to properly resolve imports. +namespace margelo::nitro::rive { enum class Semantics; } // Forward declaration of `HybridViewModelInstanceSpec` to properly resolve imports. namespace margelo::nitro::rive { class HybridViewModelInstanceSpec; } // Forward declaration of `DataBindMode` to properly resolve imports. @@ -36,6 +38,7 @@ namespace margelo::nitro::rive { struct UnifiedRiveEvent; } #include "HybridRiveFileSpec.hpp" #include "Alignment.hpp" #include "Fit.hpp" +#include "Semantics.hpp" #include "HybridViewModelInstanceSpec.hpp" #include "DataBindMode.hpp" #include "DataBindByName.hpp" @@ -86,6 +89,8 @@ namespace margelo::nitro::rive { virtual void setFit(std::optional fit) = 0; virtual std::optional getLayoutScaleFactor() = 0; virtual void setLayoutScaleFactor(std::optional layoutScaleFactor) = 0; + virtual std::optional getSemantics() = 0; + virtual void setSemantics(std::optional semantics) = 0; virtual std::optional, DataBindMode, DataBindByName>> getDataBind() = 0; virtual void setDataBind(const std::optional, DataBindMode, DataBindByName>>& dataBind) = 0; virtual std::function getOnError() = 0; diff --git a/nitrogen/generated/shared/c++/Semantics.hpp b/nitrogen/generated/shared/c++/Semantics.hpp new file mode 100644 index 00000000..0d96e12e --- /dev/null +++ b/nitrogen/generated/shared/c++/Semantics.hpp @@ -0,0 +1,63 @@ +/// +/// Semantics.hpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © Marc Rousavy @ Margelo +/// + +#pragma once + +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif + +namespace margelo::nitro::rive { + + /** + * An enum which can be represented as a JavaScript enum (Semantics). + */ + enum class Semantics { + OFF SWIFT_NAME(off) = 0, + ON SWIFT_NAME(on) = 1, + AUTOMATIC SWIFT_NAME(automatic) = 2, + } CLOSED_ENUM; + +} // namespace margelo::nitro::rive + +namespace margelo::nitro { + + // C++ Semantics <> JS Semantics (enum) + template <> + struct JSIConverter final { + static inline margelo::nitro::rive::Semantics fromJSI(jsi::Runtime& runtime, const jsi::Value& arg) { + int enumValue = JSIConverter::fromJSI(runtime, arg); + return static_cast(enumValue); + } + static inline jsi::Value toJSI(jsi::Runtime& runtime, margelo::nitro::rive::Semantics arg) { + int enumValue = static_cast(arg); + return JSIConverter::toJSI(runtime, enumValue); + } + static inline bool canConvert(jsi::Runtime&, const jsi::Value& value) { + if (!value.isNumber()) { + return false; + } + double number = value.getNumber(); + int integer = static_cast(number); + if (number != integer) { + // The integer is not the same value as the double - we truncated floating points. + // Enums are all integers, so the input floating point number is obviously invalid. + return false; + } + // Check if we are within the bounds of the enum. + return integer >= 0 && integer <= 2; + } + }; + +} // namespace margelo::nitro diff --git a/nitrogen/generated/shared/c++/views/HybridRiveViewComponent.cpp b/nitrogen/generated/shared/c++/views/HybridRiveViewComponent.cpp index d3f60ffa..048e34e5 100644 --- a/nitrogen/generated/shared/c++/views/HybridRiveViewComponent.cpp +++ b/nitrogen/generated/shared/c++/views/HybridRiveViewComponent.cpp @@ -96,6 +96,16 @@ namespace margelo::nitro::rive::views { throw std::runtime_error(std::string("RiveView.layoutScaleFactor: ") + exc.what()); } }()), + semantics([&]() -> CachedProp> { + try { + const react::RawValue* rawValue = rawProps.at("semantics", nullptr, nullptr); + if (rawValue == nullptr) return sourceProps.semantics; + const auto& [runtime, value] = (std::pair)*rawValue; + return CachedProp>::fromRawValue(*runtime, value, sourceProps.semantics); + } catch (const std::exception& exc) { + throw std::runtime_error(std::string("RiveView.semantics: ") + exc.what()); + } + }()), dataBind([&]() -> CachedProp, DataBindMode, DataBindByName>>> { try { const react::RawValue* rawValue = rawProps.at("dataBind", nullptr, nullptr); @@ -136,6 +146,7 @@ namespace margelo::nitro::rive::views { case hashString("alignment"): return true; case hashString("fit"): return true; case hashString("layoutScaleFactor"): return true; + case hashString("semantics"): return true; case hashString("dataBind"): return true; case hashString("onError"): return true; case hashString("hybridRef"): return true; diff --git a/nitrogen/generated/shared/c++/views/HybridRiveViewComponent.hpp b/nitrogen/generated/shared/c++/views/HybridRiveViewComponent.hpp index aa25dc25..59742f91 100644 --- a/nitrogen/generated/shared/c++/views/HybridRiveViewComponent.hpp +++ b/nitrogen/generated/shared/c++/views/HybridRiveViewComponent.hpp @@ -22,6 +22,7 @@ #include "HybridRiveFileSpec.hpp" #include "Alignment.hpp" #include "Fit.hpp" +#include "Semantics.hpp" #include "HybridViewModelInstanceSpec.hpp" #include "DataBindMode.hpp" #include "DataBindByName.hpp" @@ -57,6 +58,7 @@ namespace margelo::nitro::rive::views { CachedProp> alignment; CachedProp> fit; CachedProp> layoutScaleFactor; + CachedProp> semantics; CachedProp, DataBindMode, DataBindByName>>> dataBind; CachedProp> onError; CachedProp& /* ref */)>>> hybridRef; diff --git a/nitrogen/generated/shared/json/RiveViewConfig.json b/nitrogen/generated/shared/json/RiveViewConfig.json index 8eab6292..c2d5d94b 100644 --- a/nitrogen/generated/shared/json/RiveViewConfig.json +++ b/nitrogen/generated/shared/json/RiveViewConfig.json @@ -11,6 +11,7 @@ "alignment": true, "fit": true, "layoutScaleFactor": true, + "semantics": true, "dataBind": true, "onError": true, "hybridRef": true diff --git a/src/core/Semantics.ts b/src/core/Semantics.ts new file mode 100644 index 00000000..8613a3dd --- /dev/null +++ b/src/core/Semantics.ts @@ -0,0 +1,19 @@ +export enum Semantics { + /** + * Accessibility semantics are disabled. No accessibility elements are + * created, regardless of screen-reader state. + */ + Off, + + /** + * Accessibility semantics are always active. Elements are created and kept + * up-to-date on every frame. + */ + On, + + /** + * Accessibility semantics activate and deactivate automatically based on + * whether a screen reader (e.g. VoiceOver) is currently running. + */ + Automatic, +} diff --git a/src/index.tsx b/src/index.tsx index 80bdd5c8..fa28c7f9 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -42,6 +42,7 @@ export type { export type { BindableArtboard } from './specs/BindableArtboard.nitro'; export { Fit } from './core/Fit'; export { Alignment } from './core/Alignment'; +export { Semantics } from './core/Semantics'; export { RiveFileFactory } from './core/RiveFile'; export { RiveImages } from './core/RiveImages'; export type { RiveImage } from './specs/RiveImage.nitro'; diff --git a/src/specs/RiveView.nitro.ts b/src/specs/RiveView.nitro.ts index 8927fc4f..b5a262bc 100644 --- a/src/specs/RiveView.nitro.ts +++ b/src/specs/RiveView.nitro.ts @@ -5,6 +5,7 @@ import type { } from 'react-native-nitro-modules'; import type { RiveFile } from './RiveFile.nitro'; import { Fit } from '../core/Fit'; +import { Semantics } from '../core/Semantics'; import type { ViewModelInstance } from './ViewModel.nitro'; import type { Alignment } from '../core/Alignment'; import type { UnifiedRiveEvent, RiveEvent } from '../core/Events'; @@ -37,6 +38,16 @@ export interface RiveViewProps extends HybridViewProps { fit?: Fit; /** The scale factor to apply to the Rive graphic when using Fit.Layout */ layoutScaleFactor?: number; + /** + * Exposes accessibility semantics authored in the Rive editor to the + * platform screen reader (VoiceOver). Defaults to Semantics.Off. + * + * Only supported on the experimental (default) iOS backend so far; ignored + * elsewhere. Android support is pending the upstream rive-android runtime. + * + * @see https://rive.app/docs/runtimes/apple/semantics + */ + semantics?: Semantics; /** The view model instance to bind, to the state machine. Defaults to DataBindMode.Auto */ dataBind?: ViewModelInstance | DataBindMode | DataBindByName; /** Callback function that is called when an error occurs */