patternfly_yew/components/table/
column.rs

1use crate::prelude::{
2    AsClasses, ExtendClasses, Icon, Order, TableHeaderContext, TableHeaderSortBy, TextModifier,
3};
4use std::fmt::Debug;
5use yew::prelude::*;
6
7/// Properties for [`TableColumn`]
8#[derive(Clone, Debug, PartialEq, Properties)]
9pub struct TableColumnProperties<C>
10where
11    C: Clone + Eq + 'static,
12{
13    /// The column (id) of the column
14    pub index: C,
15    #[prop_or_default]
16    pub label: Option<String>,
17    #[prop_or_default]
18    pub center: bool,
19    #[prop_or_default]
20    pub width: ColumnWidth,
21    #[prop_or_default]
22    pub text_modifier: Option<TextModifier>,
23    #[prop_or_default]
24    pub expandable: bool,
25
26    #[doc(hidden)]
27    #[prop_or_default]
28    pub(crate) first_tree_column: bool,
29
30    // Current sortby status
31    #[prop_or_default]
32    pub sortby: Option<TableHeaderSortBy<C>>,
33
34    #[prop_or_default]
35    pub onsort: Option<Callback<TableHeaderSortBy<C>>>,
36}
37
38#[derive(Copy, Clone, Default, Eq, PartialEq, Debug)]
39pub enum ColumnWidth {
40    #[default]
41    Default,
42    /// Percentage modifier
43    ///
44    /// From 10 to 90, rounded to the nearest ten. Values outside of the limit will be replaced
45    /// with the limit.
46    Percent(u16),
47    /// Maximize width
48    WidthMax,
49    /// Minimize with, without triggering text wrapping
50    FitContent,
51}
52
53fn round(p: u16) -> u16 {
54    if p <= 10 {
55        return 10;
56    }
57    if p >= 90 {
58        return 90;
59    }
60
61    // round to the nearest ten
62    ((p + 5) / 10) * 10
63}
64
65impl AsClasses for ColumnWidth {
66    fn extend_classes(&self, classes: &mut Classes) {
67        match self {
68            Self::Default => {}
69            Self::Percent(p) => classes.push(classes!(format!("pf-m-width-{}", round(*p)))),
70            Self::WidthMax => classes.push(classes!("pf-m-width-max")),
71            Self::FitContent => classes.push(classes!("pf-m-fit-content")),
72        }
73    }
74}
75
76/// The Table Column component.
77///
78/// ## Properties
79///
80/// Define by [`TableColumnProperties`].
81#[function_component(TableColumn)]
82pub fn table_column<K>(props: &TableColumnProperties<K>) -> Html
83where
84    K: Clone + Eq + 'static,
85{
86    let table_header_context = use_context::<TableHeaderContext<K>>();
87
88    let mut class = classes!("pf-v5-c-table__th");
89
90    if props.first_tree_column {
91        class.push(classes!("pf-v5-c-table__tree-view-title-header-cell"));
92    }
93
94    if props.center {
95        class.push(classes!("pf-m-center"));
96    }
97
98    if props.onsort.is_some() {
99        class.push(classes!("pf-v5-c-table__sort"));
100    }
101
102    class.extend_from(&props.width);
103    class.extend_from(&props.text_modifier);
104
105    match &props.label {
106        None => html! (<th></th>),
107        Some(label) => {
108            let th_content = if let Some(onsort) = &props.onsort {
109                let header_context = table_header_context.expect(
110                    "Column must be inside TableHeader, the expected context is defined there",
111                );
112
113                // If status of sort is not provided by user then use the context
114                let sortby = if props.sortby.is_some() {
115                    &props.sortby
116                } else {
117                    &header_context.sortby
118                };
119
120                let sort_by_next_status = match sortby {
121                    Some(val) => {
122                        if val.index == props.index {
123                            class.push(classes!("pf-m-selected"));
124                        }
125
126                        if val.index == props.index {
127                            let icon = match val.order {
128                                Order::Ascending => Icon::LongArrowAltUp,
129                                Order::Descending => Icon::LongArrowAltDown,
130                            };
131                            (icon, val.order)
132                        } else {
133                            (Icon::ArrowsAltV, Order::Descending)
134                        }
135                    }
136                    None => (Icon::ArrowsAltV, Order::Descending),
137                };
138
139                html_nested!(
140                    <button
141                        title={label.clone()}
142                        type="button"
143                        class="pf-v5-c-table__button"
144                        onclick={
145                            {
146                                // Emit sorting in context and in user callback
147                                let onsort_context = header_context.onsort.clone();
148                                let onsort = onsort.clone();
149
150                                let index = props.index.clone();
151                                let order = sort_by_next_status.1;
152
153                                Callback::from(move |_| {
154                                    let sort_by = TableHeaderSortBy {
155                                        index: index.clone(),
156                                        order: !order
157                                    };
158                                    onsort_context.emit(sort_by.clone());
159                                    onsort.emit(sort_by.clone());
160                                })
161                            }
162                        }
163                    >
164                        <div class="pf-v5-c-table__button-content">
165                            <span class="pf-v5-c-table__text">{ &label }</span>
166                            <span class="pf-v5-c-table__sort-indicator">
167                                {sort_by_next_status.0}
168                            </span>
169                        </div>
170                    </button>
171                )
172            } else {
173                html_nested!(
174                    <>{ &label }</>
175                )
176            };
177
178            html!(
179                <th title={label.clone()} {class} scope="col" role="columnheader">
180                    {th_content}
181                </th>
182            )
183        }
184    }
185}
186
187#[cfg(test)]
188mod test {
189    use super::*;
190
191    #[test]
192    fn test_round() {
193        assert_eq!(round(0), 10);
194        assert_eq!(round(10), 10);
195        assert_eq!(round(50), 50);
196        assert_eq!(round(54), 50);
197        assert_eq!(round(55), 60);
198        assert_eq!(round(56), 60);
199        assert_eq!(round(100), 90);
200        assert_eq!(round(100), 90);
201        assert_eq!(round(200), 90);
202    }
203}