Skip to content

Commit 0e528a5

Browse files
devongovettdannify
andauthored
Initial DatePicker + TimeField implementation (#2220)
* Fix updating isFocused in useFocusRing * Add useEvent hook and use it in useScrollWheel * Improved keyboard event handling * Improve styling and add granularity option * Better support for manipulating eras and time * Various i18n and UI fixes * Handle timezones * Improve software keyboard support * Add hideTimeZone and placeholderValue props * Add comparison functions etc. * Refactoring. Add builtin label support, etc. * Fix existing tests * Fix react 17 * Fix DST bug * Fix issues on mobile Co-authored-by: Danni <[email protected]>
1 parent 7018c13 commit 0e528a5

File tree

96 files changed

+3562
-1195
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

96 files changed

+3562
-1195
lines changed

packages/@adobe/spectrum-css-temp/components/inputgroup/index.css

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@ governing permissions and limitations under the License.
161161
}
162162
}
163163
.spectrum-Datepicker-endField {
164-
.spectrum-InputGroup-field {
164+
.spectrum-InputGroup-input {
165165
flex: 1;
166166
border-inline-start: 0;
167167
border-radius: 0;

packages/@internationalized/date/src/CalendarDate.ts

Lines changed: 171 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,17 +10,25 @@
1010
* governing permissions and limitations under the License.
1111
*/
1212

13-
import {Calendar} from './types';
13+
import {add, addTime, addZoned, cycleDate, cycleTime, cycleZoned, set, setTime, setZoned, subtract, subtractTime, subtractZoned} from './manipulation';
14+
import {Calendar, CycleOptions, CycleTimeOptions, DateField, DateFields, Disambiguation, Duration, OverflowBehavior, TimeField, TimeFields} from './types';
15+
import {compareDate, compareTime} from './queries';
16+
import {dateTimeToString, dateToString, timeToString, zonedDateTimeToString} from './string';
1417
import {GregorianCalendar} from './calendars/GregorianCalendar';
18+
import {toCalendarDateTime, toDate, toZoned, zonedToDate} from './conversion';
1519

1620
function shiftArgs(args: any[]) {
1721
let calendar: Calendar = typeof args[0] === 'object'
1822
? args.shift()
1923
: new GregorianCalendar();
2024

21-
let era = typeof args[0] === 'string'
22-
? args.shift()
23-
: calendar.getCurrentEra();
25+
let era: string;
26+
if (typeof args[0] === 'string') {
27+
era = args.shift();
28+
} else {
29+
let eras = calendar.getEras();
30+
era = eras[eras.length - 1];
31+
}
2432

2533
let year = args.shift();
2634
let month = args.shift();
@@ -51,6 +59,42 @@ export class CalendarDate {
5159
this.calendar.balanceDate(this);
5260
}
5361
}
62+
63+
copy(): CalendarDate {
64+
if (this.era) {
65+
return new CalendarDate(this.calendar, this.era, this.year, this.month, this.day);
66+
} else {
67+
return new CalendarDate(this.calendar, this.year, this.month, this.day);
68+
}
69+
}
70+
71+
add(duration: Duration) {
72+
return add(this, duration);
73+
}
74+
75+
subtract(duration: Duration) {
76+
return subtract(this, duration);
77+
}
78+
79+
set(fields: DateFields, behavior?: OverflowBehavior) {
80+
return set(this, fields, behavior);
81+
}
82+
83+
cycle(field: DateField, amount: number, options?: CycleOptions) {
84+
return cycleDate(this, field, amount, options);
85+
}
86+
87+
toDate(timeZone: string) {
88+
return toDate(this, timeZone);
89+
}
90+
91+
toString() {
92+
return dateToString(this);
93+
}
94+
95+
compare(b: CalendarDate) {
96+
return compareDate(this, b);
97+
}
5498
}
5599

56100
export class Time {
@@ -60,6 +104,34 @@ export class Time {
60104
public readonly second: number = 0,
61105
public readonly millisecond: number = 0
62106
) {}
107+
108+
copy(): Time {
109+
return new Time(this.hour, this.minute, this.second, this.millisecond);
110+
}
111+
112+
add(duration: Duration) {
113+
return addTime(this, duration);
114+
}
115+
116+
subtract(duration: Duration) {
117+
return subtractTime(this, duration);
118+
}
119+
120+
set(fields: TimeFields, behavior?: OverflowBehavior) {
121+
return setTime(this, fields, behavior);
122+
}
123+
124+
cycle(field: TimeField, amount: number, options?: CycleTimeOptions) {
125+
return cycleTime(this, field, amount, options);
126+
}
127+
128+
toString() {
129+
return timeToString(this);
130+
}
131+
132+
compare(b: Time) {
133+
return compareTime(this, b);
134+
}
63135
}
64136

65137
export class CalendarDateTime extends CalendarDate {
@@ -79,4 +151,99 @@ export class CalendarDateTime extends CalendarDate {
79151
this.second = args.shift() || 0;
80152
this.millisecond = args.shift() || 0;
81153
}
154+
155+
copy(): CalendarDateTime {
156+
if (this.era) {
157+
return new CalendarDateTime(this.calendar, this.era, this.year, this.month, this.day, this.hour, this.minute, this.second, this.millisecond);
158+
} else {
159+
return new CalendarDateTime(this.calendar, this.year, this.month, this.day, this.hour, this.minute, this.second, this.millisecond);
160+
}
161+
}
162+
163+
set(fields: DateFields & TimeFields, behavior?: OverflowBehavior) {
164+
return set(setTime(this, fields, behavior), fields, behavior);
165+
}
166+
167+
cycle(field: DateField | TimeField, amount: number, options?: CycleTimeOptions) {
168+
switch (field) {
169+
case 'era':
170+
case 'year':
171+
case 'month':
172+
case 'day':
173+
return cycleDate(this, field, amount, options);
174+
default:
175+
return cycleTime(this, field, amount, options);
176+
}
177+
}
178+
179+
toString() {
180+
return dateTimeToString(this);
181+
}
182+
183+
compare(b: CalendarDate | CalendarDateTime) {
184+
let res = compareDate(this, b);
185+
if (res === 0) {
186+
return compareTime(this, toCalendarDateTime(b));
187+
}
188+
189+
return res;
190+
}
191+
}
192+
193+
export class ZonedDateTime extends CalendarDateTime {
194+
public readonly timeZone: string;
195+
public readonly offset: number;
196+
197+
constructor(year: number, month: number, day: number, timeZone: string, offset: number, hour?: number, minute?: number, second?: number, millisecond?: number);
198+
constructor(calendar: Calendar, year: number, month: number, day: number, timeZone: string, offset: number, hour?: number, minute?: number, second?: number, millisecond?: number);
199+
constructor(calendar: Calendar, era: string, year: number, month: number, day: number, timeZone: string, offset: number, hour?: number, minute?: number, second?: number, millisecond?: number);
200+
constructor(...args: any[]) {
201+
let [calendar, era, year, month, day] = shiftArgs(args);
202+
let timeZone = args.shift();
203+
let offset = args.shift();
204+
super(calendar, era, year, month, day, ...args);
205+
this.timeZone = timeZone;
206+
this.offset = offset;
207+
}
208+
209+
copy(): ZonedDateTime {
210+
if (this.era) {
211+
return new ZonedDateTime(this.calendar, this.era, this.year, this.month, this.day, this.timeZone, this.offset, this.hour, this.minute, this.second, this.millisecond);
212+
} else {
213+
return new ZonedDateTime(this.calendar, this.year, this.month, this.day, this.timeZone, this.offset, this.hour, this.minute, this.second, this.millisecond);
214+
}
215+
}
216+
217+
add(duration: Duration) {
218+
return addZoned(this, duration);
219+
}
220+
221+
subtract(duration: Duration) {
222+
return subtractZoned(this, duration);
223+
}
224+
225+
set(fields: DateFields & TimeFields, behavior?: OverflowBehavior, disambiguation?: Disambiguation) {
226+
return setZoned(this, fields, behavior, disambiguation);
227+
}
228+
229+
cycle(field: DateField | TimeField, amount: number, options?: CycleTimeOptions) {
230+
return cycleZoned(this, field, amount, options);
231+
}
232+
233+
toDate() {
234+
return zonedToDate(this);
235+
}
236+
237+
toString() {
238+
return zonedDateTimeToString(this);
239+
}
240+
241+
toAbsoluteString() {
242+
return this.toDate().toISOString();
243+
}
244+
245+
compare(b: CalendarDate | CalendarDateTime | ZonedDateTime) {
246+
// TODO: Is this a bad idea??
247+
return this.toDate() - toZoned(b, this.timeZone).toDate();
248+
}
82249
}

packages/@internationalized/date/src/calendars/BuddhistCalendar.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ export class BuddhistCalendar extends GregorianCalendar {
3838
);
3939
}
4040

41-
getCurrentEra() {
42-
return 'BE';
41+
getEras() {
42+
return ['BE'];
4343
}
4444
}

packages/@internationalized/date/src/calendars/EthiopicCalendar.ts

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -102,8 +102,12 @@ export class EthiopicCalendar implements Calendar {
102102
return 365 + getLeapDay(date.year);
103103
}
104104

105-
getCurrentEra() {
106-
return 'AM';
105+
getYearsInEra(): number {
106+
return 9999;
107+
}
108+
109+
getEras() {
110+
return ['AA', 'AM'];
107111
}
108112
}
109113

@@ -117,8 +121,8 @@ export class EthiopicAmeteAlemCalendar extends EthiopicCalendar {
117121
return date;
118122
}
119123

120-
getCurrentEra() {
121-
return 'AA';
124+
getEras() {
125+
return ['AA'];
122126
}
123127
}
124128

@@ -163,7 +167,7 @@ export class CopticCalendar extends EthiopicCalendar {
163167
date.year += years;
164168
}
165169

166-
getCurrentEra() {
167-
return 'CE';
170+
getEras() {
171+
return ['BCE', 'CE'];
168172
}
169173
}

packages/@internationalized/date/src/calendars/GregorianCalendar.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,12 @@ export class GregorianCalendar implements Calendar {
9191
return isLeapYear(date.year) ? 366 : 365;
9292
}
9393

94-
getCurrentEra() {
95-
return 'AD';
94+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
95+
getYearsInEra(date: CalendarDate): number {
96+
return 9999;
97+
}
98+
99+
getEras() {
100+
return ['BC', 'AD'];
96101
}
97102
}

packages/@internationalized/date/src/calendars/HebrewCalendar.ts

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515

1616
import {Calendar} from '../types';
1717
import {CalendarDate} from '../CalendarDate';
18-
import {mod} from '../utils';
18+
import {mod, Mutable} from '../utils';
1919

2020
const HEBREW_EPOCH = 347997;
2121

@@ -175,7 +175,23 @@ export class HebrewCalendar implements Calendar {
175175
return getDaysInYear(date.year);
176176
}
177177

178-
getCurrentEra() {
179-
return 'AM';
178+
getYearsInEra(): number {
179+
return 9999;
180+
}
181+
182+
getEras() {
183+
return ['AM'];
184+
}
185+
186+
addYears(date: Mutable<CalendarDate>, years: number) {
187+
// Keep date in the same month when switching between leap years and non leap years
188+
let nextYear = date.year + years;
189+
if (isLeapYear(date.year) && !isLeapYear(nextYear) && date.month > 6) {
190+
date.month--;
191+
} else if (!isLeapYear(date.year) && isLeapYear(nextYear) && date.month > 6) {
192+
date.month++;
193+
}
194+
195+
date.year = nextYear;
180196
}
181197
}

packages/@internationalized/date/src/calendars/IndianCalendar.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,11 @@ export class IndianCalendar extends GregorianCalendar {
109109
return 30;
110110
}
111111

112-
getCurrentEra() {
113-
return 'saka';
112+
getYearsInEra(): number {
113+
return 9999;
114+
}
115+
116+
getEras() {
117+
return ['saka'];
114118
}
115119
}

packages/@internationalized/date/src/calendars/IslamicCalendar.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,8 +70,12 @@ export class IslamicCivilCalendar implements Calendar {
7070
return isLeapYear(date.year) ? 355 : 354;
7171
}
7272

73-
getCurrentEra() {
74-
return 'AH';
73+
getYearsInEra(): number {
74+
return 9999;
75+
}
76+
77+
getEras() {
78+
return ['AH'];
7579
}
7680
}
7781

packages/@internationalized/date/src/calendars/JapaneseCalendar.ts

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,25 @@ export class JapaneseCalendar extends GregorianCalendar {
8888
}
8989
}
9090

91-
getCurrentEra() {
92-
return ERA_NAMES[ERA_NAMES.length - 1];
91+
getEras() {
92+
return ERA_NAMES;
93+
}
94+
95+
getYearsInEra(date: CalendarDate): number {
96+
let gregorianDate = toGregorian(date);
97+
let era = findEraFromGregorianDate(gregorianDate);
98+
let next = ERA_START_DATES[era + 1];
99+
if (next == null) {
100+
return 9999;
101+
}
102+
103+
let cur = ERA_START_DATES[era];
104+
let years = next[0] - cur[0];
105+
106+
if (date.month < next[1] || (date.month === next[1] && date.day < next[2])) {
107+
years++;
108+
}
109+
110+
return years;
93111
}
94112
}

packages/@internationalized/date/src/calendars/PersianCalendar.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,11 @@ export class PersianCalendar implements Calendar {
8282
return isLeapYear(date.year) ? 30 : 29;
8383
}
8484

85-
getCurrentEra() {
86-
return 'AP';
85+
getEras() {
86+
return ['AP'];
87+
}
88+
89+
getYearsInEra(): number {
90+
return 9999;
8791
}
8892
}

0 commit comments

Comments
 (0)