diff --git a/package-lock.json b/package-lock.json
index 790cba7627..9bf3b39011 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -41396,6 +41396,7 @@
"@instructure/ui-utils": "10.2.2",
"@instructure/ui-view": "10.2.2",
"@instructure/uid": "10.2.2",
+ "moment-timezone": "^0.5.45",
"prop-types": "^15.8.1"
},
"devDependencies": {
diff --git a/packages/ui-calendar/package.json b/packages/ui-calendar/package.json
index 15b9ee4334..08d2564654 100644
--- a/packages/ui-calendar/package.json
+++ b/packages/ui-calendar/package.json
@@ -48,6 +48,7 @@
"@instructure/ui-utils": "10.2.2",
"@instructure/ui-view": "10.2.2",
"@instructure/uid": "10.2.2",
+ "moment-timezone": "^0.5.45",
"prop-types": "^15.8.1"
},
"peerDependencies": {
diff --git a/packages/ui-calendar/src/Calendar/README.md b/packages/ui-calendar/src/Calendar/README.md
index 6d56fd8057..809b455d75 100644
--- a/packages/ui-calendar/src/Calendar/README.md
+++ b/packages/ui-calendar/src/Calendar/README.md
@@ -14,396 +14,5 @@ to the `Calendar` component.
---
type: example
---
-
+
```
-
-### Default config with additional props
-
-- ```js
- class Example extends React.Component {
- state = {
- selectedDate: '',
- visibleMonth: '2025-05'
- }
-
- render = () => (
-
- this.setState({ visibleMonth: requestedMonth })
- }
- onRequestRenderPrevMonth={(_e, requestedMonth) =>
- this.setState({ visibleMonth: requestedMonth })
- }
- onDateSelected={(date) => {
- this.setState({ selectedDate: date })
- }}
- />
- )
- }
- render()
- ```
-
-- ```js
- const Example = () => {
- const [selectedDate, setSelectedDate] = useState('')
- const [visibleMonth, setVisibleMonth] = useState('2025-05')
-
- return (
-
- setVisibleMonth(requestedMonth)
- }
- onRequestRenderPrevMonth={(_e, requestedMonth) =>
- setVisibleMonth(requestedMonth)
- }
- onDateSelected={(date) => {
- setSelectedDate(date)
- }}
- />
- )
- }
- render()
- ```
-
-### With year picker
-
-- ```js
- class Example extends React.Component {
- state = {
- selectedDate: '',
- visibleMonth: '2024-02'
- }
-
- render = () => (
-
- this.setState({ visibleMonth: requestedMonth })
- }
- onRequestRenderPrevMonth={(_e, requestedMonth) =>
- this.setState({ visibleMonth: requestedMonth })
- }
- onDateSelected={(date) => {
- this.setState({ selectedDate: date })
- }}
- withYearPicker={{
- screenReaderLabel: 'Year picker',
- startYear: 1999,
- endYear: 2024,
- maxHeight: '200px'
- }}
- />
- )
- }
- render()
- ```
-
-- ```js
- const Example = () => {
- const [selectedDate, setSelectedDate] = useState('')
- const [visibleMonth, setVisibleMonth] = useState('2024-02')
-
- return (
-
- setVisibleMonth(requestedMonth)
- }
- onRequestRenderPrevMonth={(_e, requestedMonth) =>
- setVisibleMonth(requestedMonth)
- }
- onDateSelected={(date) => {
- setSelectedDate(date)
- }}
- withYearPicker={{
- screenReaderLabel: 'Year picker',
- startYear: 1999,
- endYear: 2024,
- maxHeight: '200px'
- }}
- />
- )
- }
- render()
- ```
-
-### Composing a Calendar in your Application
-
-By design, the `Calendar` component does not dictate which date libraries or
-formats you use in your application. The following example demonstrates how a
-basic `Calendar` might be created using utilities from
-[Moment.js](https://momentjs.com/docs/#/parsing/).
-
-- ```js
- class Example extends React.Component {
- state = {
- todayDate: parseDate('2019-08-16').toISOString(),
- renderedDate: parseDate('2019-08-02').toISOString()
- }
-
- generateMonth = () => {
- const date = parseDate(this.state.renderedDate)
- .startOf('month')
- .startOf('week')
-
- return Array.apply(null, Array(Calendar.DAY_COUNT)).map(() => {
- const currentDate = date.clone()
- date.add(1, 'days')
- return currentDate
- })
- }
-
- renderWeekdayLabels = () => {
- const date = parseDate(this.state.renderedDate).startOf('week')
-
- return Array.apply(null, Array(7)).map(() => {
- const currentDate = date.clone()
- date.add(1, 'day')
-
- return (
-
- {currentDate.format('dd')}
-
- )
- })
- }
-
- handleRenderNextMonth = (event) => {
- this.modifyRenderedMonth(1)
- }
-
- handleRenderPrevMonth = (event) => {
- this.modifyRenderedMonth(-1)
- }
-
- modifyRenderedMonth = (step) => {
- this.setState(({ renderedDate }) => {
- const date = parseDate(renderedDate)
- date.add(step, 'month')
- return { renderedDate: date.toISOString() }
- })
- }
-
- renderDay(date) {
- const { renderedDate, todayDate } = this.state
-
- return (
-
- {date.format('D')}
-
- )
- }
-
- render() {
- const date = parseDate(this.state.renderedDate)
-
- const buttonProps = (type = 'prev') => ({
- size: 'small',
- withBackground: false,
- withBorder: false,
- renderIcon:
- type === 'prev' ? (
-
- ) : (
-
- ),
- screenReaderLabel: type === 'prev' ? 'Previous month' : 'Next month'
- })
-
- return (
- }
- renderNextMonthButton={}
- renderNavigationLabel={
-
- {date.format('MMMM')}
- {date.format('YYYY')}
-
- }
- renderWeekdayLabels={this.renderWeekdayLabels()}
- onRequestRenderNextMonth={this.handleRenderNextMonth}
- onRequestRenderPrevMonth={this.handleRenderPrevMonth}
- >
- {this.generateMonth().map((date) => this.renderDay(date))}
-
- )
- }
- }
-
- const locale = 'en-us'
- const timezone = 'America/Denver'
-
- const parseDate = (dateStr) => {
- return moment.tz(dateStr, [moment.ISO_8601], locale, timezone)
- }
-
- render()
- ```
-
-- ```js
- const Example = () => {
- const [renderedDate, setRenderedDate] = useState(
- parseDate('2019-08-02').toISOString()
- )
-
- const todayDate = parseDate('2019-08-16').toISOString()
- const generateMonth = () => {
- const date = parseDate(renderedDate).startOf('month').startOf('week')
-
- return Array.apply(null, Array(Calendar.DAY_COUNT)).map(() => {
- const currentDate = date.clone()
- date.add(1, 'days')
- return currentDate
- })
- }
-
- const renderWeekdayLabels = () => {
- const date = parseDate(renderedDate).startOf('week')
-
- return Array.apply(null, Array(7)).map(() => {
- const currentDate = date.clone()
- date.add(1, 'day')
-
- return (
-
- {currentDate.format('dd')}
-
- )
- })
- }
-
- const handleRenderNextMonth = (event) => {
- modifyRenderedMonth(1)
- }
-
- const handleRenderPrevMonth = (event) => {
- modifyRenderedMonth(-1)
- }
-
- const modifyRenderedMonth = (step) => {
- const date = parseDate(renderedDate)
- date.add(step, 'month')
- setRenderedDate(date.toISOString())
- }
-
- const renderDay = (date) => {
- return (
-
- {date.format('D')}
-
- )
- }
-
- const date = parseDate(renderedDate)
-
- const buttonProps = (type = 'prev') => ({
- size: 'small',
- withBackground: false,
- withBorder: false,
- renderIcon:
- type === 'prev' ? (
-
- ) : (
-
- ),
- screenReaderLabel: type === 'prev' ? 'Previous month' : 'Next month'
- })
-
- return (
- }
- renderNextMonthButton={}
- renderNavigationLabel={
-
- {date.format('MMMM')}
- {date.format('YYYY')}
-
- }
- renderWeekdayLabels={renderWeekdayLabels()}
- onRequestRenderNextMonth={handleRenderNextMonth}
- onRequestRenderPrevMonth={handleRenderPrevMonth}
- >
- {generateMonth().map((date) => renderDay(date))}
-
- )
- }
-
- const locale = 'en-us'
- const timezone = 'America/Denver'
-
- const parseDate = (dateStr) => {
- return moment.tz(dateStr, [moment.ISO_8601], locale, timezone)
- }
-
- render()
- ```
-
-#### Some dates to keep track of
-
-- `todayDate` - the date that represents today
-- `renderedDate` - the date that the user is viewing as they navigate the `Calendar`
-
-#### Generating a month
-
-We generate a month based on the `renderedDate` value. The `Calendar` always
-displays 6 weeks or 42 days (42 is defined as a constant `Calendar.DAY_COUNT`),
-so we pad our month values with days from the previous and next month if
-necessary. The complete implementation can be seen in the `generateMonth` function
-in our example.
-
-#### Rendering days
-
-Using the month data, we can now map it to children of type `Calendar.Day`.
-As we render each day, if it is outside the current month we can set the
-`isOutsideMonth` prop. We can also set the `isToday` prop if it is the current
-date. For accessibility, it is recommended that you provide more information to
-each `Calendar.Day` using the label prop. This label will help screen readers to
-have important context as the `Calendar` is navigated. It should include the day,
-month, and the year (Ex. instead of `1` we would provide `1 August 2019`).
-
-#### Rendering weekday labels
-
-`Calendar` requires you to provide an array of 7 labels that correspond to each
-day of the week via the `renderWeekdayLabels` prop. The visible portion of the
-label should be abbreviated (no longer than three characters). Note that screen
-readers will read this content preceding each date as the `Calendar` is navigated.
-Consider using [AccessibleContent](#AccessibleContent) with the `alt` prop
-containing the full day name for assistive technologies and the children containing
-the abbreviation. ex. `[Sun, ...]`
-
-#### Rendering next and previous month buttons
-
-The `renderNextMonthButton` and `renderPrevMonthButton` can be supplied using the
-[IconButton](#IconButton) component with the `size` prop set to
-`small`, the `withBackground` and `withBorder` props both set to `false`, and the `renderIcon` prop set to [IconArrowOpenStart](#iconography) or
-[IconArrowOpenEnd](#iconography).
diff --git a/packages/ui-calendar/src/Calendar/index.tsx b/packages/ui-calendar/src/Calendar/index.tsx
index 6c05fc0394..a9c5af00e7 100644
--- a/packages/ui-calendar/src/Calendar/index.tsx
+++ b/packages/ui-calendar/src/Calendar/index.tsx
@@ -41,6 +41,7 @@ import { testable } from '@instructure/ui-testable'
import { withStyle, jsx } from '@instructure/emotion'
import { Locale, DateTime, ApplyLocaleContext } from '@instructure/ui-i18n'
+import moment from 'moment-timezone'
import type { Moment } from '@instructure/ui-i18n'
import generateStyle from './styles'
@@ -474,10 +475,23 @@ class Calendar extends Component {
const currDate = DateTime.getFirstDayOfWeek(
visibleMonth.clone().startOf('month')
)
+
const arr: Moment[] = []
for (let i = 0; i < Calendar.DAY_COUNT; i++) {
- arr.push(currDate.clone())
- currDate.add({ days: 1 })
+
+ // This is needed because moment's `.add({days: 1})` function has a bug that happens when the date added lands perfectly onto the DST cutoff,
+ // in these cases adding 1 day results in 23 hours added instead,
+ // so `moment.tz('2024-09-07T00:00:00', 'America/Santiago').add({days: 1})` results
+ // in "Sat Sep 07 2024 23:00:00 GMT-0400" instead of "Sun Sep 08 2024 00:00:00 GMT-0400".
+ // which would cause duplicate dates in the calendar.
+ // More info on the bug: https://github.com/moment/moment/issues/4743
+ if (currDate.clone().format('HH') === '23') {
+ arr.push(currDate.clone().add({hours: 1}))
+ } else {
+ arr.push(currDate.clone())
+ }
+
+ currDate.add({days: 1})
}
return arr.map((date) => {
const dateStr = date.toISOString()
diff --git a/packages/ui-date-time-input/src/DateTimeInput/index.tsx b/packages/ui-date-time-input/src/DateTimeInput/index.tsx
index 081d7149ee..4bbfcb2f7e 100644
--- a/packages/ui-date-time-input/src/DateTimeInput/index.tsx
+++ b/packages/ui-date-time-input/src/DateTimeInput/index.tsx
@@ -432,6 +432,17 @@ class DateTimeInput extends Component {
for (let i = 0; i < Calendar.DAY_COUNT; i++) {
arr.push(currDate.clone())
currDate.add({ days: 1 })
+
+ // This is needed because moment's `.add({days: 123})` function has a bug that happens when the date added lands perfectly onto the DST cutoff,
+ // in these cases adding 1 day results in 23 hours added instead,
+ // so `moment.tz('2024-09-07T00:00:00', 'America/Santiago').add({days: 1})` results
+ // in "Sat Sep 07 2024 23:00:00 GMT-0400" instead of "Sun Sep 08 2024 00:00:00 GMT-0400".
+ // which would cause duplicate dates in the calendar.
+ // More info and source for the workaround: https://github.com/moment/moment/issues/4743
+ // currDate = DateTime.safeAddDays(currDate, 1)
+ if (currDate.clone().format('HH') === '23') {
+ currDate.add({hours: 1})
+ }
}
return arr.map((date) => {
const dateStr = date.toISOString()
diff --git a/packages/ui-i18n/src/DateTime.ts b/packages/ui-i18n/src/DateTime.ts
index 6b186c574f..eda413daf5 100644
--- a/packages/ui-i18n/src/DateTime.ts
+++ b/packages/ui-i18n/src/DateTime.ts
@@ -29,7 +29,7 @@ import moment, { Moment } from 'moment-timezone'
* category: utilities/i18n
* ---
* @deprecated
- * #### DEPRECATION WARNING: Will be removed in v9, which wil include a
+ * #### DEPRECATION WARNING: Will be removed in a future version, which will include a
* time library agnostic API.
* A wrapper for [moment](https://momentjs.com/) utils.
* @module DateTime