@transferwise/components 0.0.0-experimental-b77dac7 → 0.0.0-experimental-fa3aa98

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (154) hide show
  1. package/build/Calendar/Calendar.js +94 -16
  2. package/build/Calendar/Calendar.js.map +1 -1
  3. package/build/Calendar/Calendar.messages.js +12 -0
  4. package/build/Calendar/Calendar.messages.js.map +1 -1
  5. package/build/Calendar/Calendar.messages.mjs +12 -0
  6. package/build/Calendar/Calendar.messages.mjs.map +1 -1
  7. package/build/Calendar/Calendar.mjs +95 -17
  8. package/build/Calendar/Calendar.mjs.map +1 -1
  9. package/build/Calendar/DateHeader/DateHeader.js +5 -1
  10. package/build/Calendar/DateHeader/DateHeader.js.map +1 -1
  11. package/build/Calendar/DateHeader/DateHeader.mjs +5 -1
  12. package/build/Calendar/DateHeader/DateHeader.mjs.map +1 -1
  13. package/build/Calendar/DayCalendar/DayCalendar.js +14 -0
  14. package/build/Calendar/DayCalendar/DayCalendar.js.map +1 -1
  15. package/build/Calendar/DayCalendar/DayCalendar.mjs +14 -0
  16. package/build/Calendar/DayCalendar/DayCalendar.mjs.map +1 -1
  17. package/build/Calendar/DayCalendar/Table/DayCalendarTable.js +53 -19
  18. package/build/Calendar/DayCalendar/Table/DayCalendarTable.js.map +1 -1
  19. package/build/Calendar/DayCalendar/Table/DayCalendarTable.mjs +53 -19
  20. package/build/Calendar/DayCalendar/Table/DayCalendarTable.mjs.map +1 -1
  21. package/build/Calendar/MonthCalendar/MonthCalendar.js +6 -0
  22. package/build/Calendar/MonthCalendar/MonthCalendar.js.map +1 -1
  23. package/build/Calendar/MonthCalendar/MonthCalendar.mjs +6 -0
  24. package/build/Calendar/MonthCalendar/MonthCalendar.mjs.map +1 -1
  25. package/build/Calendar/MonthCalendar/Table/MonthCalendarTable.js +8 -3
  26. package/build/Calendar/MonthCalendar/Table/MonthCalendarTable.js.map +1 -1
  27. package/build/Calendar/MonthCalendar/Table/MonthCalendarTable.mjs +8 -3
  28. package/build/Calendar/MonthCalendar/Table/MonthCalendarTable.mjs.map +1 -1
  29. package/build/Calendar/{tableLink → TableLink}/TableLink.js +26 -11
  30. package/build/Calendar/TableLink/TableLink.js.map +1 -0
  31. package/build/Calendar/{tableLink → TableLink}/TableLink.mjs +26 -11
  32. package/build/Calendar/TableLink/TableLink.mjs.map +1 -0
  33. package/build/Calendar/YearCalendar/Table/YearCalendarTable.js +8 -3
  34. package/build/Calendar/YearCalendar/Table/YearCalendarTable.js.map +1 -1
  35. package/build/Calendar/YearCalendar/Table/YearCalendarTable.mjs +8 -3
  36. package/build/Calendar/YearCalendar/Table/YearCalendarTable.mjs.map +1 -1
  37. package/build/Calendar/YearCalendar/YearCalendar.js +7 -0
  38. package/build/Calendar/YearCalendar/YearCalendar.js.map +1 -1
  39. package/build/Calendar/YearCalendar/YearCalendar.mjs +7 -0
  40. package/build/Calendar/YearCalendar/YearCalendar.mjs.map +1 -1
  41. package/build/DateLookup/DateLookup.js +20 -9
  42. package/build/DateLookup/DateLookup.js.map +1 -1
  43. package/build/DateLookup/DateLookup.mjs +20 -9
  44. package/build/DateLookup/DateLookup.mjs.map +1 -1
  45. package/build/DefinitionList/DefinitionList.js +1 -2
  46. package/build/DefinitionList/DefinitionList.js.map +1 -1
  47. package/build/DefinitionList/DefinitionList.mjs +1 -2
  48. package/build/DefinitionList/DefinitionList.mjs.map +1 -1
  49. package/build/common/DateUtils/hasDisabledDatesInMonth/hasDisabledDatesInMonth.js +33 -0
  50. package/build/common/DateUtils/hasDisabledDatesInMonth/hasDisabledDatesInMonth.js.map +1 -0
  51. package/build/common/DateUtils/hasDisabledDatesInMonth/hasDisabledDatesInMonth.mjs +31 -0
  52. package/build/common/DateUtils/hasDisabledDatesInMonth/hasDisabledDatesInMonth.mjs.map +1 -0
  53. package/build/common/DateUtils/isDateDisabled/isDateDisabled.js +40 -0
  54. package/build/common/DateUtils/isDateDisabled/isDateDisabled.js.map +1 -0
  55. package/build/common/DateUtils/isDateDisabled/isDateDisabled.mjs +38 -0
  56. package/build/common/DateUtils/isDateDisabled/isDateDisabled.mjs.map +1 -0
  57. package/build/i18n/en.json +4 -0
  58. package/build/i18n/en.json.js +4 -0
  59. package/build/i18n/en.json.js.map +1 -1
  60. package/build/i18n/en.json.mjs +4 -0
  61. package/build/i18n/en.json.mjs.map +1 -1
  62. package/build/index.js +2 -2
  63. package/build/index.mjs +1 -1
  64. package/build/main.css +123 -73
  65. package/build/styles/Calendar/Calendar.css +112 -37
  66. package/build/styles/Calendar/TableLink/TableLink.css +115 -0
  67. package/build/styles/DefinitionList/DefinitionList.css +0 -30
  68. package/build/styles/main.css +123 -73
  69. package/build/types/Calendar/Calendar.d.ts +16 -3
  70. package/build/types/Calendar/Calendar.d.ts.map +1 -1
  71. package/build/types/Calendar/Calendar.messages.d.ts +20 -0
  72. package/build/types/Calendar/Calendar.messages.d.ts.map +1 -1
  73. package/build/types/Calendar/Calendar.types.d.ts +32 -0
  74. package/build/types/Calendar/Calendar.types.d.ts.map +1 -0
  75. package/build/types/Calendar/DateHeader/DateHeader.d.ts +2 -0
  76. package/build/types/Calendar/DateHeader/DateHeader.d.ts.map +1 -1
  77. package/build/types/Calendar/DayCalendar/DayCalendar.d.ts +5 -1
  78. package/build/types/Calendar/DayCalendar/DayCalendar.d.ts.map +1 -1
  79. package/build/types/Calendar/DayCalendar/Table/DayCalendarTable.d.ts +4 -0
  80. package/build/types/Calendar/DayCalendar/Table/DayCalendarTable.d.ts.map +1 -1
  81. package/build/types/Calendar/MonthCalendar/MonthCalendar.d.ts +1 -0
  82. package/build/types/Calendar/MonthCalendar/MonthCalendar.d.ts.map +1 -1
  83. package/build/types/Calendar/MonthCalendar/Table/MonthCalendarTable.d.ts +2 -1
  84. package/build/types/Calendar/MonthCalendar/Table/MonthCalendarTable.d.ts.map +1 -1
  85. package/build/types/Calendar/{tableLink → TableLink}/TableLink.d.ts +3 -1
  86. package/build/types/Calendar/TableLink/TableLink.d.ts.map +1 -0
  87. package/build/types/Calendar/TableLink/index.d.ts.map +1 -0
  88. package/build/types/Calendar/YearCalendar/Table/YearCalendarTable.d.ts +2 -1
  89. package/build/types/Calendar/YearCalendar/Table/YearCalendarTable.d.ts.map +1 -1
  90. package/build/types/Calendar/YearCalendar/YearCalendar.d.ts +1 -0
  91. package/build/types/Calendar/YearCalendar/YearCalendar.d.ts.map +1 -1
  92. package/build/types/Calendar/index.d.ts +1 -0
  93. package/build/types/Calendar/index.d.ts.map +1 -1
  94. package/build/types/DateLookup/DateLookup.d.ts +24 -5
  95. package/build/types/DateLookup/DateLookup.d.ts.map +1 -1
  96. package/build/types/DateLookup/index.d.ts +1 -0
  97. package/build/types/DateLookup/index.d.ts.map +1 -1
  98. package/build/types/DefinitionList/DefinitionList.d.ts.map +1 -1
  99. package/build/types/common/DateUtils/hasDisabledDatesInMonth/hasDisabledDatesInMonth.d.ts +3 -0
  100. package/build/types/common/DateUtils/hasDisabledDatesInMonth/hasDisabledDatesInMonth.d.ts.map +1 -0
  101. package/build/types/common/DateUtils/index.d.ts +2 -0
  102. package/build/types/common/DateUtils/index.d.ts.map +1 -1
  103. package/build/types/common/DateUtils/isDateDisabled/isDateDisabled.d.ts +3 -0
  104. package/build/types/common/DateUtils/isDateDisabled/isDateDisabled.d.ts.map +1 -0
  105. package/build/types/index.d.ts +3 -2
  106. package/build/types/index.d.ts.map +1 -1
  107. package/build/types/test-utils/index.d.ts +8 -0
  108. package/build/types/test-utils/index.d.ts.map +1 -1
  109. package/package.json +1 -1
  110. package/src/Calendar/Calendar.css +112 -37
  111. package/src/Calendar/Calendar.disabledDates.test.tsx +518 -0
  112. package/src/Calendar/Calendar.less +5 -35
  113. package/src/Calendar/Calendar.messages.ts +21 -0
  114. package/src/Calendar/Calendar.test.tsx +288 -27
  115. package/src/Calendar/Calendar.tsx +136 -19
  116. package/src/Calendar/Calendar.types.ts +35 -0
  117. package/src/Calendar/DateHeader/DateHeader.tsx +6 -0
  118. package/src/Calendar/DayCalendar/DayCalendar.tsx +33 -1
  119. package/src/Calendar/DayCalendar/Table/DayCalendarTable.tsx +62 -28
  120. package/src/Calendar/MonthCalendar/MonthCalendar.tsx +8 -0
  121. package/src/Calendar/MonthCalendar/Table/MonthCalendarTable.tsx +15 -3
  122. package/src/Calendar/TableLink/TableLink.css +115 -0
  123. package/src/Calendar/TableLink/TableLink.less +68 -0
  124. package/src/Calendar/{tableLink → TableLink}/TableLink.tsx +29 -14
  125. package/src/Calendar/YearCalendar/Table/YearCalendarTable.tsx +15 -3
  126. package/src/Calendar/YearCalendar/YearCalendar.tsx +9 -0
  127. package/src/Calendar/_stories/Calendar.accessibility.docs.mdx +118 -0
  128. package/src/Calendar/_stories/Calendar.story.tsx +138 -4
  129. package/src/Calendar/_stories/Calendar.test.story.tsx +74 -0
  130. package/src/Calendar/index.ts +7 -0
  131. package/src/DateLookup/DateLookup.disabledDates.test.tsx +293 -0
  132. package/src/DateLookup/DateLookup.test.tsx +34 -37
  133. package/src/DateLookup/DateLookup.tsx +37 -13
  134. package/src/DateLookup/_stories/DateLookup.accessibility.docs.mdx +108 -0
  135. package/src/DateLookup/_stories/DateLookup.story.tsx +124 -2
  136. package/src/DateLookup/index.ts +6 -0
  137. package/src/DefinitionList/DefinitionList.css +0 -30
  138. package/src/DefinitionList/DefinitionList.less +0 -24
  139. package/src/DefinitionList/DefinitionList.tsx +0 -1
  140. package/src/common/DateUtils/hasDisabledDatesInMonth/hasDisabledDatesInMonth.test.ts +98 -0
  141. package/src/common/DateUtils/hasDisabledDatesInMonth/hasDisabledDatesInMonth.ts +45 -0
  142. package/src/common/DateUtils/index.ts +2 -0
  143. package/src/common/DateUtils/isDateDisabled/isDateDisabled.test.ts +169 -0
  144. package/src/common/DateUtils/isDateDisabled/isDateDisabled.ts +59 -0
  145. package/src/i18n/en.json +4 -0
  146. package/src/index.ts +3 -2
  147. package/src/main.css +123 -73
  148. package/build/Calendar/tableLink/TableLink.js.map +0 -1
  149. package/build/Calendar/tableLink/TableLink.mjs.map +0 -1
  150. package/build/types/Calendar/tableLink/TableLink.d.ts.map +0 -1
  151. package/build/types/Calendar/tableLink/index.d.ts.map +0 -1
  152. package/src/DefinitionList/_stories/DefinitionList.test.story.tsx +0 -168
  153. /package/build/types/Calendar/{tableLink → TableLink}/index.d.ts +0 -0
  154. /package/src/Calendar/{tableLink → TableLink}/index.ts +0 -0
