Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(datetime): add header text to multiple selection; improve header consistency between modes #25817

Merged
merged 18 commits into from
Aug 29, 2022
Merged
Show file tree
Hide file tree
Changes from 15 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
3 changes: 2 additions & 1 deletion angular/src/directives/proxies-list.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@

import * as d from './proxies';

export const DIRECTIVES = [
Expand Down Expand Up @@ -76,5 +77,5 @@ export const DIRECTIVES = [
d.IonThumbnail,
d.IonTitle,
d.IonToggle,
d.IonToolbar,
d.IonToolbar
];
4 changes: 2 additions & 2 deletions angular/src/directives/proxies.ts
Original file line number Diff line number Diff line change
Expand Up @@ -524,14 +524,14 @@ export declare interface IonDatetime extends Components.IonDatetime {

@ProxyCmp({
defineCustomElementFn: undefined,
inputs: ['cancelText', 'clearText', 'color', 'dayValues', 'disabled', 'doneText', 'firstDayOfWeek', 'hourCycle', 'hourValues', 'isDateEnabled', 'locale', 'max', 'min', 'minuteValues', 'mode', 'monthValues', 'multiple', 'name', 'preferWheel', 'presentation', 'readonly', 'showClearButton', 'showDefaultButtons', 'showDefaultTimeLabel', 'showDefaultTitle', 'size', 'value', 'yearValues'],
inputs: ['cancelText', 'clearText', 'color', 'dayValues', 'disabled', 'doneText', 'firstDayOfWeek', 'hourCycle', 'hourValues', 'isDateEnabled', 'locale', 'max', 'min', 'minuteValues', 'mode', 'monthValues', 'multiple', 'name', 'preferWheel', 'presentation', 'readonly', 'showClearButton', 'showDefaultButtons', 'showDefaultTimeLabel', 'showDefaultTitle', 'size', 'titleSelectedDatesFormatter', 'value', 'yearValues'],
methods: ['confirm', 'reset', 'cancel']
})
@Component({
selector: 'ion-datetime',
changeDetection: ChangeDetectionStrategy.OnPush,
template: '<ng-content></ng-content>',
inputs: ['cancelText', 'clearText', 'color', 'dayValues', 'disabled', 'doneText', 'firstDayOfWeek', 'hourCycle', 'hourValues', 'isDateEnabled', 'locale', 'max', 'min', 'minuteValues', 'mode', 'monthValues', 'multiple', 'name', 'preferWheel', 'presentation', 'readonly', 'showClearButton', 'showDefaultButtons', 'showDefaultTimeLabel', 'showDefaultTitle', 'size', 'value', 'yearValues']
inputs: ['cancelText', 'clearText', 'color', 'dayValues', 'disabled', 'doneText', 'firstDayOfWeek', 'hourCycle', 'hourValues', 'isDateEnabled', 'locale', 'max', 'min', 'minuteValues', 'mode', 'monthValues', 'multiple', 'name', 'preferWheel', 'presentation', 'readonly', 'showClearButton', 'showDefaultButtons', 'showDefaultTimeLabel', 'showDefaultTitle', 'size', 'titleSelectedDatesFormatter', 'value', 'yearValues']
})
export class IonDatetime {
protected el: HTMLElement;
Expand Down
1 change: 1 addition & 0 deletions core/api.txt
Original file line number Diff line number Diff line change
Expand Up @@ -397,6 +397,7 @@ ion-datetime,prop,showDefaultButtons,boolean,false,false,false
ion-datetime,prop,showDefaultTimeLabel,boolean,true,false,false
ion-datetime,prop,showDefaultTitle,boolean,false,false,false
ion-datetime,prop,size,"cover" | "fixed",'fixed',false,false
ion-datetime,prop,titleSelectedDatesFormatter,((numberOfDates: number) => string | undefined) | undefined,undefined,false,false
ion-datetime,prop,value,null | string | string[] | undefined,undefined,false,false
ion-datetime,prop,yearValues,number | number[] | string | undefined,undefined,false,false
ion-datetime,method,cancel,cancel(closeOverlay?: boolean) => Promise<void>
Expand Down
14 changes: 11 additions & 3 deletions core/src/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* It contains typing information for all components that exist in this project.
*/
import { HTMLStencilElement, JSXBase } from "@stencil/core/internal";
import { AccordionGroupChangeEventDetail, ActionSheetAttributes, ActionSheetButton, AlertButton, AlertInput, AnimationBuilder, AutocompleteTypes, BreadcrumbCollapsedClickEventDetail, CheckboxChangeEventDetail, Color, ComponentProps, ComponentRef, DatetimeChangeEventDetail, DatetimePresentation, DomRenderFn, FooterHeightFn, FrameworkDelegate, HeaderFn, HeaderHeightFn, InputChangeEventDetail, ItemHeightFn, ItemRenderFn, ItemReorderEventDetail, LoadingAttributes, MenuChangeEventDetail, ModalAttributes, ModalBreakpointChangeEventDetail, ModalHandleBehavior, NavComponent, NavComponentWithProps, NavOptions, OverlayEventDetail, PickerAttributes, PickerButton, PickerColumn, PopoverAttributes, PopoverSize, PositionAlign, PositionReference, PositionSide, RadioGroupChangeEventDetail, RangeChangeEventDetail, RangeKnobMoveEndEventDetail, RangeKnobMoveStartEventDetail, RangeValue, RefresherEventDetail, RouteID, RouterDirection, RouterEventDetail, RouterOutletOptions, RouteWrite, ScrollBaseDetail, ScrollDetail, SearchbarChangeEventDetail, SegmentButtonLayout, SegmentChangeEventDetail, SelectChangeEventDetail, SelectInterface, SelectPopoverOption, Side, SpinnerTypes, StyleEventDetail, SwipeGestureHandler, TabBarChangedEventDetail, TabButtonClickEventDetail, TabButtonLayout, TextareaChangeEventDetail, TextFieldTypes, ToastButton, ToggleChangeEventDetail, TransitionDoneFn, TransitionInstruction, TriggerAction, ViewController } from "./interface";
import { AccordionGroupChangeEventDetail, ActionSheetAttributes, ActionSheetButton, AlertButton, AlertInput, AnimationBuilder, AutocompleteTypes, BreadcrumbCollapsedClickEventDetail, CheckboxChangeEventDetail, Color, ComponentProps, ComponentRef, DatetimeChangeEventDetail, DatetimePresentation, DomRenderFn, FooterHeightFn, FrameworkDelegate, HeaderFn, HeaderHeightFn, InputChangeEventDetail, ItemHeightFn, ItemRenderFn, ItemReorderEventDetail, LoadingAttributes, MenuChangeEventDetail, ModalAttributes, ModalBreakpointChangeEventDetail, ModalHandleBehavior, NavComponent, NavComponentWithProps, NavOptions, OverlayEventDetail, PickerAttributes, PickerButton, PickerColumn, PopoverAttributes, PopoverSize, PositionAlign, PositionReference, PositionSide, RadioGroupChangeEventDetail, RangeChangeEventDetail, RangeKnobMoveEndEventDetail, RangeKnobMoveStartEventDetail, RangeValue, RefresherEventDetail, RouteID, RouterDirection, RouterEventDetail, RouterOutletOptions, RouteWrite, ScrollBaseDetail, ScrollDetail, SearchbarChangeEventDetail, SegmentButtonLayout, SegmentChangeEventDetail, SelectChangeEventDetail, SelectInterface, SelectPopoverOption, Side, SpinnerTypes, StyleEventDetail, SwipeGestureHandler, TabBarChangedEventDetail, TabButtonClickEventDetail, TabButtonLayout, TextareaChangeEventDetail, TextFieldTypes, TitleSelectedDatesFormatter, ToastButton, ToggleChangeEventDetail, TransitionDoneFn, TransitionInstruction, TriggerAction, ViewController } from "./interface";
import { IonicSafeString } from "./utils/sanitization";
import { AlertAttributes } from "./components/alert/alert-interface";
import { CounterFormatter } from "./components/item/item-interface";
Expand Down Expand Up @@ -826,13 +826,17 @@ export namespace Components {
*/
"showDefaultTimeLabel": boolean;
/**
* If `true`, a header will be shown above the calendar picker. On `ios` mode this will include the slotted title, and on `md` mode this will include the slotted title and the selected date.
* If `true`, a header will be shown above the calendar picker. This will include both the slotted title, and the selected date.
*/
"showDefaultTitle": boolean;
/**
* If `cover`, the `ion-datetime` will expand to cover the full width of its container. If `fixed`, the `ion-datetime` will have a fixed width.
*/
"size": 'cover' | 'fixed';
/**
* A callback used to format the header text that shows how many dates are selected. Only used if there are 0 or more than 1 selected (i.e. unused for exactly 1). By default, the header text is set to "numberOfDates days". If this function returns undefined, the header text will not render -- only the `title` slot.
*/
"titleSelectedDatesFormatter"?: TitleSelectedDatesFormatter;
/**
* The value of the datetime as a valid ISO 8601 datetime string. Should be an array of strings if `multiple="true"`.
*/
Expand Down Expand Up @@ -4802,13 +4806,17 @@ declare namespace LocalJSX {
*/
"showDefaultTimeLabel"?: boolean;
/**
* If `true`, a header will be shown above the calendar picker. On `ios` mode this will include the slotted title, and on `md` mode this will include the slotted title and the selected date.
* If `true`, a header will be shown above the calendar picker. This will include both the slotted title, and the selected date.
*/
"showDefaultTitle"?: boolean;
/**
* If `cover`, the `ion-datetime` will expand to cover the full width of its container. If `fixed`, the `ion-datetime` will have a fixed width.
*/
"size"?: 'cover' | 'fixed';
/**
* A callback used to format the header text that shows how many dates are selected. Only used if there are 0 or more than 1 selected (i.e. unused for exactly 1). By default, the header text is set to "numberOfDates days". If this function returns undefined, the header text will not render -- only the `title` slot.
*/
"titleSelectedDatesFormatter"?: TitleSelectedDatesFormatter;
/**
* The value of the datetime as a valid ISO 8601 datetime string. Should be an array of strings if `multiple="true"`.
*/
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions core/src/components/datetime/datetime-interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,5 @@ export interface DatetimeParts {
}

export type DatetimePresentation = 'date-time' | 'time-date' | 'date' | 'time' | 'month' | 'year' | 'month-year';

export type TitleSelectedDatesFormatter = (numberOfDates: number) => string | undefined;
6 changes: 5 additions & 1 deletion core/src/components/datetime/datetime.ios.scss
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,16 @@
@include padding($datetime-ios-padding, $datetime-ios-padding, $datetime-ios-padding, $datetime-ios-padding);

border-bottom: $datetime-ios-border-color;

font-size: 14px;
}

:host .datetime-header .datetime-title {
color: var(--title-color);
}

font-size: 14px;
:host .datetime-header .datetime-selected-date {
@include margin(10px, null, null, null);
}
Comment on lines +24 to 33
Copy link
Contributor Author

@averyjohnston averyjohnston Aug 24, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This styling was fairly arbitrary on my part, since we don't have anything existing to go on as far as native comparisons go. I just re-used the same font size and picked a margin that looked good with the existing design.


// Calendar / Header / Action Buttons
Expand Down
47 changes: 37 additions & 10 deletions core/src/components/datetime/datetime.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import type {
DatetimeParts,
Mode,
StyleEventDetail,
TitleSelectedDatesFormatter,
} from '../../interface';
import { startFocusVisible } from '../../utils/focus-visible';
import { getElementRoot, raf, renderHiddenInput } from '../../utils/helpers';
Expand Down Expand Up @@ -313,6 +314,17 @@ export class Datetime implements ComponentInterface {
*/
@Prop() firstDayOfWeek = 0;

/**
* A callback used to format the header text that shows how many
* dates are selected. Only used if there are 0 or more than 1
* selected (i.e. unused for exactly 1). By default, the header
* text is set to "numberOfDates days".
*
* If this function returns undefined, the header text will not
* render -- only the `title` slot.
*/
@Prop() titleSelectedDatesFormatter?: TitleSelectedDatesFormatter;

/**
* If `true`, multiple dates can be selected at once. Only
* applies to `presentation="date"` and `preferWheel="false"`.
Expand Down Expand Up @@ -388,9 +400,8 @@ export class Datetime implements ComponentInterface {

/**
* If `true`, a header will be shown above the calendar
* picker. On `ios` mode this will include the
* slotted title, and on `md` mode this will include
* the slotted title and the selected date.
* picker. This will include both the slotted title, and
* the selected date.
*/
@Prop() showDefaultTitle = false;

Expand Down Expand Up @@ -2071,20 +2082,36 @@ export class Datetime implements ComponentInterface {
</ion-popover>,
];
}
private renderCalendarViewHeader(mode: Mode) {
private renderCalendarViewHeader() {
const hasSlottedTitle = this.el.querySelector('[slot="title"]') !== null;
if (!hasSlottedTitle && !this.showDefaultTitle) {
return;
}

const { activeParts, titleSelectedDatesFormatter: titleSelectedDatesFormatter } = this;
averyjohnston marked this conversation as resolved.
Show resolved Hide resolved
const isArray = Array.isArray(activeParts);

let headerText: string | undefined;
if (isArray && activeParts.length !== 1) {
headerText = `${activeParts.length} days`; // default/fallback for multiple selection
if (titleSelectedDatesFormatter !== undefined) {
try {
headerText = titleSelectedDatesFormatter(activeParts.length);
} catch (e) {
printIonError('Exception in provided `titleSelectedDatesFormatter`: ', e);
}
}
} else {
// for exactly 1 day selected (multiple set or not), show a formatted version of that
headerText = getMonthAndDay(this.locale, isArray ? activeParts[0] : activeParts);
}

return (
<div class="datetime-header">
<div class="datetime-title">
<slot name="title">Select Date</slot>
</div>
{mode === 'md' && !this.multiple && (
<div class="datetime-selected-date">{getMonthAndDay(this.locale, this.activeParts as DatetimeParts)}</div>
)}
{headerText && <div class="datetime-selected-date">{headerText}</div>}
</div>
);
}
Expand Down Expand Up @@ -2137,15 +2164,15 @@ export class Datetime implements ComponentInterface {
switch (presentation) {
case 'date-time':
return [
this.renderCalendarViewHeader(mode),
this.renderCalendarViewHeader(),
this.renderCalendar(mode),
this.renderCalendarViewMonthYearPicker(),
this.renderTime(),
this.renderFooter(),
];
case 'time-date':
return [
this.renderCalendarViewHeader(mode),
this.renderCalendarViewHeader(),
this.renderTime(),
this.renderCalendar(mode),
this.renderCalendarViewMonthYearPicker(),
Expand All @@ -2159,7 +2186,7 @@ export class Datetime implements ComponentInterface {
return [this.renderWheelView(), this.renderFooter()];
default:
return [
this.renderCalendarViewHeader(mode),
this.renderCalendarViewHeader(),
this.renderCalendar(mode),
this.renderCalendarViewMonthYearPicker(),
this.renderFooter(),
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
39 changes: 39 additions & 0 deletions core/src/components/datetime/test/multiple/datetime.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,4 +157,43 @@ test.describe('datetime: multiple date selection (functionality)', () => {
await ionChangeSpy.next();
await expect(datetime).toHaveJSProperty('value', [`${year}-${month}-01`]);
});

test('header text should update correctly', async ({ page }) => {
const datetime = await setup(page, 'withHeader');
const header = datetime.locator('.datetime-selected-date');
const juneButtons = datetime.locator('[data-month="6"][data-day]');

await expect(header).toHaveText('Wed, Jun 1');

await juneButtons.nth(1).click();
await expect(header).toHaveText('2 days');

await juneButtons.nth(0).click();
await expect(header).toHaveText('Thu, Jun 2');

await juneButtons.nth(1).click();
await expect(header).toHaveText('0 days');
});

test('header text should update correctly with custom formatter', async ({ page }) => {
const datetime = await setup(page, 'customFormatter');
const header = datetime.locator('.datetime-selected-date');
const juneButtons = datetime.locator('[data-month="6"][data-day]');

await expect(header).toHaveText('Selected: 3');

await juneButtons.nth(1).click();
await juneButtons.nth(2).click();
await expect(header).toHaveText('Wed, Jun 1');

await juneButtons.nth(0).click();
await expect(header).toHaveText('Selected: 0');
});

test('header text should not render if custom formatter returns undefined', async ({ page }) => {
const datetime = await setup(page, 'formatterReturnsUndefined');
const header = datetime.locator('.datetime-selected-date');

await expect(header).toHaveCount(0);
});
});
30 changes: 30 additions & 0 deletions core/src/components/datetime/test/multiple/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,28 @@ <h2>With Header</h2>
show-default-title="true"
></ion-datetime>
</div>
<div class="grid-item">
<h2>With Header, Custom Formatter</h2>
<ion-datetime
locale="en-US"
id="customFormatter"
presentation="date"
multiple="true"
value="2022-06-01"
show-default-title="true"
></ion-datetime>
</div>
<div class="grid-item">
<h2>With Header, Custom Formatter Returns Undefined</h2>
<ion-datetime
locale="en-US"
id="formatterReturnsUndefined"
presentation="date"
multiple="true"
value="2022-06-01"
show-default-title="true"
></ion-datetime>
</div>
</div>
</ion-content>
</ion-app>
Expand All @@ -107,6 +129,14 @@ <h2>With Header</h2>
document.querySelector('#multipleDefaultValues').value = ['2022-06-01', '2022-06-02', '2022-06-03'];
document.querySelector('#multipleValuesSeparateMonths').value = ['2022-04-01', '2022-05-01', '2022-06-01'];
document.querySelector('#multipleFalseArrayValue').value = ['2022-06-01', '2022-06-02', '2022-06-03'];

const customFormatterDatetime = document.querySelector('#customFormatter');
customFormatterDatetime.value = ['2022-06-01', '2022-06-02', '2022-06-03'];
customFormatterDatetime.titleSelectedDatesFormatter = (numberOfDates) => `Selected: ${numberOfDates}`;

const formatterReturnsUndefinedDatetime = document.querySelector('#formatterReturnsUndefined');
formatterReturnsUndefinedDatetime.value = ['2022-06-01', '2022-06-02', '2022-06-03'];
formatterReturnsUndefinedDatetime.titleSelectedDatesFormatter = () => undefined;
</script>
</body>
</html>
1 change: 1 addition & 0 deletions packages/vue/src/proxies.ts
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,7 @@ export const IonDatetime = /*@__PURE__*/ defineContainer<JSX.IonDatetime>('ion-d
'minuteValues',
'locale',
'firstDayOfWeek',
'titleSelectedDatesFormatter',
'multiple',
'value',
'showDefaultTitle',
Expand Down