Repository: adobe/react-spectrum
File: packages/@react-stately/datepicker/src/IncompleteDate.ts (TypeScript)

Code before:
    switch (field) {
      case 'era': {
        let eras = this.calendar.getEras();
        let index = eras.indexOf(res.era!);
        index = cycleValue(index, amount, 0, eras.length - 1);
        res.era = eras[index];
        break;
      }
      case 'year': {
        // Use CalendarDate to cycle so that we update the era when going between 1 AD and 1 BC.
        let date = new CalendarDate(this.calendar, this.era ?? placeholder.era, this.year ?? placeholder.year, this.month ?? 1, this.day ?? 1);
        date = date.cycle(field, amount, {round: field === 'year'});
        res.era = date.era;
        res.year = date.year;
        break;
      }
      case 'month':
        res.month = cycleValue(res.month ?? 1, amount, 1, this.calendar.getMaximumMonthsInYear());
        break;
      case 'day':
        // Allow incrementing up to the maximum number of days in any month.
        res.day = cycleValue(res.day ?? 1, amount, 1, this.calendar.getMaximumDaysInMonth());
        break;
      case 'hour': {
        // if date is fully defined and we have a time zone, use ZonedDateTime to cycle so DST fallback is properly handled
        if ('timeZone' in placeholder && res.year != null && res.month != null && res.day != null) {
          let hour = res.hour ?? 0;
          hour = fromHourCycle(hour, res.dayPeriod ?? 0, this.hourCycle);
          // get the offset of the current date time in case we don't have a offset stored already (can't just use placeholder offset since that is a different datetime).
          if (res.offset == null) {
            let calendarDateTime = new CalendarDateTime(
              this.calendar,
              res.era ?? placeholder.era,
              res.year,
              res.month,
              res.day,
              hour,
              res.minute ?? placeholder.minute,
              res.second ?? placeholder.second,
              res.millisecond ?? placeholder.millisecond
            );

            res.offset = toZoned(calendarDateTime, placeholder.timeZone).offset;
          }

          let date = new ZonedDateTime(this.calendar, res.era ?? placeholder.era, res.year, res.month, res.day, placeholder.timeZone, res.offset, hour, res.minute ?? placeholder.minute, res.second ?? placeholder.second ?? 0, res.millisecond ?? placeholder.millisecond ?? 0);
          date = date.cycle('hour', amount, {hourCycle: this.hourCycle === 'h12' ? 12 : 24, round: false});
          let [dayPeriod, adjustedHour] = toHourCycle(date.hour, this.hourCycle);
          res.hour = adjustedHour;
          res.dayPeriod = dayPeriod;
          res.offset = date.offset;

Review comment (suggestion):
One case where this might not work is in TimeField when you pass a ZonedDateTime as the placeholder. In that case the year/month/day segments are not displayed so they'll always be null. We kinda need to do the same `isComplete` check we do elsewhere, taking the `maxGranularity` into account, and using the placeholder value for any unset fields.

Code after:
    switch (field) {
      case 'era': {
        let eras = this.calendar.getEras();
        let index = eras.indexOf(res.era!);
        index = cycleValue(index, amount, 0, eras.length - 1);
        res.era = eras[index];
        break;
      }
      case 'year': {
        // Use CalendarDate to cycle so that we update the era when going between 1 AD and 1 BC.
        let date = new CalendarDate(this.calendar, this.era ?? placeholder.era, this.year ?? placeholder.year, this.month ?? 1, this.day ?? 1);
        date = date.cycle(field, amount, {round: field === 'year'});
        res.era = date.era;
        res.year = date.year;
        break;
      }
      case 'month':
        res.month = cycleValue(res.month ?? 1, amount, 1, this.calendar.getMaximumMonthsInYear());
        break;
      case 'day':
        // Allow incrementing up to the maximum number of days in any month.
        res.day = cycleValue(res.day ?? 1, amount, 1, this.calendar.getMaximumDaysInMonth());
        break;
      case 'hour': {
        // if date is fully defined or it is just a time field, and we have a time zone, use toValue to get a ZonedDateTime to cycle
        // so DST fallback is properly handled
        let hasDateSegements = displaySegments.some(s => ['year', 'month', 'day'].includes(s));
        if ('timeZone' in placeholder && (!hasDateSegements || (res.year != null && res.month != null && res.day != null))) {
          let date = this.toValue(placeholder) as ZonedDateTime;
          date = date.cycle('hour', amount, {hourCycle: this.hourCycle === 'h12' ? 12 : 24, round: false});
          let [dayPeriod, adjustedHour] = toHourCycle(date.hour, this.hourCycle);
          res.hour = adjustedHour;
          res.dayPeriod = dayPeriod;
          res.offset = date.offset;
        } else {
          let hours = res.hour ?? 0;
          let limits = this.getSegmentLimits('hour')!;
          res.hour = cycleValue(hours, amount, limits.minValue, limits.maxValue);
          if (res.dayPeriod == null && 'hour' in placeholder) {
            res.dayPeriod = toHourCycle(placeholder.hour, this.hourCycle)[0];
          }
        }
        break;
      }
      case 'dayPeriod':
        res.dayPeriod = cycleValue(res.dayPeriod ?? 0, amount, 0, 1);
        break;
      case 'minute':
        res.minute = cycleValue(res.minute ?? 0, amount, 0, 59, true);
        break;
      case 'second':