@@ -0,0 +1,35 @@
1
+ export interface DisabledDateRange {
2
+ start: Date;
3
+ end: Date;
4
+ }
5
+
6
+ export type WeekDayType = 'mon' | 'tue' | 'wed' | 'thu' | 'fri' | 'sat' | 'sun';
7
+
8
+ export interface DisabledRules {
9
+ /** Days of the week to disable (e.g. `['sat', 'sun']` for weekends). */
10
+ weekdays?: WeekDayType[];
11
+ /** Specific calendar dates to disable. */
12
+ dates?: Date[];
13
+ /** Inclusive date ranges to disable. */
14
+ ranges?: DisabledDateRange[];
15
+ /**
16
+ * Human-readable explanation shown in an InlinePrompt below the calendar grid when a
17
+ * disabled date is focused or clicked. Required for accessibility: it is linked to
18
+ * disabled cells via `aria-describedby` so screen reader users understand why a date
19
+ * is unavailable. Also used as the prompt text when `min`/`max` out-of-range dates
20
+ * are focused, overriding the built-in default message.
21
+ */
22
+ reason: string;
23
+ }
24
+
25
+ export interface DateChangeMeta {
26
+ /** Whether the focused date is selectable (not disabled or out of range). */
27
+ isEnabled: boolean;
28
+ }
29
+
30
+ export interface OnDateSelectParams {
31
+ /** The date that was focused or clicked. */
32
+ date: Date;
33
+ /** Whether the date is selectable (not disabled or out of range). */
34
+ isEnabled: boolean;
35
+ }
@@ -15,6 +15,8 @@ interface DateHeaderProps {
15
15
  onPreviousClick: () => void;
16
16
  onNextClick: () => void;
17
17
  dateMode: string;
18
+ isPreviousDisabled?: boolean;
19
+ isNextDisabled?: boolean;
18
20
  }
