Skip to content

Commit

Permalink
feat(datetime): add header text to multiple selection; improve header…
Browse files Browse the repository at this point in the history
… consistency between modes (#25817)

Co-authored-by: Sean Perkins <sean@ionic.io>
  • Loading branch information
amandaejohnston and sean-perkins committed Aug 29, 2022
1 parent ae6aa0c commit 8a1b3c5
Show file tree
Hide file tree
Showing 127 changed files with 103 additions and 16 deletions.
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,((selectedDates: string[]) => string) | 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".
*/
"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".
*/
"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
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
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 = (selectedDates: string[]) => string;
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);
}

// Calendar / Header / Action Buttons
Expand Down
44 changes: 34 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,14 @@ 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".
*/
@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 +397,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 +2079,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 } = this;
const isArray = Array.isArray(activeParts);

let headerText: string;
if (isArray && activeParts.length !== 1) {
headerText = `${activeParts.length} days`; // default/fallback for multiple selection
if (titleSelectedDatesFormatter !== undefined) {
try {
headerText = titleSelectedDatesFormatter(convertDataToISO(activeParts));
} 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>
)}
<div class="datetime-selected-date">{headerText}</div>
</div>
);
}
Expand Down Expand Up @@ -2137,15 +2161,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 +2183,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
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
32 changes: 32 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,36 @@ 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');
});
});
15 changes: 15 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,17 @@ <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>
</ion-content>
</ion-app>
Expand All @@ -107,6 +118,10 @@ <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 = (selectedDates) => `Selected: ${selectedDates.length}`;
</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

0 comments on commit 8a1b3c5

Please sign in to comment.