mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2025-08-20 12:29:55 +08:00
fix(datetime): recalculate day column when month is changed (#17846)
Co-Authored-By: KillerCodeMonkey <bengtler@gmail.com> Co-Authored-By: olivercodes <boliver@linux.com> Co-Authored-By: liamdebeasi <liamdebeasi@users.noreply.github.com>
This commit is contained in:
@ -261,11 +261,36 @@ export class Datetime implements ComponentInterface {
|
|||||||
|
|
||||||
const pickerOptions = this.generatePickerOptions();
|
const pickerOptions = this.generatePickerOptions();
|
||||||
const picker = await this.pickerCtrl.create(pickerOptions);
|
const picker = await this.pickerCtrl.create(pickerOptions);
|
||||||
|
|
||||||
this.isExpanded = true;
|
this.isExpanded = true;
|
||||||
picker.onDidDismiss().then(() => {
|
picker.onDidDismiss().then(() => {
|
||||||
this.isExpanded = false;
|
this.isExpanded = false;
|
||||||
this.setFocus();
|
this.setFocus();
|
||||||
});
|
});
|
||||||
|
picker.addEventListener('ionPickerColChange', async (event: any) => {
|
||||||
|
const data = event.detail;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Don't bother checking for non-dates as things like hours or minutes
|
||||||
|
* are always going to have the same number of column options
|
||||||
|
*/
|
||||||
|
if (data.name !== 'month' && data.name !== 'day' && data.name !== 'year') { return; }
|
||||||
|
|
||||||
|
const colSelectedIndex = data.selectedIndex;
|
||||||
|
const colOptions = data.options;
|
||||||
|
|
||||||
|
const changeData: any = {};
|
||||||
|
changeData[data.name] = {
|
||||||
|
value: colOptions[colSelectedIndex].value
|
||||||
|
};
|
||||||
|
|
||||||
|
this.updateDatetimeValue(changeData);
|
||||||
|
const columns = this.generateColumns();
|
||||||
|
|
||||||
|
picker.columns = columns;
|
||||||
|
|
||||||
|
await this.validate(picker);
|
||||||
|
});
|
||||||
await this.validate(picker);
|
await this.validate(picker);
|
||||||
await picker.present();
|
await picker.present();
|
||||||
}
|
}
|
||||||
@ -300,6 +325,7 @@ export class Datetime implements ComponentInterface {
|
|||||||
text: this.cancelText,
|
text: this.cancelText,
|
||||||
role: 'cancel',
|
role: 'cancel',
|
||||||
handler: () => {
|
handler: () => {
|
||||||
|
this.updateDatetimeValue(this.value);
|
||||||
this.ionCancel.emit();
|
this.ionCancel.emit();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { Component, ComponentInterface, Element, Prop, QueueApi } from '@stencil/core';
|
import { Component, ComponentInterface, Element, Event, EventEmitter, Prop, QueueApi, Watch } from '@stencil/core';
|
||||||
|
|
||||||
import { Gesture, GestureDetail, Mode, PickerColumn } from '../../interface';
|
import { Gesture, GestureDetail, Mode, PickerColumn } from '../../interface';
|
||||||
import { hapticSelectionChanged } from '../../utils/haptic';
|
import { hapticSelectionChanged } from '../../utils/haptic';
|
||||||
@ -36,8 +36,18 @@ export class PickerColumnCmp implements ComponentInterface {
|
|||||||
|
|
||||||
@Prop({ context: 'queue' }) queue!: QueueApi;
|
@Prop({ context: 'queue' }) queue!: QueueApi;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emitted when the selected value has changed
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
@Event() ionPickerColChange!: EventEmitter<PickerColumn>;
|
||||||
|
|
||||||
/** Picker column data */
|
/** Picker column data */
|
||||||
@Prop() col!: PickerColumn;
|
@Prop() col!: PickerColumn;
|
||||||
|
@Watch('col')
|
||||||
|
protected colChanged() {
|
||||||
|
this.refresh();
|
||||||
|
}
|
||||||
|
|
||||||
componentWillLoad() {
|
componentWillLoad() {
|
||||||
let pickerRotateFactor = 0;
|
let pickerRotateFactor = 0;
|
||||||
@ -88,6 +98,10 @@ export class PickerColumnCmp implements ComponentInterface {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private emitColChange() {
|
||||||
|
this.ionPickerColChange.emit(this.col);
|
||||||
|
}
|
||||||
|
|
||||||
private setSelected(selectedIndex: number, duration: number) {
|
private setSelected(selectedIndex: number, duration: number) {
|
||||||
// if there is a selected index, then figure out it's y position
|
// if there is a selected index, then figure out it's y position
|
||||||
// if there isn't a selected index, then just use the top y position
|
// if there isn't a selected index, then just use the top y position
|
||||||
@ -98,6 +112,8 @@ export class PickerColumnCmp implements ComponentInterface {
|
|||||||
// set what y position we're at
|
// set what y position we're at
|
||||||
cancelAnimationFrame(this.rafId);
|
cancelAnimationFrame(this.rafId);
|
||||||
this.update(y, duration, true);
|
this.update(y, duration, true);
|
||||||
|
|
||||||
|
this.emitColChange();
|
||||||
}
|
}
|
||||||
|
|
||||||
private update(y: number, duration: number, saveY: boolean) {
|
private update(y: number, duration: number, saveY: boolean) {
|
||||||
@ -207,6 +223,9 @@ export class PickerColumnCmp implements ComponentInterface {
|
|||||||
if (notLockedIn) {
|
if (notLockedIn) {
|
||||||
// isn't locked in yet, keep decelerating until it is
|
// isn't locked in yet, keep decelerating until it is
|
||||||
this.rafId = requestAnimationFrame(() => this.decelerate());
|
this.rafId = requestAnimationFrame(() => this.decelerate());
|
||||||
|
} else {
|
||||||
|
this.velocity = 0;
|
||||||
|
this.emitColChange();
|
||||||
}
|
}
|
||||||
|
|
||||||
} else if (this.y % this.optHeight !== 0) {
|
} else if (this.y % this.optHeight !== 0) {
|
||||||
@ -277,10 +296,12 @@ export class PickerColumnCmp implements ComponentInterface {
|
|||||||
if (this.bounceFrom > 0) {
|
if (this.bounceFrom > 0) {
|
||||||
// bounce back up
|
// bounce back up
|
||||||
this.update(this.minY, 100, true);
|
this.update(this.minY, 100, true);
|
||||||
|
this.emitColChange();
|
||||||
return;
|
return;
|
||||||
} else if (this.bounceFrom < 0) {
|
} else if (this.bounceFrom < 0) {
|
||||||
// bounce back down
|
// bounce back down
|
||||||
this.update(this.maxY, 100, true);
|
this.update(this.maxY, 100, true);
|
||||||
|
this.emitColChange();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -308,6 +329,15 @@ export class PickerColumnCmp implements ComponentInterface {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Only update selected value if column has a
|
||||||
|
* velocity of 0. If it does not, then the
|
||||||
|
* column is animating might land on
|
||||||
|
* a value different than the value at
|
||||||
|
* selectedIndex
|
||||||
|
*/
|
||||||
|
if (this.velocity !== 0) { return; }
|
||||||
|
|
||||||
const selectedIndex = clamp(min, this.col.selectedIndex || 0, max);
|
const selectedIndex = clamp(min, this.col.selectedIndex || 0, max);
|
||||||
if (this.col.prevSelected !== selectedIndex || forceRefresh) {
|
if (this.col.prevSelected !== selectedIndex || forceRefresh) {
|
||||||
const y = (selectedIndex * this.optHeight) * -1;
|
const y = (selectedIndex * this.optHeight) * -1;
|
||||||
|
19
core/src/components/picker-column/test/standalone/e2e.ts
Normal file
19
core/src/components/picker-column/test/standalone/e2e.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import { testPickerColumn } from '../test.utils';
|
||||||
|
|
||||||
|
const TEST_TYPE = 'standalone';
|
||||||
|
|
||||||
|
test('picker-column: standalone', async () => {
|
||||||
|
await testPickerColumn(TEST_TYPE, '#single-column-button');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('picker-column:multi-column standalone', async () => {
|
||||||
|
await testPickerColumn(TEST_TYPE, '#multiple-column-button');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('picker-column:rtl: standalone', async () => {
|
||||||
|
await testPickerColumn(TEST_TYPE, '#single-column-button', true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('picker-column:multi-column:rtl standalone', async () => {
|
||||||
|
await testPickerColumn(TEST_TYPE, '#multiple-column-button', true);
|
||||||
|
});
|
92
core/src/components/picker-column/test/standalone/index.html
Normal file
92
core/src/components/picker-column/test/standalone/index.html
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html dir="ltr">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Picker Column - Standalone</title>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||||||
|
<link href="../../../../../css/core.css" rel="stylesheet">
|
||||||
|
<link href="../../../../../scripts/testing/styles.css" rel="stylesheet">
|
||||||
|
<script src="../../../../../scripts/testing/scripts.js"></script>
|
||||||
|
<script src="../../../../../dist/ionic.js"></script>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<ion-picker-controller></ion-picker-controller>
|
||||||
|
<ion-button onclick="openPicker()" id="single-column-button">Open Single Column Picker</ion-button>
|
||||||
|
<ion-button onclick="openPicker(2, 5, multiColumnOptions)" id="multiple-column-button">Open Multi Column Picker</ion-button>
|
||||||
|
<script>
|
||||||
|
const pickerController = document.querySelector('ion-picker-controller');
|
||||||
|
const defaultColumnOptions = [
|
||||||
|
[
|
||||||
|
'Dog',
|
||||||
|
'Cat',
|
||||||
|
'Bird',
|
||||||
|
'Lizard',
|
||||||
|
'Chinchilla'
|
||||||
|
]
|
||||||
|
]
|
||||||
|
|
||||||
|
const multiColumnOptions = [
|
||||||
|
[
|
||||||
|
'Minified',
|
||||||
|
'Responsive',
|
||||||
|
'Full Stack',
|
||||||
|
'Mobile First',
|
||||||
|
'Serverless'
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'Tomato',
|
||||||
|
'Avocado',
|
||||||
|
'Onion',
|
||||||
|
'Potato',
|
||||||
|
'Artichoke'
|
||||||
|
]
|
||||||
|
]
|
||||||
|
|
||||||
|
async function openPicker(numColumns = 1, numOptions = 5, columnOptions = defaultColumnOptions) {
|
||||||
|
const picker = await pickerController.create({
|
||||||
|
columns: this.getColumns(numColumns, numOptions, columnOptions),
|
||||||
|
buttons: [
|
||||||
|
{
|
||||||
|
text: 'Cancel',
|
||||||
|
role: 'cancel'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Confirm',
|
||||||
|
handler: (value) => {
|
||||||
|
console.log(`Got Value ${value}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
await picker.present();
|
||||||
|
}
|
||||||
|
|
||||||
|
function getColumns(numColumns, numOptions, columnOptions) {
|
||||||
|
let columns = [];
|
||||||
|
for (let i = 0; i < numColumns; i++) {
|
||||||
|
columns.push({
|
||||||
|
name: `col-${i}`,
|
||||||
|
options: this.getColumnOptions(i, numOptions, columnOptions)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return columns;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getColumnOptions(columnIndex, numOptions, columnOptions) {
|
||||||
|
let options = [];
|
||||||
|
for (let i = 0; i < numOptions; i++) {
|
||||||
|
options.push({
|
||||||
|
text: columnOptions[columnIndex][i % numOptions],
|
||||||
|
value: i
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return options;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
60
core/src/components/picker-column/test/test.utils.ts
Normal file
60
core/src/components/picker-column/test/test.utils.ts
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
import { newE2EPage } from '@stencil/core/testing';
|
||||||
|
|
||||||
|
import { cleanScreenshotName, dragElementBy, generateE2EUrl, listenForEvent, waitForFunctionTestContext } from '../../../utils/test/utils';
|
||||||
|
|
||||||
|
export async function testPickerColumn(
|
||||||
|
type: string,
|
||||||
|
selector: string,
|
||||||
|
rtl = false,
|
||||||
|
screenshotName: string = cleanScreenshotName(selector)
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const pageUrl = generateE2EUrl('picker-column', type, rtl);
|
||||||
|
if (rtl) {
|
||||||
|
screenshotName = `${screenshotName} rtl`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const page = await newE2EPage({
|
||||||
|
url: pageUrl
|
||||||
|
});
|
||||||
|
|
||||||
|
const screenshotCompares = [];
|
||||||
|
|
||||||
|
const openButton = await page.find(selector);
|
||||||
|
await openButton.click();
|
||||||
|
await page.waitFor(250);
|
||||||
|
|
||||||
|
screenshotCompares.push(await page.compareScreenshot(`${screenshotName}`));
|
||||||
|
|
||||||
|
// Setup counter
|
||||||
|
let colChangeCounter: any;
|
||||||
|
|
||||||
|
// Expose an event callback method
|
||||||
|
const COL_CHANGE = 'onIonPickerColChange';
|
||||||
|
await page.exposeFunction(COL_CHANGE, () => {
|
||||||
|
colChangeCounter.count += 1;
|
||||||
|
});
|
||||||
|
|
||||||
|
const columns = await page.$$('ion-picker-column');
|
||||||
|
for (const column of Array.from(columns)) {
|
||||||
|
colChangeCounter = { count: 0 };
|
||||||
|
|
||||||
|
// Attach a listener to element with a callback
|
||||||
|
await listenForEvent(page, 'ionPickerColChange', column, COL_CHANGE);
|
||||||
|
|
||||||
|
// Simulate a column drag
|
||||||
|
await dragElementBy(column, page, 0, 100);
|
||||||
|
|
||||||
|
// Wait for ionPickerColChange event to be emitted once
|
||||||
|
await waitForFunctionTestContext((payload: any) => {
|
||||||
|
return payload.colChangeCounter.count === 1;
|
||||||
|
}, { colChangeCounter });
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const screenshotCompare of screenshotCompares) {
|
||||||
|
expect(screenshotCompare).toMatchScreenshot();
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
@ -13,3 +13,77 @@ export function cleanScreenshotName(screenshotName: string): string {
|
|||||||
.replace(/[^0-9a-zA-Z\s]/gi, '')
|
.replace(/[^0-9a-zA-Z\s]/gi, '')
|
||||||
.toLowerCase();
|
.toLowerCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Listens for an event and fires a callback
|
||||||
|
* @param page - The Puppeteer `page` object
|
||||||
|
* @param eventType: string - The event name to listen for. ex: `ionPickerColChange`
|
||||||
|
* @param element: HTMLElement - An HTML element
|
||||||
|
* @param callbackName: string - The name of the callback function to
|
||||||
|
* call when the event is fired.
|
||||||
|
*
|
||||||
|
* Note: The callback function must be added using
|
||||||
|
* page.exposeFunction prior to calling this function.
|
||||||
|
*/
|
||||||
|
export const listenForEvent = async (page: any, eventType: string, element: any, callbackName: string): Promise<any> => {
|
||||||
|
try {
|
||||||
|
return await page.evaluate((scopeEventType: string, scopeElement: any, scopeCallbackName: string) => {
|
||||||
|
scopeElement.addEventListener(scopeEventType, (e: any) => {
|
||||||
|
(window as any)[scopeCallbackName](e);
|
||||||
|
});
|
||||||
|
}, eventType, element, callbackName);
|
||||||
|
} catch (err) {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Drags an element by (x, y) pixels
|
||||||
|
* @param element: HTMLElement - The HTML Element to drag
|
||||||
|
* @param page - The Puppeteer 'page' object
|
||||||
|
* @param x: number - Amount to drag `element` by on the x-axis
|
||||||
|
* @param y: number - Amount to drag `element` by on the y-axis
|
||||||
|
*/
|
||||||
|
export const dragElementBy = async (element: any, page: any, x = 0, y = 0): Promise<void> => {
|
||||||
|
try {
|
||||||
|
const boundingBox = await element.boundingBox();
|
||||||
|
|
||||||
|
const startX = boundingBox.x + boundingBox.width / 2;
|
||||||
|
const startY = boundingBox.y + boundingBox.height / 2;
|
||||||
|
|
||||||
|
const endX = startX + x;
|
||||||
|
const endY = startY + y;
|
||||||
|
|
||||||
|
const midX = endX / 2;
|
||||||
|
const midY = endY / 2;
|
||||||
|
|
||||||
|
await page.mouse.move(startX, startY);
|
||||||
|
await page.mouse.down();
|
||||||
|
await page.mouse.move(midX, midY);
|
||||||
|
await page.mouse.move(endX, endY);
|
||||||
|
await page.mouse.up();
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wait for a function to return true
|
||||||
|
* This method runs in the context of the
|
||||||
|
* test whereas page.waitForFunction runs in
|
||||||
|
* the context of the browser
|
||||||
|
* @param fn - The function to run
|
||||||
|
* @param params: any - Any parameters that the fn needs
|
||||||
|
* @param interval: number - Interval to run setInterval on
|
||||||
|
*/
|
||||||
|
export const waitForFunctionTestContext = async (fn: any, params: any, interval = 16): Promise<any> => {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
const intervalId = setInterval(() => {
|
||||||
|
if (fn(params)) {
|
||||||
|
clearInterval(intervalId);
|
||||||
|
return resolve();
|
||||||
|
}
|
||||||
|
}, interval);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
Reference in New Issue
Block a user