Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .changeset/empty-bats-hunt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
"@siemens/ix-angular": minor
"@siemens/ix": minor
"@siemens/ix-react": minor
"@siemens/ix-vue": minor
---

Add property **itemCountOptions** to **ix-pagination.

Fixes #2103
4 changes: 2 additions & 2 deletions packages/angular/src/components.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2043,14 +2043,14 @@ export declare interface IxNumberInput extends Components.IxNumberInput {


@ProxyCmp({
inputs: ['advanced', 'ariaLabelChevronLeftIconButton', 'ariaLabelChevronRightIconButton', 'ariaLabelPageSelection', 'count', 'hideItemCount', 'i18nItems', 'i18nOf', 'i18nPage', 'itemCount', 'selectedPage']
inputs: ['advanced', 'ariaLabelChevronLeftIconButton', 'ariaLabelChevronRightIconButton', 'ariaLabelPageSelection', 'count', 'hideItemCount', 'i18nItems', 'i18nOf', 'i18nPage', 'itemCount', 'itemCountOptions', 'selectedPage']
})
@Component({
selector: 'ix-pagination',
changeDetection: ChangeDetectionStrategy.OnPush,
template: '<ng-content></ng-content>',
// eslint-disable-next-line @angular-eslint/no-inputs-metadata-property
inputs: ['advanced', 'ariaLabelChevronLeftIconButton', 'ariaLabelChevronRightIconButton', 'ariaLabelPageSelection', 'count', 'hideItemCount', 'i18nItems', 'i18nOf', 'i18nPage', 'itemCount', 'selectedPage'],
inputs: ['advanced', 'ariaLabelChevronLeftIconButton', 'ariaLabelChevronRightIconButton', 'ariaLabelPageSelection', 'count', 'hideItemCount', 'i18nItems', 'i18nOf', 'i18nPage', 'itemCount', 'itemCountOptions', 'selectedPage'],
outputs: ['pageSelected', 'itemCountChanged'],
standalone: false
})
Expand Down
4 changes: 2 additions & 2 deletions packages/angular/standalone/src/components.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2144,14 +2144,14 @@ export declare interface IxNumberInput extends Components.IxNumberInput {

@ProxyCmp({
defineCustomElementFn: defineIxPagination,
inputs: ['advanced', 'ariaLabelChevronLeftIconButton', 'ariaLabelChevronRightIconButton', 'ariaLabelPageSelection', 'count', 'hideItemCount', 'i18nItems', 'i18nOf', 'i18nPage', 'itemCount', 'selectedPage']
inputs: ['advanced', 'ariaLabelChevronLeftIconButton', 'ariaLabelChevronRightIconButton', 'ariaLabelPageSelection', 'count', 'hideItemCount', 'i18nItems', 'i18nOf', 'i18nPage', 'itemCount', 'itemCountOptions', 'selectedPage']
})
@Component({
selector: 'ix-pagination',
changeDetection: ChangeDetectionStrategy.OnPush,
template: '<ng-content></ng-content>',
// eslint-disable-next-line @angular-eslint/no-inputs-metadata-property
inputs: ['advanced', 'ariaLabelChevronLeftIconButton', 'ariaLabelChevronRightIconButton', 'ariaLabelPageSelection', 'count', 'hideItemCount', 'i18nItems', 'i18nOf', 'i18nPage', 'itemCount', 'selectedPage'],
inputs: ['advanced', 'ariaLabelChevronLeftIconButton', 'ariaLabelChevronRightIconButton', 'ariaLabelPageSelection', 'count', 'hideItemCount', 'i18nItems', 'i18nOf', 'i18nPage', 'itemCount', 'itemCountOptions', 'selectedPage'],
outputs: ['pageSelected', 'itemCountChanged'],
})
export class IxPagination {
Expand Down
10 changes: 10 additions & 0 deletions packages/core/src/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2755,6 +2755,11 @@ export namespace Components {
* @default 15
*/
"itemCount": number;
/**
* Custom item count options for advanced mode. Provide an array of numbers to display in the items per page dropdown. If not provided or empty, defaults to [10, 15, 20, 40, 100]. Only positive integers greater than 0 are valid. Invalid values and duplicates are automatically filtered out.
* @since 4.3.0
*/
"itemCountOptions"?: number[];
/**
* Zero based index of currently selected page
* @default 0
Expand Down Expand Up @@ -8810,6 +8815,11 @@ declare namespace LocalJSX {
* @default 15
*/
"itemCount"?: number;
/**
* Custom item count options for advanced mode. Provide an array of numbers to display in the items per page dropdown. If not provided or empty, defaults to [10, 15, 20, 40, 100]. Only positive integers greater than 0 are valid. Invalid values and duplicates are automatically filtered out.
* @since 4.3.0
*/
"itemCountOptions"?: number[];
/**
* Item count change event
*/
Expand Down
108 changes: 103 additions & 5 deletions packages/core/src/components/pagination/pagination.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
h,
Host,
Prop,
Watch,
} from '@stencil/core';
import { BaseButton, BaseButtonProps } from '../button/base-button';
import { a11yBoolean } from '../utils/a11y';
Expand All @@ -41,6 +42,9 @@ export class Pagination {
};

private readonly maxCountPages = 7;
private readonly defaultItemCountOptions = [10, 15, 20, 40, 100];
private readonly itemCountOptionsPropName = 'itemCountOptions';
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can be hard coded in the console.warnings

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can be hard coded in the console.warnings

private verifiedItemCountOptions?: number[];

@Element() hostElement!: HTMLIxPaginationElement;

Expand All @@ -60,6 +64,16 @@ export class Pagination {
*/
@Prop() hideItemCount = false;

/**
* Custom item count options for advanced mode.
* Provide an array of numbers to display in the items per page dropdown.
* If not provided or empty, defaults to [10, 15, 20, 40, 100].
* Only positive integers greater than 0 are valid. Invalid values and duplicates are automatically filtered out.
*
* @since 4.3.0
*/
@Prop() itemCountOptions?: number[];

/**
* Total number of pages
*/
Expand Down Expand Up @@ -118,6 +132,87 @@ export class Pagination {
*/
@Event() itemCountChanged!: EventEmitter<number>;

@Watch('itemCountOptions')
onItemCountOptionsChange() {
if (this.advanced && !this.hideItemCount) {
this.verifiedItemCountOptions = this.getValidItemCountOptions();
this.verifyEmptyItemCountOptions();
this.verifyAllInvalidItemCountOptions();
this.verifyItemCountMismatch();
}
}

componentWillLoad() {
if (!this.advanced || this.hideItemCount) {
return;
}

this.verifiedItemCountOptions = this.getValidItemCountOptions();
this.verifyEmptyItemCountOptions();
this.verifyAllInvalidItemCountOptions();
this.verifyItemCountMismatch();
}
Comment on lines +135 to +154
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The logic within onItemCountOptionsChange and componentWillLoad is duplicated. To improve maintainability and prevent potential inconsistencies, this logic should be extracted into a single private method.

  private _verifyItemCountOptions() {
    if (!this.advanced || this.hideItemCount) {
      return;
    }

    this.verifiedItemCountOptions = this.getValidItemCountOptions();
    this.verifyEmptyItemCountOptions();
    this.verifyAllInvalidItemCountOptions();
    this.verifyItemCountMismatch();
  }

  @Watch('itemCountOptions')
  onItemCountOptionsChange() {
    this._verifyItemCountOptions();
  }

  componentWillLoad() {
    this._verifyItemCountOptions();
  }


private verifyEmptyItemCountOptions(): void {
if (this.itemCountOptions?.length !== 0) {
return;
}

console.warn(
`[ix-pagination] ${this.itemCountOptionsPropName} is an empty array. Falling back to default options: [${this.defaultItemCountOptions.join(', ')}]`
);
}

private verifyAllInvalidItemCountOptions(): void {
if (
!this.itemCountOptions?.length ||
(this.verifiedItemCountOptions?.length ?? 0) > 0
) {
return;
}

console.warn(
`[ix-pagination] All values in ${this.itemCountOptionsPropName} are invalid. ` +
`Only positive integers are allowed. Falling back to default options: [${this.defaultItemCountOptions.join(', ')}]`
);
this.verifiedItemCountOptions = this.defaultItemCountOptions;
}

private verifyItemCountMismatch(): void {
if (!this.verifiedItemCountOptions?.length) {
return;
}

if (this.verifiedItemCountOptions.includes(this.itemCount)) {
return;
}

const displayOptions =
this.verifiedItemCountOptions.length > 5
? `${this.verifiedItemCountOptions.slice(0, 5).join(', ')} ...`
: this.verifiedItemCountOptions.join(', ');

console.warn(
`[ix-pagination] Configuration mismatch: itemCount value "${
this.itemCount
}" is not present in ${this.itemCountOptionsPropName} [${displayOptions}]. ` +
`This will result in an invalid dropdown state. Please either add ${this.itemCount} to ${this.itemCountOptionsPropName} or set itemCount to one of the available options.`
);
}

private getValidItemCountOptions(): number[] {
return Array.from(
new Set(
(this.itemCountOptions?.length
? this.itemCountOptions
: this.defaultItemCountOptions
)
.filter((option) => option > 0 && Number.isInteger(option))
.sort((current, next) => current - next)
)
);
}

private selectPage(index: number) {
const oldIndex = this.selectedPage;

Expand Down Expand Up @@ -312,11 +407,14 @@ export class Pagination {
this.itemCountChanged.emit(count);
}}
>
<ix-select-item label="10" value="10"></ix-select-item>
<ix-select-item label="15" value="15"></ix-select-item>
<ix-select-item label="20" value="20"></ix-select-item>
<ix-select-item label="40" value="40"></ix-select-item>
<ix-select-item label="100" value="100"></ix-select-item>
{(
this.verifiedItemCountOptions || this.getValidItemCountOptions()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Calling getValidItemCountOptions() within the render function can lead to performance degradation, as it will be executed on every render cycle. The verifiedItemCountOptions property is initialized in componentWillLoad and updated by a watcher, ensuring it holds the correct value when the dropdown is rendered. You can safely rely on verifiedItemCountOptions directly.

                this.verifiedItemCountOptions

).map((option) => (
<ix-select-item
label={`${option}`}
value={`${option}`}
></ix-select-item>
))}
</ix-select>
</span>
)}
Expand Down
102 changes: 102 additions & 0 deletions packages/core/src/components/pagination/test/pagination.ct.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,3 +164,105 @@ regressionTest(
expect(await pageSelected$).toBe(9);
}
);

regressionTest(
'should use custom itemCountOptions',
async ({ mount, page }) => {
await mount(`
<ix-pagination advanced count="10">
</ix-pagination>
`);
const pagination = page.locator('ix-pagination');

await pagination.evaluate((elm: HTMLIxPaginationElement) => {
elm.itemCountOptions = [5, 25, 50, 100];
});

await pagination.getByRole('button').nth(-1).click();

const dropdownItems = pagination.locator('ix-dropdown-item');
const expectedValues = ['5', '25', '50', '100'];
await expect(dropdownItems).toHaveCount(expectedValues.length);

for (let index = 0; index < expectedValues.length; index++) {
await expect(dropdownItems.nth(index)).toContainText(
expectedValues[index]
);
}
}
);

regressionTest(
'should sort itemCountOptions in ascending order',
async ({ mount, page }) => {
await mount(`
<ix-pagination advanced count="10">
</ix-pagination>
`);
const pagination = page.locator('ix-pagination');

await pagination.evaluate((elm: HTMLIxPaginationElement) => {
elm.itemCountOptions = [100, 5, 50, 25];
});

await pagination.getByRole('button').nth(-1).click();

const dropdownItems = pagination.locator('ix-dropdown-item');
const expectedValues = ['5', '25', '50', '100'];

for (let index = 0; index < expectedValues.length; index++) {
await expect(dropdownItems.nth(index)).toContainText(
expectedValues[index]
);
}
}
);

regressionTest(
'should filter out zero and negative values from itemCountOptions',
async ({ mount, page }) => {
await mount(`
<ix-pagination advanced count="10">
</ix-pagination>
`);
const pagination = page.locator('ix-pagination');

await pagination.evaluate((elm: HTMLIxPaginationElement) => {
elm.itemCountOptions = [0, -5, 10, 20, -1];
elm.count = 11;
});
await pagination.getByRole('button').nth(-1).click();

const dropdownItems = pagination.locator('ix-dropdown-item');
await expect(dropdownItems).toHaveCount(2);
await expect(dropdownItems.nth(0)).toContainText('10');
await expect(dropdownItems.nth(1)).toContainText('20');
}
);

regressionTest(
'should use default options when itemCountOptions is empty',
async ({ mount, page }) => {
await mount(`
<ix-pagination advanced count="10">
</ix-pagination>
`);
const pagination = page.locator('ix-pagination');

await pagination.evaluate((elm: HTMLIxPaginationElement) => {
elm.itemCountOptions = [];
});

await pagination.getByRole('button').nth(-1).click();

const dropdownItems = pagination.locator('ix-dropdown-item');
const expectedValues = ['10', '15', '20', '40', '100'];
await expect(dropdownItems).toHaveCount(expectedValues.length);

for (let index = 0; index < expectedValues.length; index++) {
await expect(dropdownItems.nth(index)).toContainText(
expectedValues[index]
);
}
}
);
1 change: 1 addition & 0 deletions packages/react/src/components.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1414,6 +1414,7 @@ export const IxPagination: StencilReactComponent<IxPaginationElement, IxPaginati
advanced: 'advanced',
itemCount: 'item-count',
hideItemCount: 'hide-item-count',
itemCountOptions: 'item-count-options',
count: 'count',
selectedPage: 'selected-page',
i18nPage: 'i18n-page',
Expand Down
1 change: 1 addition & 0 deletions packages/vue/src/components.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1039,6 +1039,7 @@ export const IxPagination: StencilVueComponent<JSX.IxPagination> = /*@__PURE__*/
'advanced',
'itemCount',
'hideItemCount',
'itemCountOptions',
'count',
'selectedPage',
'i18nPage',
Expand Down
Loading