Skip to main content

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-v6-c-table__th");
89
90    if props.first_tree_column {
91        class.push(classes!("pf-v6-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-v6-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 />),
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-v6-c-table__button"
144                        onclick={{
145                                // Emit sorting in context and in user callback
146                                let onsort_context = header_context.onsort.clone();
147                                let onsort = onsort.clone();
148
149                                let index = props.index.clone();
150                                let order = sort_by_next_status.1;
151
152                                Callback::from(move |_| {
153                                    let sort_by = TableHeaderSortBy {
154                                        index: index.clone(),
155                                        order: !order
156                                    };
157                                    onsort_context.emit(sort_by.clone());
158                                    onsort.emit(sort_by.clone());
159                                })
160                            }}
161                    >
162                        <div class="pf-v6-c-table__button-content">
163                            <span class="pf-v6-c-table__text">{ label }</span>
164                            <span class="pf-v6-c-table__sort-indicator">
165                                { sort_by_next_status.0 }
166                            </span>
167                        </div>
168                    </button>
169                )
170            } else {
171                html_nested!({ label.into() })
172            };
173
174            html!(
175                <th title={label.clone()} {class} scope="col" role="columnheader">
176                    { th_content }
177                </th>
178            )
179        }
180    }
181}
182
183#[cfg(test)]
184mod test {
185    use super::*;
186
187    #[test]
188    fn test_round() {
189        assert_eq!(round(0), 10);
190        assert_eq!(round(10), 10);
191        assert_eq!(round(50), 50);
192        assert_eq!(round(54), 50);
193        assert_eq!(round(55), 60);
194        assert_eq!(round(56), 60);
195        assert_eq!(round(100), 90);
196        assert_eq!(round(100), 90);
197        assert_eq!(round(200), 90);
198    }
199}