diff --git a/Cargo.lock b/Cargo.lock index acc3b7b..6c0f207 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -719,6 +719,34 @@ dependencies = [ "thiserror", ] +[[package]] +name = "popper-rs" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a14458f5df1782ff0999819ae4cc310d2fcddd2ec62ae61585dacf3af72ccb4f" +dependencies = [ + "gloo-utils 0.2.0", + "js-sys", + "popper-rs-sys", + "serde", + "serde_json", + "wasm-bindgen", + "web-sys", + "yew", +] + +[[package]] +name = "popper-rs-sys" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21ef286df3991db98cfab510303f0d7a73b185364f6bfde3584a0b99d2119c17" +dependencies = [ + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "prettyplease" version = "0.2.15" @@ -1125,6 +1153,8 @@ dependencies = [ "anyhow", "convert_case", "gloo-console 0.3.0", + "gloo-utils 0.2.0", + "popper-rs", "wasm-bindgen", "web-sys", "yew", diff --git a/examples/Cargo.lock b/examples/Cargo.lock index 1f71e00..ca93744 100644 --- a/examples/Cargo.lock +++ b/examples/Cargo.lock @@ -760,6 +760,34 @@ dependencies = [ "thiserror", ] +[[package]] +name = "popper-rs" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a14458f5df1782ff0999819ae4cc310d2fcddd2ec62ae61585dacf3af72ccb4f" +dependencies = [ + "gloo-utils 0.2.0", + "js-sys", + "popper-rs-sys", + "serde", + "serde_json", + "wasm-bindgen", + "web-sys", + "yew", +] + +[[package]] +name = "popper-rs-sys" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21ef286df3991db98cfab510303f0d7a73b185364f6bfde3584a0b99d2119c17" +dependencies = [ + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "prettyplease" version = "0.2.15" @@ -1166,6 +1194,10 @@ dependencies = [ "anyhow", "convert_case", "gloo-console 0.3.0", + "gloo-utils 0.2.0", + "popper-rs", + "wasm-bindgen", + "web-sys", "yew", ] diff --git a/examples/basics/src/main.rs b/examples/basics/src/main.rs index 98e4602..f8ad1a3 100644 --- a/examples/basics/src/main.rs +++ b/examples/basics/src/main.rs @@ -7,18 +7,42 @@ use gloo_console::debug; use wasm_bindgen::JsCast; use web_sys::HtmlElement; -enum Msg {} -struct Model {} +enum Msg { + ToggleTooltip, + ShowTooltip, + HideTooltip, +} + +struct Model { + tooltip_show: bool, +} impl Component for Model { type Message = Msg; type Properties = (); fn create(_ctx: &Context) -> Self { - Self {} + Self { + tooltip_show: false, + } } - fn view(&self, _ctx: &Context) -> Html { + fn update(&mut self, _ctx: &Context, msg: Self::Message) -> bool { + match msg { + Msg::ToggleTooltip => { + self.tooltip_show = !self.tooltip_show; + } + Msg::ShowTooltip => { + self.tooltip_show = true; + } + Msg::HideTooltip => { + self.tooltip_show = false; + } + } + true + } + + fn view(&self, ctx: &Context) -> Html { let brand = BrandType::BrandIcon { text: AttrValue::from("Yew Bootstrap"), url: Some(AttrValue::from("https://yew.rs")), @@ -39,6 +63,9 @@ impl Component for Model { event.prevent_default(); }); + let tooltip_click_p_ref = NodeRef::default(); + let tooltip_link_ref = NodeRef::default(); + html! { <> {include_inline()} @@ -351,6 +378,153 @@ impl Component for Model {

{"Animated"}

+ +

{"Tooltip"}

+

+ {"The "} + + {"yew-bootstrap"} + {" forms example"} + + {" demonstrates using a tooltip with many types of form control."} +

+ + {"Open the Forms example on "}{BI::GITHUB}{" GitHub"} + +

{"Buttons with tooltips (on focus or hover)"}

+

{"These buttons always show tooltips on focus or on hover."}

+ + { + for [ + (Color::Primary, Placement::Auto), + (Color::Secondary, Placement::Top), + (Color::Warning, Placement::Bottom), + (Color::Success, Placement::Left), + (Color::Info, Placement::Right), + ].iter().map(|(color, placement)| { + let btn_ref = NodeRef::default(); + + html_nested! { + <> + + + {format!("Tooltip for button, placed at {placement:?}.")} + + + } + }) + } + +

{"Manually-triggered tooltip on an element"}

+

+ {"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt "} + {"ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation "} + {"ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in "} + {"reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur "} + {"sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id "} + {"est laborum."} +

+ + + + + + + {"Tooltip toggled manually, targetted to the "} + {"

"} + {" tag."} + +

{"trigger_on_focus"}{" and "}{"trigger_on_hover"}{" options"}

+

+ {"These buttons will always show tooltips on hover, but have different rules for showing on "} + {"focus depending on how your browser responds to the "}{"hover"}{", "} + {"any-hover"}{" and "}{"any-pointer"}{" "} + + {"Interaction Media Features"} + + {" media queries. "} + + {"Depending on browser support"} + + {", These will trigger based on which types of pointing devices (eg: mouse, touchscreen, "} + {"Wiimote, stylus) are available or in use."} +

+ + { + for [ + (Color::Primary, TooltipFocusTrigger::Always), + (Color::Secondary, TooltipFocusTrigger::IfAnyPointerNoneOrCoarse), + (Color::Danger, TooltipFocusTrigger::IfHoverNone), + (Color::Warning, TooltipFocusTrigger::IfAnyHoverNone), + (Color::Info, TooltipFocusTrigger::Never), + ].iter().map(|(color, trigger_on_focus)| { + let btn_ref = NodeRef::default(); + + html_nested! { + <> + + + {"Tooltip for button with "} + + {format!("trigger_on_focus={trigger_on_focus:?}")} + + + + } + }) + } + +

{"These buttons either always or never trigger on focus or on hover, regardless of media queries."}

+ + { + for [ + (Color::Primary, TooltipFocusTrigger::Always, true), + (Color::Secondary, TooltipFocusTrigger::Always, false), + (Color::Info, TooltipFocusTrigger::Never, true), + (Color::Warning, TooltipFocusTrigger::Never, false), + ].iter().map(|(color, trigger_on_focus, trigger_on_hover)| { + let btn_ref = NodeRef::default(); + + html_nested! { + <> + + + {"Tooltip for button with "} + + {format!("trigger_on_focus={trigger_on_focus:?} trigger_on_hover={trigger_on_hover:?}")} + + + + } + }) + } +

{"Vertical/Horizontal rule"}

diff --git a/examples/forms/src/main.rs b/examples/forms/src/main.rs index eeb0943..08f9e61 100644 --- a/examples/forms/src/main.rs +++ b/examples/forms/src/main.rs @@ -29,6 +29,7 @@ struct Model { value_checkbox: bool, number_value: AttrValue, number_feedback: FormControlValidation, + show_checkbox_tooltip: bool, } impl Component for Model { @@ -49,6 +50,7 @@ impl Component for Model { value_checkbox: false, number_value: AttrValue::from(""), number_feedback: FormControlValidation::None, + show_checkbox_tooltip: true, } } @@ -101,9 +103,16 @@ impl Component for Model { }, Msg::InputBoolChanged { name, value } => { self.input_changes.push(format!("{name} changed: {}", if value { "checked " } else { "unchecked" })); - if &name[..] == "input-checkbox-callback" { - self.value_checkbox = value; + match &name[..] { + "input-checkbox-callback" => { + self.value_checkbox = value; + } + "input-tooltip-checkbox" => { + self.show_checkbox_tooltip = !value; + } + _ => {} } + true }, _ => false @@ -160,6 +169,10 @@ impl Component for Model { icon: BI::ROCKET, }; + let tooltip_select_ref = NodeRef::default(); + let tooltip_checkbox_ref = NodeRef::default(); + let tooltip_textarea_ref = NodeRef::default(); + html! { <> {include_inline()} @@ -372,6 +385,91 @@ impl Component for Model { value="05:00" /> +

{ "Fields with tooltips"}

+ + + + {"Tooltip for input control, shown when focussed."} + + + + + + + + + {"Tooltip for select control, shown when focussed or hovered."} + + + + {"You must accept the terms and conditions to hide this tooltip. Even though this "} + {"tooltip visually blocks other form elements, they can still receive events."} + + { + for [ + TooltipFocusTrigger::IfNoHover, + TooltipFocusTrigger::IfNoAnyHover, + TooltipFocusTrigger::Never, + ].iter().enumerate().map(|(i, trigger_on_focus)| { + let input_ref = NodeRef::default(); + + html_nested! { + <> + + + {format!("Tooltip for input with {trigger_on_focus:?}.")} + + + } + }) + } +

{ "Floating fields " }

{ diff --git a/packages/yew-bootstrap/Cargo.toml b/packages/yew-bootstrap/Cargo.toml index cdef251..0159caa 100644 --- a/packages/yew-bootstrap/Cargo.toml +++ b/packages/yew-bootstrap/Cargo.toml @@ -19,9 +19,12 @@ name = "yew_bootstrap" [dependencies] yew = { version = "0.21", features = ["csr"] } gloo-console = "0.3" +wasm-bindgen = "0.2.*" +web-sys = { version = "0.3.*", features = ["HtmlElement", "MediaQueryList", "MediaQueryListEvent"] } +popper-rs = { version = "0.3.0", features = ["yew"] } +gloo-utils = "0.2.0" [dev-dependencies] -wasm-bindgen = "0.2.*" web-sys = { version = "0.3.*", features = ["HtmlTextAreaElement", "HtmlSelectElement"] } [build-dependencies] diff --git a/packages/yew-bootstrap/README.md b/packages/yew-bootstrap/README.md index b5a726c..7003edd 100644 --- a/packages/yew-bootstrap/README.md +++ b/packages/yew-bootstrap/README.md @@ -76,7 +76,7 @@ This project uses [semantic versioning](https://semver.org/). - [ ] Scrollspy - [x] Spinner ([component::Spinner]) - [ ] Toast -- [ ] Tooltips +- [x] Tooltips ([component::Tooltip]) ### Helpers diff --git a/packages/yew-bootstrap/src/component/mod.rs b/packages/yew-bootstrap/src/component/mod.rs index 1e2a761..e7076c3 100644 --- a/packages/yew-bootstrap/src/component/mod.rs +++ b/packages/yew-bootstrap/src/component/mod.rs @@ -17,6 +17,7 @@ mod navbar; mod row; mod spinner; mod progress; +mod tooltip; pub use self::accordion::*; pub use self::alert::*; @@ -35,3 +36,4 @@ pub use self::navbar::*; pub use self::row::*; pub use self::spinner::*; pub use self::progress::*; +pub use self::tooltip::*; diff --git a/packages/yew-bootstrap/src/component/tooltip.rs b/packages/yew-bootstrap/src/component/tooltip.rs new file mode 100644 index 0000000..bc2fd27 --- /dev/null +++ b/packages/yew-bootstrap/src/component/tooltip.rs @@ -0,0 +1,657 @@ +//! Implements tooltip suppport. +//! +//! `yew` presumes it has exclusive control of the DOM, which conflicts with the +//! Bootstrap's assumption that it also has exclusive control of the DOM. +//! +//! So, we need to re-implement the Tooltip plugin using `yew`... +//! +//! * +//! * + +pub use popper_rs::prelude::Placement; +use popper_rs::{ + prelude::{use_popper, Modifier, Offset, Options, Strategy}, + state::ApplyAttributes, +}; +use wasm_bindgen::{closure::Closure, JsCast}; +use web_sys::{HtmlElement, MediaQueryList, MediaQueryListEvent}; +use yew::{html::IntoPropValue, platform::spawn_local, prelude::*}; + +/// Media query to indicate that the primary pointing device is missing or does +/// not support hovering. +/// +/// Reference: [Media Queries Level 4: Hover Capability](https://www.w3.org/TR/mediaqueries-4/#hover) +const MEDIA_QUERY_HOVER_NONE: &'static str = "(hover: none)"; + +/// Media query to indicate that there is no pointing device which supports +/// hovering. +/// +/// Reference: [Media Queries Level 4: All Available Interaction Capabilities](https://www.w3.org/TR/mediaqueries-4/#any-input) +const MEDIA_QUERY_ANY_HOVER_NONE: &'static str = "(any-hover: none)"; + +/// Media query to indicate that there are either no pointing devices, or a +/// pointing device only supports coarse input. +/// +/// Reference: [Media Queries Level 4: All Available Interaction Capabilities](https://www.w3.org/TR/mediaqueries-4/#any-input) +const MEDIA_QUERY_ANY_POINTER_NONE_OR_COARSE: &'static str = + "(any-pointer: none) or (any-pointer: coarse)"; + +/// Trigger options for [`TooltipProps::trigger_on_focus`]. +/// +/// This allows tooltips to be selectively enabled on focus, depending on the +/// result of which [Interaction Media Features][0] the user's device supports. +/// +/// [0]: https://www.w3.org/TR/mediaqueries-4/#mf-interaction +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub enum TooltipFocusTrigger { + /// Always show the tooltip on element focus. + /// + /// This is the default option, and provides a reliable and accessible + /// alternative when using a non-hover-capable device (such as a + /// touchscreen) or navigating with a keyboard on a device that *also* has a + /// pointing device. + /// + /// Because of the many side-effects, browser and platform bugs that come + /// from attempting to *selectively* disable showing tooltips on focus, this + /// is generally the best choice, but may lead to unexpected tooltip display + /// for users on a desktop browser with a traditional mouse. + #[default] + Always, + + /// Show the tooltip on element focus only if the *primary* pointing device + /// does *not* support hovering (eg: touchscreen), or there are no pointing + /// devices connected (`hover: none`). + /// + /// If the primary pointing device supports hovering (eg: mouse, trackpad, + /// trackball, smart pen, Wiimote, Leap Motion Controller), the tooltip will + /// not be shown when the element has focus. + /// + /// Figuring out what the "primary" pointing device actually is + /// [can be complicated to answer for some devices][0]. They generally err + /// towards reporting the use and presence of an ordinary mouse with hover + /// capabilities (eg: [Firefox bug 1851244][1]), even when there are no + /// pointing devices connected, or used with a touchscreen. + /// + /// Both [Chromium][2] and [Firefox][3] on Windows erroneously report + /// touch-only devices as having an ordinary mouse with hover capabilities + /// if the device lacks an auto-rotation sensor (even if disabled), which is + /// generally the case for non-tablet devices like external touchscreen + /// monitors and all-in-one PCs). + /// + /// [Some Android devices also erroneously report `hover: hover`][4], even + /// when they only have a touch screen. + /// + /// For someone who primarily uses a keyboard to interact with their + /// computer, but has a mouse plugged in (eg: a laptop with a built-in + /// trackpad, or a virtual device), their browser will still report a + /// primary pointing device which is "hover capable", even when they have no + /// way to hover. + /// + /// These implementation problems and shortfalls make the `hover: none` + /// media query unreliable, and an unreliable indicator of user preferences. + /// + /// [0]: https://firefox-source-docs.mozilla.org/widget/windows/windows-pointing-device/index.html#selection-of-the-primary-pointing-device + /// [1]: https://bugzilla.mozilla.org/show_bug.cgi?id=1851244 + /// [2]: https://issues.chromium.org/issues/366055333 + /// [3]: https://bugzilla.mozilla.org/show_bug.cgi?id=1918292 + /// [4]: https://issues.chromium.org/issues/41445959 + IfHoverNone, + + /// Trigger showing the tooltip on element focus only if *all* pointing + /// devices connected to the device do not support hovering, or there there + /// are no pointing devices connected (`any-hover: none`). + /// + /// For a device with *only* one non-hovering pointing device (eg: a mobile + /// phone with a touch screen or basic stylus), this is the same as + /// [`TooltipFocusTrigger::IfHoverNone`]. + /// + /// For a device with *both* hovering and non-hovering pointing device(s) + /// (eg: a laptop with a trackpad and touchscreen, or a tablet with both pen + /// and touch input), this option will never show will never show the + /// tooltip on focus. + /// + /// Unfortunately, [there is no way to detect if not *all* pointer devices support hovering][1]. + /// + /// Most desktop browsers will *always* report the presence of an ordinary + /// (hover-capable) mouse, even if none is attached. This can be caused by: + /// + /// * a wireless mouse dongle which is plugged in, but the wireless mouse + /// itself is turned off + /// + /// * the presence of a PS/2 mouse controller + /// + /// * the presence of a virtual mouse device + /// + /// * a touch screen which does not have an automatic rotation sensor + /// (but this will report hover events from touch), due to [Chromium][2] + /// and [Firefox][3] bugs. + /// + /// These issues may also impact someone who primarily uses a keyboard to + /// interact with their computer. + /// + /// These implementation problems and shortfalls make the `any-hover: none` + /// media query an unreliable indicator of user preferences. + /// + /// [1]: https://github.com/w3c/csswg-drafts/issues/5462 + /// [2]: https://issues.chromium.org/issues/366055333 + /// [3]: https://bugzilla.mozilla.org/show_bug.cgi?id=1918292 + IfAnyHoverNone, + + /// Trigger showing tooltips on element focus only if: + /// + /// * there are no pointer devices present (`any-pointer: none`), **or**, + /// * there are *coarse* pointer devices present (`any-pointer: coarse`), + /// such as a touchscreen, Wiimote or Leap Motion Controller + /// + /// This is a work-around for there being + /// [no way for a browser to report that not all devices support `hover`][1], + /// and the complex heuristics required (which all browsers lack) to + /// determine which is the "primary" pointing device on desktop and laptop + /// computers. + /// + /// The intent of this mode is that tooltips will be shown on `focus` for + /// devices with touchscreens, *regardless* of whether they have an + /// auto-rotation sensor. + /// + /// The side-effects are: + /// + /// * hovering `coarse` pointer devices (like the Wiimote and Leap Motion + /// Controller) will *also* show tooltips on focus, even though they can + /// hover + /// * traditional-style laptops with touchscreens (ie: not foldable or + /// convertible into a tablet) will *also* show tooltips on focus, even + /// though using the touchscreen as a primary pointing device is very + /// uncomfortable (because it requires reaching over the keyboard) + /// * non-hovering `fine` pointer devices (like basic stylus digitisers) + /// will *not* show tooltips on focus, even though they can't hover + /// * a user primarily using non-pointer (keyboard) input but with at least + /// one `fine` pointing device connected (such as a laptop with built-in + /// trackpad) will never see tooltips on focus + /// * [Safari doesn't fire `focus` events][2] on components on click or + /// touch if it does not accept keyboard input (eg: `` and ` +/// +/// {"Tooltip for button."} +/// +/// +/// } +/// } +/// ``` +/// +/// [0]: https://getbootstrap.com/docs/5.3/components/tooltips/ +/// [2]: https://github.com/react-bootstrap/react-bootstrap/blob/master/src/Tooltip.tsx +/// [`children`]: TooltipProps::children +/// [`disabled`]: TooltipProps::disabled +/// [portal]: https://yew.rs/docs/advanced-topics/portals +/// [`popper-rs`]: https://github.com/ctron/popper-rs/ +/// [`show`]: TooltipProps::show +/// [`target`]: TooltipProps::target +/// [trigger_on_focus]: TooltipProps::trigger_on_focus +/// [trigger_on_hover]: TooltipProps::trigger_on_hover +/// [wrapper element]: https://getbootstrap.com/docs/5.3/components/tooltips/#disabled-elements +#[function_component] +pub fn Tooltip(props: &TooltipProps) -> Html { + let tooltip_ref = use_node_ref(); + + // Adapted from https://github.com/ctron/popper-rs/blob/main/examples/yew/src/example/basic.rs + let options = use_memo(props.placement, |placement| Options { + placement: (*placement).into(), + modifiers: vec![Modifier::Offset(Offset { + skidding: 0, + distance: 6, + })], + strategy: Strategy::Fixed, + ..Default::default() + }); + + let popper = use_popper(props.target.clone(), tooltip_ref.clone(), options).unwrap(); + + let focused = use_state_eq(|| false); + let focus_should_trigger = use_state_eq(|| props.trigger_on_focus.should_trigger()); + let hovered = use_state_eq(|| false); + + let onshow = { + let focused = focused.clone(); + let hovered = hovered.clone(); + Callback::from(move |evt_type: String| match evt_type.as_str() { + "mouseenter" => hovered.set(true), + "focusin" => focused.set(true), + _ => {} + }) + }; + + let onhide = { + let focused = focused.clone(); + let hovered = hovered.clone(); + Callback::from(move |evt_type: String| match evt_type.as_str() { + "mouseleave" => hovered.set(false), + "focusout" => focused.set(false), + _ => {} + }) + }; + + let focus_should_trigger_listener = { + let focus_should_trigger = focus_should_trigger.clone(); + + Callback::from(move |v: bool| { + focus_should_trigger.set(v); + }) + }; + + use_effect_with(props.trigger_on_focus, |trigger_on_focus| { + let r = if let Some(media_query_list) = trigger_on_focus.media_queries() { + let media_query_list_listener = Closure::::wrap(Box::new( + move |e: MediaQueryListEvent| { + focus_should_trigger_listener.emit(e.matches()); + }, + )); + + let _ = media_query_list.add_event_listener_with_callback( + "change", + media_query_list_listener.as_ref().unchecked_ref(), + ); + + Some((media_query_list_listener, media_query_list)) + } else { + // Current trigger_on_focus rule doesn't need a MediaQueryList change event listener. + None + }; + + move || { + if let Some((media_query_list_listener, media_query_list)) = r { + let _ = media_query_list.remove_event_listener_with_callback( + "change", + media_query_list_listener.as_ref().unchecked_ref(), + ); + + drop(media_query_list_listener); + } + } + }); + + if props.disabled { + // Whenever this component is disabled, explicitly set our focus and + // hover state to false. + focused.set(false); + hovered.set(false); + } + + let show = !props.disabled + && (props.show + || (*focused && *focus_should_trigger) + || (*hovered && props.trigger_on_hover)); + let data_show = show.then(AttrValue::default); + + use_effect_with((show, popper.instance.clone()), |(show, popper)| { + if *show { + let popper = popper.clone(); + + spawn_local(async move { + popper.update().await; + }); + } + }); + + use_effect_with( + (tooltip_ref.clone(), popper.state.attributes.popper.clone()), + |(tooltip_ref, attributes)| { + tooltip_ref.apply_attributes(attributes); + }, + ); + + // Attach event handlers. These are always wired up, just we ignore the + // result when they're disabled. + use_effect_with(props.target.clone(), |target_ref| { + let show_listener = Closure::::wrap(Box::new(move |e: Event| { + onshow.emit(e.type_()); + })); + let hide_listener = Closure::::wrap(Box::new(move |e: Event| { + onhide.emit(e.type_()); + })); + let target_elem = target_ref.cast::(); + + if let Some(target_elem) = &target_elem { + let _ = target_elem.add_event_listener_with_callback( + "focusin", + show_listener.as_ref().unchecked_ref(), + ); + let _ = target_elem.add_event_listener_with_callback( + "focusout", + hide_listener.as_ref().unchecked_ref(), + ); + + let _ = target_elem.add_event_listener_with_callback( + "mouseenter", + show_listener.as_ref().unchecked_ref(), + ); + let _ = target_elem.add_event_listener_with_callback( + "mouseleave", + hide_listener.as_ref().unchecked_ref(), + ); + }; + + move || { + if let Some(target_elem) = target_elem { + let _ = target_elem.remove_event_listener_with_callback( + "focusin", + show_listener.as_ref().unchecked_ref(), + ); + let _ = target_elem.remove_event_listener_with_callback( + "focusout", + hide_listener.as_ref().unchecked_ref(), + ); + let _ = target_elem.remove_event_listener_with_callback( + "mouseenter", + show_listener.as_ref().unchecked_ref(), + ); + let _ = target_elem.remove_event_listener_with_callback( + "mouseleave", + hide_listener.as_ref().unchecked_ref(), + ); + } + drop(show_listener); + drop(hide_listener); + } + }); + + use_effect_with( + (props.target.clone(), props.id.clone(), show), + |(target_ref, tooltip_id, show)| { + let Some(target_elem) = target_ref.cast::() else { + return; + }; + + match (tooltip_id, show) { + (Some(tooltip_id), true) => { + let _ = target_elem.set_attribute("aria-describedby", tooltip_id); + } + _ => { + let _ = target_elem.remove_attribute("aria-describedby"); + } + } + }, + ); + + let mut class = classes!["tooltip", "bs-tooltip-auto"]; + if props.fade { + class.push("fade"); + } + if show { + class.push("show"); + } + + let mut popper_style = popper.state.styles.popper.clone(); + // Make sure `` doesn't interfere with events going to other + // elements, even when hidden. + popper_style.insert("pointer-events".to_string(), "none".to_string()); + + create_portal( + html_nested! { +