Skip to content

Commit

Permalink
Add prefers_default_focus and focus_scope modifiers (#940)
Browse files Browse the repository at this point in the history
* Add `prefers_default_focus` modifier

* Cleanup focus_scope modifier

* Document reset_focus

* Disable reset_focus on iOS
  • Loading branch information
carson-katri committed May 23, 2023
1 parent f3154c8 commit d4fac65
Show file tree
Hide file tree
Showing 6 changed files with 169 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,20 @@ struct NamespaceContext<R: RootRegistry>: View {
@Namespace private var namespace
@Environment(\.namespaces) private var namespaces

#if !os(iOS)
@Environment(\.resetFocus) private var resetFocus
#endif

var body: some View {
context.buildChildren(of: element)
.environment(\.namespaces, namespaces.merging([id: namespace], uniquingKeysWith: { $1 }))
#if !os(iOS)
.onReceive(context.coordinator.receiveEvent("reset_focus")) { event in
guard let namespace = event["namespace"] as? String,
namespace == id
else { return }
resetFocus(in: self.namespace)
}
#endif
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
//
// FocusScopeModifier.swift
// LiveViewNative
//
// Created by Carson Katri on 5/18/2023.
//
import SwiftUI

/// Associates an element hierarchy with a namespace.
///
/// Use a ``NamespaceContext`` element to create a focus namespace.
/// Provide the `id` of the namespace to the ``namespace`` argument.
///
/// ```html
/// <NamespaceContext id={:my_namespace}>
/// <VStack modifiers={focus_scope(@native, namespace: :my_namespace)}>
/// <TextField>Username</TextField>
/// <TextField modifiers={prefers_default_focus(@native, namespace: :my_namespace)}>Password</TextField>
/// </VStack>
/// </NamespaceContext>
/// ```
///
/// ## Arguments
/// * ``namespace``
#if swift(>=5.8)
@_documentation(visibility: public)
#endif
@available(macOS 13.0, tvOS 14.0, watchOS 7.0, *)
struct FocusScopeModifier: ViewModifier, Decodable {
/// The namespace to associate this hierarchy with.
///
/// Use a ``NamespaceContext`` element to create a namespace.
#if swift(>=5.8)
@_documentation(visibility: public)
#endif
private let namespace: String

@Environment(\.namespaces) private var namespaces

init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)

self.namespace = try container.decode(String.self, forKey: .namespace)
}

func body(content: Content) -> some View {
if let namespace = namespaces[namespace] {
content
#if !os(iOS)
.focusScope(namespace)
#endif
} else {
content
}
}

enum CodingKeys: CodingKey {
case namespace
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
//
// PrefersDefaultFocusModifier.swift
// LiveViewNative
//
// Created by Carson Katri on 5/16/2023.
//
import SwiftUI

/// Gives default focus to the element in a ``NamespaceContext``.
///
/// Use a ``NamespaceContext`` element to create a focus namespace.
/// Provide the `id` of the namespace to the ``namespace`` argument.
///
/// ```html
/// <NamespaceContext id={:my_namespace}>
/// <VStack modifiers={focus_scope(@native, :my_namespace)}>
/// <TextField>Username</TextField>
/// <TextField modifiers={prefers_default_focus(@native, namespace: :my_namespace)}>Password</TextField>
/// <Button phx-click="reset_focus" phx-value-namespace={:my_namespace}>Reset Focus</Button>
/// </VStack>
/// </NamespaceContext>
/// ```
///
/// Push the `reset_focus` event to set focus back to the preferred defaults.
///
/// ```elixir
/// def handle_event("reset_focus", %{ "namespace" => namespace }, socket) do
/// {:noreply, push_event(socket, :reset_focus, %{ namespace: namespace })}
/// end
/// ```
///
/// ## Arguments
/// * ``prefersDefaultFocus``
/// * ``namespace``
#if swift(>=5.8)
@_documentation(visibility: public)
#endif
@available(macOS 13.0, tvOS 14.0, watchOS 7.0, *)
struct PrefersDefaultFocusModifier: ViewModifier, Decodable {
/// Enables/disables the effect of this modifier. Defaults to `true`.
#if swift(>=5.8)
@_documentation(visibility: public)
#endif
private let prefersDefaultFocus: Bool

/// The namespace where this element prefers default focus.
///
/// Use a ``NamespaceContext`` element to create a namespace.
#if swift(>=5.8)
@_documentation(visibility: public)
#endif
private let namespace: String

@Environment(\.namespaces) private var namespaces

init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)

self.prefersDefaultFocus = try container.decode(Bool.self, forKey: .prefersDefaultFocus)
self.namespace = try container.decode(String.self, forKey: .namespace)
}

func body(content: Content) -> some View {
if let namespace = namespaces[namespace] {
content
#if !os(iOS)
.prefersDefaultFocus(prefersDefaultFocus, in: namespace)
#endif
} else {
content
}
}

enum CodingKeys: CodingKey {
case prefersDefaultFocus
case namespace
}
}
9 changes: 9 additions & 0 deletions lib/live_view_native_swift_ui/modifiers/focus/focus_scope.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
defmodule LiveViewNativeSwiftUi.Modifiers.FocusScope do
use LiveViewNativePlatform.Modifier

alias LiveViewNativeSwiftUi.Types.Namespace

modifier_schema "focus_scope" do
field :namespace, Namespace
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
defmodule LiveViewNativeSwiftUi.Modifiers.PrefersDefaultFocus do
use LiveViewNativePlatform.Modifier

alias LiveViewNativeSwiftUi.Types.Namespace

modifier_schema "prefers_default_focus" do
field :prefers_default_focus, :boolean, default: true
field :namespace, Namespace
end
end

0 comments on commit d4fac65

Please sign in to comment.