stratum_components/typography/
heading.rs1use crate::common::merge_classes;
4use stratum_core::aria::AriaAttributes;
5use stratum_core::render::{AttrValue, RenderOutput};
6
7#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
9pub enum HeadingLevel {
10 H1,
11 #[default]
12 H2,
13 H3,
14 H4,
15 H5,
16 H6,
17}
18
19impl HeadingLevel {
20 pub fn tag(&self) -> &'static str {
21 match self {
22 HeadingLevel::H1 => "h1",
23 HeadingLevel::H2 => "h2",
24 HeadingLevel::H3 => "h3",
25 HeadingLevel::H4 => "h4",
26 HeadingLevel::H5 => "h5",
27 HeadingLevel::H6 => "h6",
28 }
29 }
30
31 pub fn aria_level(&self) -> u8 {
32 match self {
33 HeadingLevel::H1 => 1,
34 HeadingLevel::H2 => 2,
35 HeadingLevel::H3 => 3,
36 HeadingLevel::H4 => 4,
37 HeadingLevel::H5 => 5,
38 HeadingLevel::H6 => 6,
39 }
40 }
41}
42
43#[derive(Debug, Clone, PartialEq, Default)]
45pub struct HeadingProps {
46 pub level: HeadingLevel,
47 pub class: Option<String>,
48 pub id: Option<String>,
49}
50
51pub struct Heading;
52
53impl Heading {
54 pub fn classes(props: &HeadingProps) -> String {
55 let size_cls = match props.level {
56 HeadingLevel::H1 => "scroll-m-20 text-4xl font-extrabold tracking-tight lg:text-5xl",
57 HeadingLevel::H2 => "scroll-m-20 text-3xl font-semibold tracking-tight",
58 HeadingLevel::H3 => "scroll-m-20 text-2xl font-semibold tracking-tight",
59 HeadingLevel::H4 => "scroll-m-20 text-xl font-semibold tracking-tight",
60 HeadingLevel::H5 => "scroll-m-20 text-lg font-semibold tracking-tight",
61 HeadingLevel::H6 => "scroll-m-20 text-base font-semibold tracking-tight",
62 };
63 merge_classes(size_cls, &props.class)
64 }
65
66 pub fn render(props: &HeadingProps) -> RenderOutput {
67 let classes = Self::classes(props);
68 let mut aria = AriaAttributes::new();
69 aria.level = Some(props.level.aria_level());
70
71 let mut output = RenderOutput::new()
72 .with_tag(props.level.tag())
73 .with_class(classes)
74 .with_aria(aria);
75
76 if let Some(ref id) = props.id {
77 output = output.with_attr("id", AttrValue::String(id.clone()));
78 }
79
80 output
81 }
82}
83
84#[cfg(test)]
85mod tests {
86 use super::*;
87
88 #[test]
89 fn heading_h1_classes() {
90 let props = HeadingProps {
91 level: HeadingLevel::H1,
92 ..Default::default()
93 };
94 let classes = Heading::classes(&props);
95 assert!(classes.contains("text-4xl"));
96 assert!(classes.contains("font-extrabold"));
97 }
98
99 #[test]
100 fn heading_h3_tag() {
101 let props = HeadingProps {
102 level: HeadingLevel::H3,
103 ..Default::default()
104 };
105 let output = Heading::render(&props);
106 assert_eq!(output.effective_tag(), "h3");
107 assert_eq!(output.aria.level, Some(3));
108 }
109
110 #[test]
111 fn heading_default_is_h2() {
112 let props = HeadingProps::default();
113 let output = Heading::render(&props);
114 assert_eq!(output.effective_tag(), "h2");
115 assert_eq!(output.aria.level, Some(2));
116 }
117
118 #[test]
119 fn heading_all_levels() {
120 let levels = [
121 HeadingLevel::H1,
122 HeadingLevel::H2,
123 HeadingLevel::H3,
124 HeadingLevel::H4,
125 HeadingLevel::H5,
126 HeadingLevel::H6,
127 ];
128 for level in levels {
129 let props = HeadingProps {
130 level,
131 ..Default::default()
132 };
133 let output = Heading::render(&props);
134 assert_eq!(output.aria.level, Some(level.aria_level()));
135 }
136 }
137}