@instructure/ui-calendar 9.0.2-snapshot-19 → 9.0.2-snapshot-24

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.
package/CHANGELOG.md CHANGED
@@ -3,9 +3,12 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
- ## [9.0.2-snapshot-19](https://github.com/instructure/instructure-ui/compare/v9.0.1...v9.0.2-snapshot-19) (2024-06-13)
6
+ ## [9.0.2-snapshot-24](https://github.com/instructure/instructure-ui/compare/v9.0.1...v9.0.2-snapshot-24) (2024-06-14)
7
7
 
8
- **Note:** Version bump only for package @instructure/ui-calendar
8
+
9
+ ### Features
10
+
11
+ * **shared-types,ui-calendar,ui-select:** add yearpicker functionality to calendar ([2c0c6e0](https://github.com/instructure/instructure-ui/commit/2c0c6e081d96fc821c1296df5c4f1fba9a8c162c))
9
12
 
10
13
 
11
14
 
@@ -40,6 +40,7 @@ import { Day } from './Day';
40
40
  import { propTypes, allowedProps } from './props';
41
41
  import { IconButton } from '@instructure/ui-buttons';
42
42
  import { IconArrowOpenStartSolid, IconArrowOpenEndSolid } from '@instructure/ui-icons';
43
+ import { SimpleSelect } from '@instructure/ui-simple-select';
43
44
 
44
45
  /**
45
46
  ---
@@ -98,6 +99,9 @@ let Calendar = (_dec = withStyle(generateStyle, generateComponentTheme), _dec2 =
98
99
  const visibleMonth = this.state.visibleMonth;
99
100
  const newDate = visibleMonth.clone();
100
101
  if (direction === 'prev') {
102
+ if (!this.hasPrevMonth) {
103
+ return;
104
+ }
101
105
  if (onRequestRenderPrevMonth) {
102
106
  onRequestRenderPrevMonth(e, newDate.subtract({
103
107
  months: 1
@@ -108,6 +112,9 @@ let Calendar = (_dec = withStyle(generateStyle, generateComponentTheme), _dec2 =
108
112
  months: 1
109
113
  });
110
114
  } else {
115
+ if (!this.hasNextMonth) {
116
+ return;
117
+ }
111
118
  if (onRequestRenderNextMonth) {
112
119
  onRequestRenderNextMonth(e, newDate.add({
113
120
  months: 1
@@ -122,6 +129,19 @@ let Calendar = (_dec = withStyle(generateStyle, generateComponentTheme), _dec2 =
122
129
  visibleMonth: newDate
123
130
  });
124
131
  };
132
+ this.handleYearChange = (e, year) => {
133
+ const withYearPicker = this.props.withYearPicker;
134
+ const visibleMonth = this.state.visibleMonth;
135
+ const newDate = visibleMonth.clone();
136
+ if (withYearPicker !== null && withYearPicker !== void 0 && withYearPicker.onRequestYearChange) {
137
+ withYearPicker.onRequestYearChange(e, year);
138
+ return;
139
+ }
140
+ newDate.year(year);
141
+ this.setState({
142
+ visibleMonth: newDate
143
+ });
144
+ };
125
145
  // date is returned es a ISO string, like 2021-09-14T22:00:00.000Z
126
146
  this.handleDayClick = (event, {
127
147
  date
@@ -152,10 +172,21 @@ let Calendar = (_dec = withStyle(generateStyle, generateComponentTheme), _dec2 =
152
172
  get role() {
153
173
  return this.props.role === 'listbox' ? this.props.role : void 0;
154
174
  }
175
+ get hasPrevMonth() {
176
+ return !this.props.withYearPicker || this.props.withYearPicker && Number(this.state.visibleMonth.clone().subtract({
177
+ months: 1
178
+ }).format('YYYY')) >= this.props.withYearPicker.startYear;
179
+ }
180
+ get hasNextMonth() {
181
+ return !this.props.withYearPicker || this.props.withYearPicker && Number(this.state.visibleMonth.clone().add({
182
+ months: 1
183
+ }).format('YYYY')) <= this.props.withYearPicker.endYear;
184
+ }
155
185
  renderHeader() {
156
186
  const _this$props5 = this.props,
157
187
  renderNavigationLabel = _this$props5.renderNavigationLabel,
158
- styles = _this$props5.styles;
188
+ styles = _this$props5.styles,
189
+ withYearPicker = _this$props5.withYearPicker;
159
190
  const visibleMonth = this.state.visibleMonth;
160
191
  const _this$renderMonthNavi = this.renderMonthNavigationButtons(),
161
192
  prevButton = _this$renderMonthNavi.prevButton,
@@ -164,9 +195,31 @@ let Calendar = (_dec = withStyle(generateStyle, generateComponentTheme), _dec2 =
164
195
  onClick: createChainedFunction(button.props.onClick, onClick)
165
196
  });
166
197
  const style = [styles === null || styles === void 0 ? void 0 : styles.navigation, ...(prevButton || nextButton ? [styles === null || styles === void 0 ? void 0 : styles.navigationWithButtons] : [])];
167
- return jsx("div", {
198
+ const yearList = [];
199
+ if (withYearPicker) {
200
+ const startYear = withYearPicker.startYear,
201
+ endYear = withYearPicker.endYear;
202
+ for (let year = endYear; year >= startYear; year--) {
203
+ yearList.push(year);
204
+ }
205
+ }
206
+ return jsx("div", null, jsx("div", {
168
207
  css: style
169
- }, prevButton && cloneButton(prevButton, this.handleMonthChange('prev')), renderNavigationLabel ? callRenderProp(renderNavigationLabel) : jsx("span", null, jsx("div", null, visibleMonth.format('MMMM')), jsx("div", null, visibleMonth.format('YYYY'))), nextButton && cloneButton(nextButton, this.handleMonthChange('next')));
208
+ }, prevButton && cloneButton(prevButton, this.handleMonthChange('prev')), renderNavigationLabel ? callRenderProp(renderNavigationLabel) : jsx("span", null, jsx("div", null, visibleMonth.format('MMMM')), !withYearPicker ? jsx("div", null, visibleMonth.format('YYYY')) : null), nextButton && cloneButton(nextButton, this.handleMonthChange('next'))), withYearPicker ? jsx("div", {
209
+ css: styles === null || styles === void 0 ? void 0 : styles.yearPicker
210
+ }, jsx(SimpleSelect, {
211
+ width: "90px",
212
+ renderLabel: "",
213
+ assistiveText: withYearPicker.screenReaderLabel,
214
+ value: Number(visibleMonth.format('YYYY')),
215
+ onChange: (e, {
216
+ value
217
+ }) => this.handleYearChange(e, Number(value))
218
+ }, yearList.map(year => jsx(SimpleSelect.Option, {
219
+ key: year,
220
+ id: `opt-${year}`,
221
+ value: year
222
+ }, `${year}`)))) : null);
170
223
  }
171
224
  renderBody() {
172
225
  return jsx("table", {
@@ -41,7 +41,8 @@ const propTypes = {
41
41
  role: PropTypes.oneOf(['table', 'listbox']),
42
42
  selectedDate: PropTypes.string,
43
43
  timezone: PropTypes.string,
44
- visibleMonth: PropTypes.string
44
+ visibleMonth: PropTypes.string,
45
+ withYearPicker: PropTypes.object
45
46
  };
46
47
  const allowedProps = ['as', 'children', 'currentDate', 'disabledDates', 'locale', 'onDateSelected', 'onRequestRenderNextMonth', 'onRequestRenderPrevMonth', 'renderNavigationLabel', 'renderNextMonthButton', 'renderPrevMonthButton', 'renderWeekdayLabels', 'role', 'selectedDate', 'timezone', 'visibleMonth'];
47
48
  export { propTypes, allowedProps };
@@ -30,7 +30,8 @@
30
30
  * @param {Object} componentTheme The theme variable object.
31
31
  * @return {Object} The final style object, which will be used in the component
32
32
  */
33
- const generateStyle = componentTheme => {
33
+ const generateStyle = (componentTheme, props) => {
34
+ const withYearPicker = props.withYearPicker;
34
35
  return {
35
36
  navigation: {
36
37
  label: 'calendar__navigation',
@@ -40,7 +41,7 @@ const generateStyle = componentTheme => {
40
41
  fontFamily: componentTheme.fontFamily,
41
42
  fontSize: componentTheme.fontSize,
42
43
  fontWeight: componentTheme.fontWeight,
43
- marginBottom: componentTheme.navMargin
44
+ marginBottom: withYearPicker ? componentTheme.navWithYearMargin : componentTheme.navMargin
44
45
  },
45
46
  navigationWithButtons: {
46
47
  label: 'calendar__navigation--withButtons',
@@ -52,6 +53,12 @@ const generateStyle = componentTheme => {
52
53
  label: 'calendar__weekdayHeader',
53
54
  textAlign: 'center',
54
55
  maxWidth: componentTheme.maxHeaderWidth
56
+ },
57
+ yearPicker: {
58
+ display: 'flex',
59
+ justifyContent: 'center',
60
+ alignItems: 'center',
61
+ marginBottom: componentTheme.yearPickerMargin
55
62
  }
56
63
  };
57
64
  };
@@ -44,6 +44,8 @@ const generateComponentTheme = theme => {
44
44
  color: colors === null || colors === void 0 ? void 0 : colors.textDarkest,
45
45
  background: colors === null || colors === void 0 ? void 0 : colors.backgroundLightest,
46
46
  navMargin: spacing === null || spacing === void 0 ? void 0 : spacing.small,
47
+ navWithYearMargin: spacing === null || spacing === void 0 ? void 0 : spacing.xSmall,
48
+ yearPickerMargin: spacing === null || spacing === void 0 ? void 0 : spacing.mediumSmall,
47
49
  maxHeaderWidth: spacing === null || spacing === void 0 ? void 0 : spacing.medium
48
50
  };
49
51
  return {
@@ -27,6 +27,7 @@ var _props = require("./props");
27
27
  var _IconButton3 = require("@instructure/ui-buttons/lib/IconButton");
28
28
  var _IconArrowOpenStartSolid = require("@instructure/ui-icons/lib/IconArrowOpenStartSolid.js");
29
29
  var _IconArrowOpenEndSolid = require("@instructure/ui-icons/lib/IconArrowOpenEndSolid.js");
30
+ var _SimpleSelect = require("@instructure/ui-simple-select/lib/SimpleSelect");
30
31
  var _dec, _dec2, _class, _Calendar, _IconButton, _IconButton2;
31
32
  /*
32
33
  * The MIT License (MIT)
@@ -109,6 +110,9 @@ let Calendar = exports.Calendar = (_dec = (0, _emotion.withStyle)(_styles.defaul
109
110
  const visibleMonth = this.state.visibleMonth;
110
111
  const newDate = visibleMonth.clone();
111
112
  if (direction === 'prev') {
113
+ if (!this.hasPrevMonth) {
114
+ return;
115
+ }
112
116
  if (onRequestRenderPrevMonth) {
113
117
  onRequestRenderPrevMonth(e, newDate.subtract({
114
118
  months: 1
@@ -119,6 +123,9 @@ let Calendar = exports.Calendar = (_dec = (0, _emotion.withStyle)(_styles.defaul
119
123
  months: 1
120
124
  });
121
125
  } else {
126
+ if (!this.hasNextMonth) {
127
+ return;
128
+ }
122
129
  if (onRequestRenderNextMonth) {
123
130
  onRequestRenderNextMonth(e, newDate.add({
124
131
  months: 1
@@ -133,6 +140,19 @@ let Calendar = exports.Calendar = (_dec = (0, _emotion.withStyle)(_styles.defaul
133
140
  visibleMonth: newDate
134
141
  });
135
142
  };
143
+ this.handleYearChange = (e, year) => {
144
+ const withYearPicker = this.props.withYearPicker;
145
+ const visibleMonth = this.state.visibleMonth;
146
+ const newDate = visibleMonth.clone();
147
+ if (withYearPicker !== null && withYearPicker !== void 0 && withYearPicker.onRequestYearChange) {
148
+ withYearPicker.onRequestYearChange(e, year);
149
+ return;
150
+ }
151
+ newDate.year(year);
152
+ this.setState({
153
+ visibleMonth: newDate
154
+ });
155
+ };
136
156
  // date is returned es a ISO string, like 2021-09-14T22:00:00.000Z
137
157
  this.handleDayClick = (event, {
138
158
  date
@@ -163,10 +183,21 @@ let Calendar = exports.Calendar = (_dec = (0, _emotion.withStyle)(_styles.defaul
163
183
  get role() {
164
184
  return this.props.role === 'listbox' ? this.props.role : void 0;
165
185
  }
186
+ get hasPrevMonth() {
187
+ return !this.props.withYearPicker || this.props.withYearPicker && Number(this.state.visibleMonth.clone().subtract({
188
+ months: 1
189
+ }).format('YYYY')) >= this.props.withYearPicker.startYear;
190
+ }
191
+ get hasNextMonth() {
192
+ return !this.props.withYearPicker || this.props.withYearPicker && Number(this.state.visibleMonth.clone().add({
193
+ months: 1
194
+ }).format('YYYY')) <= this.props.withYearPicker.endYear;
195
+ }
166
196
  renderHeader() {
167
197
  const _this$props5 = this.props,
168
198
  renderNavigationLabel = _this$props5.renderNavigationLabel,
169
- styles = _this$props5.styles;
199
+ styles = _this$props5.styles,
200
+ withYearPicker = _this$props5.withYearPicker;
170
201
  const visibleMonth = this.state.visibleMonth;
171
202
  const _this$renderMonthNavi = this.renderMonthNavigationButtons(),
172
203
  prevButton = _this$renderMonthNavi.prevButton,
@@ -175,9 +206,31 @@ let Calendar = exports.Calendar = (_dec = (0, _emotion.withStyle)(_styles.defaul
175
206
  onClick: (0, _createChainedFunction.createChainedFunction)(button.props.onClick, onClick)
176
207
  });
177
208
  const style = [styles === null || styles === void 0 ? void 0 : styles.navigation, ...(prevButton || nextButton ? [styles === null || styles === void 0 ? void 0 : styles.navigationWithButtons] : [])];
178
- return (0, _emotion.jsx)("div", {
209
+ const yearList = [];
210
+ if (withYearPicker) {
211
+ const startYear = withYearPicker.startYear,
212
+ endYear = withYearPicker.endYear;
213
+ for (let year = endYear; year >= startYear; year--) {
214
+ yearList.push(year);
215
+ }
216
+ }
217
+ return (0, _emotion.jsx)("div", null, (0, _emotion.jsx)("div", {
179
218
  css: style
180
- }, prevButton && cloneButton(prevButton, this.handleMonthChange('prev')), renderNavigationLabel ? (0, _callRenderProp.callRenderProp)(renderNavigationLabel) : (0, _emotion.jsx)("span", null, (0, _emotion.jsx)("div", null, visibleMonth.format('MMMM')), (0, _emotion.jsx)("div", null, visibleMonth.format('YYYY'))), nextButton && cloneButton(nextButton, this.handleMonthChange('next')));
219
+ }, prevButton && cloneButton(prevButton, this.handleMonthChange('prev')), renderNavigationLabel ? (0, _callRenderProp.callRenderProp)(renderNavigationLabel) : (0, _emotion.jsx)("span", null, (0, _emotion.jsx)("div", null, visibleMonth.format('MMMM')), !withYearPicker ? (0, _emotion.jsx)("div", null, visibleMonth.format('YYYY')) : null), nextButton && cloneButton(nextButton, this.handleMonthChange('next'))), withYearPicker ? (0, _emotion.jsx)("div", {
220
+ css: styles === null || styles === void 0 ? void 0 : styles.yearPicker
221
+ }, (0, _emotion.jsx)(_SimpleSelect.SimpleSelect, {
222
+ width: "90px",
223
+ renderLabel: "",
224
+ assistiveText: withYearPicker.screenReaderLabel,
225
+ value: Number(visibleMonth.format('YYYY')),
226
+ onChange: (e, {
227
+ value
228
+ }) => this.handleYearChange(e, Number(value))
229
+ }, yearList.map(year => (0, _emotion.jsx)(_SimpleSelect.SimpleSelect.Option, {
230
+ key: year,
231
+ id: `opt-${year}`,
232
+ value: year
233
+ }, `${year}`)))) : null);
181
234
  }
182
235
  renderBody() {
183
236
  return (0, _emotion.jsx)("table", {
@@ -48,6 +48,7 @@ const propTypes = exports.propTypes = {
48
48
  role: _propTypes.default.oneOf(['table', 'listbox']),
49
49
  selectedDate: _propTypes.default.string,
50
50
  timezone: _propTypes.default.string,
51
- visibleMonth: _propTypes.default.string
51
+ visibleMonth: _propTypes.default.string,
52
+ withYearPicker: _propTypes.default.object
52
53
  };
53
54
  const allowedProps = exports.allowedProps = ['as', 'children', 'currentDate', 'disabledDates', 'locale', 'onDateSelected', 'onRequestRenderNextMonth', 'onRequestRenderPrevMonth', 'renderNavigationLabel', 'renderNextMonthButton', 'renderPrevMonthButton', 'renderWeekdayLabels', 'role', 'selectedDate', 'timezone', 'visibleMonth'];
@@ -36,7 +36,8 @@ exports.default = void 0;
36
36
  * @param {Object} componentTheme The theme variable object.
37
37
  * @return {Object} The final style object, which will be used in the component
38
38
  */
39
- const generateStyle = componentTheme => {
39
+ const generateStyle = (componentTheme, props) => {
40
+ const withYearPicker = props.withYearPicker;
40
41
  return {
41
42
  navigation: {
42
43
  label: 'calendar__navigation',
@@ -46,7 +47,7 @@ const generateStyle = componentTheme => {
46
47
  fontFamily: componentTheme.fontFamily,
47
48
  fontSize: componentTheme.fontSize,
48
49
  fontWeight: componentTheme.fontWeight,
49
- marginBottom: componentTheme.navMargin
50
+ marginBottom: withYearPicker ? componentTheme.navWithYearMargin : componentTheme.navMargin
50
51
  },
51
52
  navigationWithButtons: {
52
53
  label: 'calendar__navigation--withButtons',
@@ -58,6 +59,12 @@ const generateStyle = componentTheme => {
58
59
  label: 'calendar__weekdayHeader',
59
60
  textAlign: 'center',
60
61
  maxWidth: componentTheme.maxHeaderWidth
62
+ },
63
+ yearPicker: {
64
+ display: 'flex',
65
+ justifyContent: 'center',
66
+ alignItems: 'center',
67
+ marginBottom: componentTheme.yearPickerMargin
61
68
  }
62
69
  };
63
70
  };
@@ -50,6 +50,8 @@ const generateComponentTheme = theme => {
50
50
  color: colors === null || colors === void 0 ? void 0 : colors.textDarkest,
51
51
  background: colors === null || colors === void 0 ? void 0 : colors.backgroundLightest,
52
52
  navMargin: spacing === null || spacing === void 0 ? void 0 : spacing.small,
53
+ navWithYearMargin: spacing === null || spacing === void 0 ? void 0 : spacing.xSmall,
54
+ yearPickerMargin: spacing === null || spacing === void 0 ? void 0 : spacing.mediumSmall,
53
55
  maxHeaderWidth: spacing === null || spacing === void 0 ? void 0 : spacing.medium
54
56
  };
55
57
  return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@instructure/ui-calendar",
3
- "version": "9.0.2-snapshot-19",
3
+ "version": "9.0.2-snapshot-24",
4
4
  "description": "A calendar component.",
5
5
  "author": "Instructure, Inc. Engineering and Product Design",
6
6
  "module": "./es/index.js",
@@ -23,29 +23,30 @@
23
23
  },
24
24
  "license": "MIT",
25
25
  "devDependencies": {
26
- "@instructure/ui-babel-preset": "9.0.2-snapshot-19",
27
- "@instructure/ui-color-utils": "9.0.2-snapshot-19",
28
- "@instructure/ui-test-locator": "9.0.2-snapshot-19",
29
- "@instructure/ui-test-utils": "9.0.2-snapshot-19",
30
- "@instructure/ui-themes": "9.0.2-snapshot-19",
26
+ "@instructure/ui-babel-preset": "9.0.2-snapshot-24",
27
+ "@instructure/ui-color-utils": "9.0.2-snapshot-24",
28
+ "@instructure/ui-test-locator": "9.0.2-snapshot-24",
29
+ "@instructure/ui-test-utils": "9.0.2-snapshot-24",
30
+ "@instructure/ui-themes": "9.0.2-snapshot-24",
31
31
  "@testing-library/jest-dom": "^6.4.5",
32
32
  "@testing-library/react": "^15.0.7"
33
33
  },
34
34
  "dependencies": {
35
35
  "@babel/runtime": "^7.24.5",
36
- "@instructure/console": "9.0.2-snapshot-19",
37
- "@instructure/emotion": "9.0.2-snapshot-19",
38
- "@instructure/shared-types": "9.0.2-snapshot-19",
39
- "@instructure/ui-a11y-content": "9.0.2-snapshot-19",
40
- "@instructure/ui-buttons": "9.0.2-snapshot-19",
41
- "@instructure/ui-i18n": "9.0.2-snapshot-19",
42
- "@instructure/ui-icons": "9.0.2-snapshot-19",
43
- "@instructure/ui-prop-types": "9.0.2-snapshot-19",
44
- "@instructure/ui-react-utils": "9.0.2-snapshot-19",
45
- "@instructure/ui-testable": "9.0.2-snapshot-19",
46
- "@instructure/ui-utils": "9.0.2-snapshot-19",
47
- "@instructure/ui-view": "9.0.2-snapshot-19",
48
- "@instructure/uid": "9.0.2-snapshot-19",
36
+ "@instructure/console": "9.0.2-snapshot-24",
37
+ "@instructure/emotion": "9.0.2-snapshot-24",
38
+ "@instructure/shared-types": "9.0.2-snapshot-24",
39
+ "@instructure/ui-a11y-content": "9.0.2-snapshot-24",
40
+ "@instructure/ui-buttons": "9.0.2-snapshot-24",
41
+ "@instructure/ui-i18n": "9.0.2-snapshot-24",
42
+ "@instructure/ui-icons": "9.0.2-snapshot-24",
43
+ "@instructure/ui-prop-types": "9.0.2-snapshot-24",
44
+ "@instructure/ui-react-utils": "9.0.2-snapshot-24",
45
+ "@instructure/ui-simple-select": "9.0.2-snapshot-24",
46
+ "@instructure/ui-testable": "9.0.2-snapshot-24",
47
+ "@instructure/ui-utils": "9.0.2-snapshot-24",
48
+ "@instructure/ui-view": "9.0.2-snapshot-24",
49
+ "@instructure/uid": "9.0.2-snapshot-24",
49
50
  "prop-types": "^15.8.1"
50
51
  },
51
52
  "peerDependencies": {
@@ -48,6 +48,43 @@ class Example extends React.Component {
48
48
  render(<Example />)
49
49
  ```
50
50
 
51
+ ### With year picker
52
+
53
+ ```js
54
+ ---
55
+ type: example
56
+ ---
57
+ class Example extends React.Component {
58
+ state = {
59
+ selectedDate: "",
60
+ visibleMonth:"2024-02"
61
+ }
62
+
63
+ setVisibleMonth = () => {
64
+ this.setState({visibleMonth: "2028-09"})
65
+ }
66
+ render = () =>
67
+ <Calendar
68
+ visibleMonth={this.state.visibleMonth}
69
+ currentDate="2024-02-29"
70
+ disabledDates={['2023-12-22', '2023-12-12', '2023-12-11']}
71
+ selectedDate={this.state.selectedDate}
72
+ onRequestRenderNextMonth={(_e, requestedMonth) => this.setState({visibleMonth: requestedMonth})}
73
+ onRequestRenderPrevMonth={(_e, requestedMonth) => this.setState({visibleMonth: requestedMonth})}
74
+ onDateSelected={(date)=>{
75
+ this.setState({selectedDate: date})
76
+ }}
77
+ withYearPicker={{
78
+ screenReaderLabel: "Year picker",
79
+ startYear:1999,
80
+ endYear:2024,
81
+ maxHeight: "200px"
82
+ }}
83
+ />
84
+ }
85
+ render(<Example />)
86
+ ```
87
+
51
88
  ### Composing a Calendar in your Application
52
89
 
53
90
  By design, the `Calendar` component does not dictate which date libraries or
@@ -58,6 +58,8 @@ import {
58
58
  IconArrowOpenEndSolid
59
59
  } from '@instructure/ui-icons'
60
60
 
61
+ import { SimpleSelect } from '@instructure/ui-simple-select'
62
+
61
63
  /**
62
64
  ---
63
65
  category: components
@@ -142,6 +144,26 @@ class Calendar extends Component<CalendarProps, CalendarState> {
142
144
  return this.props.role === 'listbox' ? this.props.role : undefined
143
145
  }
144
146
 
147
+ get hasPrevMonth() {
148
+ return (
149
+ !this.props.withYearPicker ||
150
+ (this.props.withYearPicker &&
151
+ Number(
152
+ this.state.visibleMonth.clone().subtract({ months: 1 }).format('YYYY')
153
+ ) >= this.props.withYearPicker.startYear)
154
+ )
155
+ }
156
+
157
+ get hasNextMonth() {
158
+ return (
159
+ !this.props.withYearPicker ||
160
+ (this.props.withYearPicker &&
161
+ Number(
162
+ this.state.visibleMonth.clone().add({ months: 1 }).format('YYYY')
163
+ ) <= this.props.withYearPicker.endYear)
164
+ )
165
+ }
166
+
145
167
  renderMonthNavigationButtons = () => {
146
168
  const { renderNextMonthButton, renderPrevMonthButton } = this.props
147
169
 
@@ -176,6 +198,9 @@ class Calendar extends Component<CalendarProps, CalendarState> {
176
198
  const { visibleMonth } = this.state
177
199
  const newDate = visibleMonth.clone()
178
200
  if (direction === 'prev') {
201
+ if (!this.hasPrevMonth) {
202
+ return
203
+ }
179
204
  if (onRequestRenderPrevMonth) {
180
205
  onRequestRenderPrevMonth(
181
206
  e,
@@ -185,6 +210,9 @@ class Calendar extends Component<CalendarProps, CalendarState> {
185
210
  }
186
211
  newDate.subtract({ months: 1 })
187
212
  } else {
213
+ if (!this.hasNextMonth) {
214
+ return
215
+ }
188
216
  if (onRequestRenderNextMonth) {
189
217
  onRequestRenderNextMonth(
190
218
  e,
@@ -197,8 +225,23 @@ class Calendar extends Component<CalendarProps, CalendarState> {
197
225
  this.setState({ visibleMonth: newDate })
198
226
  }
199
227
 
228
+ handleYearChange = (
229
+ e: React.SyntheticEvent<Element, Event>,
230
+ year: number
231
+ ) => {
232
+ const { withYearPicker } = this.props
233
+ const { visibleMonth } = this.state
234
+ const newDate = visibleMonth.clone()
235
+ if (withYearPicker?.onRequestYearChange) {
236
+ withYearPicker.onRequestYearChange(e, year)
237
+ return
238
+ }
239
+ newDate.year(year)
240
+ this.setState({ visibleMonth: newDate })
241
+ }
242
+
200
243
  renderHeader() {
201
- const { renderNavigationLabel, styles } = this.props
244
+ const { renderNavigationLabel, styles, withYearPicker } = this.props
202
245
  const { visibleMonth } = this.state
203
246
  const { prevButton, nextButton } = this.renderMonthNavigationButtons()
204
247
 
@@ -218,18 +261,59 @@ class Calendar extends Component<CalendarProps, CalendarState> {
218
261
  ...(prevButton || nextButton ? [styles?.navigationWithButtons] : [])
219
262
  ]
220
263
 
264
+ const yearList: number[] = []
265
+
266
+ if (withYearPicker) {
267
+ const { startYear, endYear } = withYearPicker
268
+ for (let year = endYear; year >= startYear!; year--) {
269
+ yearList.push(year)
270
+ }
271
+ }
272
+
221
273
  return (
222
- <div css={style}>
223
- {prevButton && cloneButton(prevButton, this.handleMonthChange('prev'))}
224
- {renderNavigationLabel ? (
225
- callRenderProp(renderNavigationLabel)
226
- ) : (
227
- <span>
228
- <div>{visibleMonth.format('MMMM')}</div>
229
- <div>{visibleMonth.format('YYYY')}</div>
230
- </span>
231
- )}
232
- {nextButton && cloneButton(nextButton, this.handleMonthChange('next'))}
274
+ <div>
275
+ <div css={style}>
276
+ {prevButton &&
277
+ cloneButton(prevButton, this.handleMonthChange('prev'))}
278
+ {renderNavigationLabel ? (
279
+ callRenderProp(renderNavigationLabel)
280
+ ) : (
281
+ <span>
282
+ <div>{visibleMonth.format('MMMM')}</div>
283
+ {!withYearPicker ? (
284
+ <div>{visibleMonth.format('YYYY')}</div>
285
+ ) : null}
286
+ </span>
287
+ )}
288
+ {nextButton &&
289
+ cloneButton(nextButton, this.handleMonthChange('next'))}
290
+ </div>
291
+
292
+ {withYearPicker ? (
293
+ <div css={styles?.yearPicker}>
294
+ <SimpleSelect
295
+ width="90px"
296
+ renderLabel=""
297
+ assistiveText={withYearPicker.screenReaderLabel}
298
+ value={Number(visibleMonth.format('YYYY'))}
299
+ onChange={(
300
+ e: React.SyntheticEvent<Element, Event>,
301
+ {
302
+ value
303
+ }: {
304
+ value?: string | number | undefined
305
+ id?: string | undefined
306
+ }
307
+ ) => this.handleYearChange(e, Number(value))}
308
+ >
309
+ {yearList.map((year) => (
310
+ <SimpleSelect.Option key={year} id={`opt-${year}`} value={year}>
311
+ {`${year}`}
312
+ </SimpleSelect.Option>
313
+ ))}
314
+ </SimpleSelect>
315
+ </div>
316
+ ) : null}
233
317
  </div>
234
318
  )
235
319
  }
@@ -152,6 +152,24 @@ type CalendarOwnProps = {
152
152
  * Visible month for the rendered calendar. Formatted as an ISO date string.
153
153
  */
154
154
  visibleMonth?: string
155
+
156
+ /**
157
+ * If set, years can be picked from a dropdown.
158
+ * It accepts an object.
159
+ * screenReaderLabel: string // e.g.: i18n("pick a year")
160
+ *
161
+ * onRequestYearChange?:(e: React.MouseEvent,requestedYear: number): void // if set, on year change, only this will be called and no internal change will take place
162
+ *
163
+ * startYear: number // e.g.: 2001, sets the start year of the selectable list
164
+ *
165
+ * endYear: number // e.g.: 2030, sets the end year of the selectable list
166
+ */
167
+ withYearPicker?: {
168
+ screenReaderLabel: string
169
+ onRequestYearChange?: (e: any, requestedYear: number) => void
170
+ startYear: number
171
+ endYear: number
172
+ }
155
173
  }
156
174
 
157
175
  type PropKeys = keyof CalendarOwnProps
@@ -163,7 +181,7 @@ type CalendarProps = CalendarOwnProps &
163
181
  OtherHTMLAttributes<CalendarOwnProps>
164
182
 
165
183
  type CalendarStyle = ComponentStyle<
166
- 'navigation' | 'navigationWithButtons' | 'weekdayHeader'
184
+ 'navigation' | 'navigationWithButtons' | 'weekdayHeader' | 'yearPicker'
167
185
  >
168
186
 
169
187
  const propTypes: PropValidators<PropKeys> = {
@@ -184,7 +202,8 @@ const propTypes: PropValidators<PropKeys> = {
184
202
  role: PropTypes.oneOf(['table', 'listbox']),
185
203
  selectedDate: PropTypes.string,
186
204
  timezone: PropTypes.string,
187
- visibleMonth: PropTypes.string
205
+ visibleMonth: PropTypes.string,
206
+ withYearPicker: PropTypes.object
188
207
  }
189
208
 
190
209
  const allowedProps: AllowedPropKeys = [
@@ -23,7 +23,7 @@
23
23
  */
24
24
 
25
25
  import type { CalendarTheme } from '@instructure/shared-types'
26
- import type { CalendarStyle } from './props'
26
+ import type { CalendarStyle, CalendarProps } from './props'
27
27
 
28
28
  /**
29
29
  * ---
@@ -33,7 +33,11 @@ import type { CalendarStyle } from './props'
33
33
  * @param {Object} componentTheme The theme variable object.
34
34
  * @return {Object} The final style object, which will be used in the component
35
35
  */
36
- const generateStyle = (componentTheme: CalendarTheme): CalendarStyle => {
36
+ const generateStyle = (
37
+ componentTheme: CalendarTheme,
38
+ props: CalendarProps
39
+ ): CalendarStyle => {
40
+ const { withYearPicker } = props
37
41
  return {
38
42
  navigation: {
39
43
  label: 'calendar__navigation',
@@ -43,7 +47,9 @@ const generateStyle = (componentTheme: CalendarTheme): CalendarStyle => {
43
47
  fontFamily: componentTheme.fontFamily,
44
48
  fontSize: componentTheme.fontSize,
45
49
  fontWeight: componentTheme.fontWeight,
46
- marginBottom: componentTheme.navMargin
50
+ marginBottom: withYearPicker
51
+ ? componentTheme.navWithYearMargin
52
+ : componentTheme.navMargin
47
53
  },
48
54
  navigationWithButtons: {
49
55
  label: 'calendar__navigation--withButtons',
@@ -55,6 +61,12 @@ const generateStyle = (componentTheme: CalendarTheme): CalendarStyle => {
55
61
  label: 'calendar__weekdayHeader',
56
62
  textAlign: 'center',
57
63
  maxWidth: componentTheme.maxHeaderWidth
64
+ },
65
+ yearPicker: {
66
+ display: 'flex',
67
+ justifyContent: 'center',
68
+ alignItems: 'center',
69
+ marginBottom: componentTheme.yearPickerMargin
58
70
  }
59
71
  }
60
72
  }