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 all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
ff40877
feat(datetime): add num selected days to header; add header to ios mode
amandaejohnston Aug 24, 2022
fc44f50
feat(datetime): add header formatter prop
amandaejohnston Aug 24, 2022
65b3135
test(datetime): add header text tests
amandaejohnston Aug 24, 2022
21bd832
chore(): lint
amandaejohnston Aug 24, 2022
4d217bd
chore(): add updated snapshots
Ionitron Aug 24, 2022
a6a881f
fix(datetime): update showDefaultTitle description
amandaejohnston Aug 24, 2022
b0cb38d
fix(datetime): update showDefaultTitle description (for real this time)
amandaejohnston Aug 24, 2022
3ba1c21
refactor(datetime): clarify formatter param name
amandaejohnston Aug 25, 2022
801c515
Update core/src/components/datetime/datetime.tsx
amandaejohnston Aug 25, 2022
8539409
Merge branch 'FW-1906' of https://github.com/ionic-team/ionic-framewo…
amandaejohnston Aug 25, 2022
306659c
refactor(datetime): assign default header text in advance
amandaejohnston Aug 25, 2022
b4a24a9
feat(datetime): allow header formatter to return undefined
amandaejohnston Aug 25, 2022
2f04c21
chore(): lint
amandaejohnston Aug 25, 2022
1d1d077
refactor(datetime): rename formatter prop
amandaejohnston Aug 25, 2022
0597945
test(datetime): remove test.only
amandaejohnston Aug 25, 2022
7d2c6a8
feat(datetime): formatter takes array of selected dates instead of count
amandaejohnston Aug 26, 2022
99455d7
fix(datetime): revert formatter being able to return undefined
amandaejohnston Aug 26, 2022
52f0ae8
chore(angular): lint
amandaejohnston Aug 29, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 2 additions & 2 deletions angular/src/directives/proxies.ts
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
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
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
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
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

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
44 changes: 34 additions & 10 deletions core/src/components/datetime/datetime.tsx
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>
amandaejohnston marked this conversation as resolved.
Show resolved Hide resolved
</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
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
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
Expand Up @@ -286,6 +286,7 @@ export const IonDatetime = /*@__PURE__*/ defineContainer<JSX.IonDatetime>('ion-d
'minuteValues',
'locale',
'firstDayOfWeek',
'titleSelectedDatesFormatter',
'multiple',
'value',
'showDefaultTitle',
Expand Down