radix_leptos_primitives/components/pagination/
items.rs1use 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#[component]
10pub fn PaginationList(
11 #[prop(optional)]
13 class: Option<String>,
14 #[prop(optional)]
16 style: Option<String>,
17 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 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#[component]
43pub fn PaginationItem(
44 #[prop(optional)]
46 page: Option<PaginationPage>,
47 #[prop(optional)]
49 current: Option<bool>,
50 #[prop(optional)]
52 disabled: Option<bool>,
53 #[prop(optional)]
55 class: Option<String>,
56 #[prop(optional)]
58 style: Option<String>,
59 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 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 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 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 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#[component]
135pub fn PaginationFirst(
136 #[prop(optional)]
138 text: Option<String>,
139 #[prop(optional)]
141 icon: Option<String>,
142 #[prop(optional)]
144 class: Option<String>,
145 #[prop(optional)]
147 style: Option<String>,
148 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 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 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#[component]
202pub fn PaginationPrevious(
203 #[prop(optional)]
205 text: Option<String>,
206 #[prop(optional)]
208 icon: Option<String>,
209 #[prop(optional)]
211 class: Option<String>,
212 #[prop(optional)]
214 style: Option<String>,
215 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 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 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#[component]
270pub fn PaginationNext(
271 #[prop(optional)]
273 text: Option<String>,
274 #[prop(optional)]
276 icon: Option<String>,
277 #[prop(optional)]
279 class: Option<String>,
280 #[prop(optional)]
282 style: Option<String>,
283 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 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 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#[component]
338pub fn PaginationLast(
339 #[prop(optional)]
341 text: Option<String>,
342 #[prop(optional)]
344 icon: Option<String>,
345 #[prop(optional)]
347 class: Option<String>,
348 #[prop(optional)]
350 style: Option<String>,
351 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 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 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#[component]
405pub fn PaginationEllipsis(
406 #[prop(optional)]
408 text: Option<String>,
409 #[prop(optional)]
411 class: Option<String>,
412 #[prop(optional)]
414 style: Option<String>,
415 children: Children,
417) -> impl IntoView {
418 let ellipsis_id = generate_id("pagination-ellipsis");
419
420 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#[component]
443pub fn PaginationInfo(
444 #[prop(optional)]
446 format: Option<String>,
447 #[prop(optional)]
449 class: Option<String>,
450 #[prop(optional)]
452 style: Option<String>,
453 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 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 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}", ¤t.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#[component]
516pub fn PaginationContent(
517 #[prop(optional)]
519 class: Option<String>,
520 #[prop(optional)]
522 style: Option<String>,
523 children: Children,
525) -> impl IntoView {
526 let content_id = generate_id("pagination-content");
527
528 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 let _list_id = generate_id("pagination-list");
553 assert!(!_list_id.is_empty());
554 }
555
556 #[test]
557 fn test_pagination_item_creation() {
558 let _item_id = generate_id("pagination-item");
560 assert!(!_item_id.is_empty());
561 }
562
563 #[test]
564 fn test_pagination_ellipsis_creation() {
565 let _ellipsis_id = generate_id("pagination-ellipsis");
567 assert!(!_ellipsis_id.is_empty());
568 }
569}