diff --git a/Sources/TUIkit/Modifiers/AlertPresentationModifier.swift b/Sources/TUIkit/Modifiers/AlertPresentationModifier.swift index f4c3115..9553f79 100644 --- a/Sources/TUIkit/Modifiers/AlertPresentationModifier.swift +++ b/Sources/TUIkit/Modifiers/AlertPresentationModifier.swift @@ -57,6 +57,9 @@ public struct AlertPresentationModifier FrameBuffer { // If not presented, just return base content guard isPresented.wrappedValue else { @@ -82,6 +85,8 @@ extension AlertPresentationModifier: Renderable { actions: { actions } ) + let focusManager = context.environment.focusManager + // Render dimmed base with an isolated context. // The base content's buttons and key handlers register into a // throwaway FocusManager and KeyEventDispatcher so they don't @@ -90,11 +95,19 @@ extension AlertPresentationModifier: Renderable { let isolatedContext = context.isolatedForBackground() let dimmedBuffer = TUIkit.renderToBuffer(dimmedBase, context: isolatedContext) - // Clear the real focus manager so the alert's buttons become - // the only registered focusables (auto-focus picks the first one). - context.environment.focusManager.clear() + // Register an alert focus section and activate it. + // The alert section becomes the active section, so Tab/arrows + // only navigate within the alert's focusable elements (buttons). + let sectionID = Self.alertSectionID + focusManager.registerSection(id: sectionID) + focusManager.activateSection(id: sectionID) - let alertBuffer = TUIkit.renderToBuffer(alert, context: context) + // Set the alert section in the context so child focusables + // (buttons in the alert) register in the alert section. + var alertContext = context + alertContext.activeFocusSectionID = sectionID + + let alertBuffer = TUIkit.renderToBuffer(alert, context: alertContext) guard !dimmedBuffer.isEmpty else { return alertBuffer diff --git a/Sources/TUIkit/Modifiers/ModalPresentationModifier.swift b/Sources/TUIkit/Modifiers/ModalPresentationModifier.swift index 856a724..c489afc 100644 --- a/Sources/TUIkit/Modifiers/ModalPresentationModifier.swift +++ b/Sources/TUIkit/Modifiers/ModalPresentationModifier.swift @@ -42,12 +42,17 @@ public struct ModalPresentationModifier: View { // MARK: - Renderable extension ModalPresentationModifier: Renderable { + /// A stable section ID for modal focus sections. + private static var modalSectionID: String { "__modal__" } + func renderToBuffer(context: RenderContext) -> FrameBuffer { // If not presented, just return base content guard isPresented.wrappedValue else { return TUIkit.renderToBuffer(content, context: context) } + let focusManager = context.environment.focusManager + // Render dimmed base with an isolated context. // The base content's buttons and key handlers register into a // throwaway FocusManager and KeyEventDispatcher so they don't @@ -56,11 +61,20 @@ extension ModalPresentationModifier: Renderable { let isolatedContext = context.isolatedForBackground() let dimmedBuffer = TUIkit.renderToBuffer(dimmedBase, context: isolatedContext) - // Clear the real focus manager so the modal's buttons become - // the only registered focusables (auto-focus picks the first one). - context.environment.focusManager.clear() + // Register a modal focus section and activate it. + // This replaces the previous focusManager.clear() approach. + // The modal section becomes the active section, so Tab/arrows + // only navigate within the modal's focusable elements. + let sectionID = Self.modalSectionID + focusManager.registerSection(id: sectionID) + focusManager.activateSection(id: sectionID) - let modalBuffer = TUIkit.renderToBuffer(modal, context: context) + // Set the modal section in the context so child focusables + // (buttons in the modal) register in the modal section. + var modalContext = context + modalContext.activeFocusSectionID = sectionID + + let modalBuffer = TUIkit.renderToBuffer(modal, context: modalContext) guard !dimmedBuffer.isEmpty else { return modalBuffer