Skip to content
Permalink
Browse files

fix(datetime): recalculate day column when month or year is changed (#…

…17815)

Co-Authored-By: KillerCodeMonkey<bengtler@gmail.com>
Co-Authored-By: olivercodes <boliver@linux.com>
Co-Authored-By: liamdebeasi <liamdebeasi@users.noreply.github.com>
  • Loading branch information...
liamdebeasi and olivercodes committed Mar 21, 2019
1 parent aca78f5 commit 9273f97a0cfbb007f20d4977bde3933d6933f9e5
@@ -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;
@@ -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);
});
@@ -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>
@@ -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);
});
};

0 comments on commit 9273f97

Please sign in to comment.
You can’t perform that action at this time.