chore(): sync with main
@ -33,6 +33,14 @@ export class PickerColumnCmp implements ComponentInterface {
|
||||
private rafId?: ReturnType<typeof requestAnimationFrame>;
|
||||
private tmrId?: ReturnType<typeof setTimeout>;
|
||||
private noAnimate = true;
|
||||
// `colDidChange` is a flag that gets set when the column is changed
|
||||
// dynamically. When this flag is set, the column will refresh
|
||||
// after the component re-renders to incorporate the new column data.
|
||||
// This is necessary because `this.refresh` queries for the option elements,
|
||||
// so it needs to wait for the latest elements to be available in the DOM.
|
||||
// Ex: column is created with 3 options. User updates the column data
|
||||
// to have 5 options. The column will still think it only has 3 options.
|
||||
private colDidChange = false;
|
||||
|
||||
@Element() el!: HTMLElement;
|
||||
|
||||
@ -46,7 +54,7 @@ export class PickerColumnCmp implements ComponentInterface {
|
||||
@Prop() col!: PickerColumn;
|
||||
@Watch('col')
|
||||
protected colChanged() {
|
||||
this.refresh();
|
||||
this.colDidChange = true;
|
||||
}
|
||||
|
||||
async connectedCallback() {
|
||||
@ -74,21 +82,32 @@ export class PickerColumnCmp implements ComponentInterface {
|
||||
onEnd: (ev) => this.onEnd(ev),
|
||||
});
|
||||
this.gesture.enable();
|
||||
// Options have not been initialized yet
|
||||
// Animation must be disabled through the `noAnimate` flag
|
||||
// Otherwise, the options will render
|
||||
// at the top of the column and transition down
|
||||
this.tmrId = setTimeout(() => {
|
||||
this.noAnimate = false;
|
||||
// After initialization, `refresh()` will be called
|
||||
// At this point, animation will be enabled. The options will
|
||||
// animate as they are being selected.
|
||||
this.refresh(true);
|
||||
}, 250);
|
||||
}
|
||||
|
||||
componentDidLoad() {
|
||||
const colEl = this.optsEl;
|
||||
if (colEl) {
|
||||
// DOM READ
|
||||
// We perfom a DOM read over a rendered item, this needs to happen after the first render
|
||||
this.optHeight = colEl.firstElementChild ? colEl.firstElementChild.clientHeight : 0;
|
||||
}
|
||||
this.onDomChange();
|
||||
}
|
||||
|
||||
this.refresh();
|
||||
componentDidUpdate() {
|
||||
// Options may have changed since last update.
|
||||
if (this.colDidChange) {
|
||||
// Animation must be disabled through the `onDomChange` parameter.
|
||||
// Otherwise, the recently added options will render
|
||||
// at the top of the column and transition down
|
||||
this.onDomChange(true, false);
|
||||
this.colDidChange = false;
|
||||
}
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
@ -331,7 +350,7 @@ export class PickerColumnCmp implements ComponentInterface {
|
||||
}
|
||||
}
|
||||
|
||||
private refresh(forceRefresh?: boolean) {
|
||||
private refresh(forceRefresh?: boolean, animated?: boolean) {
|
||||
let min = this.col.options.length - 1;
|
||||
let max = 0;
|
||||
const options = this.col.options;
|
||||
@ -356,11 +375,22 @@ export class PickerColumnCmp implements ComponentInterface {
|
||||
const selectedIndex = clamp(min, this.col.selectedIndex ?? 0, max);
|
||||
if (this.col.prevSelected !== selectedIndex || forceRefresh) {
|
||||
const y = selectedIndex * this.optHeight * -1;
|
||||
const duration = animated ? TRANSITION_DURATION : 0;
|
||||
this.velocity = 0;
|
||||
this.update(y, TRANSITION_DURATION, true);
|
||||
this.update(y, duration, true);
|
||||
}
|
||||
}
|
||||
|
||||
private onDomChange(forceRefresh?: boolean, animated?: boolean) {
|
||||
const colEl = this.optsEl;
|
||||
if (colEl) {
|
||||
// DOM READ
|
||||
// We perfom a DOM read over a rendered item, this needs to happen after the first render or after the the column has changed
|
||||
this.optHeight = colEl.firstElementChild ? colEl.firstElementChild.clientHeight : 0;
|
||||
}
|
||||
this.refresh(forceRefresh, animated);
|
||||
}
|
||||
|
||||
render() {
|
||||
const col = this.col;
|
||||
const mode = getIonMode(this);
|
||||
|
||||
@ -0,0 +1,32 @@
|
||||
import { h } from '@stencil/core';
|
||||
import { newSpecPage } from '@stencil/core/testing';
|
||||
|
||||
import { PickerColumnCmp } from '../picker-column';
|
||||
|
||||
describe('picker-column: dynamic options', () => {
|
||||
/**
|
||||
* Issue: https://github.com/ionic-team/ionic-framework/issues/21763
|
||||
*/
|
||||
it('should add an option', async () => {
|
||||
const defaultOptions = [
|
||||
{ text: 'Dog', value: 'dog' },
|
||||
{ text: 'Cat', value: 'cat' },
|
||||
];
|
||||
|
||||
const page = await newSpecPage({
|
||||
components: [PickerColumnCmp],
|
||||
template: () => <ion-picker-column col={{ options: defaultOptions }}></ion-picker-column>,
|
||||
});
|
||||
|
||||
const pickerCol = page.body.querySelector('ion-picker-column');
|
||||
|
||||
pickerCol.col = {
|
||||
options: [...defaultOptions, { text: 'Carrot', value: 'carrot' }],
|
||||
};
|
||||
|
||||
await page.waitForChanges();
|
||||
|
||||
const pickerOpt = pickerCol.querySelector('.picker-opt:nth(2)');
|
||||
expect(pickerOpt.getAttribute('style')).toContain('transform');
|
||||
});
|
||||
});
|
||||
@ -1,22 +1,20 @@
|
||||
import { expect } from '@playwright/test';
|
||||
import { configs, test } from '@utils/test/playwright';
|
||||
|
||||
import { testPickerColumn } from '../test.utils';
|
||||
|
||||
configs().forEach(({ title, screenshot, config }) => {
|
||||
configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) => {
|
||||
test.describe(title('picker-column'), () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
test('should present picker without ion-app', async ({ page }) => {
|
||||
await page.goto('/src/components/picker-column/test/standalone', config);
|
||||
});
|
||||
test.describe('single column', () => {
|
||||
test('should not have any visual regressions', async ({ page }) => {
|
||||
await testPickerColumn(page, screenshot, '#single-column-button', 'single');
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('multiple columns', () => {
|
||||
test('should not have any visual regressions', async ({ page }) => {
|
||||
await testPickerColumn(page, screenshot, '#multiple-column-button', 'multiple');
|
||||
});
|
||||
const ionPickerDidPresent = await page.spyOnEvent('ionPickerDidPresent');
|
||||
|
||||
const picker = page.locator('ion-picker');
|
||||
|
||||
await page.click('#single-column-button');
|
||||
|
||||
await ionPickerDidPresent.next();
|
||||
|
||||
await expect(picker).toBeVisible();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
Before Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 11 KiB |