Skip to content

feat(datetime-button): add selecting interaction support #25620

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

Merged
merged 24 commits into from
Jul 14, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
f15a41d
feat(datetime-button): add base component
liamdebeasi Jul 12, 2022
be5ef5a
chore(): lint
liamdebeasi Jul 12, 2022
c2a3669
chore(): prettier
liamdebeasi Jul 12, 2022
e931d28
feat(datetime-button): add base styles and examples
liamdebeasi Jul 12, 2022
059f118
feat(datetime-button): add base functionality
liamdebeasi Jul 12, 2022
98879f0
feat(datetime-button): add support for active states
liamdebeasi Jul 12, 2022
c6e594b
feat(datetime-button): ensure overlays are sized correctly
liamdebeasi Jul 12, 2022
4aa1bec
chore(): remove stubs
liamdebeasi Jul 12, 2022
a802aa5
Revert "feat(datetime-button): ensure overlays are sized correctly"
liamdebeasi Jul 12, 2022
a4ac7a2
test(datetime-button): add basic rendering tests
liamdebeasi Jul 12, 2022
bf1faeb
chore(): lint
liamdebeasi Jul 12, 2022
e4e9958
chore(): lint
liamdebeasi Jul 12, 2022
ae63db5
test(): fix test
liamdebeasi Jul 12, 2022
e817a2d
docs(): add slot documentation
liamdebeasi Jul 12, 2022
2adf417
Merge remote-tracking branch 'origin/FW-546' into 546-base
liamdebeasi Jul 12, 2022
cd7642b
Merge remote-tracking branch 'origin/546-base' into 546-styles
liamdebeasi Jul 12, 2022
31289c8
Merge remote-tracking branch 'origin/546-styles' into 546-text
liamdebeasi Jul 12, 2022
a251ba0
chore(): typo
liamdebeasi Jul 12, 2022
34b122b
chore(); update comment
liamdebeasi Jul 12, 2022
d340b4b
chore(): remove default case
liamdebeasi Jul 12, 2022
0ea7d46
chore(): log element for easier debugging
liamdebeasi Jul 13, 2022
5285ea6
fix(datetime-button): ignore timezone returns from datetime
liamdebeasi Jul 13, 2022
2e24efb
chore(): add todo
liamdebeasi Jul 13, 2022
8f4ea63
chore(): sync with FW-546
liamdebeasi Jul 14, 2022
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
5 changes: 5 additions & 0 deletions core/src/components/datetime-button/datetime-button.scss
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,8 @@

appearance: none;
}

:host(.time-active) #time-button,
:host(.date-active) #date-button {
color: current-color(base);
}
220 changes: 210 additions & 10 deletions core/src/components/datetime-button/datetime-button.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
import type { ComponentInterface } from '@stencil/core';
import { Component, Host, Prop, State, h } from '@stencil/core';
import { Component, Element, Host, Prop, State, h } from '@stencil/core';

import { getIonMode } from '../../global/ionic-global';
import type { Color, DatetimePresentation } from '../../interface';
import { componentOnReady, addEventListener } from '../../utils/helpers';
import { printIonError } from '../../utils/logging';
import { createColorClasses } from '../../utils/theme';

import { getToday } from '../datetime/utils/data';
import { getMonthAndYear, getMonthDayAndYear, getLocalizedDateTime, getLocalizedTime } from '../datetime/utils/format';
import { is24Hour } from '../datetime/utils/helpers';
import { parseDate } from '../datetime/utils/parse';
/**
* @virtualProp {"ios" | "md"} mode - The mode determines which platform styles to use.
*
Expand All @@ -17,10 +22,15 @@ import { createColorClasses } from '../../utils/theme';
shadow: true,
})
export class DatetimeButton implements ComponentInterface {
// STUBs
private datetimeEl: HTMLIonDatetimeElement | null = null;

@Element() el!: HTMLIonDatetimeButtonElement;

@State() datetimePresentation?: DatetimePresentation = 'date-time';
@State() dateText?: string = 'May 1, 2022';
@State() timeText?: string = '12:30 PM';
@State() dateText?: string;
@State() timeText?: string;
@State() datetimeActive = false;
@State() selectedButton?: 'date' | 'time';

/**
* The color to use from your application's color palette.
Expand All @@ -40,8 +50,193 @@ export class DatetimeButton implements ComponentInterface {
*/
@Prop() datetime?: string;

