radix_leptos_primitives/components/pagination/
items.rs

1use leptos::children::Children;
2use leptos::context::use_context;
3use leptos::prelude::*;
4
5use super::context::{PaginationContext, PaginationPage};
6use crate::utils::{merge_optional_classes, generate_id};
7
8/// PaginationList component for the pagination items container
9#[component]
10pub fn PaginationList(
11    /// CSS classes
12    #[prop(optional)]
13    class: Option<String>,
14    /// CSS styles
15    #[prop(optional)]
16    style: Option<String>,
17    /// Child content (pagination items)
18    children: Children,
19) -> impl IntoView {
20    let _context =
21        use_context::<PaginationContext>().expect("PaginationList must be used within Pagination");
22    let list_id = generate_id("pagination-list");
23
24    // Build base classes
25    let base_classes = "radix-pagination-list";
26    let combined_class = merge_optional_classes(Some(base_classes), class.as_deref())
27        .unwrap_or_else(|| base_classes.to_string());
28
29    view! {
30        <ul
31            id=list_id
32            class=combined_class
33            style=style.unwrap_or_default()
34            role="list"
35        >
36            {children()}
37        </ul>
38    }
39}
40
41/// PaginationItem component for individual pagination items
42#[component]
43pub fn PaginationItem(
44    /// The pagination item this component represents
45    #[prop(optional)]
46    page: Option<PaginationPage>,
47    /// Whether this item is current
48    #[prop(optional)]
49    current: Option<bool>,
50    /// Whether this item is disabled
51    #[prop(optional)]
52    disabled: Option<bool>,
53    /// CSS classes
54    #[prop(optional)]
55    class: Option<String>,
56    /// CSS styles
57    #[prop(optional)]
58    style: Option<String>,
59    /// Child content
60    children: Children,
61) -> impl IntoView {
62    let context =
63        use_context::<PaginationContext>().expect("PaginationItem must be used within Pagination");
64    let item_id = generate_id("pagination-item");
65
66    let page_clone = page.clone();
67    let handle_click = move |event: web_sys::MouseEvent| {
68        event.prevent_default();
69
70        if let Some(page) = page_clone.clone() {
71            if !page._disabled {
72                // Call the page change handler
73                if let Some(callback) = context.on_page_change {
74                    callback.run(page.number);
75                }
76            }
77        }
78    };
79
80    let page_forcurrent = page.clone();
81    let page_fordisabled = page.clone();
82
83    // Determine if this item is current
84    let iscurrent = Memo::new(move |_| {
85        if let Some(current) = current {
86            current
87        } else if let Some(page) = page_forcurrent.as_ref() {
88            page._current
89        } else {
90            false
91        }
92    });
93
94    // Determine if this item is disabled
95    let isdisabled = Memo::new(move |_| {
96        if let Some(disabled) = disabled {
97            disabled
98        } else if let Some(page) = page_fordisabled.as_ref() {
99            page._disabled
100        } else {
101            false
102        }
103    });
104
105    // Build base classes
106    let base_classes = "radix-pagination-item";
107    let combined_class = merge_optional_classes(Some(base_classes), class.as_deref())
108        .unwrap_or_else(|| base_classes.to_string());
109
110    view! {
111        <li
112            id=item_id
113            class=combined_class
114            style=style.unwrap_or_default()
115            data-current=iscurrent.get()
116            data-disabled=isdisabled.get()
117            role="listitem"
118        >
119            <button
120                class="radix-pagination-button"
121                data-current=iscurrent.get()
122                data-disabled=isdisabled.get()
123                type="button"
124                role="button"
125                on:click=handle_click
126            >
127                {children()}
128            </button>
129        </li>
130    }
131}
132
133/// PaginationFirst component for first page button
134#[component]
135pub fn PaginationFirst(
136    /// Button text
137    #[prop(optional)]
138    text: Option<String>,
139    /// Button icon
140    #[prop(optional)]
141    icon: Option<String>,
142    /// CSS classes
143    #[prop(optional)]
144    class: Option<String>,
145    /// CSS styles
146    #[prop(optional)]
147    style: Option<String>,
148    /// Child content
149    children: Children,
150) -> impl IntoView {
151    let context =
152        use_context::<PaginationContext>().expect("PaginationFirst must be used within Pagination");
153    let first_id = generate_id("pagination-first");
154
155    let handle_click = move |event: web_sys::MouseEvent| {
156        event.prevent_default();
157
158        if context.current_page.get() > 1 {
159            // Call the page change handler
160            if let Some(callback) = context.on_page_change {
161                callback.run(1);
162            }
163        }
164    };
165
166    let isdisabled = Memo::new(move |_| context.current_page.get() <= 1);
167
168    // Build base classes
169    let base_classes = "radix-pagination-first";
170    let combined_class = merge_optional_classes(Some(base_classes), class.as_deref())
171        .unwrap_or_else(|| base_classes.to_string());
172
173    view! {
174        <li
175            id=first_id
176            class=combined_class
177            style=style.unwrap_or_default()
178            data-disabled=isdisabled.get()
179            role="listitem"
180        >
181            <button
182                class="radix-pagination-button"
183                data-disabled=isdisabled.get()
184                type="button"
185                role="button"
186                on:click=handle_click
187            >
188                {icon.map(|icon_text| view! {
189                    <span class="radix-pagination-icon">{icon_text}</span>
190                })}
191                {text.map(|button_text| view! {
192                    <span class="radix-pagination-text">{button_text}</span>
193                })}
194                {children()}
195            </button>
196        </li>
197    }
198}
199
200/// PaginationPrevious component for previous page button
201#[component]
202pub fn PaginationPrevious(
203    /// Button text
204    #[prop(optional)]
205    text: Option<String>,
206    /// Button icon
207    #[prop(optional)]
208    icon: Option<String>,
209    /// CSS classes
210    #[prop(optional)]
211    class: Option<String>,
212    /// CSS styles
213    #[prop(optional)]
214    style: Option<String>,
215    /// Child content
216    children: Children,
217) -> impl IntoView {
218    let context = use_context::<PaginationContext>()
219        .expect("PaginationPrevious must be used within Pagination");
220    let prev_id = generate_id("pagination-previous");
221
222    let handle_click = move |event: web_sys::MouseEvent| {
223        event.prevent_default();
224
225        let current = context.current_page.get();
226        if current > 1 {
227            // Call the page change handler
228            if let Some(callback) = context.on_page_change {
229                callback.run(current - 1);
230            }
231        }
232    };
233
234    let isdisabled = Memo::new(move |_| context.current_page.get() <= 1);
235
236    // Build base classes
237    let base_classes = "radix-pagination-previous";
238    let combined_class = merge_optional_classes(Some(base_classes), class.as_deref())
239        .unwrap_or_else(|| base_classes.to_string());
240
241    view! {
242        <li
243            id=prev_id
244            class=combined_class
245            style=style.unwrap_or_default()
246            data-disabled=isdisabled.get()
247            role="listitem"
248        >
249            <button
250                class="radix-pagination-button"
251                data-disabled=isdisabled.get()
252                type="button"
253                role="button"
254                on:click=handle_click
255            >
256                {icon.map(|icon_text| view! {
257                    <span class="radix-pagination-icon">{icon_text}</span>
258                })}
259                {text.map(|button_text| view! {
260                    <span class="radix-pagination-text">{button_text}</span>
261                })}
262                {children()}
263            </button>
264        </li>
265    }
266}
267
268/// PaginationNext component for next page button
269#[component]
270pub fn PaginationNext(
271    /// Button text
272    #[prop(optional)]
273    text: Option<String>,
274    /// Button icon
275    #[prop(optional)]
276    icon: Option<String>,
277    /// CSS classes
278    #[prop(optional)]
279    class: Option<String>,
280    /// CSS styles
281    #[prop(optional)]
282    style: Option<String>,
283    /// Child content
284    children: Children,
285) -> impl IntoView {
286    let context =
287        use_context::<PaginationContext>().expect("PaginationNext must be used within Pagination");
288    let next_id = generate_id("pagination-next");
289
290    let handle_click = move |event: web_sys::MouseEvent| {
291        event.prevent_default();
292
293        let current = context.current_page.get();
294        if current < context.total_pages {
295            // Call the page change handler
296            if let Some(callback) = context.on_page_change {
297                callback.run(current + 1);
298            }
299        }
300    };
301
302    let isdisabled = Memo::new(move |_| context.current_page.get() >= context.total_pages);
303
304    // Build base classes
305    let base_classes = "radix-pagination-next";
306    let combined_class = merge_optional_classes(Some(base_classes), class.as_deref())
307        .unwrap_or_else(|| base_classes.to_string());
308
309    view! {
310        <li
311            id=next_id
312            class=combined_class
313            style=style.unwrap_or_default()
314            data-disabled=isdisabled.get()
315            role="listitem"
316        >
317            <button
318                class="radix-pagination-button"
319                data-disabled=isdisabled.get()
320                type="button"
321                role="button"
322                on:click=handle_click
323            >
324                {icon.map(|icon_text| view! {
325                    <span class="radix-pagination-icon">{icon_text}</span>
326                })}
327                {text.map(|button_text| view! {
328                    <span class="radix-pagination-text">{button_text}</span>
329                })}
330                {children()}
331            </button>
332        </li>
333    }
334}
335
336/// PaginationLast component for last page button
337#[component]
338pub fn PaginationLast(
339    /// Button text
340    #[prop(optional)]
341    text: Option<String>,
342    /// Button icon
343    #[prop(optional)]
344    icon: Option<String>,
345    /// CSS classes
346    #[prop(optional)]
347    class: Option<String>,
348    /// CSS styles
349    #[prop(optional)]
350    style: Option<String>,
351    /// Child content
352    children: Children,
353) -> impl IntoView {
354    let context =
355        use_context::<PaginationContext>().expect("PaginationLast must be used within Pagination");
356    let last_id = generate_id("pagination-last");
357
358    let handle_click = move |event: web_sys::MouseEvent| {
359        event.prevent_default();
360
361        if context.current_page.get() < context.total_pages {
362            // Call the page change handler
363            if let Some(callback) = context.on_page_change {
364                callback.run(context.total_pages);
365            }
366        }
367    };
368
369    let isdisabled = Memo::new(move |_| context.current_page.get() >= context.total_pages);
370
371    // Build base classes
372    let base_classes = "radix-pagination-last";
373    let combined_class = merge_optional_classes(Some(base_classes), class.as_deref())
374        .unwrap_or_else(|| base_classes.to_string());
375
376    view! {
377        <li
378            id=last_id
379            class=combined_class
380            style=style.unwrap_or_default()
381            data-disabled=isdisabled.get()
382            role="listitem"
383        >
384            <button
385                class="radix-pagination-button"
386                data-disabled=isdisabled.get()
387                type="button"
388                role="button"
389                on:click=handle_click
390            >
391                {icon.map(|icon_text| view! {
392                    <span class="radix-pagination-icon">{icon_text}</span>
393                })}
394                {text.map(|button_text| view! {
395                    <span class="radix-pagination-text">{button_text}</span>
396                })}
397                {children()}
398            </button>
399        </li>
400    }
401}
402
403/// PaginationEllipsis component for truncated page ranges
404#[component]
405pub fn PaginationEllipsis(
406    /// Ellipsis text
407    #[prop(optional)]
408    text: Option<String>,
409    /// CSS classes
410    #[prop(optional)]
411    class: Option<String>,
412    /// CSS styles
413    #[prop(optional)]
414    style: Option<String>,
415    /// Child content
416    children: Children,
417) -> impl IntoView {
418    let ellipsis_id = generate_id("pagination-ellipsis");
419
420    // Build base classes
421    let base_classes = "radix-pagination-ellipsis";
422    let combined_class = merge_optional_classes(Some(base_classes), class.as_deref())
423        .unwrap_or_else(|| base_classes.to_string());
424
425    view! {
426        <li
427            id=ellipsis_id
428            class=combined_class
429            style=style.unwrap_or_default()
430            role="separator"
431            aria-hidden="true"
432        >
433            <span class="radix-pagination-ellipsis-text">
434                {text.unwrap_or_else(|| "…".to_string())}
435            </span>
436            {children()}
437        </li>
438    }
439}
440
441/// PaginationInfo component for displaying pagination information
442#[component]
443pub fn PaginationInfo(
444    /// Information format
445    #[prop(optional)]
446    format: Option<String>,
447    /// CSS classes
448    #[prop(optional)]
449    class: Option<String>,
450    /// CSS styles
451    #[prop(optional)]
452    style: Option<String>,
453    /// Child content
454    children: Children,
455) -> impl IntoView {
456    let context =
457        use_context::<PaginationContext>().expect("PaginationInfo must be used within Pagination");
458    let info_id = generate_id("pagination-info");
459
460    // Calculate pagination information
461    let start_item = Memo::new(move |_| {
462        let current = context.current_page.get();
463        let page_size = context.page_size;
464        ((current - 1) * page_size) + 1
465    });
466
467    let end_item = Memo::new(move |_| {
468        let current = context.current_page.get();
469        let page_size = context.page_size;
470        let total_items = context.total_items;
471        std::cmp::min(current * page_size, total_items)
472    });
473
474    let total_items = context.total_items;
475
476    // Build base classes
477    let base_classes = "radix-pagination-info";
478    let combined_class = merge_optional_classes(Some(base_classes), class.as_deref())
479        .unwrap_or_else(|| base_classes.to_string());
480
481    view! {
482        <div
483            id=info_id
484            class=combined_class
485            style=style.unwrap_or_default()
486            role="status"
487            aria-live="polite"
488        >
489            {if let Some(format_str) = format {
490                let start = start_item.get();
491                let end = end_item.get();
492                let total = total_items;
493                let current = context.current_page.get();
494                let total_pages = context.total_pages;
495
496                let info_text = format_str
497                    .replace("{start}", &start.to_string())
498                    .replace("{end}", &end.to_string())
499                    .replace("{total}", &total.to_string())
500                    .replace("{current}", &current.to_string())
501                    .replace("{total_pages}", &total_pages.to_string());
502
503                view! {
504                    <span class="radix-pagination-info-text">{info_text}</span>
505                }
506            } else {
507                view! { <span class="radix-pagination-info-text">{String::new()}</span> }
508            }}
509            {children()}
510        </div>
511    }
512}
513
514/// PaginationContent component for wrapping pagination content
515#[component]
516pub fn PaginationContent(
517    /// CSS classes
518    #[prop(optional)]
519    class: Option<String>,
520    /// CSS styles
521    #[prop(optional)]
522    style: Option<String>,
523    /// Child content
524    children: Children,
525) -> impl IntoView {
526    let content_id = generate_id("pagination-content");
527
528    // Build base classes
529    let base_classes = "radix-pagination-content";
530    let combined_class = merge_optional_classes(Some(base_classes), class.as_deref())
531        .unwrap_or_else(|| base_classes.to_string());
532
533    view! {
534        <div
535            id=content_id
536            class=combined_class
537            style=style.unwrap_or_default()
538        >
539            {children()}
540        </div>
541    }
542}
543
544#[cfg(test)]
545mod items_tests {
546    use super::*;
547use crate::utils::{merge_optional_classes, generate_id};
548
549    #[test]
550    fn test_pagination_list_creation() {
551        // Test that PaginationList can be created without runtime
552        let _list_id = generate_id("pagination-list");
553        assert!(!_list_id.is_empty());
554    }
555
556    #[test]
557    fn test_pagination_item_creation() {
558        // Test that PaginationItem can be created without runtime
559        let _item_id = generate_id("pagination-item");
560        assert!(!_item_id.is_empty());
561    }
562
563    #[test]
564    fn test_pagination_ellipsis_creation() {
565        // Test that PaginationEllipsis can be created without runtime
566        let _ellipsis_id = generate_id("pagination-ellipsis");
567        assert!(!_ellipsis_id.is_empty());
568    }
569}