19
21
 
20
22
  const DateHeader: React.FC<DateHeaderProps> = ({
@@ -23,6 +25,8 @@ const DateHeader: React.FC<DateHeaderProps> = ({
23
25
  onPreviousClick,
24
26
  onNextClick,
25
27
  dateMode,
28
+ isPreviousDisabled,
29
+ isNextDisabled,
26
30
  }) => {
27
31
  const intl = useIntl();
28
32
  const { isMobile } = useLayout();
@@ -34,6 +38,7 @@ const DateHeader: React.FC<DateHeaderProps> = ({
34
38
  priority="minimal"
35
39
  className="d-inline-flex"
36
40
  aria-label={`${intl.formatMessage(messages.previous)} ${dateMode}`}
41
+ disabled={isPreviousDisabled}
37
42
  onClick={onPreviousClick}
38
43
  >
39
44
  <ChevronLeft className="left-single-direction" />
@@ -61,6 +66,7 @@ const DateHeader: React.FC<DateHeaderProps> = ({
61
66
  className="d-inline-flex"
62
67
  aria-label={`${intl.formatMessage(messages.next)} ${dateMode}`}
63
68
  priority="minimal"
69
+ disabled={isNextDisabled}
64
70
  onClick={onNextClick}
65
71
  >
66
72
  <ChevronRight className="right-single-direction" />
@@ -2,6 +2,7 @@ import { formatDate } from '@transferwise/formatting';
2
2
  import { PureComponent } from 'react';
3
3
 
4
4
  import { MonthFormat } from '../../common';
5
+ import type { DisabledRules } from '../Calendar.types';
5
6
  import messages from '../Calendar.messages';
6
7
  import DateHeader from '../DateHeader';
7
8
 
@@ -10,11 +11,14 @@ import { injectIntl, type WrappedComponentProps } from '../../common/injectIntl'
10
11
 
11
12
  interface DayCalendarProps extends WrappedComponentProps {
12
13
  selectedDate: Date | null;
14
+ focusedDate?: Date | null;
13
15
  min: Date | null;
14
16
  max: Date | null;
15
17
  viewMonth: number;
16
18
  viewYear: number;
17
19
  monthFormat: `${MonthFormat}`;
20
+ disabledRules?: DisabledRules;
21
+ disabledReasonId?: string;
18
22
  onSelect: (date: Date) => void;
19
23
  onLabelClick: () => void;
20
24
  onViewDateUpdate: (date: { month: number; year: number }) => void;
@@ -40,15 +44,29 @@ class DayCalendar extends PureComponent<DayCalendarProps> {
40
44
  render() {
41
45
  const {
42
46
  selectedDate,
47
+ focusedDate,
43
48
  min,
44
49
  max,
45
50
  viewMonth,
46
51
  viewYear,
47
52
  intl: { locale, formatMessage },
48
53
  monthFormat,
54
+ disabledRules,
55
+ disabledReasonId,
49
56
  onLabelClick,
50
57
  onSelect,
51
58
  } = this.props;
59
+ const prevMonth = viewMonth <= 0 ? 11 : viewMonth - 1;
60
+ const prevYear = viewMonth <= 0 ? viewYear - 1 : viewYear;
61
+ const nextMonth = viewMonth >= 11 ? 0 : viewMonth + 1;
62
+ const nextYear = viewMonth >= 11 ? viewYear + 1 : viewYear;
63
+ const isPreviousDisabled = !!(
64
+ min && new Date(prevYear, prevMonth) < new Date(min.getFullYear(), min.getMonth())
65
+ );
66
+ const isNextDisabled = !!(
67
+ max && new Date(nextYear, nextMonth) > new Date(max.getFullYear(), max.getMonth())
68
+ );
69
+
52
70
  return (
53
71
  <>
54
72
  <DateHeader
@@ -57,11 +75,25 @@ class DayCalendar extends PureComponent<DayCalendarProps> {
57
75
  year: 'numeric',
58
76
  })}
59
77
  dateMode={formatMessage(messages.month)}
78
+ isPreviousDisabled={isPreviousDisabled}
79
+ isNextDisabled={isNextDisabled}
60
80
  onLabelClick={onLabelClick}
61
81
  onPreviousClick={this.selectPreviousMonth}
62
82
  onNextClick={this.selectNextMonth}
63
83
  />
64
- <DayCalendarTable {...{ selectedDate, min, max, viewMonth, viewYear, onSelect }} />
84
+ <DayCalendarTable
85
+ {...{
86
+ selectedDate,
87
+ focusedDate,
88
+ min,
89
+ max,
90
+ viewMonth,
91
+ viewYear,
92
+ disabledRules,
93
+ disabledReasonId,
94
+ onSelect,
95
+ }}
96
+ />
65
97
  </>
66
98
  );
67
99
  }
@@ -2,19 +2,28 @@ import { formatDate } from '@transferwise/formatting';
2
2
  import { PureComponent } from 'react';
3
3
  import { injectIntl, type WrappedComponentProps } from '../../../common/injectIntl';
4
4
 
5
- import { getDayNames, isWithinRange } from '../../../common/DateUtils';
5
+ import { getDayNames, isWithinRange, isDateDisabled } from '../../../common/DateUtils';
6
+ import type { DisabledRules } from '../../Calendar.types';
6
7
  import { getFocusableTime } from '../../GetFocusableTime/GetFocusableTime';
7
8
  import { getStartOfDay } from '../../GetStartOfDay';
8
- import TableLink from '../../tableLink';
9
+ import TableLink from '../../TableLink';
9
10
 
10
11
  const SHORT_DAY_FORMAT: Intl.DateTimeFormatOptions = { day: 'numeric' };
12
+ const LONG_DAY_FORMAT: Intl.DateTimeFormatOptions = {
13
+ day: 'numeric',
14
+ month: 'long',
15
+ year: 'numeric',
16
+ };
11
17
 
12
18
  interface DayCalendarTableProps extends WrappedComponentProps {
13
19
  selectedDate: Date | null;
20
+ focusedDate?: Date | null;
14
21
  min: Date | null;
15
22
  max: Date | null;
16
23
  viewMonth: number;
17
24
  viewYear: number;
25
+ disabledRules?: DisabledRules;
26
+ disabledReasonId?: string;
18
27
  onSelect: (date: Date) => void;
19
28
  }
20
29
 
@@ -63,21 +72,36 @@ class DayCalendarTable extends PureComponent<DayCalendarTableProps> {
63
72
  onSelect(new Date(viewYear, viewMonth, day));
64
73
  };
65
74
 
66
- isDisabled = (day: number) => {
75
+ isOutOfRange = (day: number) => {
67
76
  if (day < 0) {
68
77
  return true;
69
78
  }
70
79
  const { min, max, viewMonth, viewYear } = this.props;
71
80
  const date = new Date(viewYear, viewMonth, day);
72
-
73
81
  return !isWithinRange(date, min, max);
74
82
  };
75
83
 
84
+ isDisabled = (day: number) => {
85
+ if (day < 0) {
86
+ return false;
87
+ }
88
+ const { viewMonth, viewYear, disabledRules } = this.props;
89
+ const date = new Date(viewYear, viewMonth, day);
90
+ return isDateDisabled(date, disabledRules);
91
+ };
92
+
93
+ isUnavailable = (day: number) => this.isOutOfRange(day) || this.isDisabled(day);
94
+
76
95
  isActive = (day: number) => {
77
96
  const { selectedDate, viewMonth, viewYear } = this.props;
78
97
  return !!(selectedDate && Number(new Date(viewYear, viewMonth, day)) === Number(selectedDate));
79
98
  };
80
99
 
100
+ isFocused = (day: number) => {
101
+ const { focusedDate, viewMonth, viewYear } = this.props;
102
+ return !!(focusedDate && Number(new Date(viewYear, viewMonth, day)) === Number(focusedDate));
103
+ };
104
+
81
105
  isToday = (day: number) => {
82
106
  const { viewMonth, viewYear } = this.props;
83
107
  return Number(getStartOfDay(new Date())) === Number(new Date(viewYear, viewMonth, day));
@@ -86,15 +110,15 @@ class DayCalendarTable extends PureComponent<DayCalendarTableProps> {
86
110
  getAutofocusDay = (weeks: number[][]) => {
87
111
  const days = weeks.flatMap((week) => week);
88
112
  return getFocusableTime({
89
- isActive: this.isActive,
113
+ isActive: this.isFocused,
90
114
  isNow: this.isToday,
91
- isDisabled: this.isDisabled,
115
+ isDisabled: this.isUnavailable,
92
116
  timeSpan: days,
93
117
  });
94
118
  };
95
119
 
96
120
  render() {
97
- const { viewMonth, viewYear, intl } = this.props;
121
+ const { viewMonth, viewYear, intl, disabledReasonId } = this.props;
98
122
  const weeks = this.getTableStructure();
99
123
  const autoFocusDay = this.getAutofocusDay(weeks);
100
124
  return (
@@ -124,27 +148,37 @@ class DayCalendarTable extends PureComponent<DayCalendarTableProps> {
124
148
  {/* eslint-disable react/no-array-index-key */}
125
149
  {weeks.map((week, weekIndex) => (
126
150
  <tr key={weekIndex}>
127
- {week.map((day, dayIndex) => (
128
- <td key={dayIndex} className={dayIndex > 4 ? 'weekend' : ''}>
129
- {day !== -1 && (
130
- <TableLink
131
- item={day}
132
- type="day"
133
- title={formatDate(
134
- new Date(viewYear, viewMonth, day),
135
- intl.locale,
136
- SHORT_DAY_FORMAT,
137
- )}
138
- autofocus={day === autoFocusDay}
139
- longTitle={formatDate(new Date(viewYear, viewMonth, day), intl.locale)}
140
- active={this.isActive(day)}
141
- disabled={this.isDisabled(day)}
142
- today={this.isToday(day)}
143
- onClick={this.selectDay}
144
- />
145
- )}
146
- </td>
147
- ))}
151
+ {week.map((day, dayIndex) => {
152
+ const dayOutOfRange = this.isOutOfRange(day);
153
+ const dayDisabled = this.isDisabled(day);
154
+ return (
155
+ <td key={dayIndex} className={dayIndex > 4 ? 'weekend' : ''}>
156
+ {day !== -1 && (
157
+ <TableLink
158
+ item={day}
159
+ type="day"
160
+ title={formatDate(
161
+ new Date(viewYear, viewMonth, day),
162
+ intl.locale,
163
+ SHORT_DAY_FORMAT,
164
+ )}
165
+ autofocus={day === autoFocusDay}
166
+ longTitle={formatDate(
167
+ new Date(viewYear, viewMonth, day),
168
+ intl.locale,
169
+ LONG_DAY_FORMAT,
170
+ )}
171
+ active={this.isActive(day)}
172
+ outOfRange={dayOutOfRange}
173
+ disabled={dayDisabled}
174
+ today={this.isToday(day)}
175
+ disabledReasonId={disabledReasonId}
176
+ onClick={this.selectDay}
177
+ />
178
+ )}
179
+ </td>
180
+ );
181
+ })}
148
182
  </tr>
149
183
  ))}
150
184
  {/* eslint-enable react/no-array-index-key */}
@@ -9,6 +9,7 @@ import MonthCalendarTable from './Table';
9
9
 
10
10
  interface MonthCalendarProps extends WrappedComponentProps {
11
11
  selectedDate: Date | null;
12
+ focusedDate?: Date | null;
12
13
  min: Date | null;
13
14
  max: Date | null;
14
15
  viewYear: number;
@@ -35,6 +36,7 @@ class MonthCalendar extends PureComponent<MonthCalendarProps> {
35
36
  render() {
36
37
  const {
37
38
  selectedDate,
39
+ focusedDate,
38
40
  min,
39
41
  max,
40
42
  viewYear,
@@ -42,17 +44,23 @@ class MonthCalendar extends PureComponent<MonthCalendarProps> {
42
44
  placeholder,
43
45
  onLabelClick,
44
46
  } = this.props;
47
+ const isPreviousDisabled = !!(min && viewYear - 1 < min.getFullYear());
48
+ const isNextDisabled = !!(max && viewYear + 1 > max.getFullYear());
49
+
45
50
  return (
46
51
  <>
47
52
  <DateHeader
48
53
  dateMode={formatMessage(messages.year)}
49
54
  label={formatDate(new Date(viewYear, 0), locale, { year: 'numeric' })}
55
+ isPreviousDisabled={isPreviousDisabled}
56
+ isNextDisabled={isNextDisabled}
50
57
  onLabelClick={onLabelClick}
51
58
  onPreviousClick={this.selectPreviousYear}
52
59
  onNextClick={this.selectNextYear}
53
60
  />
54
61
  <MonthCalendarTable
55
62
  selectedDate={selectedDate}
63
+ focusedDate={focusedDate}
56
64
  min={min}
57
65
  max={max}
58
66
  viewYear={viewYear}
@@ -2,10 +2,11 @@ import { formatDate } from '@transferwise/formatting';
2
2
  import { useIntl } from 'react-intl';
3
3
 
4
4
  import { getFocusableTime } from '../../GetFocusableTime/GetFocusableTime';
5
- import TableLink from '../../tableLink';
5
+ import TableLink from '../../TableLink';
6
6
 
7
7
  interface MonthCalendarTableProps {
8
8
  selectedDate: Date | null;
9
+ focusedDate?: Date | null;
9
10
  min: Date | null;
10
11
  max: Date | null;
11
12
  viewYear: number;
@@ -19,6 +20,7 @@ const MONTH_ONLY_FORMAT: Intl.DateTimeFormatOptions = { month: 'short' };
19
20
 
20
21
  const MonthCalendarTable = ({
21
22
  selectedDate,
23
+ focusedDate,
22
24
  min,
23
25
  max,
24
26
  viewYear,
@@ -34,7 +36,8 @@ const MonthCalendarTable = ({
34
36
  type="month"
35
37
  title={formatDate(new Date(viewYear, month), locale, MONTH_ONLY_FORMAT)}
36
38
  active={isActive(month)}
37
- disabled={isDisabled(month)}
39
+ outOfRange={isDisabled(month)}
40
+ disabled={false}
38
41
  today={viewYear === new Date().getFullYear() && month === new Date().getMonth()}
39
42
  autofocus={autofocusMonth === month}
40
43
  onClick={onSelect}
@@ -46,6 +49,10 @@ const MonthCalendarTable = ({
46
49
  return !!(month === selectedDate?.getMonth() && viewYear === selectedDate?.getFullYear());
47
50
  };
48
51
 
52
+ const isFocused = (month: number) => {
53
+ return !!(month === focusedDate?.getMonth() && viewYear === focusedDate?.getFullYear());
54
+ };
55
+
49
56
  const isThisMonth = (month: number) => {
50
57
  return viewYear === new Date().getFullYear() && month === new Date().getMonth();
51
58
  };
@@ -60,7 +67,12 @@ const MonthCalendarTable = ({
60
67
 
61
68
  const autofocusMonth = (() => {
62
69
  const months = Array.from({ length: ROWS * COLS }, (_, index) => index);
63
- return getFocusableTime({ isActive, isNow: isThisMonth, isDisabled, timeSpan: months });
70
+ return getFocusableTime({
71
+ isActive: isFocused,
72
+ isNow: isThisMonth,
73
+ isDisabled,
74
+ timeSpan: months,
75
+ });
64
76
  })();
65
77
 
66
78
  return (
@@ -0,0 +1,115 @@
1
+ .wds-Calendar__option--day,
2
+ .wds-Calendar__option--month,
3
+ .wds-Calendar__option--year {
4
+ width: 100%;
5
+ padding: 4px 0;
6
+ padding: var(--size-4) 0;
7
+ border: transparent;
8
+ border-radius: 10px;
9
+ border-radius: var(--radius-small);
10
+ background-color: transparent;
11
+ font-weight: 600;
12
+ font-weight: var(--font-weight-semi-bold);
13
+ color: #37517e;
14
+ color: var(--color-content-primary);
15
+ }
16
+ @media (max-width: 320px) {
17
+ .wds-Calendar__option--day,
18
+ .wds-Calendar__option--month,
19
+ .wds-Calendar__option--year {
20
+ padding: 0;
21
+ }
22
+ }
23
+ .wds-Calendar__option--day:focus,
24
+ .wds-Calendar__option--month:focus,
25
+ .wds-Calendar__option--year:focus {
26
+ background-color: var(--color-background-screen-hover);
27
+ }
28
+ .wds-Calendar__option--day.disabled,
29
+ .wds-Calendar__option--month.disabled,
30
+ .wds-Calendar__option--year.disabled,
31
+ .wds-Calendar__option--day[aria-disabled="true"],
32
+ .wds-Calendar__option--month[aria-disabled="true"],
33
+ .wds-Calendar__option--year[aria-disabled="true"] {
34
+ --ring-outline-color: var(--color-sentiment-content-primary);
35
+ color: #768e9c;
36
+ color: var(--color-content-tertiary);
37
+ cursor: not-allowed;
38
+ }
39
+ .wds-Calendar__option--day.disabled:hover,
40
+ .wds-Calendar__option--month.disabled:hover,
41
+ .wds-Calendar__option--year.disabled:hover,
42
+ .wds-Calendar__option--day[aria-disabled="true"]:hover,
43
+ .wds-Calendar__option--month[aria-disabled="true"]:hover,
44
+ .wds-Calendar__option--year[aria-disabled="true"]:hover,
45
+ .wds-Calendar__option--day.disabled:focus,
46
+ .wds-Calendar__option--month.disabled:focus,
47
+ .wds-Calendar__option--year.disabled:focus,
48
+ .wds-Calendar__option--day[aria-disabled="true"]:focus,
49
+ .wds-Calendar__option--month[aria-disabled="true"]:focus,
50
+ .wds-Calendar__option--year[aria-disabled="true"]:focus {
51
+ filter: initial;
52
+ opacity: 1;
53
+ opacity: initial;
54
+ background-color: var(--color-sentiment-background-surface);
55
+ color: var(--color-sentiment-content-primary);
56
+ }
57
+ .wds-Calendar__option--day.disabled.active,
58
+ .wds-Calendar__option--month.disabled.active,
59
+ .wds-Calendar__option--year.disabled.active,
60
+ .wds-Calendar__option--day[aria-disabled="true"].active,
61
+ .wds-Calendar__option--month[aria-disabled="true"].active,
62
+ .wds-Calendar__option--year[aria-disabled="true"].active,
63
+ .wds-Calendar__option--day.disabled.active:hover,
64
+ .wds-Calendar__option--month.disabled.active:hover,
65
+ .wds-Calendar__option--year.disabled.active:hover,
66
+ .wds-Calendar__option--day[aria-disabled="true"].active:hover,
67
+ .wds-Calendar__option--month[aria-disabled="true"].active:hover,
68
+ .wds-Calendar__option--year[aria-disabled="true"].active:hover,
69
+ .wds-Calendar__option--day.disabled.active:active,
70
+ .wds-Calendar__option--month.disabled.active:active,
71
+ .wds-Calendar__option--year.disabled.active:active,
72
+ .wds-Calendar__option--day[aria-disabled="true"].active:active,
73
+ .wds-Calendar__option--month[aria-disabled="true"].active:active,
74
+ .wds-Calendar__option--year[aria-disabled="true"].active:active,
75
+ .wds-Calendar__option--day.disabled.active:focus,
76
+ .wds-Calendar__option--month.disabled.active:focus,
77
+ .wds-Calendar__option--year.disabled.active:focus,
78
+ .wds-Calendar__option--day[aria-disabled="true"].active:focus,
79
+ .wds-Calendar__option--month[aria-disabled="true"].active:focus,
80
+ .wds-Calendar__option--year[aria-disabled="true"].active:focus {
81
+ filter: initial;
82
+ opacity: 1;
83
+ opacity: initial;
84
+ background-color: var(--color-sentiment-interactive-primary);
85
+ color: var(--color-sentiment-interactive-secondary);
86
+ }
87
+ .wds-Calendar__option--day.disabled.today,
88
+ .wds-Calendar__option--month.disabled.today,
89
+ .wds-Calendar__option--year.disabled.today,
90
+ .wds-Calendar__option--day[aria-disabled="true"].today,
91
+ .wds-Calendar__option--month[aria-disabled="true"].today,
92
+ .wds-Calendar__option--year[aria-disabled="true"].today {
93
+ text-decoration-color: #768e9c;
94
+ text-decoration-color: var(--color-content-tertiary);
95
+ }
96
+ .wds-Calendar__option--day {
97
+ height: 40px;
98
+ height: var(--size-40);
99
+ border-radius: 9999px;
100
+ border-radius: var(--radius-full);
101
+ line-height: 40px;
102
+ line-height: var(--size-40);
103
+ width: 40px;
104
+ width: var(--size-40);
105
+ padding: 0;
106
+ display: inline-flex;
107
+ align-items: center;
108
+ justify-content: center;
109
+ }
110
+ .wds-Calendar__option--day.today {
111
+ -webkit-text-decoration: underline;
112
+ text-decoration: underline;
113
+ text-decoration-thickness: 2px;
114
+ text-underline-offset: 4px;
115
+ }
@@ -0,0 +1,68 @@
1
+ @import (reference) "./../../styles/variables/neptune-tokens.less";
2
+
3
+ .wds-Calendar__option--day,
4
+ .wds-Calendar__option--month,
5
+ .wds-Calendar__option--year {
6
+ width: 100%;
7
+ padding: var(--size-4) 0;
8
+ @media (--screen-400-zoom) {
9
+ padding: 0;
10
+ }
11
+ border: transparent;
12
+ border-radius: var(--radius-small);
13
+ background-color: transparent;
14
+ font-weight: var(--font-weight-semi-bold);
15
+ color: var(--color-content-primary);
16
+
17
+ &:focus {
18
+ background-color: var(--color-background-screen-hover);
19
+ }
20
+
21
+ &.disabled,
22
+ &[aria-disabled="true"] {
23
+ --ring-outline-color: var(--color-sentiment-content-primary);
24
+ color: var(--color-content-tertiary);
25
+ cursor: not-allowed;
26
+
27
+ &:hover,
28
+ &:focus {
29
+ filter: unset;
30
+ opacity: unset;
31
+ background-color: var(--color-sentiment-background-surface);
32
+ color: var(--color-sentiment-content-primary);
33
+ }
34
+
35
+ &.active {
36
+ &,
37
+ &:hover,
38
+ &:active,
39
+ &:focus {
40
+ filter: unset;
41
+ opacity: unset;
42
+ background-color: var(--color-sentiment-interactive-primary);
43
+ color: var(--color-sentiment-interactive-secondary);
44
+ }
45
+ }
46
+
47
+ &.today {
48
+ text-decoration-color: var(--color-content-tertiary);
49
+ }
50
+ }
51
+ }
52
+
53
+ .wds-Calendar__option--day {
54
+ height: var(--size-40);
55
+ border-radius: var(--radius-full);
56
+ line-height: var(--size-40);
57
+ width: var(--size-40);
58
+ padding: 0;
59
+ display: inline-flex;
60
+ align-items: center;
61
+ justify-content: center;
62
+
63
+ &.today {
64
+ text-decoration: underline;
65
+ text-decoration-thickness: 2px;
66
+ text-underline-offset: 4px;
67
+ }
68
+ }
@@ -3,6 +3,7 @@ import { useEffect, useRef } from 'react';
3
3
  import { useIntl } from 'react-intl';
4
4
 
5
5
  import messages from '../Calendar.messages';
6
+ import SentimentSurface from '../../SentimentSurface';
6
7
 
7
8
  interface TableLinkProps {
8
9
  item: number;
@@ -10,9 +11,11 @@ interface TableLinkProps {
10
11
  title?: string;
11
12
  longTitle?: string;
12
13
  active: boolean;
14
+ outOfRange: boolean;
13
15
  disabled: boolean;
14
16
  today: boolean;
15
17
  autofocus?: boolean;
18
+ disabledReasonId?: string;
16
19
  onClick: (item: number) => void;
17
20
  }
18
21
 
@@ -22,9 +25,11 @@ const TableLink = ({
22
25
  title,
23
26
  longTitle,
24
27
  active,
28
+ outOfRange,
25
29
  disabled,
26
30
  today,
27
31
  autofocus,
32
+ disabledReasonId,
28
33
  onClick,
29
34
  }: TableLinkProps) => {
30
35
  const buttonRef = useRef<HTMLButtonElement>(null);
@@ -38,39 +43,49 @@ const TableLink = ({
38
43
  }
39
44
  }, [autofocus]);
40
45
 
41
- const onCalendarClick = (event: React.MouseEvent<HTMLButtonElement>) => {
46
+ const onCalendarClick = (event: React.MouseEvent) => {
42
47
  event.preventDefault();
43
- if (!disabled) {
44
- onClick(item);
45
- }
48
+ onClick(item);
46
49
  };
47
50
 
51
+ const isUnavailable = outOfRange || disabled;
52
+
48
53
  const calculateAriaLabel = () => {
54
+ let label = longTitle || title || '';
49
55
  if (active) {
50
- return `${longTitle || title || ''}, ${intl.formatMessage(
51
- messages.selected,
52
- )} ${intl.formatMessage(messages[type])}`;
56
+ label += `, ${intl.formatMessage(messages.selected)} ${intl.formatMessage(messages[type])}`;
57
+ }
58
+ if (isUnavailable) {
59
+ label += `, ${intl.formatMessage(messages.notAvailable)}`;
53
60
  }
54
- return longTitle || title;
61
+ return label;
55
62
  };
56
63
 
57
64
  return (
58
- <button
65
+ <SentimentSurface
66
+ // @ts-expect-error - Generic forwardRef limitation in SentimentSurface
59
67
  ref={buttonRef}
60
- type="button"
68
+ as="button"
69
+ data-calendar-cell
70
+ aria-describedby={isUnavailable ? disabledReasonId : undefined}
71
+ aria-disabled={isUnavailable || undefined}
72
+ aria-label={calculateAriaLabel()}
73
+ aria-pressed={active}
61
74
  className={clsx(
75
+ `wds-Calendar__option--${type}`,
62
76
  `tw-date-lookup-${type}-option np-text-body-default-bold`,
63
77
  { active: !!active },
64
78
  { today: !!today },
79
+ { disabled: !!isUnavailable },
65
80
  )}
66
- disabled={disabled}
81
+ hasBaseStyles={false}
82
+ sentiment={isUnavailable ? 'negative' : 'neutral'}
67
83
  tabIndex={autofocus ? 0 : -1}
68
- aria-label={calculateAriaLabel()}
69
- aria-pressed={active}
84
+ type="button"
70
85
  onClick={onCalendarClick}
71
86
  >
72
87
  {title || item}
73
- </button>
88
+ </SentimentSurface>
74
89
  );
75
90
  };
76
91