diff --git a/.changeset/hungry-cycles-drum.md b/.changeset/hungry-cycles-drum.md new file mode 100644 index 00000000000..e90fd696e25 --- /dev/null +++ b/.changeset/hungry-cycles-drum.md @@ -0,0 +1,5 @@ +--- +"@hashicorp/design-system-components": minor +--- + +`Hds::Pagination` - Converted component to Typescript \ No newline at end of file diff --git a/packages/components/src/components.ts b/packages/components/src/components.ts index 537e3e13349..6eb81d4a9b2 100644 --- a/packages/components/src/components.ts +++ b/packages/components/src/components.ts @@ -86,6 +86,13 @@ import HdsModalBodyComponent from './components/hds/modal/body.ts'; import HdsModalFooter from './components/hds/modal/footer.ts'; import HdsModalHeader from './components/hds/modal/header.ts'; import HdsPageHeader from './components/hds/page-header/index.ts'; +import HdsPaginationCompactComponent from './components/hds/pagination/compact/index.ts'; +import HdsPaginationControlInfoComponent from './components/hds/pagination/info/index.ts'; +import HdsPaginationControlArrowComponent from './components/hds/pagination/nav/arrow.ts'; +import HdsPaginationControlEllipsisComponent from './components/hds/pagination/nav/ellipsis.ts'; +import HdsPaginationControlNumberComponent from './components/hds/pagination/nav/number.ts'; +import HdsPaginationNumberedComponent from './components/hds/pagination/numbered/index.ts'; +import HdsPaginationSizeSelectorComponent from './components/hds/pagination/size-selector/index.ts'; import HdsPopoverPrimitive from './components/hds/popover-primitive/index.ts'; import HdsReveal from './components/hds/reveal/index.ts'; import HdsRichTooltip from './components/hds/rich-tooltip/index.ts'; @@ -201,6 +208,13 @@ export { HdsModalFooter, HdsModalHeader, HdsPageHeader, + HdsPaginationCompactComponent, + HdsPaginationControlInfoComponent, + HdsPaginationControlArrowComponent, + HdsPaginationControlEllipsisComponent, + HdsPaginationControlNumberComponent, + HdsPaginationNumberedComponent, + HdsPaginationSizeSelectorComponent, HdsPopoverPrimitive, HdsReveal, HdsRichTooltip, diff --git a/packages/components/src/components/hds/pagination/compact/index.hbs b/packages/components/src/components/hds/pagination/compact/index.hbs index 14861eb00ce..c808653449d 100644 --- a/packages/components/src/components/hds/pagination/compact/index.hbs +++ b/packages/components/src/components/hds/pagination/compact/index.hbs @@ -1,4 +1,3 @@ -{{! @glint-nocheck: not typesafe yet }} {{! Copyright (c) HashiCorp, Inc. SPDX-License-Identifier: MPL-2.0 diff --git a/packages/components/src/components/hds/pagination/compact/index.js b/packages/components/src/components/hds/pagination/compact/index.ts similarity index 68% rename from packages/components/src/components/hds/pagination/compact/index.js rename to packages/components/src/components/hds/pagination/compact/index.ts index 4e2325cd77c..31636d41f3a 100644 --- a/packages/components/src/components/hds/pagination/compact/index.js +++ b/packages/components/src/components/hds/pagination/compact/index.ts @@ -7,12 +7,47 @@ import Component from '@glimmer/component'; import { tracked } from '@glimmer/tracking'; import { action } from '@ember/object'; import { assert } from '@ember/debug'; +import { HdsPaginationDirectionValues } from '../types.ts'; + +import type { + HdsPaginationRoutingProps, + HdsPaginationDirections, +} from '../types'; +import type { HdsInteractiveSignature } from '../../interactive'; + +type HdsInteractiveQuery = HdsInteractiveSignature['Args']['query']; + +type HdsPaginationCompactRoutingQueryProps = HdsPaginationRoutingProps & { + queryNext?: HdsInteractiveQuery; + queryPrev?: HdsInteractiveQuery; +}; + +interface HdsPaginationCompactArgs { + ariaLabel?: string; + showLabels?: boolean; + isDisabledPrev?: boolean; + isDisabledNext?: boolean; + showSizeSelector?: boolean; + sizeSelectorLabel?: string; + pageSizes?: number[]; + currentPageSize?: number; + queryFunction?: ( + page: HdsPaginationDirections, + pageSize?: number + ) => HdsInteractiveQuery; + onPageChange?: (page: HdsPaginationDirections) => void; + onPageSizeChange?: (pageSize: number) => void; +} + +interface HdsPaginationCompactSignature { + Args: HdsPaginationCompactArgs & HdsPaginationRoutingProps; + Element: HTMLDivElement; +} // for context about the decision to use these values, see: // https://hashicorp.slack.com/archives/C03A0N1QK8S/p1673546329082759 export const DEFAULT_PAGE_SIZES = [10, 30, 50]; - -export default class HdsPaginationCompactIndexComponent extends Component { +export default class HdsPaginationCompactComponent extends Component { // This private variable is used to differentiate between // "uncontrolled" component (where the state is handled internally) and // "controlled" component (where the state is handled externally, by the consumer's code). @@ -27,10 +62,10 @@ export default class HdsPaginationCompactIndexComponent extends Component { showLabels = this.args.showLabels ?? true; // if the labels for the "prev/next" controls are visible showSizeSelector = this.args.showSizeSelector ?? false; // if the "size selector" block is visible - constructor() { - super(...arguments); + constructor(owner: unknown, args: HdsPaginationCompactSignature['Args']) { + super(owner, args); - let { queryFunction } = this.args; + const { queryFunction } = this.args; // This component works in two different ways, depending if we need to support // routing through links (`LinkTo`) for the "navigation controls", or not. @@ -51,12 +86,7 @@ export default class HdsPaginationCompactIndexComponent extends Component { } } - /** - * @param ariaLabel - * @type {string} - * @default 'Pagination' - */ - get ariaLabel() { + get ariaLabel(): string { return this.args.ariaLabel ?? 'Pagination'; } @@ -73,14 +103,13 @@ export default class HdsPaginationCompactIndexComponent extends Component { // is *always* determined by the component's internal logic (and updated according to the user interaction with it). // For this reason the "get" and "set" methods always read from or write to the private internal state (_variable). - get currentPageSize() { + get currentPageSize(): number | undefined { if (this.isControlled) { return this.args.currentPageSize; } else { return this._currentPageSize; } } - set currentPageSize(value) { if (this.isControlled) { // noop @@ -89,33 +118,31 @@ export default class HdsPaginationCompactIndexComponent extends Component { } } - /** - * @param pageSizes - * @type {array of numbers} - * @description Set the page sizes users can select from. - * @default [10, 30, 50] - */ - get pageSizes() { - let { pageSizes = DEFAULT_PAGE_SIZES } = this.args; + get pageSizes(): number[] { + const { pageSizes = DEFAULT_PAGE_SIZES } = this.args; assert( `pageSizes argument must be an array. Received: ${pageSizes}`, - Array.isArray(pageSizes) === true + Array.isArray(pageSizes) === true && pageSizes.length > 0 ); return pageSizes; } - buildQueryParamsObject(page, pageSize) { + buildQueryParamsObject( + page: HdsPaginationDirections, + pageSize?: number + ): HdsInteractiveQuery { if (this.isControlled) { - return this.args.queryFunction(page, pageSize); + // if the component is controlled, we can assert that the queryFunction is defined + return this.args.queryFunction!(page, pageSize); } else { return {}; } } - get routing() { - let routing = { + get routing(): HdsPaginationCompactRoutingQueryProps { + const routing: HdsPaginationCompactRoutingQueryProps = { route: this.args.route ?? undefined, model: this.args.model ?? undefined, models: this.args.models ?? undefined, @@ -125,11 +152,11 @@ export default class HdsPaginationCompactIndexComponent extends Component { // the "query" is dynamic and needs to be calculated if (this.isControlled) { routing.queryPrev = this.buildQueryParamsObject( - 'prev', + HdsPaginationDirectionValues.Prev, this.currentPageSize ); routing.queryNext = this.buildQueryParamsObject( - 'next', + HdsPaginationDirectionValues.Next, this.currentPageSize ); } else { @@ -141,10 +168,8 @@ export default class HdsPaginationCompactIndexComponent extends Component { } @action - onPageChange(newPage) { - this.currentPage = newPage; - - let { onPageChange } = this.args; + onPageChange(newPage: HdsPaginationDirections): void { + const { onPageChange } = this.args; if (typeof onPageChange === 'function') { onPageChange(newPage); @@ -152,8 +177,8 @@ export default class HdsPaginationCompactIndexComponent extends Component { } @action - onPageSizeChange(newPageSize) { - let { onPageSizeChange } = this.args; + onPageSizeChange(newPageSize: number): void { + const { onPageSizeChange } = this.args; // invoke the callback function if (typeof onPageSizeChange === 'function') { diff --git a/packages/components/src/components/hds/pagination/info/index.hbs b/packages/components/src/components/hds/pagination/info/index.hbs index 50b630e0442..2323b6d10d2 100644 --- a/packages/components/src/components/hds/pagination/info/index.hbs +++ b/packages/components/src/components/hds/pagination/info/index.hbs @@ -1,4 +1,3 @@ -{{! @glint-nocheck: not typesafe yet }} {{! Copyright (c) HashiCorp, Inc. SPDX-License-Identifier: MPL-2.0 diff --git a/packages/components/src/components/hds/pagination/info/index.js b/packages/components/src/components/hds/pagination/info/index.js deleted file mode 100644 index d8a3bcfb6ee..00000000000 --- a/packages/components/src/components/hds/pagination/info/index.js +++ /dev/null @@ -1,17 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: MPL-2.0 - */ - -import Component from '@glimmer/component'; - -export default class HdsPaginationInfoComponent extends Component { - /** - * @param showTotalItems - * @type {boolean} - * @description Controls the visibility of the total items - */ - get showTotalItems() { - return this.args.showTotalItems ?? true; - } -} diff --git a/packages/components/src/components/hds/pagination/info/index.ts b/packages/components/src/components/hds/pagination/info/index.ts new file mode 100644 index 00000000000..9fc0ea13aa1 --- /dev/null +++ b/packages/components/src/components/hds/pagination/info/index.ts @@ -0,0 +1,23 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: MPL-2.0 + */ + +import Component from '@glimmer/component'; +import type { HdsPaginationNumberedSignature } from '../numbered/index'; +import type { HdsTextBodySignature } from '../../text/body'; +interface HdsPaginationInfoSignature { + Args: { + itemsRangeStart: number; + itemsRangeEnd: number; + showTotalItems?: HdsPaginationNumberedSignature['Args']['showTotalItems']; + totalItems: HdsPaginationNumberedSignature['Args']['totalItems']; + }; + Element: HdsTextBodySignature['Element']; +} + +export default class HdsPaginationInfoComponent extends Component { + get showTotalItems(): boolean { + return this.args.showTotalItems ?? true; + } +} diff --git a/packages/components/src/components/hds/pagination/nav/arrow.hbs b/packages/components/src/components/hds/pagination/nav/arrow.hbs index 4f1083a8fa7..daed0ea7b31 100644 --- a/packages/components/src/components/hds/pagination/nav/arrow.hbs +++ b/packages/components/src/components/hds/pagination/nav/arrow.hbs @@ -1,4 +1,3 @@ -{{! @glint-nocheck: not typesafe yet }} {{! Copyright (c) HashiCorp, Inc. SPDX-License-Identifier: MPL-2.0 diff --git a/packages/components/src/components/hds/pagination/nav/arrow.js b/packages/components/src/components/hds/pagination/nav/arrow.js deleted file mode 100644 index 3c3531e65cf..00000000000 --- a/packages/components/src/components/hds/pagination/nav/arrow.js +++ /dev/null @@ -1,77 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: MPL-2.0 - */ - -import Component from '@glimmer/component'; -import { action } from '@ember/object'; -import { assert } from '@ember/debug'; - -export const DIRECTIONS = ['prev', 'next']; - -export default class HdsPaginationControlArrowComponent extends Component { - get content() { - let { direction } = this.args; - - assert( - `@direction for "Pagination::Nav::Arrow" must be one of the following: ${DIRECTIONS.join( - ', ' - )}; received: ${direction}`, - DIRECTIONS.includes(direction) - ); - - let content; - if (direction === 'prev') { - content = { - label: 'Previous', - icon: 'chevron-left', - ariaLabel: 'Previous page', - }; - } - if (direction === 'next') { - content = { - label: 'Next', - icon: 'chevron-right', - ariaLabel: 'Next page', - }; - } - - return content; - } - - /** - * @param showLabel - * @type {boolean} - * @default true - * @description Show the labels for the control - */ - get showLabel() { - let { showLabel = true } = this.args; - - return showLabel; - } - - /** - * Get the class names to apply to the component. - * @method classNames - * @return {string} The "class" attribute to apply to the component. - */ - get classNames() { - let classes = [ - 'hds-pagination-nav__control', - 'hds-pagination-nav__arrow', - `hds-pagination-nav__arrow--direction-${this.args.direction}`, - ]; - - return classes.join(' '); - } - - @action - onClick() { - let { onClick } = this.args; - - if (typeof onClick === 'function') { - onClick(this.args.direction); - } - } -} diff --git a/packages/components/src/components/hds/pagination/nav/arrow.ts b/packages/components/src/components/hds/pagination/nav/arrow.ts new file mode 100644 index 00000000000..80cdd06d9e7 --- /dev/null +++ b/packages/components/src/components/hds/pagination/nav/arrow.ts @@ -0,0 +1,99 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: MPL-2.0 + */ +import Component from '@glimmer/component'; +import { action } from '@ember/object'; +import { assert } from '@ember/debug'; +import { + HdsPaginationDirectionValues, + HdsPaginationDirectionAriaLabelValues, + HdsPaginationDirectionLabelValues, +} from '../types.ts'; + +import type { HdsIconSignature } from '../../icon/index.ts'; +import type { HdsInteractiveSignature } from '../../interactive'; +import type { + HdsPaginationDirections, + HdsPaginationDirectionAriaLabels, + HdsPaginationDirectionLabels, +} from '../types'; + +interface HdsPaginationControlArrowContent { + label: HdsPaginationDirectionLabels; + icon: HdsIconSignature['Args']['name']; + ariaLabel: HdsPaginationDirectionAriaLabels; +} + +interface HdsPaginationControlArrowArgs { + direction: HdsPaginationDirections; + disabled?: boolean; + showLabel?: boolean; + onClick?: (direction: HdsPaginationDirections) => void; +} + +interface HdsPaginationControlArrowSignature { + Args: HdsPaginationControlArrowArgs & HdsInteractiveSignature['Args']; + Element: HdsInteractiveSignature['Element']; +} + +export const DIRECTIONS: HdsPaginationDirections[] = [ + HdsPaginationDirectionValues.Prev, + HdsPaginationDirectionValues.Next, +]; + +export default class HdsPaginationControlArrowComponent extends Component { + get content(): HdsPaginationControlArrowContent { + const { direction } = this.args; + + assert( + `@direction for "Pagination::Nav::Arrow" must be one of the following: ${DIRECTIONS.join( + ', ' + )}; received: ${direction}`, + DIRECTIONS.includes(direction) + ); + + const hdsPaginationNavArrowContentDirectionMap: Record< + HdsPaginationDirections, + HdsPaginationControlArrowContent + > = { + [HdsPaginationDirectionValues.Prev]: { + label: HdsPaginationDirectionLabelValues.Prev, + icon: 'chevron-left', + ariaLabel: HdsPaginationDirectionAriaLabelValues.Prev, + }, + [HdsPaginationDirectionValues.Next]: { + label: HdsPaginationDirectionLabelValues.Next, + icon: 'chevron-right', + ariaLabel: HdsPaginationDirectionAriaLabelValues.Next, + }, + }; + + return hdsPaginationNavArrowContentDirectionMap[direction]; + } + + get showLabel(): boolean { + const { showLabel = true } = this.args; + + return showLabel; + } + + get classNames(): string { + const classes = [ + 'hds-pagination-nav__control', + 'hds-pagination-nav__arrow', + `hds-pagination-nav__arrow--direction-${this.args.direction}`, + ]; + + return classes.join(' '); + } + + @action + onClick(): void { + const { onClick } = this.args; + + if (typeof onClick === 'function') { + onClick(this.args.direction); + } + } +} diff --git a/packages/components/src/components/hds/pagination/nav/ellipsis.hbs b/packages/components/src/components/hds/pagination/nav/ellipsis.hbs index a9f50fac81e..e9792614948 100644 --- a/packages/components/src/components/hds/pagination/nav/ellipsis.hbs +++ b/packages/components/src/components/hds/pagination/nav/ellipsis.hbs @@ -1,4 +1,3 @@ -{{! @glint-nocheck: not typesafe yet }} {{! Copyright (c) HashiCorp, Inc. SPDX-License-Identifier: MPL-2.0 diff --git a/packages/components/src/components/hds/pagination/nav/ellipsis.ts b/packages/components/src/components/hds/pagination/nav/ellipsis.ts new file mode 100644 index 00000000000..104b4ada49d --- /dev/null +++ b/packages/components/src/components/hds/pagination/nav/ellipsis.ts @@ -0,0 +1,14 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: MPL-2.0 + */ +import TemplateOnlyComponent from '@ember/component/template-only'; + +export interface HdsApplicationPaginationNavEllipsisSignature { + Element: HTMLDivElement; +} + +const HdsApplicationPaginationNavEllipsisComponent = + TemplateOnlyComponent(); + +export default HdsApplicationPaginationNavEllipsisComponent; diff --git a/packages/components/src/components/hds/pagination/nav/number.hbs b/packages/components/src/components/hds/pagination/nav/number.hbs index 5e2790c9b26..c14b276bfcf 100644 --- a/packages/components/src/components/hds/pagination/nav/number.hbs +++ b/packages/components/src/components/hds/pagination/nav/number.hbs @@ -1,4 +1,3 @@ -{{! @glint-nocheck: not typesafe yet }} {{! Copyright (c) HashiCorp, Inc. SPDX-License-Identifier: MPL-2.0 diff --git a/packages/components/src/components/hds/pagination/nav/number.js b/packages/components/src/components/hds/pagination/nav/number.ts similarity index 50% rename from packages/components/src/components/hds/pagination/nav/number.js rename to packages/components/src/components/hds/pagination/nav/number.ts index 9e4e7a48e59..c54d67c2732 100644 --- a/packages/components/src/components/hds/pagination/nav/number.js +++ b/packages/components/src/components/hds/pagination/nav/number.ts @@ -2,14 +2,26 @@ * Copyright (c) HashiCorp, Inc. * SPDX-License-Identifier: MPL-2.0 */ - import Component from '@glimmer/component'; import { action } from '@ember/object'; import { assert } from '@ember/debug'; -export default class HdsPaginationControlNumberComponent extends Component { - get page() { - let { page } = this.args; +import type { HdsInteractiveSignature } from '../../interactive'; + +interface HdsPaginationNavNumberArgs { + page: number; + onClick: (page: number) => void; + isSelected: boolean; +} + +interface HdsPaginationNavNumberSignature { + Args: HdsPaginationNavNumberArgs & HdsInteractiveSignature['Args']; + Element: HdsInteractiveSignature['Element']; +} + +export default class HdsPaginationControlNumberComponent extends Component { + get page(): number { + const { page } = this.args; assert( '@page for "Pagination::Nav::Number" must have a valid value', @@ -19,13 +31,11 @@ export default class HdsPaginationControlNumberComponent extends Component { return page; } - /** - * Get the class names to apply to the component. - * @method classNames - * @return {string} The "class" attribute to apply to the component. - */ - get classNames() { - let classes = ['hds-pagination-nav__control', 'hds-pagination-nav__number']; + get classNames(): string { + const classes = [ + 'hds-pagination-nav__control', + 'hds-pagination-nav__number', + ]; if (this.args.isSelected) { classes.push(`hds-pagination-nav__number--is-selected`); @@ -35,8 +45,8 @@ export default class HdsPaginationControlNumberComponent extends Component { } @action - onClick() { - let { onClick } = this.args; + onClick(): void { + const { onClick } = this.args; if (typeof onClick === 'function') { onClick(this.args.page); diff --git a/packages/components/src/components/hds/pagination/numbered/index.hbs b/packages/components/src/components/hds/pagination/numbered/index.hbs index 8d7d0bcb46b..39bee9b2207 100644 --- a/packages/components/src/components/hds/pagination/numbered/index.hbs +++ b/packages/components/src/components/hds/pagination/numbered/index.hbs @@ -1,4 +1,3 @@ -{{! @glint-nocheck: not typesafe yet }} {{! Copyright (c) HashiCorp, Inc. SPDX-License-Identifier: MPL-2.0 @@ -33,7 +32,7 @@ {{else}} ; +}; + +interface HdsPaginationNumberedArgs { + ariaLabel?: string; + totalItems: number; + showLabels?: boolean; + isTruncated?: boolean; + currentPage?: number; + showInfo?: boolean; + showPageNumbers?: boolean; + showTotalItems?: boolean; + showSizeSelector?: boolean; + sizeSelectorLabel?: string; + pageSizes?: number[]; + currentPageSize?: number; + queryFunction?: (page: number, pageSize: number) => HdsInteractiveQuery; + onPageChange?: (page: number, pageSize: number) => unknown; + onPageSizeChange?: (pageSize: number) => unknown; +} + +export interface HdsPaginationNumberedSignature { + Args: HdsPaginationNumberedArgs & HdsPaginationRoutingProps; + Element: HTMLDivElement; +} + +const ELLIPSIS = '…'; // for context about the decision to use these values, see: // https://hashicorp.slack.com/archives/C03A0N1QK8S/p1673546329082759 export const DEFAULT_PAGE_SIZES = [10, 30, 50]; -/** - * Elliptize a list of pages - * - * @param pages - array with all the "pages" (integer numbers) - * @param current - "current" page (array's index) - * @param limit - number of "page numbers" to be shown at a time (should always be an odd number!) - * - * @return - array of integers ("pages") + `...` strings ("ellipsis") - */ -export const elliptize = ({ pages, current, limit = 7 }) => { +export const elliptize = ({ + pages, + current, + limit = 7, +}: ElliptizeProps): HdsPaginationElliptizedPageArray => { const length = pages.length; - const ellipsis = '…'; + let result = []; let start; let end; @@ -40,17 +84,23 @@ export const elliptize = ({ pages, current, limit = 7 }) => { start = limit - end; } - const sliceStart = pages.slice(0, start); - const sliceEnd = pages.slice(-end); + const sliceStart: HdsPaginationElliptizedPageArray = pages.slice(0, start); + const sliceEnd: HdsPaginationElliptizedPageArray = pages.slice(-end); if (sliceStart.includes(current) && sliceStart.includes(current + 1)) { // "current" (and its next sibling) is contained within the "sliceStart" block - sliceEnd.splice(0, 1, ellipsis); - result = [].concat(sliceStart, sliceEnd); + sliceEnd.splice(0, 1, ELLIPSIS); + result = ([] as HdsPaginationElliptizedPageArray).concat( + sliceStart, + sliceEnd + ); } else if (sliceEnd.includes(current - 1) && sliceEnd.includes(current)) { // "current" (and its prev sibling) is contained within the "sliceEnd" block - sliceStart.splice(-1, 1, ellipsis); - result = [].concat(sliceStart, sliceEnd); + sliceStart.splice(-1, 1, ELLIPSIS); + result = ([] as HdsPaginationElliptizedPageArray).concat( + sliceStart, + sliceEnd + ); } else { // this is a bit more tricky :) // we need to calculate how many items there are before/after the current item @@ -59,19 +109,18 @@ export const elliptize = ({ pages, current, limit = 7 }) => { const delta = (limit - 5) / 2; // this is why the limit needs to be an odd number // we slice the array starting at the "current" index, minus the delta, minus one because it's an array (zero-based) const sliceCurr = pages.slice(current - delta - 1, current + delta); - result = [].concat( - sliceStart.shift(), - ellipsis, + result = ([] as HdsPaginationElliptizedPageArray).concat( + sliceStart.shift() as number, + ELLIPSIS, sliceCurr, - ellipsis, - sliceEnd.pop() + ELLIPSIS, + sliceEnd.pop() as number ); } return result; }; - -export default class HdsPaginationNumberedIndexComponent extends Component { +export default class HdsPaginationNumberedComponent extends Component { // These two private variables are used to differentiate between // "uncontrolled" component (where the state is handled internally) and // "controlled" component (where the state is handled externally, by the consumer's code). @@ -81,7 +130,8 @@ export default class HdsPaginationNumberedIndexComponent extends Component { // at rendering time, but from that moment on they're not updated anymore, no matter what interaction the user // has with the component (the state is controlled externally, eg. via query parameters) @tracked _currentPage = this.args.currentPage ?? 1; - @tracked _currentPageSize = this.args.currentPageSize ?? this.pageSizes[0]; + // we assert that `this.pageSizes` will always be an array with at least one item + @tracked _currentPageSize = this.args.currentPageSize ?? this.pageSizes[0]!; @tracked isControlled; showInfo = this.args.showInfo ?? true; // if the "info" block is visible @@ -90,10 +140,10 @@ export default class HdsPaginationNumberedIndexComponent extends Component { showPageNumbers = this.args.showPageNumbers ?? true; // if the "page numbers" block is visible isTruncated = this.args.isTruncated ?? true; // if the list of "page numbers" is truncated - constructor() { - super(...arguments); + constructor(owner: unknown, args: HdsPaginationNumberedSignature['Args']) { + super(owner, args); - let { queryFunction } = this.args; + const { queryFunction } = this.args; // This component works in two different ways, depending if we need to support // routing through links (`LinkTo`) for the "navigation controls", or not. @@ -125,12 +175,7 @@ export default class HdsPaginationNumberedIndexComponent extends Component { ); } - /** - * @param ariaLabel - * @type {string} - * @default 'Pagination' - */ - get ariaLabel() { + get ariaLabel(): string { return this.args.ariaLabel ?? 'Pagination'; } @@ -147,30 +192,31 @@ export default class HdsPaginationNumberedIndexComponent extends Component { // is *always* determined by the component's internal logic (and updated according to the user interaction with it). // For this reason the "get" and "set" methods always read from or write to the private internal state (_variable). - get currentPage() { + get currentPage(): number { if (this.isControlled) { - return this.args.currentPage; + // if the component is controlled, `@currentPage` is asserted to be a number + return this.args.currentPage as number; } else { return this._currentPage; } } - set currentPage(value) { if (this.isControlled) { // noop } else { - this._currentPage = value; + // if `this.isControlled` is `false` + this._currentPage = value as number; } } - get currentPageSize() { + get currentPageSize(): number { if (this.isControlled) { - return this.args.currentPageSize; + // if the component is controlled, `@currentPageSize` is asserted to be a number + return this.args.currentPageSize as number; } else { return this._currentPageSize; } } - set currentPageSize(value) { if (this.isControlled) { // noop @@ -179,24 +225,19 @@ export default class HdsPaginationNumberedIndexComponent extends Component { } } - /** - * @param pageSizes - * @type {array of numbers} - * @description Set the page sizes users can select from. - * @default [10, 30, 50] - */ - get pageSizes() { - let { pageSizes = DEFAULT_PAGE_SIZES } = this.args; + get pageSizes(): number[] { + const { pageSizes = DEFAULT_PAGE_SIZES } = this.args; assert( - `pageSizes argument must be an array. Received: ${pageSizes}`, - Array.isArray(pageSizes) === true + // TODO: Add test for this + `pageSizes argument must be an array with at least one item. Received: ${pageSizes}`, + Array.isArray(pageSizes) === true && pageSizes.length > 0 ); return pageSizes; } - get itemsRangeStart() { + get itemsRangeStart(): number { // Calculate the starting range of items displayed on current page // if currentPage = 1st page and # of items per page is 10: // ( (1 - 1 = 0) * 10 = 0 ) + 1 = 1 @@ -205,7 +246,7 @@ export default class HdsPaginationNumberedIndexComponent extends Component { return (this.currentPage - 1) * this.currentPageSize + 1; } - get itemsRangeEnd() { + get itemsRangeEnd(): number { // Calculate ending range of items displayed on current page // 2 cases: 1) full page of items or 2) last page of items if (this.currentPage * this.currentPageSize < this.args.totalItems) { @@ -217,8 +258,8 @@ export default class HdsPaginationNumberedIndexComponent extends Component { } } - get pages() { - let pages = []; + get pages(): HdsPaginationElliptizedPageArray { + const pages = []; for (let i = 1; i <= this.totalPages; i++) { pages.push(i); @@ -235,16 +276,21 @@ export default class HdsPaginationNumberedIndexComponent extends Component { return Math.max(Math.ceil(this.args.totalItems / this.currentPageSize), 1); } - buildQueryParamsObject(page, pageSize) { - if (this.isControlled) { - return this.args.queryFunction(page, pageSize); + buildQueryParamsObject( + page: HdsPaginationElliptizedPageArrayItem, + pageSize: number + ): HdsInteractiveQuery { + // `page` may also be ellipsis + if (this.isControlled && typeof page === 'number') { + // if the component is controlled, `@queryFunction` is asserted to be a function + return this.args.queryFunction!(page, pageSize); } else { return {}; } } - get routing() { - let routing = { + get routing(): HdsPaginationNumberedRoutingQueryProps { + const routing: HdsPaginationNumberedRoutingQueryProps = { route: this.args.route ?? undefined, model: this.args.model ?? undefined, models: this.args.models ?? undefined, @@ -267,7 +313,7 @@ export default class HdsPaginationNumberedIndexComponent extends Component { routing.queryPages = {}; this.pages.forEach( (page) => - (routing.queryPages[page] = this.buildQueryParamsObject( + (routing.queryPages![page] = this.buildQueryParamsObject( page, this.currentPageSize )) @@ -275,7 +321,6 @@ export default class HdsPaginationNumberedIndexComponent extends Component { } else { routing.queryPrev = undefined; routing.queryNext = undefined; - routing.queryByPage = {}; } return routing; @@ -290,11 +335,14 @@ export default class HdsPaginationNumberedIndexComponent extends Component { } @action - onPageChange(page) { + onPageChange(page: HdsPaginationPage) { let gotoPageNumber; - if (page === 'prev' && this.currentPage > 1) { + if (page === HdsPaginationDirectionValues.Prev && this.currentPage > 1) { gotoPageNumber = this.currentPage - 1; - } else if (page === 'next' && this.currentPage < this.totalPages) { + } else if ( + page === HdsPaginationDirectionValues.Next && + this.currentPage < this.totalPages + ) { gotoPageNumber = this.currentPage + 1; } else { gotoPageNumber = page; @@ -302,9 +350,10 @@ export default class HdsPaginationNumberedIndexComponent extends Component { // we want to invoke the `onPageChange` callback only on actual page change if (gotoPageNumber !== this.currentPage) { - this.currentPage = gotoPageNumber; + // we have already determined that `gotoPageNumber` is not `prev` or `next` + this.currentPage = gotoPageNumber as number; - let { onPageChange } = this.args; + const { onPageChange } = this.args; if (typeof onPageChange === 'function') { onPageChange(this.currentPage, this.currentPageSize); @@ -313,8 +362,8 @@ export default class HdsPaginationNumberedIndexComponent extends Component { } @action - onPageSizeChange(newPageSize) { - let { onPageSizeChange } = this.args; + onPageSizeChange(newPageSize: number) { + const { onPageSizeChange } = this.args; if (!this.isControlled) { // notice: we agreed to reset the pagination to the first element (any alternative would result in an unpredictable UX) @@ -327,4 +376,18 @@ export default class HdsPaginationNumberedIndexComponent extends Component { onPageSizeChange(newPageSize); } } + + elliptizedPageArrayItemAsNumber = ( + item: HdsPaginationElliptizedPageArrayItem + ): number => { + if (typeof item === 'number') { + return item; + } else { + throw new Error('Expected a number, but got an ellipsis'); + } + }; + + getPageNumberQuery(page: HdsPaginationElliptizedPageArrayItem) { + return this.routing.queryPages![this.elliptizedPageArrayItemAsNumber(page)]; + } } diff --git a/packages/components/src/components/hds/pagination/size-selector/index.hbs b/packages/components/src/components/hds/pagination/size-selector/index.hbs index 54942368621..97791f2bbe7 100644 --- a/packages/components/src/components/hds/pagination/size-selector/index.hbs +++ b/packages/components/src/components/hds/pagination/size-selector/index.hbs @@ -1,4 +1,3 @@ -{{! @glint-nocheck: not typesafe yet }} {{! Copyright (c) HashiCorp, Inc. SPDX-License-Identifier: MPL-2.0 diff --git a/packages/components/src/components/hds/pagination/size-selector/index.js b/packages/components/src/components/hds/pagination/size-selector/index.ts similarity index 54% rename from packages/components/src/components/hds/pagination/size-selector/index.js rename to packages/components/src/components/hds/pagination/size-selector/index.ts index bd7beab61a9..d43ae2ab044 100644 --- a/packages/components/src/components/hds/pagination/size-selector/index.js +++ b/packages/components/src/components/hds/pagination/size-selector/index.ts @@ -8,21 +8,23 @@ import { assert } from '@ember/debug'; import { guidFor } from '@ember/object/internals'; import { action } from '@ember/object'; -export default class HdsPaginationSizeSelectorComponent extends Component { - /** - * Generates a unique ID for the pageSize select - * - * @param SizeSelectorId - */ +import type { HdsFormSelectBaseSignature } from '../../form/select/base'; + +interface HdsPaginationSizeSelectorSignature { + Args: { + pageSizes: number[]; + label?: string; + selectedSize?: number; + onChange?: (size: number) => void; + }; + Element: HTMLDivElement; +} + +export default class HdsPaginationSizeSelectorComponent extends Component { SizeSelectorId = 'pagination-size-selector-' + guidFor(this); - /** - * @param pageSizes - * @type {array of numbers} - * @description Set the page sizes users can select from. - */ - get pageSizes() { - let { pageSizes } = this.args; + get pageSizes(): number[] { + const { pageSizes } = this.args; assert( '@pageSizes for "Pagination::SizeSelector" must be defined', @@ -32,13 +34,8 @@ export default class HdsPaginationSizeSelectorComponent extends Component { return pageSizes; } - /** - * @param selectedSize - * @type integer - * @description The selected ("current") page size - */ - get selectedSize() { - let { selectedSize } = this.args; + get selectedSize(): number | undefined { + const { selectedSize } = this.args; assert( `@selectedSize for "Pagination::SizeSelector" must one of the @pageSizes provided (${this.pageSizes.join( @@ -50,24 +47,20 @@ export default class HdsPaginationSizeSelectorComponent extends Component { return selectedSize; } - /** - * @param label - * @type string - * @default "Items per page" - * @description The label text for the select - */ - get label() { - let { label = 'Items per page' } = this.args; + get label(): string { + const { label = 'Items per page' } = this.args; return label; } @action - onChange(e) { - let { onChange } = this.args; + onChange(e: Event): void { + const { onChange } = this.args; + + const target = e.target as HdsFormSelectBaseSignature['Element']; if (typeof onChange === 'function') { - onChange(parseInt(e.target.value)); + onChange(parseInt(target.value)); } } } diff --git a/packages/components/src/components/hds/pagination/types.ts b/packages/components/src/components/hds/pagination/types.ts new file mode 100644 index 00000000000..735d2863e13 --- /dev/null +++ b/packages/components/src/components/hds/pagination/types.ts @@ -0,0 +1,36 @@ +import type { HdsInteractiveSignature } from '../interactive'; + +export enum HdsPaginationDirectionValues { + Next = 'next', + Prev = 'prev', +} + +export type HdsPaginationDirections = `${HdsPaginationDirectionValues}`; + +export type HdsPaginationPage = HdsPaginationDirections | number; + +export enum HdsPaginationDirectionAriaLabelValues { + Prev = 'Previous page', + Next = 'Next page', +} + +export type HdsPaginationDirectionAriaLabels = + `${HdsPaginationDirectionAriaLabelValues}`; + +export enum HdsPaginationDirectionLabelValues { + Prev = 'Previous', + Next = 'Next', +} + +export type HdsPaginationDirectionLabels = + `${HdsPaginationDirectionLabelValues}`; + +export type HdsPaginationElliptizedPageArrayItem = string | number; + +export type HdsPaginationElliptizedPageArray = + HdsPaginationElliptizedPageArrayItem[]; + +export type HdsPaginationRoutingProps = Pick< + HdsInteractiveSignature['Args'], + 'route' | 'model' | 'models' | 'replace' +>; diff --git a/packages/components/src/template-registry.ts b/packages/components/src/template-registry.ts index ab67a072c82..b05079a38dc 100644 --- a/packages/components/src/template-registry.ts +++ b/packages/components/src/template-registry.ts @@ -128,6 +128,13 @@ import type HdsPageHeaderBadgesComponent from './components/hds/page-header/badg import type HdsPageHeaderDescriptionComponent from './components/hds/page-header/description'; import type HdsPageHeaderSubtitleComponent from './components/hds/page-header/subtitle'; import type HdsPageHeaderTitleComponent from './components/hds/page-header/title'; +import type HdsPaginationCompactComponent from './components/hds/pagination/compact/index'; +import type HdsPaginationControlInfoComponent from './components/hds/pagination/info/index'; +import type HdsPaginationControlArrowComponent from './components/hds/pagination/nav/arrow'; +import type HdsPaginationControlEllipsisComponent from './components/hds/pagination/nav/ellipsis'; +import type HdsPaginationControlNumberComponent from './components/hds/pagination/nav/number'; +import type HdsPaginationNumberedComponent from './components/hds/pagination/numbered/index'; +import type HdsPaginationSizeSelectorComponent from './components/hds/pagination/size-selector/index'; import type HdsPopoverPrimitiveComponent from './components/hds/popover-primitive'; import type HdsRevealComponent from './components/hds/reveal'; import type HdsRevealToggleButtonComponent from './components/hds/reveal/toggle/button'; @@ -627,6 +634,28 @@ export default interface HdsComponentsRegistry { 'Hds::PageHeader::Title': typeof HdsPageHeaderTitleComponent; 'hds/page-header/title': typeof HdsPageHeaderTitleComponent; + // Pagination + 'Hds::Pagination::Compact': typeof HdsPaginationCompactComponent; + 'hds/pagination/compact': typeof HdsPaginationCompactComponent; + + 'Hds::Pagination::Info': typeof HdsPaginationControlInfoComponent; + 'hds/pagination/info': typeof HdsPaginationControlInfoComponent; + + 'Hds::Pagination::Nav::Arrow': typeof HdsPaginationControlArrowComponent; + 'hds/pagination/nav/arrow': typeof HdsPaginationControlArrowComponent; + + 'Hds::Pagination::Nav::Ellipsis': typeof HdsPaginationControlEllipsisComponent; + 'hds/pagination/nav/ellipsis': typeof HdsPaginationControlEllipsisComponent; + + 'Hds::Pagination::Nav::Number': typeof HdsPaginationControlNumberComponent; + 'hds/pagination/nav/number': typeof HdsPaginationControlNumberComponent; + + 'Hds::Pagination::Numbered': typeof HdsPaginationNumberedComponent; + 'hds/pagination/numbered': typeof HdsPaginationNumberedComponent; + + 'Hds::Pagination::SizeSelector': typeof HdsPaginationSizeSelectorComponent; + 'hds/pagination/size-selector': typeof HdsPaginationSizeSelectorComponent; + // PopoverPrimitive 'Hds::PopoverPrimitive': typeof HdsPopoverPrimitiveComponent; 'hds/popover-primitive': typeof HdsPopoverPrimitiveComponent; diff --git a/showcase/tests/unit/components/hds/pagination-test.js b/showcase/tests/unit/components/hds/pagination/numbered-test.js similarity index 100% rename from showcase/tests/unit/components/hds/pagination-test.js rename to showcase/tests/unit/components/hds/pagination/numbered-test.js