diff --git a/packages/web-components/src/components.d.ts b/packages/web-components/src/components.d.ts index 93afe23f5..f2cee005e 100644 --- a/packages/web-components/src/components.d.ts +++ b/packages/web-components/src/components.d.ts @@ -778,6 +778,14 @@ export namespace Components { * Display last page number when the page count exceeds `maxPageListLength` */ "showLastPage"?: boolean; + /** + * Don't show last page when the page count exceeds `maxPageListLength` (but do show the ellipsis). Only relevant if uswds is true. + */ + "unbounded"?: boolean; + /** + * Whether or not the component will use USWDS v3 styling. + */ + "uswds"?: boolean; } interface VaPrivacyAgreement { /** @@ -2581,6 +2589,14 @@ declare namespace LocalJSX { * Display last page number when the page count exceeds `maxPageListLength` */ "showLastPage"?: boolean; + /** + * Don't show last page when the page count exceeds `maxPageListLength` (but do show the ellipsis). Only relevant if uswds is true. + */ + "unbounded"?: boolean; + /** + * Whether or not the component will use USWDS v3 styling. + */ + "uswds"?: boolean; } interface VaPrivacyAgreement { /** diff --git a/packages/web-components/src/components/va-number-input/va-number-input.tsx b/packages/web-components/src/components/va-number-input/va-number-input.tsx index d995e1b09..25bcc0162 100644 --- a/packages/web-components/src/components/va-number-input/va-number-input.tsx +++ b/packages/web-components/src/components/va-number-input/va-number-input.tsx @@ -15,6 +15,7 @@ import i18next from 'i18next'; /** * @nativeHandler onInput * @nativeHandler onBlur + * @nativeHandler onWheel * @componentName Number input * @maturityCategory use * @maturityLevel deployed @@ -133,6 +134,12 @@ export class VaNumberInput { } }; + // prevent input value from changing if user accidentally scrolls while focused on input + private handleWheel = (e: Event) => { + e.preventDefault(); + } + + connectedCallback() { i18next.on('languageChanged', () => { forceUpdate(this.el); @@ -158,6 +165,7 @@ export class VaNumberInput { uswds, handleBlur, handleInput, + handleWheel, width, messageAriaDescribedby, } = this; @@ -211,6 +219,7 @@ export class VaNumberInput { required={required || null} onInput={handleInput} onBlur={handleBlur} + onWheel={handleWheel} /> {messageAriaDescribedby && ( @@ -256,6 +265,7 @@ export class VaNumberInput { required={required || null} onInput={handleInput} onBlur={handleBlur} + onWheel={handleWheel} /> {messageAriaDescribedby && ( diff --git a/packages/web-components/src/components/va-pagination/va-pagination.css b/packages/web-components/src/components/va-pagination/va-pagination.scss similarity index 91% rename from packages/web-components/src/components/va-pagination/va-pagination.css rename to packages/web-components/src/components/va-pagination/va-pagination.scss index 213319d9f..5eb195778 100644 --- a/packages/web-components/src/components/va-pagination/va-pagination.css +++ b/packages/web-components/src/components/va-pagination/va-pagination.scss @@ -1,5 +1,13 @@ +@forward 'settings'; +@use 'usa-pagination/src/styles/_usa-pagination.scss'; +@use 'usa-icon/src/styles/_usa-icon.scss'; + @import '../../mixins/focusable.css'; +:host([uswds]) .usa-pagination .usa-current { + background-color: var(--color-primary); +} + :host { background-color: var(--color-white); border-top: 1px solid var(--color-gray-lightest); diff --git a/packages/web-components/src/components/va-pagination/va-pagination.tsx b/packages/web-components/src/components/va-pagination/va-pagination.tsx index f0f1724e8..b2fd52bda 100644 --- a/packages/web-components/src/components/va-pagination/va-pagination.tsx +++ b/packages/web-components/src/components/va-pagination/va-pagination.tsx @@ -7,6 +7,7 @@ import { h, Prop, Element, + getAssetPath } from '@stencil/core'; import classnames from 'classnames'; @@ -18,7 +19,7 @@ import classnames from 'classnames'; @Component({ tag: 'va-pagination', - styleUrl: 'va-pagination.css', + styleUrl: 'va-pagination.scss', shadow: true, }) export class VaPagination { @@ -70,6 +71,25 @@ export class VaPagination { */ @Prop() showLastPage?: boolean = false; + /** + * Don't show last page when the page count exceeds + * `maxPageListLength` (but do show the ellipsis). + * Only relevant if uswds is true. + */ + + @Prop() unbounded?: boolean = false; + + /** + * If the page total is less than or equal to this limit, show them all. + * Only relevant for uswds. + */ + SHOW_ALL_PAGES: number = 7; + + /** + * Whether or not the component will use USWDS v3 styling. + */ + @Prop() uswds?: boolean = false; + private handlePageSelect = (page, eventID) => { this.pageSelect.emit({ page }); @@ -89,6 +109,68 @@ export class VaPagination { } }; + private makeArray(start, end) { + return Array.from({ length: end - start + 1 }, (_, i) => start + i); + } + + /** + * Generate a list of the continuous pages numbers to render, i.e. + * the page numbers to render not including the first/last page + */ + private pageNumbersUswds = () => { + const { + maxPageListLength, + page: currentPage, + pages: totalPages, + unbounded + } = this; + + const radius = Math.floor(maxPageListLength / 2); + + //if the unbounded flag is set we don't include the last page + const unboundedChar = unbounded ? 0 : 1; + + let start; + let end; + + if (totalPages <= this.SHOW_ALL_PAGES) { + return this.makeArray(1, totalPages); + } + + // continuous pages start at 1 + if ( currentPage <= radius + 1 ) { + start = 1; + end = maxPageListLength >= totalPages + ? totalPages + : maxPageListLength - 1 - unboundedChar; + return this.makeArray(start, end); + } + + // continuous pages end at last page + if (currentPage + radius >= totalPages) { + end = totalPages; + start = totalPages - maxPageListLength > 0 + //subtract 2 to account for having to add ellipsis and first page + ? totalPages - (maxPageListLength - 2 - 1) + : 1; + return this.makeArray(start, end); + + // continuous pages don't start at 1 or end at last page + } else { + // subtract 2 to account for having to show the ellipsis and the "first" page + start = currentPage - (radius - 2); + if (currentPage + radius > totalPages) { + end = totalPages; + } else { + // subtract 1 to account for having to show the ellipsis + // and subtract another 1 if showing the "last" page (unbounded = false) + end = currentPage + (radius - 1 - unboundedChar); + } + } + + return this.makeArray(start, end); + } + private pageNumbers = () => { const { maxPageListLength, @@ -96,15 +178,14 @@ export class VaPagination { pages: totalPages, showLastPage, } = this; - + // Make space for "... (last page number)" if not in range of the last page. const showEllipsisAndLastPage = showLastPage && currentPage < totalPages - maxPageListLength + 1; - + const limit = showEllipsisAndLastPage ? maxPageListLength - 2 : maxPageListLength; - let end; let start; @@ -122,7 +203,7 @@ export class VaPagination { start = 1; end = totalPages + 1; } - return Array.from({ length: end - start }, (_, i) => i + start); + return this.makeArray(start, end); }; private handleKeyDown = (e, pageNumber) => { @@ -137,7 +218,7 @@ export class VaPagination { }; render() { - const { ariaLabelSuffix, page, pages, maxPageListLength, showLastPage } = + const { ariaLabelSuffix, page, pages, maxPageListLength, showLastPage, uswds } = this; if (pages === 1) { @@ -147,101 +228,181 @@ export class VaPagination { const previousAriaLabel = ariaLabelSuffix ? `Previous page ${ariaLabelSuffix}` : 'Previous page'; const nextAriaLabel = ariaLabelSuffix ? `Next page ${ariaLabelSuffix}` : 'Next page'; const lastPageAriaLabel = ariaLabelSuffix ? `Page ${pages} ${ariaLabelSuffix}` : `Page ${pages}`; + if (uswds) { + const previousIconPath = `${getAssetPath('/assets/sprite.svg')}#navigate_before`; + const nextIconPath = `${getAssetPath('/assets/sprite.svg')}#navigate_next`; + const pageNumbersToRender = this.pageNumbersUswds(); + const previousButton = page > 1 + ? + +
  • + + + Previous + +
  • + {!pageNumbersToRender.includes(1) && + +
  • + 1 +
  • + +
    } +
    + : null; + + const renderPages = pageNumbersToRender.map(pageNumber => { + const anchorClasses = classnames({ + 'usa-pagination__button': true, + 'usa-current': page === pageNumber + }) - const renderPages = this.pageNumbers().map(pageNumber => { - const pageClass = classnames({ - 'button-active': page === pageNumber, - 'button-inner': true, + return ( +
  • + {pageNumber} +
  • + ) }); - const pageAriaLabel = ariaLabelSuffix ? `Page ${pageNumber} ${ariaLabelSuffix}` : `Page ${pageNumber}`; + const nextButton = page < pages + ? + + {pages > this.SHOW_ALL_PAGES && } + {!this.unbounded && +
  • + {pages} +
  • + } +
  • + + Next + + +
  • +
    + : null; return ( -
  • - -
  • - ); - }); - - return ( - -
      - {/* START PREV BUTTON */} - {this.page > 1 && ( -
    • - -
    • - )} - {/* END PREV BUTTON */} -
    - -
      - {renderPages} - {/* START ELLIPSIS AND LAST BUTTON */} - {showLastPage && page < pages - maxPageListLength + 1 && ( - -
    • - ... + + + + ) + } else { + const renderPages = this.pageNumbers().map(pageNumber => { + const pageClass = classnames({ + 'button-active': page === pageNumber, + 'button-inner': true, + }); + + const pageAriaLabel = ariaLabelSuffix ? `Page ${pageNumber} ${ariaLabelSuffix}` : `Page ${pageNumber}`; + + return ( +
    • + +
    • + ); + }); + + return ( + +
        + {/* START PREV BUTTON */} + {this.page > 1 && ( +
      • +
      • + )} + {/* END PREV BUTTON */} +
      + +
        + {renderPages} + {/* START ELLIPSIS AND LAST BUTTON */} + {showLastPage && page < pages - maxPageListLength + 1 && ( + +
      • + ... +
      • +
      • + +
      • +
        + )} + {/* END ELLIPSIS AND LAST BUTTON */} +
      + +
        + {/* START NEXT BUTTON */} + {this.pages > this.page && (
      • - - )} - {/* END ELLIPSIS AND LAST BUTTON */} -
      - -
        - {/* START NEXT BUTTON */} - {this.pages > this.page && ( -
      • - -
      • - )} - {/* END NEXT BUTTON */} -
      -
      - ); + )} + {/* END NEXT BUTTON */} +
    +
    + ); + } } } diff --git a/packages/web-components/src/global/_formation_overrides.scss b/packages/web-components/src/global/_formation_overrides.scss index 0b10a7868..d9698bb6a 100644 --- a/packages/web-components/src/global/_formation_overrides.scss +++ b/packages/web-components/src/global/_formation_overrides.scss @@ -462,4 +462,21 @@ .usa-icon--size-9 { height: rem-override(4.5rem); width: rem-override(4.5rem); -} \ No newline at end of file +} + +.usa-pagination { + margin-bottom: rem-override(1rem); + margin-top: rem-override(1rem); + font-size: rem-override(1.06rem); +} + +.usa-pagination__item { + height: rem-override(2.5rem); + margin-left: rem-override(0.25rem); + margin-right: rem-override(0.25rem); + min-width: rem-override(2.5rem); +} + +.usa-pagination__overflow { + padding: rem-override(0.5rem); +}