- }
- else if (currentPage == SelectedPage)
+@* The pagination is a navigation landmark wrapping a list of page controls, which is the markup every
+ major design system settles on, so assistive technologies announce it as navigation and enumerate its
+ items instead of reading a bare run of buttons.
+ The list and its items are collapsed with display:contents so the buttons stay the direct flex items of
+ the root and the layout (including any gap coming through Styles.Root) is exactly what it was before the
+ list was introduced. The explicit list roles are there because a box removed by display:contents used to
+ lose its implicit semantics in some browser / screen reader combinations.
+ The page size selector, the summary and the go to page input sit outside of that list: none of them is one
+ of the pages the list enumerates, so counting them among its items would misreport how many it holds. *@
+@* GetPageHref turns every control into a link, since a range that is reachable by its own address belongs in
+ links rather than in buttons. The two forms are written out side by side rather than folded into one
+ dynamic tag: which one is rendered follows from GetPageHref alone, so a control never swaps its markup
+ while the selection moves. A link with no page to reach carries no href, and reports aria-disabled in
+ place of the disabled attribute an anchor has no use for. *@
+@* The aria-label sits before the attribute splatting so that an aria-label passed as a plain attribute
+ still wins over the default name of the landmark, while the id, the style, the class and the dir stay
+ after it and keep the ones the component renders. *@
+@if (HideOnSinglePage is false || _Count > 1)
+{
+
\ No newline at end of file
+ @if (ShowPageButtons)
+ {
+ @* Every page keeps its own key so that moving the selection only flips the attributes of the
+ already rendered control instead of replacing it, which is what keeps the keyboard focus on
+ the page the user just activated. *@
+ var pages = GeneratePages();
+ for (var i = 0; i < pages.Length; i++)
+ {
+ var pageNumber = pages[i];
+
+ if (pageNumber == EllipsisPage)
+ {
+ @* The glyph itself is hidden and the item carries the label, so the gap is reported
+ as one named item instead of being read out as a run of punctuation. *@
+
+ }
+
+
+ @* The jump closes the pagination, after the controls it is the shortcut for. The input is named on
+ its own so the visible text beside it can be dropped without leaving it unnamed, and the value is
+ tracked on every keystroke so that clearing the field after a jump actually reaches the DOM. *@
+ @if (ShowGoToPage)
+ {
+
+ @if (GoToPageText.HasValue())
+ {
+
+ }
+
+
+ }
+
+}
diff --git a/src/BlazorUI/Bit.BlazorUI/Components/Navs/Pagination/BitPagination.razor.cs b/src/BlazorUI/Bit.BlazorUI/Components/Navs/Pagination/BitPagination.razor.cs
index b11dd4ebab6..7255b3bb8b9 100644
--- a/src/BlazorUI/Bit.BlazorUI/Components/Navs/Pagination/BitPagination.razor.cs
+++ b/src/BlazorUI/Bit.BlazorUI/Components/Navs/Pagination/BitPagination.razor.cs
@@ -1,24 +1,69 @@
-using System.Text;
+using System.Globalization;
namespace Bit.BlazorUI;
///
/// Pagination component helps users easily navigate through content, allowing swift browsing across multiple pages or sections, commonly used in lists, tables, and content-rich interfaces.
///
+///
+/// The component renders a navigation landmark holding a list of page controls, marks the current page with
+/// aria-current and names every control for assistive technologies, so it can be dropped next to a grid or a
+/// list of results without any extra markup.
+///
+/// Name the landmark through whenever more than one pagination is
+/// rendered on the same page, so each of them can be told apart in the landmark list.
+///
+/// The controls are buttons by default and become links as soon as hands them an
+/// address, which is what a range that is meant to be crawled, bookmarked or opened in another tab calls for.
+///
public partial class BitPagination : BitComponentBase
{
- private int _count = 1;
- private int _middleCount = 3;
- private int _boundaryCount = 2;
+ ///
+ /// The placeholder a generated page list uses in place of the pages that are collapsed into an ellipsis.
+ ///
+ private const int EllipsisPage = -1;
+
+ private const int DefaultMiddleCount = 3;
+ private const int DefaultBoundaryCount = 2;
+
+ private static readonly int[] DefaultPageSizeOptions = [10, 25, 50, 100];
+
+ ///
+ /// The control the focus is handed over to after a navigation button disabled itself by moving the
+ /// selection to the end of the range it points at.
+ ///
+ private enum FocusTarget { None, SelectedPage, First, Previous, Next, Last }
+
+ private int _correctedPage;
+ private string? _goToPageText;
+
+ // The offered page sizes are materialized once per parameter change rather than on every render, since a
+ // consumer is free to hand over an enumerable that walks (or computes) itself each time it is read.
+ private int[] _pageSizeOptions = DefaultPageSizeOptions;
+
+ private FocusTarget _focusTarget;
+ private ElementReference _firstButtonRef;
+ private ElementReference _previousButtonRef;
+ private ElementReference _nextButtonRef;
+ private ElementReference _lastButtonRef;
+
+ // The page buttons are captured by their page number so that the one holding the selection can be found
+ // again after the range around it moved. Entries of the pages that left the range are harmless: the focus
+ // is only ever handed to the page the pagination just settled on, which is always one of the rendered ones.
+ private readonly Dictionary _pageRefs = [];
///
/// The number of items at the start and end of the pagination.
+ ///
+ /// The default value is 2.
///
- [Parameter]
- [CallOnSet(nameof(OnSetBoundaryCount))]
- public int BoundaryCount { get; set; }
+ ///
+ /// A value that is not positive falls back to the default, since a range with no fixed ends would lose
+ /// the shortcut to the first and the last pages.
+ ///
+ [Parameter] public int BoundaryCount { get; set; }
///
/// Custom CSS classes for different parts of the pagination.
@@ -33,16 +78,49 @@ public partial class BitPagination : BitComponentBase
///
/// The total number of pages.
+ ///
+ /// The default value is 1.
///
- [Parameter]
- [CallOnSet(nameof(OnSetCount))]
- public int Count { get; set; }
+ ///
+ /// A count that is not positive still leaves a single page to be on, since a pagination with no page at
+ /// all has nothing to render.
+ ///
+ [Parameter] public int Count { get; set; }
///
/// The default selected page number.
///
[Parameter] public int DefaultSelectedPage { get; set; }
+ ///
+ /// The accessible label of the item standing in for the pages an ellipsis collapses.
+ ///
+ /// The default value is "More pages".
+ ///
+ ///
+ /// The glyph itself is hidden from assistive technologies and this label is announced in its place, so
+ /// the gap in the range is reported as one item instead of being read as a run of punctuation.
+ ///
+ [Parameter] public string EllipsisAriaLabel { get; set; } = "More pages";
+
+ ///
+ /// The text of the ellipsis standing in for the pages that are collapsed out of the range.
+ ///
+ /// The default value is "•••".
+ ///
+ [Parameter] public string EllipsisText { get; set; } = "•••";
+
+ ///
+ /// The accessible label of the first button.
+ ///
+ /// The default value is "First page".
+ ///
+ ///
+ /// The value is used both as the aria-label and as the native tooltip of the button, since the button
+ /// carries an icon and no text of its own.
+ ///
+ [Parameter] public string FirstButtonAriaLabel { get; set; } = "First page";
+
///
/// The icon for the first button using custom CSS classes for external icon libraries.
/// Takes precedence over when both are set.
@@ -55,6 +133,103 @@ public partial class BitPagination : BitComponentBase
///
[Parameter] public string? FirstButtonIconName { get; set; }
+ ///
+ /// The text rendered beside the icon of the first button.
+ ///
+ ///
+ /// A navigation button carries an icon only unless it is given a text, and it widens to fit the text it
+ /// is given. The accessible name still comes from , so a short visible
+ /// text can sit next to a fuller spoken one.
+ ///
+ [Parameter] public string? FirstButtonText { get; set; }
+
+ ///
+ /// Provides the accessible label of a page button, from its one-based number and whether it is the
+ /// selected one, replacing the default "Page {number}" label.
+ ///
+ ///
+ /// This is the hook to localize the page buttons, or to make them announce what the page holds
+ /// (for example "Page 3 of 12" or "Results 21 to 30").
+ ///
+ /// The selected page also reports aria-current, so the label does not have to say that it is the
+ /// current one for a screen reader to announce it as such.
+ ///
+ [Parameter] public Func? GetPageAriaLabel { get; set; }
+
+ ///
+ /// Provides the address a page control points at, from its one-based number, which turns every control of
+ /// the pagination into a link instead of a button.
+ ///
+ ///
+ /// Pagination is navigation, so a range that is reachable by its own address (a crawler following it, a
+ /// page opened in another tab, a middle click) belongs in links rather than in buttons. The four
+ /// navigation controls ask for the address of the page they move to, so they turn into links along with
+ /// the numeric ones.
+ ///
+ /// A control with no address to point at - one at the end of the range it navigates, one the pagination is
+ /// disabled for, or one this returns nothing for - keeps its place while reporting aria-disabled and
+ /// staying out of the tab order.
+ ///
+ /// The click still reaches and , so a pagination of links
+ /// reports the page that was asked for exactly like a pagination of buttons.
+ ///
+ [Parameter] public Func? GetPageHref { get; set; }
+
+ ///
+ /// Provides the text of the summary, from the selected page and the total number of pages, replacing the
+ /// default "Page {number} of {count}" text.
+ ///
+ ///
+ /// This is the hook to localize the summary, or to report the position in terms of the items rather than
+ /// the pages (for example "Showing 21 to 30 of 240 results") from numbers only the consumer holds.
+ ///
+ /// It is only called while is on.
+ ///
+ [Parameter] public Func? GetSummary { get; set; }
+
+ ///
+ /// The accessible label of the go to page input.
+ ///
+ /// The default value is "Go to page".
+ ///
+ ///
+ /// It names the input on its own, so the visible beside it can be dropped
+ /// without leaving the input unnamed.
+ ///
+ [Parameter] public string GoToPageAriaLabel { get; set; } = "Go to page";
+
+ ///
+ /// The text rendered ahead of the go to page input.
+ ///
+ /// The default value is "Go to".
+ ///
+ ///
+ /// An empty text leaves the input on its own, which is the compact form a narrow layout calls for.
+ ///
+ [Parameter] public string? GoToPageText { get; set; } = "Go to";
+
+ ///
+ /// Renders nothing at all while there is a single page to navigate.
+ ///
+ /// The default value is false.
+ ///
+ ///
+ /// Navigation that cannot go anywhere is noise, so hiding it keeps the layout of a short result set clean.
+ /// Leave it off when the pagination sits in a fixed layout that a disappearing element would reflow.
+ ///
+ [Parameter] public bool HideOnSinglePage { get; set; }
+
+ ///
+ /// The accessible label of the last button.
+ ///
+ /// The default value is "Last page".
+ ///
+ ///
+ /// The value is used both as the aria-label and as the native tooltip of the button, since the button
+ /// carries an icon and no text of its own.
+ ///
+ [Parameter] public string LastButtonAriaLabel { get; set; } = "Last page";
+
///
/// The icon for the last button using custom CSS classes for external icon libraries.
/// Takes precedence over when both are set.
@@ -67,12 +242,49 @@ public partial class BitPagination : BitComponentBase
///
[Parameter] public string? LastButtonIconName { get; set; }
+ ///
+ /// The text rendered beside the icon of the last button.
+ ///
+ ///
+ /// A navigation button carries an icon only unless it is given a text, and it widens to fit the text it
+ /// is given. The accessible name still comes from , so a short visible
+ /// text can sit next to a fuller spoken one.
+ ///
+ [Parameter] public string? LastButtonText { get; set; }
+
+ ///
+ /// Wraps the next and previous buttons around the ends of the range, so the next button moves from the
+ /// last page to the first one and the previous button from the first page to the last one.
+ ///
+ /// The default value is false.
+ ///
+ ///
+ /// The two buttons also stay enabled at the ends of the range while this is on. The first and last
+ /// buttons are unaffected, since they always target a fixed page.
+ ///
+ [Parameter] public bool Loop { get; set; }
+
///
/// The number of items to render in the middle of the pagination.
+ ///
+ /// The default value is 3.
///
- [Parameter]
- [CallOnSet(nameof(OnSetMiddleCount))]
- public int MiddleCount { get; set; }
+ ///
+ /// A value that is not positive falls back to the default, since a middle range with nothing in it would
+ /// leave the selected page out of the pagination.
+ ///
+ [Parameter] public int MiddleCount { get; set; }
+
+ ///
+ /// The accessible label of the next button.
+ ///
+ /// The default value is "Next page".
+ ///
+ ///
+ /// The value is used both as the aria-label and as the native tooltip of the button, since the button
+ /// carries an icon and no text of its own.
+ ///
+ [Parameter] public string NextButtonAriaLabel { get; set; } = "Next page";
///
/// The icon for the next button using custom CSS classes for external icon libraries.
@@ -86,11 +298,88 @@ public partial class BitPagination : BitComponentBase
///
[Parameter] public string? NextButtonIconName { get; set; }
+ ///
+ /// The text rendered beside the icon of the next button.
+ ///
+ ///
+ /// A navigation button carries an icon only unless it is given a text, and it widens to fit the text it
+ /// is given. The accessible name still comes from , so a short visible
+ /// text can sit next to a fuller spoken one.
+ ///
+ [Parameter] public string? NextButtonText { get; set; }
+
///
/// The event callback for when selected page changes.
///
+ ///
+ /// The callback also runs when is bound one way, so a page can be requested
+ /// and applied by the consumer without giving up control of the value.
+ ///
[Parameter] public EventCallback OnChange { get; set; }
+ ///
+ /// The event callback for when the page size is picked out of the page size selector.
+ ///
+ ///
+ /// The callback also runs when is bound one way, and it is where the consumer
+ /// recomputes from the page size it was handed.
+ ///
+ [Parameter] public EventCallback OnPageSizeChange { get; set; }
+
+ ///
+ /// The number of items a page holds, which the page size selector picks.
+ ///
+ ///
+ /// The pagination only reports the number that was picked: the range of pages it renders still comes from
+ /// , which the consumer recomputes from the new page size. A value that is not positive
+ /// falls back to the first of the .
+ ///
+ [Parameter, TwoWayBound]
+ public int PageSize { get; set; }
+
+ ///
+ /// The accessible label of the page size selector.
+ ///
+ /// The default value is "Items per page".
+ ///
+ ///
+ /// It names the selector on its own, so the visible beside it can be dropped
+ /// without leaving the selector unnamed.
+ ///
+ [Parameter] public string PageSizeAriaLabel { get; set; } = "Items per page";
+
+ ///
+ /// The page sizes the page size selector offers.
+ ///
+ /// The default value is 10, 25, 50 and 100.
+ ///
+ ///
+ /// An empty list falls back to the default, since a selector with nothing to pick from would leave the
+ /// page size it reports unreachable.
+ ///
+ [Parameter] public IEnumerable? PageSizeOptions { get; set; }
+
+ ///
+ /// The text rendered ahead of the page size selector.
+ ///
+ /// The default value is "Items per page".
+ ///
+ ///
+ /// An empty text leaves the selector on its own, which is the compact form a narrow layout calls for.
+ ///
+ [Parameter] public string? PageSizeText { get; set; } = "Items per page";
+
+ ///
+ /// The accessible label of the previous button.
+ ///
+ /// The default value is "Previous page".
+ ///
+ ///
+ /// The value is used both as the aria-label and as the native tooltip of the button, since the button
+ /// carries an icon and no text of its own.
+ ///
+ [Parameter] public string PreviousButtonAriaLabel { get; set; } = "Previous page";
+
///
/// The icon for the previous button using custom CSS classes for external icon libraries.
/// Takes precedence over when both are set.
@@ -103,9 +392,31 @@ public partial class BitPagination : BitComponentBase
///
[Parameter] public string? PreviousButtonIconName { get; set; }
+ ///
+ /// The text rendered beside the icon of the previous button.
+ ///
+ ///
+ /// A navigation button carries an icon only unless it is given a text, and it widens to fit the text it
+ /// is given. The accessible name still comes from , so a short
+ /// visible text can sit next to a fuller spoken one.
+ ///
+ [Parameter] public string? PreviousButtonText { get; set; }
+
+ ///
+ /// Renders the buttons of the pagination with fully rounded (circular) corners.
+ ///
+ /// The default value is false.
+ ///
+ [Parameter, ResetClassBuilder]
+ public bool Rounded { get; set; }
+
///
/// The selected page number.
///
+ ///
+ /// The value is one-based and is clamped into the available range while rendering, so a page number
+ /// outside of never leaves the pagination without a current page.
+ ///
[Parameter, TwoWayBound]
public int SelectedPage { get; set; }
@@ -114,6 +425,18 @@ public partial class BitPagination : BitComponentBase
///
[Parameter] public bool ShowFirstButton { get; set; }
+ ///
+ /// Shows an input that jumps straight to the page number typed into it, at the end of the pagination.
+ ///
+ /// The default value is false.
+ ///
+ ///
+ /// The jump runs when the input is committed (on Enter, or when it loses the focus) and the input clears
+ /// itself afterwards. A number outside of the range lands on the nearest end of it instead of being
+ /// dropped, so a long range can be reached without knowing where it stops.
+ ///
+ [Parameter] public bool ShowGoToPage { get; set; }
+
///
/// Determines whether to show the last button.
///
@@ -124,11 +447,47 @@ public partial class BitPagination : BitComponentBase
///
[Parameter] public bool ShowNextButton { get; set; } = true;
+ ///
+ /// Determines whether to show the numeric page buttons.
+ ///
+ /// The default value is true.
+ ///
+ ///
+ /// Turning the page buttons off leaves only the navigation buttons, which is the compact pagination a
+ /// narrow layout or an unbounded result set (where the number of pages is unknown) calls for.
+ ///
+ [Parameter] public bool ShowPageButtons { get; set; } = true;
+
+ ///
+ /// Shows a selector that picks how many items a page holds, ahead of everything else in the pagination.
+ ///
+ /// The default value is false.
+ ///
+ ///
+ /// Picking a size reports it through and and changes
+ /// nothing else: the number of pages the new size adds up to is the consumer's to recompute into
+ /// , and a selected page that falls out of the shrunk range is pulled back on its own.
+ ///
+ [Parameter] public bool ShowPageSizeSelector { get; set; }
+
///
/// Determines whether to show the previous button.
///
[Parameter] public bool ShowPreviousButton { get; set; } = true;
+ ///
+ /// Shows the position in the range, which reads "Page {number} of {count}" unless
+ /// replaces it, ahead of the buttons of the pagination.
+ ///
+ /// The default value is false.
+ ///
+ ///
+ /// The summary is a status region, so a screen reader reports the new position as the page changes. That
+ /// makes it the piece to turn on along with turned off, where nothing else
+ /// tells which page of how many is the current one.
+ ///
+ [Parameter] public bool ShowSummary { get; set; }
+
///
/// The size of the buttons.
///
@@ -148,6 +507,31 @@ public partial class BitPagination : BitComponentBase
+ ///
+ /// Gives the keyboard focus to the button of the selected page, falling back to the first navigation
+ /// button that is rendered while the page buttons are turned off.
+ ///
+ ///
+ /// This is what a consumer reloading the list behind the pagination calls to put the focus back on the
+ /// navigation the reload was asked from.
+ ///
+ public ValueTask FocusAsync()
+ {
+ if (ShowPageButtons && _pageRefs.TryGetValue(_SelectedPage, out var pageRef)) return pageRef.FocusAsync();
+
+ if (ShowFirstButton) return _firstButtonRef.FocusAsync();
+
+ if (ShowPreviousButton) return _previousButtonRef.FocusAsync();
+
+ if (ShowNextButton) return _nextButtonRef.FocusAsync();
+
+ if (ShowLastButton) return _lastButtonRef.FocusAsync();
+
+ return ValueTask.CompletedTask;
+ }
+
+
+
protected override string RootElementClass => "bit-pgn";
protected override void RegisterCssClasses()
@@ -174,6 +558,8 @@ protected override void RegisterCssClasses()
BitSize.Large => "bit-pgn-lg",
_ => "bit-pgn-md"
});
+
+ ClassBuilder.Register(() => Rounded ? "bit-pgn-rnd" : string.Empty);
}
protected override void RegisterCssStyles()
@@ -185,7 +571,7 @@ protected override async Task OnInitializedAsync()
{
if (SelectedPageHasBeenSet is false && DefaultSelectedPage != 0)
{
- await AssignSelectedPage(DefaultSelectedPage);
+ await AssignSelectedPage(Math.Clamp(DefaultSelectedPage, 1, _Count));
}
if (SelectedPage == 0)
@@ -196,51 +582,173 @@ protected override async Task OnInitializedAsync()
await base.OnInitializedAsync();
}
+ protected override async Task OnParametersSetAsync()
+ {
+ _pageSizeOptions = PageSizeOptions?.ToArray() is { Length: > 0 } options ? options : DefaultPageSizeOptions;
+
+ // A selected page that fell outside of the range (a count that shrank under it, or a value that was
+ // never inside it) is written back so that the value a consumer is bound to never keeps pointing at a
+ // page that does not exist. This runs after every parameter has been applied, so a count and a
+ // selected page changing together settle on the value the consumer asked for and not on an
+ // intermediate one.
+ if (SelectedPage == _SelectedPage)
+ {
+ _correctedPage = 0;
+ }
+ else if (SelectedPage != _correctedPage)
+ {
+ // The same out of range value is only corrected once: a consumer that hands it back unchanged
+ // (a callback that drops the new page instead of storing it) would otherwise be answered with
+ // another correction on every render, and the two would keep re-rendering each other.
+ _correctedPage = SelectedPage;
+
+ await AssignSelectedPage(_SelectedPage);
+ }
+
+ await base.OnParametersSetAsync();
+ }
+
+ protected override async Task OnAfterRenderAsync(bool firstRender)
+ {
+ // A navigation button that disabled itself by reaching the end of the range it points at drops the
+ // keyboard focus on the document, so the focus is handed over to the control that took its place
+ // (the selected page, or the navigation button pointing the other way) once the new markup is there.
+ if (_focusTarget != FocusTarget.None)
+ {
+ var target = _focusTarget;
+ _focusTarget = FocusTarget.None;
+
+ switch (target)
+ {
+ case FocusTarget.SelectedPage:
+ if (_pageRefs.TryGetValue(_SelectedPage, out var pageRef))
+ {
+ await pageRef.FocusAsync();
+ }
+ break;
+ case FocusTarget.First: await _firstButtonRef.FocusAsync(); break;
+ case FocusTarget.Previous: await _previousButtonRef.FocusAsync(); break;
+ case FocusTarget.Next: await _nextButtonRef.FocusAsync(); break;
+ case FocusTarget.Last: await _lastButtonRef.FocusAsync(); break;
+ }
+ }
+
+ await base.OnAfterRenderAsync(firstRender);
+ }
+
+
+
+ // There is always at least one page to be on, so a count that is not positive still renders a pagination
+ // holding that page instead of an empty one.
+ private int _Count => Count > 0 ? Count : 1;
+
+ // The rendering runs off a clamped view of the selected page so that a value outside of the range (which
+ // a one way bound SelectedPage can hold, since the component cannot write it back) still renders a
+ // pagination with a current page and with the right buttons disabled.
+ private int _SelectedPage => Math.Clamp(SelectedPage, 1, _Count);
+
+ // A page size that was never picked falls back to the first of the offered ones, so the selector opens on
+ // a size that is actually one of its options instead of on an empty selection.
+ private int _PageSize => PageSize > 0 ? PageSize : _pageSizeOptions[0];
+
+ private int _MiddleCount => MiddleCount > 0 ? MiddleCount : DefaultMiddleCount;
+
+ private int _BoundaryCount => BoundaryCount > 0 ? BoundaryCount : DefaultBoundaryCount;
+
+ // The first and last buttons always target a fixed page, so they are the ones the loop leaves alone. The
+ // previous and next buttons only stay enabled at the ends of the range while the loop has somewhere else
+ // to take them, which a range holding a single page does not.
+ private bool _IsFirstDisabled => IsEnabled is false || _SelectedPage == 1;
+
+ private bool _IsPreviousDisabled => IsEnabled is false || (Loop ? _Count == 1 : _SelectedPage == 1);
+
+ private bool _IsNextDisabled => IsEnabled is false || (Loop ? _Count == 1 : _SelectedPage == _Count);
+
+ private bool _IsLastDisabled => IsEnabled is false || _SelectedPage == _Count;
+
+ private int _PreviousPage => Loop && _SelectedPage == 1 ? _Count : _SelectedPage - 1;
+
+ private int _NextPage => Loop && _SelectedPage == _Count ? 1 : _SelectedPage + 1;
+
+ // Every control of the pagination turns into a link at once, so the markup a control renders with never
+ // changes under it while the selection moves along the range.
+ private bool _UseLinks => GetPageHref is not null;
+
+ // A control that cannot be navigated to carries no address at all, which is what keeps it out of the tab
+ // order the way a disabled button is.
+ private string? GetHref(int page, bool disabled)
+ {
+ return disabled ? null : GetPageHref?.Invoke(page);
+ }
+
+ private string _VariantClass => Variant switch
+ {
+ BitVariant.Fill => "bit-pgn-fil",
+ BitVariant.Outline => "bit-pgn-otl",
+ BitVariant.Text => "bit-pgn-txt",
+ _ => "bit-pgn-fil"
+ };
+ private string GetPageLabel(int page, bool isSelected)
+ {
+ return GetPageAriaLabel?.Invoke(page, isSelected) ?? $"Page {page}";
+ }
- private IEnumerable GeneratePages()
+ private string GetSummaryText()
{
- if (_count <= 4 || _count <= 2 * _boundaryCount + _middleCount + 2)
+ return GetSummary?.Invoke(_SelectedPage, _Count) ?? $"Page {_SelectedPage} of {_Count}";
+ }
+
+ private int[] GeneratePages()
+ {
+ // The size of the window is worked out in long so that boundary and middle counts big enough to
+ // overflow an int still compare as wider than the count and fall back to spelling every page out.
+ var windowLength = 2L * _BoundaryCount + _MiddleCount + 2;
+
+ if (_Count <= 4 || _Count <= windowLength)
{
- return Enumerable.Range(1, _count).ToArray();
+ return Enumerable.Range(1, _Count).ToArray();
}
- var length = 2 * _boundaryCount + _middleCount + 2;
+ // The window is narrower than the count at this point, so its length fits an int.
+ var length = (int)windowLength;
var pages = new int[length];
- for (var i = 0; i < _boundaryCount; i++)
+ for (var i = 0; i < _BoundaryCount; i++)
{
pages[i] = i + 1;
}
- for (var i = 0; i < _boundaryCount; i++)
+ for (var i = 0; i < _BoundaryCount; i++)
{
- pages[length - i - 1] = _count - i;
+ pages[length - i - 1] = _Count - i;
}
int startValue;
- if (SelectedPage <= _boundaryCount + _middleCount / 2 + 1)
+ if (_SelectedPage <= _BoundaryCount + _MiddleCount / 2 + 1)
{
- startValue = _boundaryCount + 2;
+ startValue = _BoundaryCount + 2;
}
- else if (SelectedPage >= _count - _boundaryCount - _middleCount / 2)
+ else if (_SelectedPage >= _Count - _BoundaryCount - _MiddleCount / 2)
{
- startValue = _count - _boundaryCount - _middleCount;
+ startValue = _Count - _BoundaryCount - _MiddleCount;
}
else
{
- startValue = SelectedPage - _middleCount / 2;
+ startValue = _SelectedPage - _MiddleCount / 2;
}
- for (var i = 0; i < _middleCount; i++)
+ for (var i = 0; i < _MiddleCount; i++)
{
- pages[_boundaryCount + 1 + i] = startValue + i;
+ pages[_BoundaryCount + 1 + i] = startValue + i;
}
- pages[_boundaryCount] = (_boundaryCount + _middleCount / 2 + 1 < SelectedPage) ? -1 : _boundaryCount + 1;
+ pages[_BoundaryCount] = (_BoundaryCount + _MiddleCount / 2 + 1 < _SelectedPage) ? EllipsisPage : _BoundaryCount + 1;
- pages[length - _boundaryCount - 1] = (_count - _boundaryCount - _middleCount / 2 > SelectedPage) ? -1 : _count - _boundaryCount;
+ pages[length - _BoundaryCount - 1] = (_Count - _BoundaryCount - _MiddleCount / 2 > _SelectedPage) ? EllipsisPage : _Count - _BoundaryCount;
+ // An ellipsis standing in for a single page is replaced by that page, since spelling the page out
+ // costs the same room as the ellipsis hiding it.
for (var i = 0; i < length - 2; i++)
{
if (pages[i] + 2 == pages[i + 2])
@@ -254,45 +762,106 @@ private IEnumerable GeneratePages()
private async Task ChangePage(int page)
{
- if (SelectedPageHasBeenSet && SelectedPageChanged.HasDelegate is false) return;
+ if (IsEnabled is false) return;
- if (page > _count) page = _count;
+ // Every requested page lands inside the available range, so neither a wrapping navigation button nor
+ // an out of range SelectedPage can ever select a page that does not exist.
+ page = Math.Clamp(page, 1, _Count);
- if (page < 1) page = 1;
+ if (page == _SelectedPage) return;
await AssignSelectedPage(page);
+ // The callback runs even when SelectedPage is bound one way and could not be written back, so that a
+ // consumer holding the value itself still hears about the page the user asked for.
await OnChange.InvokeAsync(page);
}
- private string GetButtonClasses()
+ private async Task ChangePageFrom(FocusTarget source, int page)
{
- StringBuilder className = new StringBuilder();
+ if (IsEnabled is false) return;
+
+ var target = ResolveFocusTarget(source, Math.Clamp(page, 1, _Count));
- className.Append(' ').Append(Variant switch
+ await ChangePage(page);
+
+ // The focus only moves once the page actually changed, so a click that lands on the page already
+ // selected leaves it where the user put it.
+ if (target != FocusTarget.None && _SelectedPage == Math.Clamp(page, 1, _Count))
{
- BitVariant.Fill => "bit-pgn-fil",
- BitVariant.Outline => "bit-pgn-otl",
- BitVariant.Text => "bit-pgn-txt",
- _ => "bit-pgn-fil"
- });
+ _focusTarget = target;
+ }
+ }
+
+ // A navigation button is removed from the tab order the moment the page it moved to disables it, and the
+ // focus it was holding goes with it. The page the pagination settles on is the one the focus belongs to,
+ // and the navigation button pointing the other way is what is left to hold it once the page buttons are
+ // turned off.
+ private FocusTarget ResolveFocusTarget(FocusTarget source, int page)
+ {
+ var stillEnabled = source switch
+ {
+ FocusTarget.First => page > 1,
+ FocusTarget.Previous => Loop ? _Count > 1 : page > 1,
+ FocusTarget.Next => Loop ? _Count > 1 : page < _Count,
+ FocusTarget.Last => page < _Count,
+ _ => true
+ };
+
+ if (stillEnabled) return FocusTarget.None;
- return className.ToString();
+ if (ShowPageButtons) return FocusTarget.SelectedPage;
+
+ var goingBack = source is FocusTarget.First or FocusTarget.Previous;
+
+ if (goingBack)
+ {
+ if (ShowNextButton && page < _Count) return FocusTarget.Next;
+ if (ShowLastButton && page < _Count) return FocusTarget.Last;
+ }
+ else
+ {
+ if (ShowPreviousButton && page > 1) return FocusTarget.Previous;
+ if (ShowFirstButton && page > 1) return FocusTarget.First;
+ }
+
+ return FocusTarget.None;
}
- private void OnSetBoundaryCount()
+ private async Task HandlePageSizeChange(ChangeEventArgs e)
{
- _boundaryCount = Math.Max(1, BoundaryCount);
+ if (IsEnabled is false) return;
+
+ if (int.TryParse(e.Value?.ToString(), NumberStyles.Integer, CultureInfo.InvariantCulture, out var size) is false) return;
+
+ if (size == _PageSize) return;
+
+ await AssignPageSize(size);
+
+ // The callback runs even when PageSize is bound one way and could not be written back, so that a
+ // consumer holding the value itself still hears about the size the user asked for.
+ await OnPageSizeChange.InvokeAsync(size);
}
- private void OnSetCount()
+ private void HandleGoToPageInput(ChangeEventArgs e)
{
- _count = Math.Max(1, Count);
- _ = AssignSelectedPage(Math.Min(SelectedPage, Count));
+ _goToPageText = e.Value?.ToString();
}
- private void OnSetMiddleCount()
+ private async Task HandleGoToPageChange(ChangeEventArgs e)
{
- _middleCount = Math.Max(1, MiddleCount);
+ _goToPageText = e.Value?.ToString();
+
+ // The input clears itself so that the next jump starts from an empty field instead of from the number
+ // the previous one left behind. Assigning the text first and clearing it after keeps the two values
+ // different, which is what makes the rendered input follow.
+ var text = _goToPageText;
+ _goToPageText = string.Empty;
+
+ if (int.TryParse(text, NumberStyles.Integer, CultureInfo.InvariantCulture, out var page) is false) return;
+
+ // A number outside of the range lands on the nearest end of it, since a jump past the last page is a
+ // request for the last page and not a typing mistake worth dropping.
+ await ChangePage(page);
}
}
diff --git a/src/BlazorUI/Bit.BlazorUI/Components/Navs/Pagination/BitPagination.scss b/src/BlazorUI/Bit.BlazorUI/Components/Navs/Pagination/BitPagination.scss
index 4b321c892c3..fbf97d13a53 100644
--- a/src/BlazorUI/Bit.BlazorUI/Components/Navs/Pagination/BitPagination.scss
+++ b/src/BlazorUI/Bit.BlazorUI/Components/Navs/Pagination/BitPagination.scss
@@ -39,6 +39,15 @@
}
}
+// The list and its items only carry the semantics of the markup: they are removed from the box tree so the
+// buttons stay the direct flex items of the root and the layout of the pagination is the one the root sets
+// (a gap coming from Styles.Root included). The explicit list roles in the markup keep the semantics that
+// removing these boxes used to drop.
+.bit-pgn-lst,
+.bit-pgn-itm {
+ display: contents;
+}
+
.bit-pgn-trs {
transform: var(--bit-pgn-ico-transform-start);
}
@@ -52,18 +61,89 @@
font-size: inherit;
text-align: center;
align-items: center;
+ user-select: none;
justify-content: center;
width: var(--bit-pgn-btn-size);
height: var(--bit-pgn-btn-size);
border-radius: $shp-radius-control;
}
+// The summary sits on the same baseline as the buttons without taking their square shape, so a long text
+// (a localized one, or one counting items instead of pages) stays on a single line beside them.
+.bit-pgn-sum {
+ display: flex;
+ font-size: inherit;
+ align-items: center;
+ white-space: nowrap;
+ padding: 0 spacing(0.5);
+ height: var(--bit-pgn-btn-size);
+}
+
+// The jump sits on the same baseline as the buttons. Its input keeps the neutral surface of a text input
+// rather than the role color of the buttons, so the digits typed into it stay readable whichever color and
+// variant the pagination is rendered with.
+.bit-pgn-gtp,
+.bit-pgn-pss {
+ display: flex;
+ font-size: inherit;
+ align-items: center;
+ white-space: nowrap;
+ gap: spacing(0.5);
+ padding: 0 spacing(0.5);
+ height: var(--bit-pgn-btn-size);
+}
+
+.bit-pgn-gti,
+.bit-pgn-pse {
+ font: inherit;
+ color: $clr-fg-pri;
+ background: $clr-bg-sec;
+ padding: 0 spacing(0.5);
+ height: var(--bit-pgn-btn-size);
+ border-style: $shp-border-style;
+ border-width: $shp-border-width;
+ border-color: $clr-brd-sec;
+ border-radius: $shp-radius-control;
+
+ &:focus-visible {
+ z-index: 1;
+ @include focus-ring(var(--bit-pgn-clr-fcs, #{$clr-pri-focus}));
+ }
+
+ &[disabled] {
+ cursor: default;
+ pointer-events: none;
+ color: $clr-fg-dis;
+ background: $clr-bg-dis;
+ border-color: $clr-brd-dis;
+ }
+}
+
+// The spin buttons are dropped: the field is a jump to a page and not a value to step through, and they
+// would leave next to no room for the digits at the width the pagination gives it.
+.bit-pgn-gti {
+ text-align: center;
+ appearance: textfield;
+ width: calc(var(--bit-pgn-btn-size) * 1.75);
+
+ &::-webkit-outer-spin-button,
+ &::-webkit-inner-spin-button {
+ margin: 0;
+ appearance: none;
+ }
+}
+
+.bit-pgn-pse {
+ cursor: pointer;
+}
+
.bit-pgn-btn {
cursor: pointer;
text-align: center;
font-size: inherit;
align-items: center;
display: inline-flex;
+ text-decoration: none;
justify-content: center;
width: var(--bit-pgn-btn-size);
height: var(--bit-pgn-btn-size);
@@ -93,7 +173,10 @@
@include focus-ring(var(--bit-pgn-clr-fcs, #{$clr-pri-focus}));
}
- &[disabled] {
+ // A link has no disabled attribute of its own, so a page control it cannot reach reports aria-disabled
+ // and is painted (and made inert) by the same rule.
+ &[disabled],
+ &[aria-disabled="true"] {
cursor: default;
pointer-events: none;
color: var(--bit-pgn-clr-btn-txt-dis);
@@ -102,6 +185,15 @@
}
}
+// A navigation button carrying a text is no longer a square: it keeps the height of the others and grows
+// along the text, with the icon and the text sharing the room the way a labelled button does.
+.bit-pgn-lbl {
+ width: auto;
+ gap: spacing(0.5);
+ min-width: var(--bit-pgn-btn-size);
+ padding: 0 spacing(1);
+}
+
.bit-pgn-sel {
color: var(--bit-pgn-clr-btn-sel-txt);
border-color: var(--bit-pgn-clr-btn-sel-brd);
@@ -123,6 +215,22 @@
}
+// The pill shape applies to the ellipsis as well, so the gaps keep the outline of the buttons they stand
+// in for instead of breaking the row with a square.
+.bit-pgn-rnd {
+ .bit-pgn-btn,
+ .bit-pgn-elp {
+ border-radius: 50%;
+ }
+
+ // A navigation button carrying a text is wider than it is tall, where a 50% radius would draw an
+ // ellipse instead of the pill the round shape asks for.
+ .bit-pgn-lbl {
+ border-radius: calc(var(--bit-pgn-btn-size) / 2);
+ }
+}
+
+
// forced-colors: the selected page is conveyed by fill colors that High Contrast strips; repaint
// it with the system selection colors so it stays distinguishable from the other page buttons.
@media (forced-colors: active) {
diff --git a/src/BlazorUI/Bit.BlazorUI/Components/Navs/Pagination/BitPaginationClassStyles.cs b/src/BlazorUI/Bit.BlazorUI/Components/Navs/Pagination/BitPaginationClassStyles.cs
index c9b41e1badf..cd8e5697ebf 100644
--- a/src/BlazorUI/Bit.BlazorUI/Components/Navs/Pagination/BitPaginationClassStyles.cs
+++ b/src/BlazorUI/Bit.BlazorUI/Components/Navs/Pagination/BitPaginationClassStyles.cs
@@ -7,6 +7,41 @@ public class BitPaginationClassStyles
///
public string? Root { get; set; }
+ ///
+ /// Custom CSS classes/styles for the page size selector container of the BitPagination.
+ ///
+ public string? PageSizeSelector { get; set; }
+
+ ///
+ /// Custom CSS classes/styles for the page size label of the BitPagination.
+ ///
+ public string? PageSizeLabel { get; set; }
+
+ ///
+ /// Custom CSS classes/styles for the page size select of the BitPagination.
+ ///
+ public string? PageSizeSelect { get; set; }
+
+ ///
+ /// Custom CSS classes/styles for the summary of the BitPagination.
+ ///
+ public string? Summary { get; set; }
+
+ ///
+ /// Custom CSS classes/styles for the go to page container of the BitPagination.
+ ///
+ public string? GoToPage { get; set; }
+
+ ///
+ /// Custom CSS classes/styles for the go to page label of the BitPagination.
+ ///
+ public string? GoToPageLabel { get; set; }
+
+ ///
+ /// Custom CSS classes/styles for the go to page input of the BitPagination.
+ ///
+ public string? GoToPageInput { get; set; }
+
///
/// Custom CSS classes/styles for the button of the BitPagination.
///
diff --git a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Navs/Pagination/BitPaginationDemo.razor b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Navs/Pagination/BitPaginationDemo.razor
index 8bc54d320d8..c4268101e16 100644
--- a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Navs/Pagination/BitPaginationDemo.razor
+++ b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Navs/Pagination/BitPaginationDemo.razor
@@ -5,13 +5,15 @@
Description="Pagination component helps users easily navigate through content, allowing swift browsing across multiple pages or sections." />
+
The Count parameter is the total number of pages, and it is all the pagination needs to render. The first page is selected by default and the previous and next buttons are shown, while the buttons that cannot go anywhere are disabled.
+
@@ -29,39 +31,248 @@
-
Displays default selected page within the BitPagination component.
+
DefaultSelectedPage is the page the pagination starts on while it keeps the selection itself. Use it for an uncontrolled pagination; to drive the selection from your own state, bind SelectedPage instead.
-
Set a limit to the number of pages shown at the beginning and end of the pagination range.
+
BoundaryCount is how many pages stay visible at each end of the range, no matter which page is selected, so the first and the last pages are always one click away. The default is 2, and a value that is not positive falls back to it.
+
MiddleCount is how many pages are rendered around the selected one, between the two ellipses. The default is 3, and a value that is not positive falls back to it. An ellipsis that would hide a single page is replaced by that page, since it takes the same room.
+
+
+
+
+
+
+
- Allowing users to set the count of pages displayed in the middle portion of the pagination control.
- It also demonstrates how pages are rendered within the defined middle count range.
+ EllipsisText replaces the glyph standing in for the pages that are collapsed out of the range, and EllipsisAriaLabel the name it is announced with.
+ The glyph itself is hidden from assistive technologies, so the gap is reported as one named item instead of being read out as a run of punctuation.
-
+
+
+
-
-
There are additional navigation buttons within the Pagination component, providing quick access to the initial and final pages.
+
+
+ Beside the previous and next buttons, which are shown by default, ShowFirstButton and ShowLastButton add the jumps to the two ends of the range.
+ Each of the four can be turned off on its own through ShowPreviousButton and ShowNextButton.
+
+
+
Without the previous and next buttons:
+
+
+
+
+
+
+ FirstButtonText, PreviousButtonText, NextButtonText and LastButtonText put a text beside the icon of a navigation button, which widens to fit it.
+ The spoken name still comes from the matching AriaLabel parameter, so a short visible text can sit next to a fuller one for a screen reader.
+
+
+
+
+
+
+
+
+
+ ShowPageButtons turns the numeric page buttons off and leaves only the navigation buttons, which is the compact pagination a narrow layout calls for,
+ or an unbounded result set where the number of pages is not worth spelling out.
+
+
+
+
+
+
+
+
+
+ ShowSummary puts the position in the range ahead of the buttons, reading "Page {number} of {count}", and GetSummary replaces that text from the selected page and the total number of pages,
+ which is the hook to localize it or to count the items rather than the pages.
+
+
+ The summary is a status region, so a screen reader reports the new position as the page changes. That makes it the piece to pair with ShowPageButtons turned off,
+ where nothing else says which page of how many is the current one.
+
+
+
+
+
+
+
+
+
+
+
+ ShowPageSizeSelector opens the pagination with a selector of how many items a page holds, picked out of PageSizeOptions (10, 25, 50 and 100 by default).
+ Picking a size reports it through the two-way bound PageSize and through OnPageSizeChange, and changes nothing else: the number of pages the new size adds up to is yours to recompute into Count.
+ A selected page that falls out of the shrunk range is pulled back on its own.
+
+
+ PageSizeText is the visible text ahead of the selector and can be dropped, since PageSizeAriaLabel names it on its own.
+
+
+
+
+
Page size: @selectedPageSize, page: @pageSizeSelectedPage of @pageSizeCount
+
+
+
+
+ ShowGoToPage closes the pagination with an input that jumps straight to the page number typed into it, which is the shortcut a long range needs.
+ The jump runs when the input is committed (on Enter, or when it loses the focus) and a number past the ends of the range lands on the nearest one.
+
+
+ GoToPageText is the visible text ahead of the input and can be dropped, since GoToPageAriaLabel names the input on its own.
+
+
+
+
+
+
+
+
+
+ Loop wraps the previous and next buttons around the ends of the range: the next button moves from the last page back to the first one and the previous button from the first page to the last one.
+ Both buttons also stay enabled at the ends while it is on, unless the range holds a single page and there is nowhere to wrap to. The first and last buttons are unaffected, since they always target a fixed page.
+
+
+
-
-
Displaying custom icons feature within the Pagination navigation buttons.
+
+
HideOnSinglePage renders nothing at all while there is a single page to navigate, so a short result set is not left with navigation that cannot go anywhere.
-
+
+
(nothing is rendered above for a single page)
+
+
+
+
+
+
Each navigation button takes its own built-in icon through the IconName parameters, replacing the default chevrons.
+
+
+
+
+
+
Rounded renders the buttons (and the ellipses standing between them) as circles instead of the default rounded rectangles.
+
+
+
+
+
+
+
+
+
+
+ With @(nameof(BitPagination.SelectedPage)) bound one way the pagination only renders the page you hand it and never changes it on its own, so the clicks reach you through OnChange and it is up to you to apply them.
+ Binding it two way lets the pagination keep the value in sync by itself. OnChange runs in both cases, and only for a page the user actually asked for.
+
+
+
+
One-way:
+
+
+
+
+
+
Two-way:
+
+
+
+
+
+
OnChange:
+
+
+
+
Changed page: @onChangeSelectedPage
+
-
+
+
+ GetPageHref gives every control the address of the page it points at, which turns the whole pagination into links instead of buttons, so the range can be crawled, opened in another tab, or bookmarked.
+ The four navigation controls ask for the address of the page they move to, so they turn into links along with the numeric ones.
+
+
+ A control with no page to reach — one at the end of the range it navigates, one the pagination is disabled for, or one GetPageHref returns nothing for — keeps its place while reporting aria-disabled and staying out of the tab order.
+ The click still reaches OnChange and SelectedPage, so a pagination of links reports the page that was asked for exactly like a pagination of buttons.
+
+
+
+
+
Selected page: @linkSelectedPage
+
+
+
+
+ The pagination renders as a navigation landmark holding a list of page buttons, and marks the selected page with aria-current="page".
+ Give it a name through AriaLabel whenever more than one pagination shares a page, so each of them can be told apart in the landmark list.
+
+
+ The navigation buttons carry no text, so each of them takes its accessible name (used as its tooltip as well) from
+ FirstButtonAriaLabel, PreviousButtonAriaLabel, NextButtonAriaLabel and LastButtonAriaLabel.
+ GetPageAriaLabel replaces the default "Page {number}" label of a page button from its number and whether it is the selected one, which is the hook to localize them or to say what the page holds.
+ EllipsisAriaLabel and GoToPageAriaLabel name the collapsed range and the jump input the same way.
+
+
+ A navigation button that reaches the end of the range it points at disables itself, which would drop the keyboard focus on the document. The focus is handed over to the page the pagination settled on instead,
+ or to the navigation button pointing the other way while the page buttons are turned off, so a keyboard walk through the range never loses its place.
+
+
+
+
+
+
Offering a range of specialized color variants, providing visual cues for specific actions or states within your application.
Primary
@@ -179,32 +390,7 @@
-
-
Varying sizes for paginations tailored to meet diverse design needs, ensuring flexibility and visual hierarchy within your interface.
-
-
Small
-
-
-
-
-
-
-
Medium
-
-
-
-
-
-
-
Large
-
-
-
-
-
-
-
-
+
Use icons from external libraries like FontAwesome and Bootstrap Icons with the navigation button Icon parameters.
See the BitIconInfo section in the parameters table for usage.
Varying sizes for paginations tailored to meet diverse design needs, ensuring flexibility and visual hierarchy within your interface.
+
+
Small
+
+
+
+
+
+
+
Medium
+
+
+
+
+
+
+
Large
+
+
+
+
+
+
+
+
Empower customization by overriding default styles and classes, allowing tailored design modifications to suit specific UI requirements.
@@ -279,35 +490,11 @@
-
-
Examples of one-way and two-way data binding with BitPagination.
-
-
-
One-way:
-
-
-
-
-
-
Two-way:
-
-
-
-
-
-
OnChange:
-
-
-
-
Changed page: @onChangeSelectedPage
-
-
-
-
-
Use BitPagination in right-to-left (RTL).
+
+
Use BitPagination in right-to-left (RTL). The chevrons of the navigation buttons are mirrored along with the layout, so the next button keeps pointing at the direction the reading goes.
-
+
diff --git a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Navs/Pagination/BitPaginationDemo.razor.cs b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Navs/Pagination/BitPaginationDemo.razor.cs
index 4ae8f7cea39..5a90ec9ce18 100644
--- a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Navs/Pagination/BitPaginationDemo.razor.cs
+++ b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Navs/Pagination/BitPaginationDemo.razor.cs
@@ -9,7 +9,7 @@ public partial class BitPaginationDemo
Name = "BoundaryCount",
Type = "int",
DefaultValue = "2",
- Description = "The number of items at the start and end of the pagination."
+ Description = "The number of items at the start and end of the pagination. A value that is not positive falls back to the default."
},
new()
{
@@ -44,6 +44,27 @@ public partial class BitPaginationDemo
Description = "The default selected page number."
},
new()
+ {
+ Name = "EllipsisAriaLabel",
+ Type = "string",
+ DefaultValue = "\"More pages\"",
+ Description = "The accessible label of the item standing in for the pages an ellipsis collapses. The glyph itself is hidden from assistive technologies and this label is announced in its place."
+ },
+ new()
+ {
+ Name = "EllipsisText",
+ Type = "string",
+ DefaultValue = "\"•••\"",
+ Description = "The text of the ellipsis standing in for the pages that are collapsed out of the range."
+ },
+ new()
+ {
+ Name = "FirstButtonAriaLabel",
+ Type = "string",
+ DefaultValue = "\"First page\"",
+ Description = "The accessible label of the first button, which is used as its tooltip as well."
+ },
+ new()
{
Name = "FirstButtonIcon",
Type = "BitIconInfo?",
@@ -62,6 +83,62 @@ public partial class BitPaginationDemo
Href = "https://blazorui.bitplatform.dev/iconography",
},
new()
+ {
+ Name = "FirstButtonText",
+ Type = "string?",
+ DefaultValue = "null",
+ Description = "The text rendered beside the icon of the first button, which widens to fit it. The accessible name still comes from FirstButtonAriaLabel."
+ },
+ new()
+ {
+ Name = "GetPageAriaLabel",
+ Type = "Func?",
+ DefaultValue = "null",
+ Description = "Provides the accessible label of a page button, from its one-based number and whether it is the selected one, replacing the default \"Page {number}\" label."
+ },
+ new()
+ {
+ Name = "GetPageHref",
+ Type = "Func?",
+ DefaultValue = "null",
+ Description = "Provides the address a page control points at, from its one-based number, which turns every control of the pagination into a link instead of a button. A control with no address to point at reports aria-disabled and stays out of the tab order."
+ },
+ new()
+ {
+ Name = "GetSummary",
+ Type = "Func?",
+ DefaultValue = "null",
+ Description = "Provides the text of the summary, from the selected page and the total number of pages, replacing the default \"Page {number} of {count}\" text."
+ },
+ new()
+ {
+ Name = "GoToPageAriaLabel",
+ Type = "string",
+ DefaultValue = "\"Go to page\"",
+ Description = "The accessible label of the go to page input, which names it on its own so the visible GoToPageText beside it can be dropped."
+ },
+ new()
+ {
+ Name = "GoToPageText",
+ Type = "string?",
+ DefaultValue = "\"Go to\"",
+ Description = "The text rendered ahead of the go to page input. An empty text leaves the input on its own."
+ },
+ new()
+ {
+ Name = "HideOnSinglePage",
+ Type = "bool",
+ DefaultValue = "false",
+ Description = "Renders nothing at all while there is a single page to navigate."
+ },
+ new()
+ {
+ Name = "LastButtonAriaLabel",
+ Type = "string",
+ DefaultValue = "\"Last page\"",
+ Description = "The accessible label of the last button, which is used as its tooltip as well."
+ },
+ new()
{
Name = "LastButtonIcon",
Type = "BitIconInfo?",
@@ -80,11 +157,32 @@ public partial class BitPaginationDemo
Href = "https://blazorui.bitplatform.dev/iconography",
},
new()
+ {
+ Name = "LastButtonText",
+ Type = "string?",
+ DefaultValue = "null",
+ Description = "The text rendered beside the icon of the last button, which widens to fit it. The accessible name still comes from LastButtonAriaLabel."
+ },
+ new()
+ {
+ Name = "Loop",
+ Type = "bool",
+ DefaultValue = "false",
+ Description = "Wraps the next and previous buttons around the ends of the range, and keeps them enabled there."
+ },
+ new()
{
Name = "MiddleCount",
Type = "int",
DefaultValue = "3",
- Description = "The number of items to render in the middle of the pagination."
+ Description = "The number of items to render in the middle of the pagination. A value that is not positive falls back to the default."
+ },
+ new()
+ {
+ Name = "NextButtonAriaLabel",
+ Type = "string",
+ DefaultValue = "\"Next page\"",
+ Description = "The accessible label of the next button, which is used as its tooltip as well."
},
new()
{
@@ -105,11 +203,60 @@ public partial class BitPaginationDemo
Href = "https://blazorui.bitplatform.dev/iconography",
},
new()
+ {
+ Name = "NextButtonText",
+ Type = "string?",
+ DefaultValue = "null",
+ Description = "The text rendered beside the icon of the next button, which widens to fit it. The accessible name still comes from NextButtonAriaLabel."
+ },
+ new()
{
Name = "OnChange",
Type = "EventCallback",
DefaultValue = "null",
- Description = "The event callback for when selected page changes."
+ Description = "The event callback for when selected page changes. It also runs when SelectedPage is bound one way."
+ },
+ new()
+ {
+ Name = "OnPageSizeChange",
+ Type = "EventCallback",
+ DefaultValue = "null",
+ Description = "The event callback for when the page size is picked out of the page size selector. It also runs when PageSize is bound one way, and it is where Count is recomputed from the new page size."
+ },
+ new()
+ {
+ Name = "PageSize",
+ Type = "int",
+ DefaultValue = "0",
+ Description = "The number of items a page holds, which the page size selector picks. A value that is not positive falls back to the first of the PageSizeOptions."
+ },
+ new()
+ {
+ Name = "PageSizeAriaLabel",
+ Type = "string",
+ DefaultValue = "\"Items per page\"",
+ Description = "The accessible label of the page size selector, which names it on its own so the visible PageSizeText beside it can be dropped."
+ },
+ new()
+ {
+ Name = "PageSizeOptions",
+ Type = "IEnumerable?",
+ DefaultValue = "null",
+ Description = "The page sizes the page size selector offers. An empty list falls back to the default 10, 25, 50 and 100."
+ },
+ new()
+ {
+ Name = "PageSizeText",
+ Type = "string?",
+ DefaultValue = "\"Items per page\"",
+ Description = "The text rendered ahead of the page size selector. An empty text leaves the selector on its own."
+ },
+ new()
+ {
+ Name = "PreviousButtonAriaLabel",
+ Type = "string",
+ DefaultValue = "\"Previous page\"",
+ Description = "The accessible label of the previous button, which is used as its tooltip as well."
},
new()
{
@@ -130,11 +277,25 @@ public partial class BitPaginationDemo
Href = "https://blazorui.bitplatform.dev/iconography",
},
new()
+ {
+ Name = "PreviousButtonText",
+ Type = "string?",
+ DefaultValue = "null",
+ Description = "The text rendered beside the icon of the previous button, which widens to fit it. The accessible name still comes from PreviousButtonAriaLabel."
+ },
+ new()
+ {
+ Name = "Rounded",
+ Type = "bool",
+ DefaultValue = "false",
+ Description = "Renders the buttons of the pagination with fully rounded (circular) corners."
+ },
+ new()
{
Name = "SelectedPage",
Type = "int",
DefaultValue = "0",
- Description = "The selected page number."
+ Description = "The selected page number. It is one-based and is clamped into the available range while rendering."
},
new()
{
@@ -144,6 +305,13 @@ public partial class BitPaginationDemo
Description = "Determines whether to show the first button."
},
new()
+ {
+ Name = "ShowGoToPage",
+ Type = "bool",
+ DefaultValue = "false",
+ Description = "Shows an input that jumps straight to the page number typed into it, at the end of the pagination. The jump runs when the input is committed and a number outside of the range lands on the nearest end of it."
+ },
+ new()
{
Name = "ShowLastButton",
Type = "bool",
@@ -158,6 +326,20 @@ public partial class BitPaginationDemo
Description = "Determines whether to show the next button."
},
new()
+ {
+ Name = "ShowPageButtons",
+ Type = "bool",
+ DefaultValue = "true",
+ Description = "Determines whether to show the numeric page buttons. Turning them off leaves a compact pagination made of the navigation buttons only."
+ },
+ new()
+ {
+ Name = "ShowPageSizeSelector",
+ Type = "bool",
+ DefaultValue = "false",
+ Description = "Shows a selector that picks how many items a page holds, ahead of everything else in the pagination. Picking a size reports it through PageSize and OnPageSizeChange and changes nothing else."
+ },
+ new()
{
Name = "ShowPreviousButton",
Type = "bool",
@@ -165,6 +347,13 @@ public partial class BitPaginationDemo
Description = "Determines whether to show the previous button."
},
new()
+ {
+ Name = "ShowSummary",
+ Type = "bool",
+ DefaultValue = "false",
+ Description = "Shows the position in the range, which reads \"Page {number} of {count}\" unless GetSummary replaces it, ahead of the buttons of the pagination. It is a status region, so a screen reader reports the new position as the page changes."
+ },
+ new()
{
Name = "Size",
Type = "BitSize?",
@@ -353,6 +542,55 @@ public partial class BitPaginationDemo
Description = "Custom CSS classes/styles for the root element of the BitPagination."
},
new()
+ {
+ Name = "PageSizeSelector",
+ Type = "string?",
+ DefaultValue = "null",
+ Description = "Custom CSS classes/styles for the page size selector container of the BitPagination."
+ },
+ new()
+ {
+ Name = "PageSizeLabel",
+ Type = "string?",
+ DefaultValue = "null",
+ Description = "Custom CSS classes/styles for the page size label of the BitPagination."
+ },
+ new()
+ {
+ Name = "PageSizeSelect",
+ Type = "string?",
+ DefaultValue = "null",
+ Description = "Custom CSS classes/styles for the page size select of the BitPagination."
+ },
+ new()
+ {
+ Name = "Summary",
+ Type = "string?",
+ DefaultValue = "null",
+ Description = "Custom CSS classes/styles for the summary of the BitPagination."
+ },
+ new()
+ {
+ Name = "GoToPage",
+ Type = "string?",
+ DefaultValue = "null",
+ Description = "Custom CSS classes/styles for the go to page container of the BitPagination."
+ },
+ new()
+ {
+ Name = "GoToPageLabel",
+ Type = "string?",
+ DefaultValue = "null",
+ Description = "Custom CSS classes/styles for the go to page label of the BitPagination."
+ },
+ new()
+ {
+ Name = "GoToPageInput",
+ Type = "string?",
+ DefaultValue = "null",
+ Description = "Custom CSS classes/styles for the go to page input of the BitPagination."
+ },
+ new()
{
Name = "Button",
Type = "string?",
@@ -435,7 +673,34 @@ public partial class BitPaginationDemo
+ private const int totalItems = 240;
+ private int selectedPageSize = 10;
+ private int pageSizeSelectedPage = 1;
+ private int pageSizeCount => (int)Math.Ceiling(totalItems / (double)selectedPageSize);
+
+ private string GetPageSizeSummary(int page, int count)
+ {
+ return $"Showing {(page - 1) * selectedPageSize + 1} to {Math.Min(page * selectedPageSize, totalItems)} of {totalItems}";
+ }
+
+ private int linkSelectedPage = 1;
+
+ private string GetDemoPageHref(int page)
+ {
+ return $"#example18-page-{page}";
+ }
+
private int oneWaySelectedPage = 1;
private int twoWaySelectedPage = 2;
private int onChangeSelectedPage = 3;
+
+ private string GetResultsRangeLabel(int page, bool isSelected)
+ {
+ return $"Results {(page - 1) * 10 + 1} to {page * 10}";
+ }
+
+ private string GetItemsRangeSummary(int page, int count)
+ {
+ return $"Showing {(page - 1) * 10 + 1} to {page * 10} of {count * 10} results";
+ }
}
diff --git a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Navs/Pagination/BitPaginationDemo.razor.samples.cs b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Navs/Pagination/BitPaginationDemo.razor.samples.cs
index 2dd5e521689..a487f1e7a82 100644
--- a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Navs/Pagination/BitPaginationDemo.razor.samples.cs
+++ b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Navs/Pagination/BitPaginationDemo.razor.samples.cs
@@ -17,15 +17,150 @@ public partial class BitPaginationDemo
";
private readonly string example5RazorCode = @"
-";
+
+
+";
private readonly string example6RazorCode = @"
-";
+
+
+";
private readonly string example7RazorCode = @"
-";
+
+
+";
private readonly string example8RazorCode = @"
+
+
+";
+
+ private readonly string example9RazorCode = @"
+
+
+";
+
+ private readonly string example10RazorCode = @"
+
+
+
+
+";
+ private readonly string example10CsharpCode = @"
+private string GetItemsRangeSummary(int page, int count)
+{
+ return $""Showing {(page - 1) * 10 + 1} to {page * 10} of {count * 10} results"";
+}";
+
+ private readonly string example11RazorCode = @"
+
+
+
Page size: @selectedPageSize, page: @pageSizeSelectedPage of @pageSizeCount