leptos_aria_interactions/
text_selection.rs1use std::ptr::eq;
2use std::time::Duration;
3
4use leptos::create_rw_signal;
5use leptos::document;
6use leptos::js_sys::JsString;
7use leptos::set_timeout;
8use leptos::web_sys::Element;
9use leptos::web_sys::HtmlElement;
10use leptos::web_sys::SvgElement;
11use leptos::JsCast;
12use leptos::RwSignal;
13use leptos::Scope;
14use leptos::UntrackedGettableSignal;
15use leptos::UntrackedSettableSignal;
16use leptos_aria_utils::is_ios;
17use leptos_aria_utils::run_after_transition;
18use leptos_aria_utils::ContextProvider;
19use leptos_aria_utils::Map;
20
21#[derive(Copy, Clone)]
22pub(crate) struct SelectionContext(RwSignal<Selection>);
23
24impl ContextProvider for SelectionContext {
25 type Value = Selection;
26
27 fn from_leptos_scope(cx: Scope) -> Self {
28 Self(create_rw_signal(cx, Self::Value::default()))
29 }
30
31 fn get(&self) -> Self::Value {
32 self.0.get_untracked()
33 }
34
35 fn set(&self, value: Self::Value) {
36 self.0.set_untracked(value);
37 }
38}
39
40#[derive(Copy, Clone)]
41pub(crate) struct UserSelectContext(RwSignal<Option<String>>);
42
43impl ContextProvider for UserSelectContext {
44 type Value = Option<String>;
45
46 fn from_leptos_scope(cx: Scope) -> Self {
47 Self(create_rw_signal(cx, None))
48 }
49
50 fn get(&self) -> Self::Value {
51 self.0.get_untracked()
52 }
53
54 fn set(&self, value: Self::Value) {
55 self.0.set(value)
56 }
57}
58
59type ElementMap = Map<Element, JsString>;
60
61#[derive(Copy, Clone)]
62pub(crate) struct ElementMapContext(RwSignal<ElementMap>);
63
64impl ContextProvider for ElementMapContext {
65 type Value = ElementMap;
66
67 fn from_leptos_scope(cx: Scope) -> Self {
68 Self(create_rw_signal(cx, Default::default()))
69 }
70
71 fn get(&self) -> Self::Value {
72 self.0.get_untracked()
73 }
74
75 fn set(&self, value: Self::Value) {
76 let reference = &value;
77 self.0.set_untracked(if eq(reference, &self.get()) {
78 reference.clone()
80 } else {
81 value
82 });
83 }
84}
85
86pub(crate) fn disable_text_selection(cx: Scope, element: Option<impl AsRef<Element>>) {
87 if is_ios() {
88 let selection = SelectionContext::provide(cx);
89 let user_select = UserSelectContext::provide(cx);
90
91 if selection.get() == Selection::Default {
92 let style = document()
93 .document_element()
94 .unwrap()
95 .unchecked_ref::<HtmlElement>()
96 .style();
97 user_select.set(style.get_property_value("-webkit-user-select").ok());
98 style.set_property("-webkit-user-select", "none").ok();
99 }
100
101 selection.set(Selection::Disabled);
102 return;
103 }
104
105 let Some(target) = element.as_ref().map(|item| item.as_ref()) else {
106 return;
107 };
108
109 if !target.is_instance_of::<HtmlElement>() && !target.is_instance_of::<HtmlElement>() {
110 return;
111 }
112
113 let _should_append = true;
114 let element_list = ElementMapContext::provide(cx);
115 let style = target.unchecked_ref::<HtmlElement>().style();
116 let map = element_list.get();
117 let _cloned_target = target.clone();
118 let user_select = style.get_property_value("user-select").unwrap_or("".into());
119 map.set(target, &user_select.into());
120
121 element_list.set(map);
122}
123
124pub(crate) fn restore_text_selection(cx: Scope, element: impl AsRef<Element>) {
141 if is_ios() {
142 let selection = SelectionContext::provide(cx);
143 let user_select = UserSelectContext::provide(cx);
144
145 if selection.get() != Selection::Default {
149 return;
150 }
151
152 selection.set(Selection::Restoring);
153
154 let timeout_callback = move || {
155 if selection.get() != Selection::Default {
156 return;
157 }
158
159 let document_element: HtmlElement = document().document_element().unwrap().unchecked_into();
160
161 if document_element
162 .style()
163 .get_property_value("-webkit-user-select")
164 .ok()
165 .as_deref()
166 == Some("none")
167 {
168 document_element
169 .style()
170 .set_property(
171 "-webkit-user-select",
172 user_select.get().as_deref().unwrap_or(""),
173 )
174 .ok();
175 }
176
177 selection.set(Selection::Default);
178 user_select.set(None);
179 };
180
181 set_timeout(
182 move || run_after_transition(cx, timeout_callback),
183 Duration::from_millis(300),
184 );
185
186 return;
187 }
188
189 let target = element.as_ref();
190
191 if !target.is_instance_of::<HtmlElement>() && !target.is_instance_of::<SvgElement>() {
192 return;
193 }
194
195 let element_map = ElementMapContext::provide(cx);
196 let map = element_map.get();
197
198 let Some(found_selection) = map.get(target) else {
199 return;
200 };
201
202 let style = target.unchecked_ref::<HtmlElement>().style();
203 if style.get_property_value("user-select").ok().as_deref() == Some("none") {
204 let found_selection: String = found_selection.into();
205 style
206 .set_property("user-select", found_selection.as_str())
207 .ok();
208 }
209
210 if target
211 .get_attribute("style")
212 .as_ref()
213 .filter(|value| !value.is_empty())
214 .is_none()
215 {
216 target.remove_attribute("style").ok();
217 }
218
219 map.delete(target);
220 element_map.set(map);
221}
222
223#[derive(Copy, Clone, PartialEq)]
224pub(crate) enum Selection {
225 Default,
226 Disabled,
227 Restoring,
228}
229
230impl Default for Selection {
231 fn default() -> Self {
232 Self::Default
233 }
234}