Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions component.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"description": "",
"members": [
"preview/src/components/tabs",
"preview/src/components/table_of_contents",
"preview/src/components/dropdown_menu",
"preview/src/components/navbar",
"preview/src/components/form",
Expand Down
1 change: 1 addition & 0 deletions preview/src/components/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,7 @@ examples!(
slider[dynamic_range, range],
switch,
tabs,
table_of_contents,
textarea[outline, fade, ghost],
toast,
toggle,
Expand Down
13 changes: 13 additions & 0 deletions preview/src/components/table_of_contents/component.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"name": "table_of_contents",
"description": "A table of contents that highlights the active heading while scrolling.",
"authors": ["Dioxus Labs"],
"exclude": ["variants", "docs.md", "component.json"],
"cargoDependencies": [
{
"name": "dioxus-primitives",
"git": "https://github.com/DioxusLabs/components"
}
],
"globalAssets": ["../../../assets/dx-components-theme.css"]
}
19 changes: 19 additions & 0 deletions preview/src/components/table_of_contents/component.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
use dioxus::prelude::*;
use dioxus_primitives::table_of_contents::{self, TableOfContentsProps};

#[css_module("/src/components/table_of_contents/style.css")]
struct Styles;

#[component]
pub fn TableOfContents(props: TableOfContentsProps) -> Element {
rsx! {
table_of_contents::TableOfContents {
class: Styles::dx_table_of_contents,
scroll_spy_options: props.scroll_spy_options,
initial_data: props.initial_data,
min_depth_to_offset: props.min_depth_to_offset,
depth_offset: props.depth_offset,
attributes: props.attributes,
}
}
}
13 changes: 13 additions & 0 deletions preview/src/components/table_of_contents/docs.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
## Component Structure

Table of contents uses `use_scroll_spy()` to discover headings in the document and highlight the item closest to the configured scroll offset.

## Usage

Pass a heading selector through `scroll_spy_options` to generate controls from rendered document headings. `initial_data` is optional and only needed when server-rendered markup must include placeholder links before browser-side heading discovery runs.

Call the hook state's `reinitialize` callback after dynamically changing the heading list.

## Styling

The root element has `data-table-of-contents="true"`. Each control receives `data-depth`, and the active control receives `data-active="true"`.
2 changes: 2 additions & 0 deletions preview/src/components/table_of_contents/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
mod component;
pub use component::*;
33 changes: 33 additions & 0 deletions preview/src/components/table_of_contents/style.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
.dx-table-of-contents {
display: flex;
min-width: 14rem;
flex-direction: column;
font-size: 0.875rem;
}

.dx-table-of-contents a {
padding: 0.5rem 0.75rem 0.5rem calc(0.875rem + var(--depth) * var(--depth-offset));
border-radius: 0 0.625rem 0.625rem 0;
border-left: 2px solid var(--primary-color-6);
color: var(--secondary-color-5);
font-weight: 500;
text-decoration: none;
transition:
border-color 120ms ease,
color 120ms ease,
background-color 120ms ease,
transform 120ms ease;
}

.dx-table-of-contents a:hover {
background-color: var(--primary-color-4);
color: var(--secondary-color-1);
}

.dx-table-of-contents a[data-active="true"] {
border-left-color: var(--focused-border-color);
background-color: color-mix(in srgb, var(--focused-border-color) 18%, transparent);
color: var(--secondary-color-1);
font-weight: 700;
transform: translateX(2px);
}
132 changes: 132 additions & 0 deletions preview/src/components/table_of_contents/variants/main/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
use super::super::component::*;
use dioxus::prelude::*;
use dioxus_primitives::scroll_spy::{ScrollSpyOptions, ScrollSpyScrollHost};

#[component]
pub fn Demo() -> Element {
let scroll_spy_options = ScrollSpyOptions {
selector: "article :is(h2, h3, h4)".to_string(),
scroll_host: ScrollSpyScrollHost::Selector("[data-toc-demo-scroll-region]".to_string()),
offset: 88.0,
..Default::default()
};

rsx! {
div {
"data-toc-demo-scroll-region": "true",
display: "grid",
grid_template_columns: "minmax(0, 1fr) 16rem",
gap: "2rem",
align_items: "start",
max_height: "40rem",
overflow_y: "auto",
padding: "2rem",
border: "1px solid var(--primary-color-6)",
border_radius: "1rem",
background: "var(--primary-color-2)",
color: "var(--secondary-color-1)",

article {
max_width: "44rem",
display: "flex",
flex_direction: "column",
gap: "2.5rem",
padding_bottom: "12rem",

section {
display: "flex",
flex_direction: "column",
gap: "1rem",
h2 { id: "overview", "Overview" }
p { "The table of contents tracks headings in this document and updates the active link while the scroll container moves through long-form content." }
p { "This preview keeps the navigation pinned in view so you can verify that the highlighted entry changes as each heading crosses the configured offset." }
}

section {
display: "flex",
flex_direction: "column",
gap: "1rem",
h2 { id: "installation", "Installation" }
p { "Render the table of contents beside the article and provide initial heading data so server rendering and the hydrated client show the same navigation structure." }
p { "The preview intentionally includes enough copy to force scrolling, making it easier to validate active heading transitions instead of relying on a static layout." }

div {
display: "flex",
flex_direction: "column",
gap: "1rem",
padding_left: "1.5rem",
border_left: "1px solid var(--primary-color-6)",
h3 { id: "configuration", "Configuration" }
p { "Use the selector to scope which headings participate and choose a scroll host when the document uses an internal panel instead of the browser window." }
p { "Indentation in the rendered list reflects heading depth, so a mix of h2, h3, and h4 entries is useful when checking hierarchy." }

div {
display: "flex",
flex_direction: "column",
gap: "1rem",
padding_left: "1.25rem",
border_left: "1px solid var(--primary-color-6)",
h4 { id: "offsets", "Offsets" }
p { "Offset tuning decides when a heading becomes active. In this demo the active item flips before the heading reaches the top edge, which keeps the label change readable during slower scrolling." }
}
}
}

section {
display: "flex",
flex_direction: "column",
gap: "1rem",
h2 { id: "api", "API" }
p { "The primitive exposes initial data, selector configuration, a scroll host, and a reinitialize callback for dynamic documents that add or remove headings after first render." }
p { "The active state is still index-based in the core primitive. This preview only improves the surrounding layout and visual feedback." }

div {
display: "flex",
flex_direction: "column",
gap: "1rem",
padding_left: "1.5rem",
border_left: "1px solid var(--primary-color-6)",
h3 { id: "reinitialization", "Reinitialization" }
p { "Call reinitialize after heading content changes so the hook can rescan the document and keep the table of contents aligned with the rendered article." }
}
}

section {
display: "flex",
flex_direction: "column",
gap: "1rem",
h2 { id: "styling", "Styling" }
p { "Inactive items should stay readable but subdued. The active item needs stronger contrast, a clearer accent, and preserved indentation so the current heading stands out immediately." }
p { "Keeping the navigation sticky inside the scroll region lets the preview demonstrate both hierarchy and scroll-spy feedback in one compact example." }

div {
display: "flex",
flex_direction: "column",
gap: "1rem",
padding_left: "1.5rem",
border_left: "1px solid var(--primary-color-6)",
h3 { id: "accessibility", "Accessibility" }
p { "Consistent heading order and stable ids help keyboard users and assistive technology users move between the article and its generated navigation." }
}
}

section {
display: "flex",
flex_direction: "column",
gap: "1rem",
h2 { id: "usage-notes", "Usage Notes" }
p { "Scroll through this panel to watch the highlighted entry move from section to section. The demo now includes enough vertical space for the active item to change several times before the end of the document." }
p { "If you swap in your own content, keep a similar amount of spacing and section depth so the preview continues to exercise the component meaningfully." }
}
}

aside {
position: "sticky",
top: "1.5rem",
TableOfContents {
scroll_spy_options,
}
}
}
}
}
20 changes: 16 additions & 4 deletions preview/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1266,11 +1266,23 @@ fn WidgetMasonry() -> Element {
}
}

#[allow(unpredictable_function_pointer_comparisons)]
#[derive(Props, Clone)]
struct MasonryCardProps {
component: fn() -> Element,
#[props(default)]
popout: bool,
}

impl PartialEq for MasonryCardProps {
fn eq(&self, other: &Self) -> bool {
self.popout == other.popout && std::ptr::fn_addr_eq(self.component, other.component)
}
}

#[component]
fn MasonryCard(component: fn() -> Element, #[props(default)] popout: bool) -> Element {
let Comp = component;
let class = if popout {
fn MasonryCard(props: MasonryCardProps) -> Element {
let Comp = props.component;
let class = if props.popout {
"dx-widget-card dx-widget-card-popout"
} else {
"dx-widget-card"
Expand Down
2 changes: 2 additions & 0 deletions primitives/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,14 @@ mod portal;
pub mod progress;
pub mod radio_group;
pub mod scroll_area;
pub mod scroll_spy;
pub mod select;
mod selectable;
mod selection;
pub mod separator;
pub mod slider;
pub mod switch;
pub mod table_of_contents;
pub mod tabs;
pub mod toast;
pub mod toggle;
Expand Down
Loading
Loading