commit d622a7926b0254aac40452eca76492c9c54edf80
parent f90f371ae12d10422f0764e6b7a8620112976684
Author: triesap <tyson@radroots.org>
Date: Fri, 30 Jan 2026 14:52:06 +0000
ui: remove redundant primitives modules and deps
- delete superseded primitive modules in ui-primitives
- trim ui-primitives lib module declarations
- remove unused deps from ui-primitives cargo.toml
- update cargo.lock for dependency changes
Diffstat:
10 files changed, 0 insertions(+), 1138 deletions(-)
diff --git a/Cargo.lock b/Cargo.lock
@@ -1757,13 +1757,8 @@ dependencies = [
name = "radroots-app-ui-primitives"
version = "0.1.0"
dependencies = [
- "leptos",
- "radroots-app-ui-core",
- "send_wrapper",
"ui-primitives-core",
"ui-primitives-leptos",
- "wasm-bindgen",
- "web-sys",
]
[[package]]
diff --git a/crates/ui-primitives/Cargo.toml b/crates/ui-primitives/Cargo.toml
@@ -10,26 +10,5 @@ rust-version.workspace = true
crate-type = ["rlib"]
[dependencies]
-radroots-app-ui-core = { path = "../ui-core" }
-leptos = { workspace = true, features = ["csr"] }
ui-primitives-core = { path = "../../refs/ui-primitives/crates/ui-primitives-core" }
ui-primitives-leptos = { path = "../../refs/ui-primitives/crates/ui-primitives-leptos" }
-
-[target.'cfg(target_arch = "wasm32")'.dependencies]
-wasm-bindgen = { workspace = true }
-send_wrapper = { workspace = true }
-web-sys = { workspace = true, features = [
- "CssStyleDeclaration",
- "Document",
- "Element",
- "EventTarget",
- "HtmlCollection",
- "HtmlBodyElement",
- "FocusEvent",
- "HtmlElement",
- "KeyboardEvent",
- "Node",
- "NodeList",
- "PointerEvent",
- "Window"
-] }
diff --git a/crates/ui-primitives/src/aria_hidden.rs b/crates/ui-primitives/src/aria_hidden.rs
@@ -1,229 +0,0 @@
-#[cfg(target_arch = "wasm32")]
-use std::cell::RefCell;
-
-#[cfg(not(target_arch = "wasm32"))]
-use std::sync::{Mutex, OnceLock};
-
-#[cfg(target_arch = "wasm32")]
-use web_sys::{window, Element};
-
-#[cfg(target_arch = "wasm32")]
-#[derive(Debug, Clone)]
-struct HiddenElement {
- element: Element,
- prev_aria_hidden: Option<String>,
- prev_inert: bool,
-}
-
-#[cfg(not(target_arch = "wasm32"))]
-#[derive(Debug, Clone)]
-struct HiddenElement;
-
-#[derive(Debug, Clone)]
-struct ModalLayer {
- id: u64,
- hidden: Vec<HiddenElement>,
-}
-
-#[derive(Debug, Default)]
-struct ModalState {
- next_id: u64,
- layers: Vec<ModalLayer>,
-}
-
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub enum RadrootsAppUiModalError {
- WindowUnavailable,
- DocumentUnavailable,
- BodyUnavailable,
- AttributeUnavailable,
-}
-
-pub type RadrootsAppUiModalResult<T> = Result<T, RadrootsAppUiModalError>;
-
-#[cfg(target_arch = "wasm32")]
-pub type RadrootsAppUiModalTarget = Element;
-
-#[cfg(not(target_arch = "wasm32"))]
-pub type RadrootsAppUiModalTarget = ();
-
-#[derive(Debug)]
-pub struct RadrootsAppUiModalGuard {
- id: u64,
- active: bool,
-}
-
-impl Drop for RadrootsAppUiModalGuard {
- fn drop(&mut self) {
- if self.active {
- let _ = radroots_app_ui_modal_restore(self.id);
- self.active = false;
- }
- }
-}
-
-#[cfg(target_arch = "wasm32")]
-thread_local! {
- static MODAL_STATE: RefCell<ModalState> = RefCell::new(ModalState::default());
-}
-
-#[cfg(not(target_arch = "wasm32"))]
-static MODAL_STATE: OnceLock<Mutex<ModalState>> = OnceLock::new();
-
-#[cfg(target_arch = "wasm32")]
-fn modal_state_with<T>(f: impl FnOnce(&mut ModalState) -> T) -> T {
- MODAL_STATE.with(|state| f(&mut state.borrow_mut()))
-}
-
-#[cfg(not(target_arch = "wasm32"))]
-fn modal_state_with<T>(f: impl FnOnce(&mut ModalState) -> T) -> T {
- let mut state = MODAL_STATE
- .get_or_init(|| Mutex::new(ModalState::default()))
- .lock()
- .unwrap_or_else(|poisoned| poisoned.into_inner());
- f(&mut state)
-}
-
-pub fn radroots_app_ui_modal_hide_siblings(
- root: &RadrootsAppUiModalTarget,
-) -> RadrootsAppUiModalResult<RadrootsAppUiModalGuard> {
- let id = modal_state_with(|state| {
- let id = state.next_id;
- state.next_id = state.next_id.saturating_add(1);
- let hidden = modal_collect_hidden(root)?;
- state.layers.push(ModalLayer { id, hidden });
- Ok(id)
- })?;
- Ok(RadrootsAppUiModalGuard { id, active: true })
-}
-
-pub fn radroots_app_ui_modal_restore(id: u64) -> RadrootsAppUiModalResult<()> {
- modal_state_with(|state| {
- let index = state.layers.iter().position(|layer| layer.id == id);
- let Some(index) = index else {
- return Ok(());
- };
- let removed = state.layers.remove(index);
- modal_restore_hidden(&state.layers, removed.hidden)?;
- Ok(())
- })
-}
-
-#[cfg(target_arch = "wasm32")]
-fn modal_collect_hidden(root: &Element) -> RadrootsAppUiModalResult<Vec<HiddenElement>> {
- let window = window().ok_or(RadrootsAppUiModalError::WindowUnavailable)?;
- let document = window
- .document()
- .ok_or(RadrootsAppUiModalError::DocumentUnavailable)?;
- let body = document
- .body()
- .ok_or(RadrootsAppUiModalError::BodyUnavailable)?;
- let children = body.children();
- let mut hidden = Vec::new();
- for index in 0..children.length() {
- let Some(child) = children.item(index) else {
- continue;
- };
- if modal_is_root_or_ancestor(root, &child) {
- continue;
- }
- let prev_aria_hidden = child.get_attribute("aria-hidden");
- let prev_inert = child.has_attribute("inert");
- child
- .set_attribute("aria-hidden", "true")
- .map_err(|_| RadrootsAppUiModalError::AttributeUnavailable)?;
- child
- .set_attribute("inert", "")
- .map_err(|_| RadrootsAppUiModalError::AttributeUnavailable)?;
- hidden.push(HiddenElement {
- element: child,
- prev_aria_hidden,
- prev_inert,
- });
- }
- Ok(hidden)
-}
-
-#[cfg(not(target_arch = "wasm32"))]
-fn modal_collect_hidden(_root: &RadrootsAppUiModalTarget) -> RadrootsAppUiModalResult<Vec<HiddenElement>> {
- Ok(Vec::new())
-}
-
-#[cfg(target_arch = "wasm32")]
-fn modal_is_root_or_ancestor(root: &Element, candidate: &Element) -> bool {
- candidate.is_same_node(Some(root)) || candidate.contains(Some(root))
-}
-
-#[cfg(target_arch = "wasm32")]
-fn modal_restore_hidden(
- layers: &[ModalLayer],
- hidden: Vec<HiddenElement>,
-) -> RadrootsAppUiModalResult<()> {
- for item in hidden {
- if modal_is_hidden_by_layers(layers, &item.element) {
- continue;
- }
- match item.prev_aria_hidden {
- Some(value) => item
- .element
- .set_attribute("aria-hidden", &value)
- .map_err(|_| RadrootsAppUiModalError::AttributeUnavailable)?,
- None => item
- .element
- .remove_attribute("aria-hidden")
- .map_err(|_| RadrootsAppUiModalError::AttributeUnavailable)?,
- }
- if item.prev_inert {
- item.element
- .set_attribute("inert", "")
- .map_err(|_| RadrootsAppUiModalError::AttributeUnavailable)?;
- } else {
- item.element
- .remove_attribute("inert")
- .map_err(|_| RadrootsAppUiModalError::AttributeUnavailable)?;
- }
- }
- Ok(())
-}
-
-#[cfg(not(target_arch = "wasm32"))]
-fn modal_restore_hidden(
- _layers: &[ModalLayer],
- _hidden: Vec<HiddenElement>,
-) -> RadrootsAppUiModalResult<()> {
- Ok(())
-}
-
-#[cfg(target_arch = "wasm32")]
-fn modal_is_hidden_by_layers(layers: &[ModalLayer], element: &Element) -> bool {
- layers.iter().any(|layer| {
- layer.hidden.iter().any(|item| {
- item.element.is_same_node(Some(element))
- })
- })
-}
-
-#[cfg_attr(not(target_arch = "wasm32"), allow(dead_code))]
-#[cfg(not(target_arch = "wasm32"))]
-fn modal_is_hidden_by_layers(_layers: &[ModalLayer], _element: &RadrootsAppUiModalTarget) -> bool {
- false
-}
-
-#[cfg(test)]
-fn modal_layer_count_for_test() -> usize {
- modal_state_with(|state| state.layers.len())
-}
-
-#[cfg(test)]
-mod tests {
- use super::{modal_layer_count_for_test, radroots_app_ui_modal_hide_siblings};
-
- #[test]
- fn modal_guard_tracks_layers() {
- assert_eq!(modal_layer_count_for_test(), 0);
- let guard = radroots_app_ui_modal_hide_siblings(&()).expect("guard");
- assert_eq!(modal_layer_count_for_test(), 1);
- drop(guard);
- assert_eq!(modal_layer_count_for_test(), 0);
- }
-}
diff --git a/crates/ui-primitives/src/dismissable.rs b/crates/ui-primitives/src/dismissable.rs
@@ -1,165 +0,0 @@
-use leptos::ev::{FocusEvent, KeyboardEvent, PointerEvent};
-use leptos::html;
-use leptos::prelude::*;
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub enum RadrootsAppUiDismissableReason {
- Escape,
- PointerDownOutside,
- FocusOutside,
-}
-
-pub fn radroots_app_ui_dismissable_is_escape(key: &str) -> bool {
- key == "Escape"
-}
-
-pub fn radroots_app_ui_dismissable_is_outside(is_inside: bool) -> bool {
- !is_inside
-}
-
-#[component]
-pub fn RadrootsAppUiDismissableLayer(
- #[prop(optional)] on_dismiss: Option<Callback<RadrootsAppUiDismissableReason>>,
- #[prop(optional)] on_escape_key_down: Option<Callback<KeyboardEvent>>,
- #[prop(optional)] on_pointer_down_outside: Option<Callback<PointerEvent>>,
- #[prop(optional)] on_focus_outside: Option<Callback<FocusEvent>>,
- #[prop(optional)] disable_outside_pointer_events: bool,
- children: ChildrenFn,
-) -> impl IntoView {
- let node_ref = NodeRef::<html::Div>::new();
-
- let on_keydown = move |event: KeyboardEvent| {
- if !radroots_app_ui_dismissable_is_escape(&event.key()) {
- return;
- }
- if let Some(callback) = on_escape_key_down.as_ref() {
- callback.run(event.clone());
- }
- if let Some(callback) = on_dismiss.as_ref() {
- callback.run(RadrootsAppUiDismissableReason::Escape);
- }
- };
-
- #[cfg(target_arch = "wasm32")]
- {
- use wasm_bindgen::closure::Closure;
- use wasm_bindgen::JsCast;
- use send_wrapper::SendWrapper;
-
- let on_dismiss = on_dismiss.clone();
- let on_pointer_down_outside = on_pointer_down_outside.clone();
- let on_focus_outside = on_focus_outside.clone();
- let node_ref = node_ref;
-
- node_ref.on_load(move |root| {
- let document = match web_sys::window().and_then(|window| window.document()) {
- Some(document) => document,
- None => return,
- };
-
- if !disable_outside_pointer_events {
- let root_pointer = root.clone();
- let on_dismiss = on_dismiss.clone();
- let on_pointer_down_outside = on_pointer_down_outside.clone();
- let handler = Closure::wrap(Box::new(move |event: web_sys::PointerEvent| {
- let target = event
- .target()
- .and_then(|target| target.dyn_into::<web_sys::Node>().ok());
- let is_inside = target
- .as_ref()
- .map(|node| root_pointer.contains(Some(node)))
- .unwrap_or(false);
- if radroots_app_ui_dismissable_is_outside(is_inside) {
- if let Some(callback) = on_pointer_down_outside.as_ref() {
- callback.run(event.clone());
- }
- if let Some(callback) = on_dismiss.as_ref() {
- callback.run(RadrootsAppUiDismissableReason::PointerDownOutside);
- }
- }
- }) as Box<dyn FnMut(_)>);
- let _ = document.add_event_listener_with_callback(
- "pointerdown",
- handler.as_ref().unchecked_ref(),
- );
- let cleanup_doc = SendWrapper::new(document.clone());
- let cleanup_handler = SendWrapper::new(handler);
- on_cleanup(move || {
- let document = cleanup_doc.take();
- let handler = cleanup_handler.take();
- let _ = document.remove_event_listener_with_callback(
- "pointerdown",
- handler.as_ref().unchecked_ref(),
- );
- });
- }
-
- let root_focus = root.clone();
- let on_dismiss = on_dismiss.clone();
- let on_focus_outside = on_focus_outside.clone();
- let focus_handler = Closure::wrap(Box::new(move |event: web_sys::FocusEvent| {
- let target = event
- .target()
- .and_then(|target| target.dyn_into::<web_sys::Node>().ok());
- let is_inside = target
- .as_ref()
- .map(|node| root_focus.contains(Some(node)))
- .unwrap_or(false);
- if radroots_app_ui_dismissable_is_outside(is_inside) {
- if let Some(callback) = on_focus_outside.as_ref() {
- callback.run(event.clone());
- }
- if let Some(callback) = on_dismiss.as_ref() {
- callback.run(RadrootsAppUiDismissableReason::FocusOutside);
- }
- }
- }) as Box<dyn FnMut(_)>);
- let _ = document.add_event_listener_with_callback(
- "focusin",
- focus_handler.as_ref().unchecked_ref(),
- );
- let cleanup_doc = SendWrapper::new(document);
- let cleanup_handler = SendWrapper::new(focus_handler);
- on_cleanup(move || {
- let document = cleanup_doc.take();
- let handler = cleanup_handler.take();
- let _ = document.remove_event_listener_with_callback(
- "focusin",
- handler.as_ref().unchecked_ref(),
- );
- });
- });
- }
- #[cfg(not(target_arch = "wasm32"))]
- {
- let _ = on_pointer_down_outside;
- let _ = on_focus_outside;
- let _ = disable_outside_pointer_events;
- }
-
- view! {
- <div node_ref=node_ref on:keydown=on_keydown>
- {children()}
- </div>
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::{
- radroots_app_ui_dismissable_is_escape,
- radroots_app_ui_dismissable_is_outside,
- };
-
- #[test]
- fn dismissable_escape_match() {
- assert!(radroots_app_ui_dismissable_is_escape("Escape"));
- assert!(!radroots_app_ui_dismissable_is_escape("Enter"));
- }
-
- #[test]
- fn dismissable_outside_check() {
- assert!(radroots_app_ui_dismissable_is_outside(false));
- assert!(!radroots_app_ui_dismissable_is_outside(true));
- }
-}
diff --git a/crates/ui-primitives/src/focus.rs b/crates/ui-primitives/src/focus.rs
@@ -1,213 +0,0 @@
-use leptos::ev::KeyboardEvent;
-use leptos::html;
-use leptos::prelude::*;
-
-#[cfg(target_arch = "wasm32")]
-use wasm_bindgen::JsCast;
-
-const RADROOTS_APP_UI_FOCUSABLE_SELECTOR: &str =
- "a[href],button,textarea,input,select,[tabindex]:not([tabindex='-1'])";
-
-pub fn radroots_app_ui_focus_scope_selector() -> &'static str {
- RADROOTS_APP_UI_FOCUSABLE_SELECTOR
-}
-
-pub fn radroots_app_ui_focus_scope_next_index(
- current: usize,
- count: usize,
- shift: bool,
-) -> usize {
- if count == 0 {
- return 0;
- }
- if shift {
- if current == 0 {
- count - 1
- } else {
- current - 1
- }
- } else if current + 1 >= count {
- 0
- } else {
- current + 1
- }
-}
-
-#[component]
-pub fn RadrootsAppUiFocusScope(
- #[prop(optional)] trapped: bool,
- #[prop(optional)] auto_focus: bool,
- #[prop(optional)] return_focus: bool,
- #[prop(optional)] on_mount_auto_focus: Option<Callback<()>>,
- #[prop(optional)] on_unmount_auto_focus: Option<Callback<()>>,
- children: ChildrenFn,
-) -> impl IntoView {
- let node_ref = NodeRef::<html::Div>::new();
-
- #[cfg(target_arch = "wasm32")]
- {
- use wasm_bindgen::closure::Closure;
- use send_wrapper::SendWrapper;
-
- let on_mount_auto_focus = on_mount_auto_focus.clone();
- let on_unmount_auto_focus = on_unmount_auto_focus.clone();
- let node_ref = node_ref;
-
- node_ref.on_load(move |root| {
- let document = match web_sys::window().and_then(|window| window.document()) {
- Some(document) => document,
- None => return,
- };
- let previous_focus = document.active_element().map(SendWrapper::new);
-
- if auto_focus {
- let _ = radroots_app_ui_focus_scope_focus_first(&root, &document);
- if let Some(callback) = on_mount_auto_focus.as_ref() {
- callback.run(());
- }
- }
-
- if trapped {
- let root_focus = root.clone();
- let document_focus = document.clone();
- let handler = Closure::wrap(Box::new(move |event: web_sys::KeyboardEvent| {
- if event.key() != "Tab" {
- return;
- }
- let _ = radroots_app_ui_focus_scope_cycle(
- &root_focus,
- &document_focus,
- event.shift_key(),
- );
- }) as Box<dyn FnMut(_)>);
- let _ = root.add_event_listener_with_callback(
- "keydown",
- handler.as_ref().unchecked_ref(),
- );
- handler.forget();
- }
-
- if return_focus {
- let on_unmount_auto_focus = on_unmount_auto_focus.clone();
- let previous_focus = previous_focus;
- on_cleanup(move || {
- if let Some(element) = previous_focus {
- let element = element.take();
- let _ = element
- .dyn_ref::<web_sys::HtmlElement>()
- .map(|el| el.focus());
- }
- if let Some(callback) = on_unmount_auto_focus.as_ref() {
- callback.run(());
- }
- });
- }
- });
- }
-
- #[cfg(not(target_arch = "wasm32"))]
- {
- let _ = trapped;
- let _ = auto_focus;
- let _ = return_focus;
- let _ = on_mount_auto_focus;
- let _ = on_unmount_auto_focus;
- }
-
- let on_keydown = move |event: KeyboardEvent| {
- if trapped && event.key() == "Escape" {
- event.prevent_default();
- }
- };
-
- view! {
- <div node_ref=node_ref tabindex="-1" on:keydown=on_keydown>
- {children()}
- </div>
- }
-}
-
-#[cfg(target_arch = "wasm32")]
-fn radroots_app_ui_focus_scope_focus_first(
- root: &web_sys::Element,
- document: &web_sys::Document,
-) -> Result<(), ()> {
- let list = root
- .query_selector_all(RADROOTS_APP_UI_FOCUSABLE_SELECTOR)
- .map_err(|_| ())?;
- if list.length() == 0 {
- if let Some(element) = root.dyn_ref::<web_sys::HtmlElement>() {
- let _ = element.focus();
- }
- return Ok(());
- }
- let first = list
- .item(0)
- .and_then(|node| node.dyn_into::<web_sys::HtmlElement>().ok())
- .or_else(|| {
- document
- .active_element()
- .and_then(|el| el.dyn_into::<web_sys::HtmlElement>().ok())
- });
- if let Some(element) = first {
- let _ = element.focus();
- }
- Ok(())
-}
-
-#[cfg(target_arch = "wasm32")]
-fn radroots_app_ui_focus_scope_cycle(
- root: &web_sys::Element,
- document: &web_sys::Document,
- shift: bool,
-) -> Result<(), ()> {
- let list = root
- .query_selector_all(RADROOTS_APP_UI_FOCUSABLE_SELECTOR)
- .map_err(|_| ())?;
- let count = list.length() as usize;
- if count == 0 {
- return Ok(());
- }
- let active = document.active_element();
- let mut current_index = 0usize;
- if let Some(active) = active {
- for index in 0..count {
- let candidate = list
- .item(index as u32)
- .and_then(|node| node.dyn_into::<web_sys::Element>().ok());
- if let Some(candidate) = candidate {
- if active.is_same_node(Some(&candidate)) {
- current_index = index;
- break;
- }
- }
- }
- }
- let next_index = radroots_app_ui_focus_scope_next_index(current_index, count, shift);
- if let Some(next) = list
- .item(next_index as u32)
- .and_then(|node| node.dyn_into::<web_sys::HtmlElement>().ok())
- {
- let _ = next.focus();
- }
- Ok(())
-}
-
-#[cfg(test)]
-mod tests {
- use super::{
- radroots_app_ui_focus_scope_next_index,
- radroots_app_ui_focus_scope_selector,
- };
-
- #[test]
- fn focus_scope_selector_is_non_empty() {
- assert!(!radroots_app_ui_focus_scope_selector().is_empty());
- }
-
- #[test]
- fn focus_scope_next_index_wraps() {
- assert_eq!(radroots_app_ui_focus_scope_next_index(0, 3, true), 2);
- assert_eq!(radroots_app_ui_focus_scope_next_index(2, 3, false), 0);
- }
-}
diff --git a/crates/ui-primitives/src/lib.rs b/crates/ui-primitives/src/lib.rs
@@ -1,13 +1,5 @@
#![forbid(unsafe_code)]
-mod portal;
-mod presence;
-mod dismissable;
-mod focus;
-mod scroll_lock;
-mod roving_focus;
-mod aria_hidden;
-
pub use ui_primitives_core::dialog::DialogModel;
pub use ui_primitives_core::roving_focus::{
roving_focus_action_from_key as radroots_app_ui_roving_focus_action_from_key,
diff --git a/crates/ui-primitives/src/portal.rs b/crates/ui-primitives/src/portal.rs
@@ -1,37 +0,0 @@
-use leptos::prelude::*;
-
-#[cfg(target_arch = "wasm32")]
-use leptos::portal::Portal;
-
-#[cfg(target_arch = "wasm32")]
-pub type RadrootsAppUiPortalMount = web_sys::Element;
-
-#[cfg(not(target_arch = "wasm32"))]
-pub type RadrootsAppUiPortalMount = ();
-
-#[component]
-pub fn RadrootsAppUiPortal(
- #[prop(optional)] mount: Option<RadrootsAppUiPortalMount>,
- children: ChildrenFn,
-) -> impl IntoView {
- #[cfg(target_arch = "wasm32")]
- {
- match mount {
- Some(mount) => view! { <Portal mount=mount>{children()}</Portal> },
- None => view! { <Portal>{children()}</Portal> },
- }
- }
- #[cfg(not(target_arch = "wasm32"))]
- {
- let _ = mount;
- children()
- }
-}
-
-#[cfg(test)]
-mod tests {
- #[test]
- fn portal_availability_matches_target() {
- assert!(!cfg!(target_arch = "wasm32"));
- }
-}
diff --git a/crates/ui-primitives/src/presence.rs b/crates/ui-primitives/src/presence.rs
@@ -1,142 +0,0 @@
-use leptos::ev::{AnimationEvent, TransitionEvent};
-use leptos::prelude::*;
-use std::sync::Arc;
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub enum RadrootsAppUiPresenceState {
- Mounted,
- Exiting,
- Unmounted,
-}
-
-pub fn radroots_app_ui_presence_state_next(
- current: RadrootsAppUiPresenceState,
- present: bool,
- exit_complete: bool,
-) -> RadrootsAppUiPresenceState {
- match (current, present, exit_complete) {
- (_, true, _) => RadrootsAppUiPresenceState::Mounted,
- (RadrootsAppUiPresenceState::Mounted, false, true) => {
- RadrootsAppUiPresenceState::Unmounted
- }
- (RadrootsAppUiPresenceState::Mounted, false, false) => {
- RadrootsAppUiPresenceState::Exiting
- }
- (RadrootsAppUiPresenceState::Exiting, false, true) => {
- RadrootsAppUiPresenceState::Unmounted
- }
- (RadrootsAppUiPresenceState::Exiting, false, false) => {
- RadrootsAppUiPresenceState::Exiting
- }
- (RadrootsAppUiPresenceState::Unmounted, false, _) => {
- RadrootsAppUiPresenceState::Unmounted
- }
- }
-}
-
-#[component]
-pub fn RadrootsAppUiPresence(
- #[prop(into)] present: Signal<bool>,
- #[prop(optional)] on_exit_complete: Option<Callback<()>>,
- children: ChildrenFn,
-) -> impl IntoView {
- let state = RwSignal::new(if present.get_untracked() {
- RadrootsAppUiPresenceState::Mounted
- } else {
- RadrootsAppUiPresenceState::Unmounted
- });
-
- Effect::new(move || {
- let next = radroots_app_ui_presence_state_next(state.get(), present.get(), false);
- if next != state.get() {
- state.set(next);
- }
- });
-
- let on_exit_complete = on_exit_complete.clone();
- let end_handler = Arc::new(move || {
- let current_state = state.get_untracked();
- let next = radroots_app_ui_presence_state_next(
- current_state,
- present.get_untracked(),
- true,
- );
- if next != current_state {
- state.set(next);
- if next == RadrootsAppUiPresenceState::Unmounted {
- if let Some(callback) = on_exit_complete.as_ref() {
- callback.run(());
- }
- }
- }
- });
-
- let render = move || -> AnyView {
- if state.get() == RadrootsAppUiPresenceState::Unmounted {
- ().into_any()
- } else {
- let transition_end = {
- let end_handler = Arc::clone(&end_handler);
- move |_event: TransitionEvent| {
- end_handler();
- }
- };
- let animation_end = {
- let end_handler = Arc::clone(&end_handler);
- move |_event: AnimationEvent| {
- end_handler();
- }
- };
- view! {
- <div
- data-state=move || if present.get() { "open" } else { "closed" }
- on:transitionend=transition_end
- on:animationend=animation_end
- >
- {children()}
- </div>
- }
- .into_any()
- }
- };
-
- view! { {render} }
-}
-
-#[cfg(test)]
-mod tests {
- use super::{
- radroots_app_ui_presence_state_next,
- RadrootsAppUiPresenceState,
- };
-
- #[test]
- fn presence_state_moves_to_exiting_on_close() {
- let next = radroots_app_ui_presence_state_next(
- RadrootsAppUiPresenceState::Mounted,
- false,
- false,
- );
- assert_eq!(next, RadrootsAppUiPresenceState::Exiting);
- }
-
- #[test]
- fn presence_state_unmounts_after_exit() {
- let next = radroots_app_ui_presence_state_next(
- RadrootsAppUiPresenceState::Exiting,
- false,
- true,
- );
- assert_eq!(next, RadrootsAppUiPresenceState::Unmounted);
- }
-
- #[test]
- fn presence_state_mounts_when_present() {
- let next = radroots_app_ui_presence_state_next(
- RadrootsAppUiPresenceState::Unmounted,
- true,
- false,
- );
- assert_eq!(next, RadrootsAppUiPresenceState::Mounted);
- }
-}
diff --git a/crates/ui-primitives/src/roving_focus.rs b/crates/ui-primitives/src/roving_focus.rs
@@ -1,132 +0,0 @@
-use ui_primitives_core::roving_focus::RovingFocus;
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub enum RadrootsAppUiRovingFocusOrientation {
- Horizontal,
- Vertical,
- Both,
-}
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub enum RadrootsAppUiRovingFocusAction {
- Next,
- Prev,
- First,
- Last,
-}
-
-pub fn radroots_app_ui_roving_focus_action_from_key(
- key: &str,
- orientation: RadrootsAppUiRovingFocusOrientation,
-) -> Option<RadrootsAppUiRovingFocusAction> {
- match key {
- "Home" => Some(RadrootsAppUiRovingFocusAction::First),
- "End" => Some(RadrootsAppUiRovingFocusAction::Last),
- "ArrowLeft" => matches!(
- orientation,
- RadrootsAppUiRovingFocusOrientation::Horizontal | RadrootsAppUiRovingFocusOrientation::Both
- )
- .then_some(RadrootsAppUiRovingFocusAction::Prev),
- "ArrowRight" => matches!(
- orientation,
- RadrootsAppUiRovingFocusOrientation::Horizontal | RadrootsAppUiRovingFocusOrientation::Both
- )
- .then_some(RadrootsAppUiRovingFocusAction::Next),
- "ArrowUp" => matches!(
- orientation,
- RadrootsAppUiRovingFocusOrientation::Vertical | RadrootsAppUiRovingFocusOrientation::Both
- )
- .then_some(RadrootsAppUiRovingFocusAction::Prev),
- "ArrowDown" => matches!(
- orientation,
- RadrootsAppUiRovingFocusOrientation::Vertical | RadrootsAppUiRovingFocusOrientation::Both
- )
- .then_some(RadrootsAppUiRovingFocusAction::Next),
- _ => None,
- }
-}
-
-pub fn radroots_app_ui_roving_focus_next_index(
- current: usize,
- count: usize,
- action: RadrootsAppUiRovingFocusAction,
- looped: bool,
-) -> usize {
- if count == 0 {
- return 0;
- }
-
- let mut focus = RovingFocus::with_active(count, Some(current), looped);
- match action {
- RadrootsAppUiRovingFocusAction::First => focus.move_first().unwrap_or(0),
- RadrootsAppUiRovingFocusAction::Last => focus.move_last().unwrap_or(0),
- RadrootsAppUiRovingFocusAction::Next => focus.move_next().unwrap_or(current),
- RadrootsAppUiRovingFocusAction::Prev => focus.move_prev().unwrap_or(current),
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::{
- radroots_app_ui_roving_focus_action_from_key,
- radroots_app_ui_roving_focus_next_index,
- RadrootsAppUiRovingFocusAction,
- RadrootsAppUiRovingFocusOrientation,
- };
-
- #[test]
- fn roving_focus_action_maps_arrows() {
- assert_eq!(
- radroots_app_ui_roving_focus_action_from_key(
- "ArrowLeft",
- RadrootsAppUiRovingFocusOrientation::Horizontal
- ),
- Some(RadrootsAppUiRovingFocusAction::Prev)
- );
- assert_eq!(
- radroots_app_ui_roving_focus_action_from_key(
- "ArrowUp",
- RadrootsAppUiRovingFocusOrientation::Horizontal
- ),
- None
- );
- assert_eq!(
- radroots_app_ui_roving_focus_action_from_key(
- "ArrowDown",
- RadrootsAppUiRovingFocusOrientation::Both
- ),
- Some(RadrootsAppUiRovingFocusAction::Next)
- );
- }
-
- #[test]
- fn roving_focus_next_index_respects_loop() {
- assert_eq!(
- radroots_app_ui_roving_focus_next_index(
- 0,
- 3,
- RadrootsAppUiRovingFocusAction::Prev,
- false
- ),
- 0
- );
- assert_eq!(
- radroots_app_ui_roving_focus_next_index(
- 0,
- 3,
- RadrootsAppUiRovingFocusAction::Prev,
- true
- ),
- 2
- );
- assert_eq!(
- radroots_app_ui_roving_focus_next_index(
- 2,
- 3,
- RadrootsAppUiRovingFocusAction::Next,
- true
- ),
- 0
- );
- }
-}
diff --git a/crates/ui-primitives/src/scroll_lock.rs b/crates/ui-primitives/src/scroll_lock.rs
@@ -1,186 +0,0 @@
-use std::sync::{Mutex, OnceLock};
-
-#[cfg(target_arch = "wasm32")]
-use web_sys::{window, CssStyleDeclaration};
-
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub enum RadrootsAppUiScrollLockError {
- WindowUnavailable,
- DocumentUnavailable,
- BodyUnavailable,
- StyleUnavailable,
-}
-
-pub type RadrootsAppUiScrollLockResult<T> = Result<T, RadrootsAppUiScrollLockError>;
-
-#[derive(Debug)]
-pub struct RadrootsAppUiScrollLockGuard {
- active: bool,
-}
-
-impl Drop for RadrootsAppUiScrollLockGuard {
- fn drop(&mut self) {
- if self.active {
- let _ = radroots_app_ui_scroll_lock_release();
- self.active = false;
- }
- }
-}
-
-#[cfg_attr(not(target_arch = "wasm32"), allow(dead_code))]
-#[derive(Debug, Default, Clone)]
-struct ScrollLockSnapshot {
- scroll_y: f64,
- overflow: String,
- position: String,
- top: String,
- width: String,
-}
-
-#[derive(Debug, Default)]
-struct ScrollLockState {
- count: usize,
- snapshot: Option<ScrollLockSnapshot>,
-}
-
-static SCROLL_LOCK_STATE: OnceLock<Mutex<ScrollLockState>> = OnceLock::new();
-
-fn scroll_lock_state() -> &'static Mutex<ScrollLockState> {
- SCROLL_LOCK_STATE.get_or_init(|| Mutex::new(ScrollLockState::default()))
-}
-
-pub fn radroots_app_ui_scroll_lock_acquire() -> RadrootsAppUiScrollLockResult<RadrootsAppUiScrollLockGuard> {
- let mut state = scroll_lock_state()
- .lock()
- .unwrap_or_else(|poisoned| poisoned.into_inner());
- if state.count == 0 {
- scroll_lock_apply(&mut state)?;
- }
- state.count += 1;
- Ok(RadrootsAppUiScrollLockGuard { active: true })
-}
-
-pub fn radroots_app_ui_scroll_lock_release() -> RadrootsAppUiScrollLockResult<()> {
- let mut state = scroll_lock_state()
- .lock()
- .unwrap_or_else(|poisoned| poisoned.into_inner());
- if state.count == 0 {
- return Ok(());
- }
- state.count = state.count.saturating_sub(1);
- if state.count == 0 {
- scroll_lock_restore(&mut state)?;
- }
- Ok(())
-}
-
-#[cfg(target_arch = "wasm32")]
-fn scroll_lock_apply(state: &mut ScrollLockState) -> RadrootsAppUiScrollLockResult<()> {
- let window = window().ok_or(RadrootsAppUiScrollLockError::WindowUnavailable)?;
- let document = window
- .document()
- .ok_or(RadrootsAppUiScrollLockError::DocumentUnavailable)?;
- let body = document
- .body()
- .ok_or(RadrootsAppUiScrollLockError::BodyUnavailable)?;
- let style = body.style();
- let scroll_y = window
- .scroll_y()
- .map_err(|_| RadrootsAppUiScrollLockError::StyleUnavailable)?;
- let snapshot = ScrollLockSnapshot {
- scroll_y,
- overflow: style_value(&style, "overflow")?,
- position: style_value(&style, "position")?,
- top: style_value(&style, "top")?,
- width: style_value(&style, "width")?,
- };
- style_set(&style, "overflow", "hidden")?;
- style_set(&style, "position", "fixed")?;
- style_set(&style, "width", "100%")?;
- style_set(&style, "top", &format!("-{}px", snapshot.scroll_y))?;
- state.snapshot = Some(snapshot);
- Ok(())
-}
-
-#[cfg(not(target_arch = "wasm32"))]
-fn scroll_lock_apply(state: &mut ScrollLockState) -> RadrootsAppUiScrollLockResult<()> {
- state.snapshot = None;
- Ok(())
-}
-
-#[cfg(target_arch = "wasm32")]
-fn scroll_lock_restore(state: &mut ScrollLockState) -> RadrootsAppUiScrollLockResult<()> {
- let Some(snapshot) = state.snapshot.take() else {
- return Ok(());
- };
- let window = window().ok_or(RadrootsAppUiScrollLockError::WindowUnavailable)?;
- let document = window
- .document()
- .ok_or(RadrootsAppUiScrollLockError::DocumentUnavailable)?;
- let body = document
- .body()
- .ok_or(RadrootsAppUiScrollLockError::BodyUnavailable)?;
- let style = body.style();
- style_set(&style, "overflow", &snapshot.overflow)?;
- style_set(&style, "position", &snapshot.position)?;
- style_set(&style, "top", &snapshot.top)?;
- style_set(&style, "width", &snapshot.width)?;
- window.scroll_to_with_x_and_y(0.0, snapshot.scroll_y);
- Ok(())
-}
-
-#[cfg(not(target_arch = "wasm32"))]
-fn scroll_lock_restore(state: &mut ScrollLockState) -> RadrootsAppUiScrollLockResult<()> {
- state.snapshot = None;
- Ok(())
-}
-
-#[cfg(target_arch = "wasm32")]
-fn style_set(
- style: &CssStyleDeclaration,
- name: &str,
- value: &str,
-) -> RadrootsAppUiScrollLockResult<()> {
- style
- .set_property(name, value)
- .map_err(|_| RadrootsAppUiScrollLockError::StyleUnavailable)
-}
-
-#[cfg(target_arch = "wasm32")]
-fn style_value(style: &CssStyleDeclaration, name: &str) -> RadrootsAppUiScrollLockResult<String> {
- style
- .get_property_value(name)
- .map_err(|_| RadrootsAppUiScrollLockError::StyleUnavailable)
-}
-
-#[cfg(test)]
-fn scroll_lock_count_for_test() -> usize {
- let state = scroll_lock_state()
- .lock()
- .unwrap_or_else(|poisoned| poisoned.into_inner());
- state.count
-}
-
-#[cfg(test)]
-mod tests {
- use super::{
- radroots_app_ui_scroll_lock_acquire,
- radroots_app_ui_scroll_lock_release,
- scroll_lock_count_for_test,
- };
-
- #[test]
- fn scroll_lock_guard_updates_count() {
- assert_eq!(scroll_lock_count_for_test(), 0);
- let guard = radroots_app_ui_scroll_lock_acquire().expect("lock");
- assert_eq!(scroll_lock_count_for_test(), 1);
- drop(guard);
- assert_eq!(scroll_lock_count_for_test(), 0);
- }
-
- #[test]
- fn scroll_lock_release_is_idempotent() {
- let _ = radroots_app_ui_scroll_lock_release().expect("release");
- assert_eq!(scroll_lock_count_for_test(), 0);
- }
-}