Skip to content

Commit

Permalink
add date range picker (#1783)
Browse files Browse the repository at this point in the history
* add ember power calendar

wip

wip

arrows next to month info

Have 2 diff centers

clean up css

pass attributes

fix start and end date logic

add fixed width

remove @center arg

* add icons

* add type

* fix lint

* add aria-label for accesibility

* add aria label for nav tag

* typo

* aria attributes fundamentally not working with ember power calendar. So ignore

* ignore test not lint

* ignore in a11y testing
  • Loading branch information
tintinthong authored Nov 12, 2024
1 parent 269fb65 commit c639284
Show file tree
Hide file tree
Showing 14 changed files with 478 additions and 2 deletions.
2 changes: 2 additions & 0 deletions packages/boxel-ui/addon/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@
"ember-link": "^2.1.0",
"ember-load-initializers": "^2.1.2",
"ember-modifier": "^4.1.0",
"ember-power-calendar": "^1.2.0",
"ember-power-calendar-moment": "^1.0.2",
"ember-power-select": "^8.0.0",
"ember-resize-modifier": "^0.7.1",
"ember-set-body-class": "^1.0.2",
Expand Down
4 changes: 4 additions & 0 deletions packages/boxel-ui/addon/raw-icons/triangle-left.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions packages/boxel-ui/addon/raw-icons/triangle-right.svg
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 packages/boxel-ui/addon/src/components.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import CardContainer from './components/card-container/index.gts';
import CardContentContainer from './components/card-content-container/index.gts';
import CardHeader from './components/card-header/index.gts';
import CircleSpinner from './components/circle-spinner/index.gts';
import DateRangePicker from './components/date-range-picker/index.gts';
import DndKanbanBoard, {
type DndItem,
DndColumn,
Expand Down Expand Up @@ -74,6 +75,7 @@ export {
CardContentContainer,
CardHeader,
CircleSpinner,
DateRangePicker,
DndColumn,
DndItem,
DndKanbanBoard,
Expand Down
179 changes: 179 additions & 0 deletions packages/boxel-ui/addon/src/components/date-range-picker/index.gts
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
import { fn } from '@ember/helper';
import { on } from '@ember/modifier';
import { action } from '@ember/object';
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import PowerCalendarRange from 'ember-power-calendar/components/power-calendar-range';
import { type TPowerCalendarRangeOnSelect } from 'ember-power-calendar/components/power-calendar-range';
import powerCalendarFormatDate from 'ember-power-calendar/helpers/power-calendar-format-date';
import {
type SelectedPowerCalendarRange,
add,
} from 'ember-power-calendar/utils';

import TriangleLeftIcon from '../../icons/triangle-left.gts';
import TriangleRightIcon from '../../icons/triangle-right.gts';
import IconButton from '../icon-button/index.gts';

interface Signature {
Args: {
end?: Date | null;
onSelect: TPowerCalendarRangeOnSelect;
selected?: SelectedPowerCalendarRange;
start?: Date | null;
};
Element: HTMLElement;
}

export default class DateRangePicker extends Component<Signature> {
@tracked leftCenter: Date;
@tracked rightCenter: Date;

constructor(owner: any, args: any) {
super(owner, args);
// If both start and end are provided, use them
if (this.args.start && this.args.end) {
this.leftCenter = this.args.start;
this.rightCenter = this.args.end;
}
// If only start is provided, set right center to next month
else if (this.args.start) {
this.leftCenter = this.args.start;
this.rightCenter = add(this.args.start, 1, 'month');
}
// If only end is provided, set left center to previous month
else if (this.args.end) {
this.rightCenter = this.args.end;
this.leftCenter = add(this.args.end, -1, 'month');
}
// If neither is provided, use current date and next month
else {
const today = new Date();
this.leftCenter = today;
this.rightCenter = add(today, 1, 'month');
}
}

@action
onNavigate(side: 'left' | 'right', direction: 'previous' | 'next') {
const months = direction === 'next' ? 1 : -1;

if (side === 'left') {
const newLeftCenter = add(this.leftCenter, months, 'month');
this.leftCenter = newLeftCenter;

// If left month would overlap with right month, push right month forward
if (newLeftCenter >= this.rightCenter) {
this.rightCenter = add(newLeftCenter, 1, 'month');
}
} else {
const newRightCenter = add(this.rightCenter, months, 'month');
this.rightCenter = newRightCenter;

// If right month would overlap with left month, push left month backward
if (newRightCenter <= this.leftCenter) {
this.leftCenter = add(newRightCenter, -1, 'month');
}
}
}

<template>
<div class='date-range-picker'>
<PowerCalendarRange
@selected={{@selected}}
@onSelect={{@onSelect}}
@locale='en-US'
...attributes
as |calendar|
>
<div class='months-container'>
<div>
<calendar.Nav>
<div class='nav-container'>
<IconButton
@icon={{TriangleLeftIcon}}
aria-label='Previous month'
{{on 'click' (fn this.onNavigate 'left' 'previous')}}
/>
<div class='month-name'>
{{powerCalendarFormatDate
this.leftCenter
'MMMM yyyy'
locale=calendar.locale
}}
</div>
<IconButton
@icon={{TriangleRightIcon}}
aria-label='Next month'
{{on 'click' (fn this.onNavigate 'left' 'next')}}
/>
</div>
</calendar.Nav>
<calendar.Days @center={{this.leftCenter}} />
</div>

<div>
<calendar.Nav>
<div class='nav-container'>
<IconButton
@icon={{TriangleLeftIcon}}
aria-label='Previous month'
{{on 'click' (fn this.onNavigate 'right' 'previous')}}
/>
<div class='month-name'>
{{powerCalendarFormatDate
this.rightCenter
'MMMM yyyy'
locale=calendar.locale
}}
</div>
<IconButton
@icon={{TriangleRightIcon}}
aria-label='Next month'
{{on 'click' (fn this.onNavigate 'right' 'next')}}
/>
</div>
</calendar.Nav>
<calendar.Days @center={{this.rightCenter}} />
</div>
</div>
</PowerCalendarRange>
</div>
<style scoped>
.date-range-picker {
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
}
.month-name {
display: flex;
align-items: center;
justify-content: center;
}
.months-container {
display: flex;
flex-direction: row;
align-items: flex-start;
gap: var(--boxel-sp-lg);
}
.nav-container {
display: flex;
align-items: center;
justify-content: center;
}
</style>
{{! template-lint-disable require-scoped-style }}
<style>
.ember-power-calendar-day {
width: 2.5em; /*add fixed width to ensure cols of numbers align*/
padding: var(--boxel-sp-xxs);
}
.ember-power-calendar-week {
gap: var(--boxel-sp-xxs);
}
</style>
</template>
}
102 changes: 102 additions & 0 deletions packages/boxel-ui/addon/src/components/date-range-picker/usage.gts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import { action } from '@ember/object';
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import FreestyleUsage from 'ember-freestyle/components/freestyle/usage';
import type {
NormalizeRangeActionValue,
SelectedPowerCalendarRange,
} from 'ember-power-calendar/utils';

import DateRangePicker from './index.gts';

export default class DateRangePickerUsage extends Component {
@tracked range1: SelectedPowerCalendarRange = {
start: new Date(2024, 10, 1),
end: new Date(2024, 10, 15),
};
@tracked range2: SelectedPowerCalendarRange = {
start: new Date(2024, 9, 15),
end: new Date(2024, 12, 15),
};
@tracked range3: SelectedPowerCalendarRange | undefined;

@action
onSelect1(selected: NormalizeRangeActionValue) {
this.range1 = selected.date;
}

@action
onSelect2(selected: NormalizeRangeActionValue) {
this.range2 = selected.date;
}

@action
onSelect3(selected: NormalizeRangeActionValue) {
this.range3 = selected.date;
}

parseDate(date: Date | null | undefined) {
if (!date) {
return '';
}
const formatter = new Intl.DateTimeFormat('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric',
});
return formatter.format(date);
}

get range1String() {
return `${this.parseDate(this.range1.start)} - ${this.parseDate(
this.range1.end,
)}`;
}

get range2String() {
return `${this.parseDate(this.range2.start)} - ${this.parseDate(
this.range2.end,
)}`;
}

get range3String() {
if (!this.range3) {
return `No date selected`;
}
return `${this.parseDate(this.range3.start)} - ${this.parseDate(
this.range3.end,
)}`;
}

<template>
<FreestyleUsage @name='Date Range Picker (within month)'>
<:example>
{{this.range1String}}
<DateRangePicker
@selected={{this.range1}}
@onSelect={{this.onSelect1}}
/>
</:example>
</FreestyleUsage>
<FreestyleUsage @name='Date Range Picker (across months)'>
<:example>
{{this.range2String}}
<DateRangePicker
@selected={{this.range2}}
@onSelect={{this.onSelect2}}
@start={{this.range2.start}}
@end={{this.range2.end}}
/>
</:example>
</FreestyleUsage>
<FreestyleUsage @name='Date Range Picker (no date specified)'>
<:example>
{{this.range3String}}
<DateRangePicker
@selected={{this.range3}}
@onSelect={{this.onSelect3}}
/>
</:example>
</FreestyleUsage>
</template>
}
6 changes: 6 additions & 0 deletions packages/boxel-ui/addon/src/icons.gts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ import Send from './icons/send.gts';
import Sparkle from './icons/sparkle.gts';
import SuccessBordered from './icons/success-bordered.gts';
import ThreeDotsHorizontal from './icons/three-dots-horizontal.gts';
import TriangleLeft from './icons/triangle-left.gts';
import TriangleRight from './icons/triangle-right.gts';
import Upload from './icons/upload.gts';
import Warning from './icons/warning.gts';

Expand Down Expand Up @@ -101,6 +103,8 @@ export const ALL_ICON_COMPONENTS = [
Sparkle,
SuccessBordered,
ThreeDotsHorizontal,
TriangleLeft,
TriangleRight,
Upload,
Warning,
];
Expand Down Expand Up @@ -153,6 +157,8 @@ export {
Sparkle,
SuccessBordered,
ThreeDotsHorizontal,
TriangleLeft,
TriangleRight,
Upload,
Warning,
};
23 changes: 23 additions & 0 deletions packages/boxel-ui/addon/src/icons/triangle-left.gts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// This file is auto-generated by 'pnpm rebuild:icons'
import type { TemplateOnlyComponent } from '@ember/component/template-only';

import type { Signature } from './types.ts';

const IconComponent: TemplateOnlyComponent<Signature> = <template>
<svg
xmlns='http://www.w3.org/2000/svg'
viewBox='-3 -3 16 12'
...attributes
><path
fill='var(--icon-fill, #000)'
stroke='var(--icon-color,#000)'
stroke-linecap='round'
stroke-linejoin='round'
stroke-width='2'
d='m5.414 6.414-4-4 4-4v8Z'
/></svg>
</template>;

// @ts-expect-error this is the only way to set a name on a Template Only Component currently
IconComponent.name = 'TriangleLeft';
export default IconComponent;
23 changes: 23 additions & 0 deletions packages/boxel-ui/addon/src/icons/triangle-right.gts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// This file is auto-generated by 'pnpm rebuild:icons'
import type { TemplateOnlyComponent } from '@ember/component/template-only';

import type { Signature } from './types.ts';

const IconComponent: TemplateOnlyComponent<Signature> = <template>
<svg
xmlns='http://www.w3.org/2000/svg'
viewBox='-3 -3 16 12'
...attributes
><path
fill='var(--icon-fill, #000)'
stroke='var(--icon-color,#000)'
stroke-linecap='round'
stroke-linejoin='round'
stroke-width='2'
d='m1.414 6.414 4-4-4-4v8Z'
/></svg>
</template>;

// @ts-expect-error this is the only way to set a name on a Template Only Component currently
IconComponent.name = 'TriangleRight';
export default IconComponent;
Loading

0 comments on commit c639284

Please sign in to comment.