async componentWillLoad() {
const { datetime } = this;
if (!datetime) {
printIonError(
'An ID associated with an ion-datetime instance is required for ion-datetime-button to function properly.',
this.el
);
return;
}

const datetimeEl = (this.datetimeEl = document.getElementById(datetime) as HTMLIonDatetimeElement | null);
if (!datetimeEl) {
printIonError(`No ion-datetime instance found for ID '${datetime}'.`, this.el);
return;
}

/**
* Since the datetime can be used in any context (overlays, accordion, etc)
* we track when it is visible to determine when it is active.
* This informs which button is highlighted as well as the
* aria-expanded state.
*/
const io = new IntersectionObserver(
(entries: IntersectionObserverEntry[]) => {
const ev = entries[0];
this.datetimeActive = ev.isIntersecting;
},
{
threshold: 0.01,
}
);

io.observe(datetimeEl);

componentOnReady(datetimeEl, () => {
const datetimePresentation = (this.datetimePresentation = datetimeEl.presentation || 'date-time');

/**
* Set the initial display
* in the rendered buttons.
*
* From there, we need to listen
* for ionChange to be emitted
* from datetime so we know when
* to re-render the displayed
* text in the buttons.
*/
this.setDateTimeText();
addEventListener(datetimeEl, 'ionChange', this.setDateTimeText);

/**
* Configure the initial selected button
* in the event that the datetime is displayed
* without clicking one of the datetime buttons.
* For example, a datetime could be expanded
* in an accordion. In this case users only
* need to click the accordion header to show
* the datetime.
*/
switch (datetimePresentation) {
case 'date-time':
case 'date':
case 'month-year':
case 'month':
case 'year':
this.selectedButton = 'date';
break;
case 'time-date':
case 'time':
this.selectedButton = 'time';
break;
}
});
}

/**
* Check the value property on the linked
* ion-datetime and then format it according
* to the locale specified on ion-datetime.
*/
private setDateTimeText = () => {
const { datetimeEl, datetimePresentation } = this;

if (!datetimeEl) {
return;
}

const { value, locale, hourCycle } = datetimeEl;

/**
* Both ion-datetime and ion-datetime-button default
* to today's date and time if no value is set.
*/
const parsedDatetime = parseDate(value || getToday());
const use24Hour = is24Hour(locale, hourCycle);

// TODO(FW-1865) - Remove once FW-1831 is fixed.
parsedDatetime.tzOffset = undefined;

switch (datetimePresentation) {
case 'date-time':
case 'time-date':
this.dateText = getMonthDayAndYear(locale, parsedDatetime);
this.timeText = getLocalizedTime(locale, parsedDatetime, use24Hour);
break;
case 'date':
this.dateText = getMonthDayAndYear(locale, parsedDatetime);
break;
case 'time':
this.timeText = getLocalizedTime(locale, parsedDatetime, use24Hour);
break;
case 'month-year':
this.dateText = getMonthAndYear(locale, parsedDatetime);
break;
case 'month':
this.dateText = getLocalizedDateTime(locale, parsedDatetime, { month: 'long' });
break;
case 'year':
this.dateText = getLocalizedDateTime(locale, parsedDatetime, { year: 'numeric' });
break;
}
};

private handleDateClick = () => {
const { datetimeEl, datetimePresentation } = this;

if (!datetimeEl) {
return;
}

/**
* When clicking the date button,
* we need to make sure that only a date
* picker is displayed. For presentation styles
* that display content other than a date picker,
* we need to update the presentation style.
*/
switch (datetimePresentation) {
case 'date-time':
case 'time-date':
datetimeEl.presentation = 'date';
break;
}

/**
* Track which button was clicked
* so that it can have the correct
* activated styles applied when
* the modal/popover containing
* the datetime is opened.
*/
this.selectedButton = 'date';
};

private handleTimeClick = () => {
const { datetimeEl, datetimePresentation } = this;

if (!datetimeEl) {
return;
}

/**
* When clicking the time button,
* we need to make sure that only a time
* picker is displayed. For presentation styles
* that display content other than a time picker,
* we need to update the presentation style.
*/
switch (datetimePresentation) {
case 'date-time':
case 'time-date':
datetimeEl.presentation = 'time';
break;
}

/**
* Track which button was clicked
* so that it can have the correct
* activated styles applied when
* the modal/popover containing
* the datetime is opened.
*/
this.selectedButton = 'time';
};

render() {
const { color, dateText, timeText, datetimePresentation } = this;
const { color, dateText, timeText, datetimePresentation, selectedButton, datetimeActive } = this;

const showDateTarget =
!datetimePresentation ||
Expand All @@ -53,25 +248,30 @@ export class DatetimeButton implements ComponentInterface {
<Host
class={createColorClasses(color, {
[mode]: true,
[`${selectedButton}-active`]: datetimeActive,
})}
>
{showDateTarget && (
<div class="date-target-container">
<div class="date-target-container" onClick={() => this.handleDateClick()}>
<slot name="date-target">
{/*
The button is added inside of the <slot> so that
devs do not create nested interactives if they
decide to add in a custom ion-button.
*/}
<button id="date-button">{dateText}</button>
<button id="date-button" aria-expanded={datetimeActive ? 'true' : 'false'}>
{dateText}
</button>
</slot>
</div>
)}

{showTimeTarget && (
<div class="time-target-container">
<div class="time-target-container" onClick={() => this.handleTimeClick()}>
<slot name="time-target">
<button id="time-button">{timeText}</button>
<button id="time-button" aria-expanded={datetimeActive ? 'true' : 'false'}>
{timeText}
</button>
</slot>
</div>
)}
Expand Down
Loading