Skip to content
Merged
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
26 changes: 26 additions & 0 deletions core/src/components/datetime/datetime.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
Expand Down Expand Up @@ -300,6 +325,7 @@ export class Datetime implements ComponentInterface {
text: this.cancelText,
role: 'cancel',
handler: () => {
this.updateDatetimeValue(this.value);
this.ionCancel.emit();
}
},
Expand Down
32 changes: 31 additions & 1 deletion core/src/components/picker-column/picker-column.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand All @@ -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) {
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -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;
Expand Down
19 changes: 19 additions & 0 deletions core/src/components/picker-column/test/standalone/e2e.ts
Original file line number Diff line number Diff line change
@@ -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 changes: 92 additions & 0 deletions core/src/components/picker-column/test/standalone/index.html
Original file line number Diff line number Diff line change
@@ -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 changes: 60 additions & 0 deletions core/src/components/picker-column/test/test.utils.ts
Original file line number Diff line number Diff line change
@@ -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;
}
}
74 changes: 74 additions & 0 deletions core/src/utils/test/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
};