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 picker = await this.pickerCtrl.create(pickerOptions);
|
||||
|
||||
this.isExpanded = true;
|
||||
picker.onDidDismiss().then(() => {
|
||||
this.isExpanded = false;
|
||||
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 picker.present();
|
||||
}
|
||||
@ -300,6 +325,7 @@ export class Datetime implements ComponentInterface {
|
||||
text: this.cancelText,
|
||||
role: 'cancel',
|
||||
handler: () => {
|
||||
this.updateDatetimeValue(this.value);
|
||||
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 { hapticSelectionChanged } from '../../utils/haptic';
|
||||
@ -36,8 +36,18 @@ export class PickerColumnCmp implements ComponentInterface {
|
||||
|
||||
@Prop({ context: 'queue' }) queue!: QueueApi;
|
||||
|
||||
/**
|
||||
* Emitted when the selected value has changed
|
||||
* @internal
|
||||
*/
|
||||
@Event() ionPickerColChange!: EventEmitter<PickerColumn>;
|
||||
|
||||
/** Picker column data */
|
||||
@Prop() col!: PickerColumn;
|
||||
@Watch('col')
|
||||
protected colChanged() {
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
componentWillLoad() {
|
||||
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) {
|
||||
// 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
|
||||
@ -98,6 +112,8 @@ export class PickerColumnCmp implements ComponentInterface {
|
||||
// set what y position we're at
|
||||
cancelAnimationFrame(this.rafId);
|
||||
this.update(y, duration, true);
|
||||
|
||||
this.emitColChange();
|
||||
}
|
||||
|
||||
private update(y: number, duration: number, saveY: boolean) {
|
||||
@ -207,6 +223,9 @@ export class PickerColumnCmp implements ComponentInterface {
|
||||
if (notLockedIn) {
|
||||
// isn't locked in yet, keep decelerating until it is
|
||||
this.rafId = requestAnimationFrame(() => this.decelerate());
|
||||
} else {
|
||||
this.velocity = 0;
|
||||
this.emitColChange();
|
||||
}
|
||||
|
||||
} else if (this.y % this.optHeight !== 0) {
|
||||
@ -277,10 +296,12 @@ export class PickerColumnCmp implements ComponentInterface {
|
||||
if (this.bounceFrom > 0) {
|
||||
// bounce back up
|
||||
this.update(this.minY, 100, true);
|
||||
this.emitColChange();
|
||||
return;
|
||||
} else if (this.bounceFrom < 0) {
|
||||
// bounce back down
|
||||
this.update(this.maxY, 100, true);
|
||||
this.emitColChange();
|
||||
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);
|
||||
if (this.col.prevSelected !== selectedIndex || forceRefresh) {
|
||||
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, '')
|
||||
.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