Repository: az-digital/az_quickstart
Pull request #4153: Closes #2028, #4141: Repeating Events date list pushes Event Description (far) down the page
Author: login: bberndt-uaz
Labels: name: visual change
description: Introduces a visual change
name: needs-CWS-testing
description: Needs manual pre/post release testing by Campus Web Services
name: 2.13.x only
name: needs-documentation
description: Needs documentation on quickstart.arizona.edu
Created: 2025-03-25T23:58:48Z | Merged: 2025-04-02T21:43:18Z

<!--- Provide a general summary of your changes in the Title above -->

## Description
<!--- Describe your changes in detail (include keywords close/fix/resolve) -->
Modifies the display of recurring events on the event page to show the next event occurrence along with an accordion showing the list of all occurrences.

Questions:

1. Should the new presentation only be applied when an event has a certain number of event instances? If so, what should that number be?
   - (Mar 28, 2025) Per AZ Digital discussion, we can use the new presentation whenever an event has more than one instance.
2. Should the term "instance" be used or "occurrence"?
   - (Apr 2, 2025) I found that Google, Outlook, and Apple calendars all use “occurrences” so I updated the PR to use that term.

Follow-up work:

1. Add a line describing the recurrence above the "Next event" line.
2. Look into supporting reduced padding on the accordion.
3. See if the module hook code can be more concise in handling the Add to Calendar button.

### Release notes
<!--- Delete this line (and the closing comment line) to enable release notes.

If this change requires release notes: provide a summary of changes, how to use this change, and any related links. This content will be pasted in the [release notes](https://github.com/az-digital/az_quickstart/releases). Use markdown format to ensure proper pasting of information. [Release notes example](https://github.com/az-digital/az_quickstart/releases/tag/2.11.0)

Make sure to add the `release notes` label to this PR.

```
Add markdown of release notes here.
```

Delete this line to enable release notes.  -->


## Related issues
<!--- This project only accepts pull requests related to open issues -->
<!--- If suggesting a new feature or change, please discuss it in an issue first -->
<!--- If fixing a bug, there should be an issue describing it with steps to reproduce -->
<!--- Please link to the issue here: -->
 - #2028 
 - #4141

## Documentation
Updated https://quickstart.arizona.edu/create-content/events to add information about creating recurring events and their display on the event page.

## How to test
<!--- Please describe in detail how reviewers can test your changes -->
<!--- Include details of your testing environment and the tests you ran -->
Probo review site: https://7b5e2985-cedf-4a13-ab91-3a35916c835d--pr-4153.probo.build

Example events:
 - Some future instances, some past: [Weekend Canyon Running Training](https://7b5e2985-cedf-4a13-ab91-3a35916c835d--pr-4153.probo.build/events/weekend-canyon-running-training)
 - All future instances: [Skeptical Astronomy Literature Survey](https://7b5e2985-cedf-4a13-ab91-3a35916c835d--pr-4153.probo.build/events/skeptical-astronomy-literature-survey)
 - All past instances: [Planetary Engineering II (advanced topics)](https://7b5e2985-cedf-4a13-ab91-3a35916c835d--pr-4153.probo.build/events/planetary-engineering-ii-advanced-topics)

## Types of changes
<!--- What types of changes does your code introduce? Put an `x` in all the boxes that apply: -->

### Arizona Quickstart (install profile, custom modules, custom theme)
- **Patch release changes**
   - [ ] Bug fix
   - [ ] Accessibility, performance, or security improvement
   - [ ] Critical institutional link or brand change
   - [ ] Adding experimental module
   - [ ] Update experimental module
- **Minor release changes**
   - [ ] New feature
   - [x] Breaking or visual change to existing behavior
   - [ ] Upgrade experimental module to stable
   - [ ] Enable existing module by default or database update
   - [ ] Non-critical brand change
   - [ ] New internal API or API improvement with backwards compatibility
   - [ ] Risky or disruptive cleanup to comply with coding standards
   - [ ] High-risk or disruptive change (requires upgrade path, risks regression, etc.)
- **Other or unknown**
   - [ ] Other or unknown

### Drupal core
- **Patch release changes**
   - [ ] Security update
   - [ ] Patch level release (non-security bug-fix release)
   - [ ] Patch removal that's no longer necessary
- **Minor release changes**
   - [ ] Major or minor level update
- **Other or unknown**
   - [ ] Other or unknown

### Drupal contrib projects
- **Patch release changes**
   - [ ] Security update
   - [ ] Patch or minor level update
   - [ ] Add new module
   - [ ] Patch removal that's no longer necessary
- **Minor release changes**
   - [ ] Major level update
- **Other or unknown**
   - [ ] Other or unknown

## Checklist
<!--- Go over all the following points, and put an `x` in all the boxes that apply. -->
<!--- If you're unsure about any of these, don't hesitate to ask. We're here to help! -->
- [x] My code follows the code style of this project.
- [ ] My change requires a change to the documentation.
- [ ] I have updated the documentation accordingly.
- [x] I have read the **CONTRIBUTING** document.
- [ ] I have added tests to cover my changes.
- [x] All new and existing tests passed.
- [ ] My change requires release notes.


Linked issue #2028: Repeating Events date list pushes Event Description (far) down the page
In 2.5 we modified Events page view to put Where and When above subtitle, event description, etc. 

However, multiday/repeated events show a list of all event dates, which effectively forces important description content far down the page, particularly on mobile.

Examples:

https://asuatoday.arizona.edu/events/students-sustainability-work-day-community-garden-1400-e-mabel-st
https://quickstart.arizona.edu/events/arizona-sites-basics-training-session

Possible solutions/ideas:

- Make the list of dates behave like an accordion that allows a user to expand the full list of dates (but how does one know what their specific date of interest is?)
- Showing the full list of dates doesn't seem particularly useful for a user - they are generally considering a specific date for a repeated event.  User always clicks on a specific event date, never an event 'series'.  However, when they arrive on the event, they see a list of dates, not the date they were interested in.
- Necessary to show full list of dates at all?  Possible to only show the date of the event they clicked on?
- Make long date list a drop down menu that defaults to the date of the event clicked on by user when they selected the event
Linked issue comments: user: login: bberndt-uaz
created_at: 2022-11-28T22:48:13Z
body: I think the ideal solution for this would present the event recurrence in a similar way to Google Calendar and Outlook:

Google Calendar:
<img width="326" alt="image" src="https://user-images.githubusercontent.com/74572157/204395746-87a5d863-e3f4-4f6b-8ce1-3508a80c2bf3.png">

Outlook:
<img width="284" alt="image" src="https://user-images.githubusercontent.com/74572157/204392072-b14281c7-be2b-42c1-bf7c-c27094770eff.png">

Then, below the short version that displays the recurrence with start and end dates, an accordion could allow users to view the full list of dates. In #1100, @trackleft recommends copying the solution from the [master calendar](https://news.arizona.edu/calendar) (see an [example recurring event](https://news.arizona.edu/calendar/59616-weight-watchers-banner-university-medical-center)).

From what I can tell, right now we don't have the capability to show only the date the user clicked on, so I think that would be a separate issue to consider.
author_association: CONTRIBUTOR
user: login: trackleft
created_at: 2023-07-10T23:46:29Z
body: FYI this is the JS, from the news site:
```
  Drupal.behaviors.uanews_repeat_display = {
    attach: function(context, settings) {
      if ($('.date-repeat-rule').length > 0) {
        if ($('.fix-repeat-format').children().length > 0) {
          var startDateFromProp = null;
          var now = Date.now();
          var startDate = null;
          var active = false;
          $.each( $('.fix-repeat-format').children(), function( i, val ) {
            if (val != undefined || $(val).attr('content') != undefined) {
              startDateFromProp = $(val).attr('content');
              var startDate = new Date(startDateFromProp);
              if (startDate >= now && active == false) {
                $(val).prepend('<label class="text-white">Next:&nbsp;</label>') ;
                $(val).addClass('list-group-item active');
                active = true;
                return false;
              }
              else {
                $(val).addClass('disabled');
              }
            }
          });
          $.each( $('.fix-repeat-format').children(), function( i, val ) {
            if (i == 1) {
              if (active == true) {
                $( val ).replaceWith( "<em>" + $( val ).text() + "</em>" );
              }
              else {
                $( val ).replaceWith( '<em>This event occurred in the past.</em>' );
              }
            }
            if (i > 1 && $(val).hasClass('active') != true) {
              $(val).addClass('list-group-item');
              if (active == true) {
                $(val).hide();
              }
            }
          });
          if (active == true) {
            $('.fix-repeat-format').append('<div class="panel-footer"><a href="javascript:void(0);" class="date-show-more"><i class="ua-brand-bottom-corner" aria-hidden="true"></i> More dates</a><//div>');
          }
          $('.date-show-more').on("click", function () {
            if ($(this).hasClass('open')) {
              $(this).removeClass('open').html('<i class="ua-brand-bottom-corner"></i>More dates');
              $.each( $('.fix-repeat-format').children(), function( i, val ) {
                if (i > 1 && $(val).hasClass('panel-footer') == 0 && $(val).hasClass('active') != true) {
                  $(val).hide();
                }
              });
            }
            else {
              $(this).addClass('open').html('<i class="ua-brand-top-corner"></i>Fewer dates');
              $.each( $('.fix-repeat-format').children(), function( i, val ) {
                if (i > 1 && $(val).hasClass('panel-footer') == 0 && $(val).hasClass('active') != true) {
                  $(val).show();
                }
              });
            }
          });
        }
      }
    }
  };
  ```
author_association: MEMBER
user: login: trackleft
created_at: 2023-07-10T23:52:16Z
body: What happens in Outlook when there are exceptions?

Weekly Tuesday and Thursday until December 28th 2023 except on July 4th 2023,  November 23rd 2023, December 25th 2023.
author_association: MEMBER

Diff:
diff --git a/modules/custom/az_event/az_event.info.yml b/modules/custom/az_event/az_event.info.yml
index 0c332c9043..224d2d5f4e 100644
--- a/modules/custom/az_event/az_event.info.yml
+++ b/modules/custom/az_event/az_event.info.yml
@@ -4,6 +4,7 @@ description: 'Provides Event content type and related configuration.'
 core_version_requirement: ^9 || ^10
 package: 'The University of Arizona'
 dependencies:
+  - az_accordion
   - az_core
   - az_media
   - az_paragraphs_contact
diff --git a/modules/custom/az_event/az_event.module b/modules/custom/az_event/az_event.module
index 54c5ff90cc..42d5b659e4 100644
--- a/modules/custom/az_event/az_event.module
+++ b/modules/custom/az_event/az_event.module
@@ -27,6 +27,7 @@ function az_event_theme($existing, $type, $theme, $path) {
         'description' => NULL,
         'location' => NULL,
         'modal' => NULL,
+        'attributes' => NULL,
       ],
       'render element' => 'children',
     ],
@@ -107,21 +108,101 @@ function az_event_node_view(array &$build, EntityInterface $entity, EntityViewDi
       }
     }
 
-    // Add to Calendar.
+    // For the full content view mode, modify the display of recurring events.
     if (($view_mode === 'full') && ($entity instanceof NodeInterface)) {
-      foreach ($entity->field_az_event_date as $item) {
-        $now = \Drupal::time()->getCurrentTime();
-        if (($item->value >= $now) || (!empty($item->end_value) && ($item->end_value >= $now))) {
-          $build['field_az_event_date'][] = [
-            '#theme' => 'az_event_calendar_add_button',
-            '#title' => $entity->getTitle(),
-            '#start_date' => $item->value ?? '',
-            '#end_date' => $item->end_value ?? '',
-            '#description' => $entity->field_az_body->value ?? '',
-            '#location' => $entity->field_az_location->title ?? '',
-            '#modal' => Html::getUniqueId('calendar-link-modal'),
+      $now = \Drupal::time()->getCurrentTime();
+      /** @var \Drupal\Core\Field\FieldItemList $event_dates */
+      $event_dates = $entity->field_az_event_date;
+      $number_of_occurrences = $event_dates->count();
+      if ($number_of_occurrences > 1) {
+        // Initialize the accordion body build array.
+        $accordion_body = $build['field_az_event_date'];
+        foreach ($entity->field_az_event_date as $item) {
+          unset($accordion_body[$item->getName()]);
+        }
+        $item_index = 0;
+
+        // Add any upcoming occurrences to the accordion body.
+        $next_occurrence = NULL;
+        $add_to_calendar_button = NULL;
+        /** @var \Drupal\smart_date\Plugin\Field\FieldType\SmartDateItem $item */
+        foreach ($entity->field_az_event_date as $item) {
+          if (!empty($item->end_value) && $item->end_value > $now) {
+            if (!isset($next_occurrence)) {
+              $next_occurrence = $item->getName();
+              $upcoming_events_heading = t("Upcoming Events");
+              $accordion_body[$item_index++] = [
+                '#markup' => "<h3 class='h5 mt-0'>{$upcoming_events_heading}</h3>",
+              ];
+              // Add to Calendar button for an event with multiple occurrences.
+              $add_to_calendar_button = [
+                '#theme' => 'az_event_calendar_add_button',
+                '#title' => $entity->getTitle(),
+                '#start_date' => $item->value ?? '',
+                '#end_date' => $item->end_value ?? '',
+                '#description' => $entity->field_az_body->value ?? '',
+                '#location' => $entity->field_az_location->title ?? '',
+                '#modal' => Html::getUniqueId('calendar-link-modal'),
+                '#attributes' => ['class' => 'mb-4'],
+              ];
+            }
+            $accordion_body[$item_index++] = $build['field_az_event_date'][$item->getName()];
+          }
+        }
+
+        // Add any past occurrences to the accordion body.
+        if (!isset($next_occurrence) || $next_occurrence !== 0) {
+          $past_events_heading = t("Past Events");
+          $accordion_body[$item_index++] = [
+            '#markup' => "<h3 class='h5" . (isset($next_occurrence) ? '' : ' mt-0') . "'>{$past_events_heading}</h3>",
           ];
-          break;
+          for ($i = 0; $i < ($next_occurrence ?? $number_of_occurrences); $i++) {
+            $accordion_body[$item_index++] = $build['field_az_event_date'][$i];
+          }
+        }
+
+        // Replace/unset the items in the build array for the field.
+        $item_index = 0;
+        if (isset($next_occurrence)) {
+          $build['field_az_event_date'][$item_index] = $build['field_az_event_date'][$next_occurrence];
+          $next_event_text = t("Next event");
+          $build['field_az_event_date'][$item_index]['#prefix'] = "<div class='mb-3'><strong>{$next_event_text}:</strong> ";
+          $build['field_az_event_date'][$item_index]['#suffix'] = '</div>';
+          $item_index++;
+          if (isset($add_to_calendar_button)) {
+            $build['field_az_event_date'][$item_index] = $add_to_calendar_button;
+            $item_index++;
+          }
+        }
+        $build['field_az_event_date'][$item_index++] = [
+          '#theme' => 'az_accordion',
+          '#title' => t("View all occurrences"),
+          '#body' => $accordion_body,
+          '#attributes' => ['class' => 'accordion'],
+          '#accordion_item_id' => "event-" . $entity->id(),
+          '#collapsed' => 'collapse',
+          '#aria_expanded' => 'false',
+        ];
+        for ($i = $item_index; $i < $number_of_occurrences; $i++) {
+          unset($build['field_az_event_date'][$i]);
+        }
+      }
+      else {
+        // Add to Calendar button for an event with a single occurrence.
+        foreach ($entity->field_az_event_date as $item) {
+          if (($item->value >= $now) || (!empty($item->end_value) && ($item->end_value >= $now))) {
+            $build['field_az_event_date'][] = [
+              '#theme' => 'az_event_calendar_add_button',
+              '#title' => $entity->getTitle(),
+              '#start_date' => $item->value ?? '',
+              '#end_date' => $item->end_value ?? '',
+              '#description' => $entity->field_az_body->value ?? '',
+              '#location' => $entity->field_az_location->title ?? '',
+              '#modal' => Html::getUniqueId('calendar-link-modal'),
+              '#attributes' => ['class' => 'mt-3'],
+            ];
+            break;
+          }
         }
       }
     }
diff --git a/modules/custom/az_event/templates/az-event-calendar-add-button.html.twig b/modules/custom/az_event/templates/az-event-calendar-add-button.html.twig
index bbcf36d5df..ac7fe0784b 100644
--- a/modules/custom/az_event/templates/az-event-calendar-add-button.html.twig
+++ b/modules/custom/az_event/templates/az-event-calendar-add-button.html.twig
@@ -6,14 +6,12 @@
 * @ingroup themeable
 */
 #}
-{% set classes = [
-    'clear-both',
-] %}
+{% set attributes = attributes.addClass('clear-both') %}
 {% set links = calendar_links(title, date(start_date), date(end_date), FALSE, description, location) %}
 
-<div{{ attributes.addClass(classes) }}>
+<div{{ attributes }}>
   <!-- Button trigger modal -->
-  <button type="button" class="btn btn-hollow-primary btn-sm text-capitalize mt-3" data-toggle="modal" data-target="#{{ modal }}">
+  <button type="button" class="btn btn-hollow-primary btn-sm text-capitalize" data-toggle="modal" data-target="#{{ modal }}">
     <span class="material-icons-sharp mr-2 align-bottom">calendar_month</span>{{ 'Add to Calendar'|t }}
   </button>
 


Reviews:
user: login: danahertzberg
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-26T20:44:33Z
user: login: danahertzberg
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-26T20:45:34Z
user: login: bberndt-uaz
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-27T22:35:26Z
user: login: joshuasosa
body: Tested on Probo and it looks good!

My only note is that reading "instances" over and over feels wordier than it needs to be.
![image](https://github.com/user-attachments/assets/2bdb936d-d139-4ff2-bab4-1e14445e62a2)
![image](https://github.com/user-attachments/assets/df1c1129-3bf7-403b-b4a5-2e18c0721e09)

My suggestion would be to just remove the word "instances" in all cases. I think just saying "View all events," "Upcoming Events," etc or similar gets the point across. But I'm also ok with it as it is.
state: DISMISSED
author_association: CONTRIBUTOR
submitted_at: 2025-03-29T01:42:19Z
user: login: trackleft
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-31T17:08:58Z
user: login: bberndt-uaz
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-01T18:59:28Z
user: login: joshuasosa
state: DISMISSED
author_association: CONTRIBUTOR
submitted_at: 2025-04-01T23:18:09Z
user: login: alyssakw31
body: Looks good to me, I approve
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-02T00:10:22Z
user: login: tadean
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-02T01:03:47Z
user: login: danahertzberg
state: DISMISSED
author_association: CONTRIBUTOR
submitted_at: 2025-04-02T17:17:42Z
user: login: bberndt-uaz
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-02T17:26:51Z
user: login: joeparsons
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-02T18:36:35Z
user: login: joeparsons
state: DISMISSED
author_association: MEMBER
submitted_at: 2025-04-02T20:02:42Z
user: login: joshuasosa
state: DISMISSED
author_association: CONTRIBUTOR
submitted_at: 2025-04-02T20:23:05Z
user: login: joeparsons
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-02T20:44:07Z
user: login: joeparsons
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-02T21:06:30Z
user: login: tadean
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-02T21:06:47Z

Review comments:
diff_hunk: @@ -0,0 +1,96 @@
+{#
+/**
+ * @file
+ * Theme override for a field.
+ *
+ * Available variables:
+ * - attributes: HTML attributes for the containing element.
+ * - label_hidden: Whether to show the field label or not.
+ * - title_attributes: HTML attributes for the title.
+ * - label: The label for the field.
+ * - multiple: TRUE if a field can contain multiple items.
+ * - items: List of all the field items. Each item contains:
+ *   - attributes: List of HTML attributes for each item.
+ *   - content: The field item's content.
+ * - entity_type: The entity type to which the field belongs.
+ * - field_name: The name of the field.
+ * - field_type: The type of the field.
+ * - label_display: The display settings for the label.
+ *
+ *
+ * @see template_preprocess_field()
+ */
+#}
+{%
+  set classes = [
+    'field',
+    'field--name-' ~ field_name|clean_class,
+    'field--type-' ~ field_type|clean_class,
+    'field--label-' ~ label_display,
+  ]
+%}
+{%
+  set title_classes = [
+    'field__label',
+    'h3',
+    label_display == 'visually_hidden' ? 'visually-hidden',
+  ]
+%}
+
+{% if label_hidden %}
+  {% if multiple %}
+    <div{{ attributes.addClass(classes, 'field__items') }}>
+      {% if element['#view_mode'] == 'full' and items|length > 1 %}
+        {% set accordion_item_id = element['#object'].id %}
+        {% for item in items %}
+          {% if item['content']['#nextInstance'] == true %}
+            <div class="mb-3">
+              <div{{ item.attributes.addClass('field__item') }}>Next event: {{ item.content }}</div>
path: modules/custom/az_event/templates/field--node--field-az-event-date--az-event.html.twig
user: login: danahertzberg
body: ```suggestion
              <div{{ item.attributes.addClass('field__item') }}><strong>Next event:</strong> {{ item.content }}</div>
```
created_at: 2025-03-26T20:44:33Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,96 @@
+{#
+/**
+ * @file
+ * Theme override for a field.
+ *
+ * Available variables:
+ * - attributes: HTML attributes for the containing element.
+ * - label_hidden: Whether to show the field label or not.
+ * - title_attributes: HTML attributes for the title.
+ * - label: The label for the field.
+ * - multiple: TRUE if a field can contain multiple items.
+ * - items: List of all the field items. Each item contains:
+ *   - attributes: List of HTML attributes for each item.
+ *   - content: The field item's content.
+ * - entity_type: The entity type to which the field belongs.
+ * - field_name: The name of the field.
+ * - field_type: The type of the field.
+ * - label_display: The display settings for the label.
+ *
+ *
+ * @see template_preprocess_field()
+ */
+#}
+{%
+  set classes = [
+    'field',
+    'field--name-' ~ field_name|clean_class,
+    'field--type-' ~ field_type|clean_class,
+    'field--label-' ~ label_display,
+  ]
+%}
+{%
+  set title_classes = [
+    'field__label',
+    'h3',
+    label_display == 'visually_hidden' ? 'visually-hidden',
+  ]
+%}
+
+{% if label_hidden %}
+  {% if multiple %}
+    <div{{ attributes.addClass(classes, 'field__items') }}>
+      {% if element['#view_mode'] == 'full' and items|length > 1 %}
+        {% set accordion_item_id = element['#object'].id %}
+        {% for item in items %}
+          {% if item['content']['#nextInstance'] == true %}
+            <div class="mb-3">
+              <div{{ item.attributes.addClass('field__item') }}>Next event: {{ item.content }}</div>
+            </div>
+          {% endif %}
+        {% endfor %}
+        <div class='accordion h-100'>
+          <div class="card">
+            <div class="card-header">
+              <h3 class="my-0">
+                <button class="btn btn-link" type="button" data-toggle="collapse" data-target="#event-{{ accordion_item_id }}" aria-expanded="false" aria-controls="event-{{ accordion_item_id }}">
+                  View all event instances
path: modules/custom/az_event/templates/field--node--field-az-event-date--az-event.html.twig
user: login: danahertzberg
body: Can this button have less padding so it's not so prominent?
created_at: 2025-03-26T20:45:33Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,96 @@
+{#
+/**
+ * @file
+ * Theme override for a field.
+ *
+ * Available variables:
+ * - attributes: HTML attributes for the containing element.
+ * - label_hidden: Whether to show the field label or not.
+ * - title_attributes: HTML attributes for the title.
+ * - label: The label for the field.
+ * - multiple: TRUE if a field can contain multiple items.
+ * - items: List of all the field items. Each item contains:
+ *   - attributes: List of HTML attributes for each item.
+ *   - content: The field item's content.
+ * - entity_type: The entity type to which the field belongs.
+ * - field_name: The name of the field.
+ * - field_type: The type of the field.
+ * - label_display: The display settings for the label.
+ *
+ *
+ * @see template_preprocess_field()
+ */
+#}
+{%
+  set classes = [
+    'field',
+    'field--name-' ~ field_name|clean_class,
+    'field--type-' ~ field_type|clean_class,
+    'field--label-' ~ label_display,
+  ]
+%}
+{%
+  set title_classes = [
+    'field__label',
+    'h3',
+    label_display == 'visually_hidden' ? 'visually-hidden',
+  ]
+%}
+
+{% if label_hidden %}
+  {% if multiple %}
+    <div{{ attributes.addClass(classes, 'field__items') }}>
+      {% if element['#view_mode'] == 'full' and items|length > 1 %}
+        {% set accordion_item_id = element['#object'].id %}
+        {% for item in items %}
+          {% if item['content']['#nextInstance'] == true %}
+            <div class="mb-3">
+              <div{{ item.attributes.addClass('field__item') }}>Next event: {{ item.content }}</div>
+            </div>
+          {% endif %}
+        {% endfor %}
+        <div class='accordion h-100'>
+          <div class="card">
+            <div class="card-header">
+              <h3 class="my-0">
+                <button class="btn btn-link" type="button" data-toggle="collapse" data-target="#event-{{ accordion_item_id }}" aria-expanded="false" aria-controls="event-{{ accordion_item_id }}">
+                  View all event instances
path: modules/custom/az_event/templates/field--node--field-az-event-date--az-event.html.twig
user: login: bberndt-uaz
body: @danahertzberg My goal was to use the standard AZ Accordion styling for the list of event instances, but maybe we could modify AZ Accordion to allow adjustment of that padding? Could this be a follow-up issue?
created_at: 2025-03-27T22:35:26Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,96 @@
+{#
+/**
+ * @file
+ * Theme override for a field.
+ *
+ * Available variables:
+ * - attributes: HTML attributes for the containing element.
+ * - label_hidden: Whether to show the field label or not.
+ * - title_attributes: HTML attributes for the title.
+ * - label: The label for the field.
+ * - multiple: TRUE if a field can contain multiple items.
+ * - items: List of all the field items. Each item contains:
+ *   - attributes: List of HTML attributes for each item.
+ *   - content: The field item's content.
+ * - entity_type: The entity type to which the field belongs.
+ * - field_name: The name of the field.
+ * - field_type: The type of the field.
+ * - label_display: The display settings for the label.
+ *
+ *
+ * @see template_preprocess_field()
+ */
+#}
+{%
+  set classes = [
+    'field',
+    'field--name-' ~ field_name|clean_class,
+    'field--type-' ~ field_type|clean_class,
+    'field--label-' ~ label_display,
+  ]
+%}
+{%
+  set title_classes = [
+    'field__label',
+    'h3',
+    label_display == 'visually_hidden' ? 'visually-hidden',
+  ]
+%}
+
+{% if label_hidden %}
+  {% if multiple %}
+    <div{{ attributes.addClass(classes, 'field__items') }}>
+      {% if element['#view_mode'] == 'full' and items|length > 1 %}
+        {% set accordion_item_id = element['#object'].id %}
+        {% for item in items %}
+          {% if item['content']['#nextInstance'] == true %}
+            <div class="mb-3">
+              <div{{ item.attributes.addClass('field__item') }}>Next event: {{ item.content }}</div>
+            </div>
+          {% endif %}
+        {% endfor %}
+        <div class='accordion h-100'>
+          <div class="card">
+            <div class="card-header">
+              <h3 class="my-0">
+                <button class="btn btn-link" type="button" data-toggle="collapse" data-target="#event-{{ accordion_item_id }}" aria-expanded="false" aria-controls="event-{{ accordion_item_id }}">
+                  View all event instances
path: modules/custom/az_event/templates/field--node--field-az-event-date--az-event.html.twig
user: login: trackleft
body: Here is an alternative using button dropdowns: https://digital.arizona.edu/arizona-bootstrap/docs/2.0/components/dropdowns/#text
created_at: 2025-03-31T17:08:58Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,96 @@
+{#
+/**
+ * @file
+ * Theme override for a field.
+ *
+ * Available variables:
+ * - attributes: HTML attributes for the containing element.
+ * - label_hidden: Whether to show the field label or not.
+ * - title_attributes: HTML attributes for the title.
+ * - label: The label for the field.
+ * - multiple: TRUE if a field can contain multiple items.
+ * - items: List of all the field items. Each item contains:
+ *   - attributes: List of HTML attributes for each item.
+ *   - content: The field item's content.
+ * - entity_type: The entity type to which the field belongs.
+ * - field_name: The name of the field.
+ * - field_type: The type of the field.
+ * - label_display: The display settings for the label.
+ *
+ *
+ * @see template_preprocess_field()
+ */
+#}
+{%
+  set classes = [
+    'field',
+    'field--name-' ~ field_name|clean_class,
+    'field--type-' ~ field_type|clean_class,
+    'field--label-' ~ label_display,
+  ]
+%}
+{%
+  set title_classes = [
+    'field__label',
+    'h3',
+    label_display == 'visually_hidden' ? 'visually-hidden',
+  ]
+%}
+
+{% if label_hidden %}
+  {% if multiple %}
+    <div{{ attributes.addClass(classes, 'field__items') }}>
+      {% if element['#view_mode'] == 'full' and items|length > 1 %}
+        {% set accordion_item_id = element['#object'].id %}
+        {% for item in items %}
+          {% if item['content']['#nextInstance'] == true %}
+            <div class="mb-3">
+              <div{{ item.attributes.addClass('field__item') }}>Next event: {{ item.content }}</div>
+            </div>
+          {% endif %}
+        {% endfor %}
+        <div class='accordion h-100'>
+          <div class="card">
+            <div class="card-header">
+              <h3 class="my-0">
+                <button class="btn btn-link" type="button" data-toggle="collapse" data-target="#event-{{ accordion_item_id }}" aria-expanded="false" aria-controls="event-{{ accordion_item_id }}">
+                  View all event instances
path: modules/custom/az_event/templates/field--node--field-az-event-date--az-event.html.twig
user: login: bberndt-uaz
body: Per AZ Digital discussion:

- Dana is OK with adjusting the accordion styling as a follow-up.
- We decided against the button dropdown since the list of recurring events can be very, very long (see the example from #4141).
created_at: 2025-04-01T18:59:27Z
author_association: CONTRIBUTOR
diff_hunk: @@ -95,6 +95,67 @@ function az_event_node_view(array &$build, EntityInterface $entity, EntityViewDi
         ];
       }
     }
+
+    // For the full content view mode, modify the display of recurring events.
+    if (($view_mode === 'full') && ($entity instanceof NodeInterface)) {
+      /** @var \Drupal\Core\Field\FieldItemList $eventDates */
+      $eventDates = $entity->field_az_event_date;
+      $numberOfRecurringInstances = $eventDates->count();
+      if ($numberOfRecurringInstances > 1) {
+        // Initialize the accordion body build array.
+        $accordionBody = $build['field_az_event_date'];
+        foreach ($entity->field_az_event_date as $item) {
+          unset($accordionBody[$item->getName()]);
+        }
+        $itemIndex = 0;
+
+        // Add any upcoming instances to the accordion body.
+        $nextRecurringInstance = NULL;
+        $now = \Drupal::time()->getCurrentTime();
+        foreach ($entity->field_az_event_date as $item) {
+          if ($item->end_value > $now) {
+            if (!isset($nextRecurringInstance)) {
+              $nextRecurringInstance = $item->getName();
+              $accordionBody[$itemIndex++] = [
+                '#markup' => '<h3 class="h5 mt-0">Upcoming Events</h3>',
+              ];
+            }
+            $accordionBody[$itemIndex++] = $build['field_az_event_date'][$item->getName()];
+          }
+        }
+
+        // Add any past instances to the accordion body.
+        if (!isset($nextRecurringInstance) || $nextRecurringInstance !== 0) {
+          $accordionBody[$itemIndex++] = [
+            '#markup' => "<h3 class='h5" . (isset($nextRecurringInstance) ? '' : ' mt-0') . "'>Past Events</h3>",
path: modules/custom/az_event/az_event.module
user: login: tadean
body: Should these strings be passed to `TranslatableMarkup` so they can be localized? (I also think it'd be okay for this to be a followup)
created_at: 2025-04-02T01:03:47Z
author_association: CONTRIBUTOR
diff_hunk: @@ -95,6 +95,67 @@ function az_event_node_view(array &$build, EntityInterface $entity, EntityViewDi
         ];
       }
     }
+
+    // For the full content view mode, modify the display of recurring events.
+    if (($view_mode === 'full') && ($entity instanceof NodeInterface)) {
+      /** @var \Drupal\Core\Field\FieldItemList $eventDates */
+      $eventDates = $entity->field_az_event_date;
+      $numberOfRecurringInstances = $eventDates->count();
+      if ($numberOfRecurringInstances > 1) {
+        // Initialize the accordion body build array.
+        $accordionBody = $build['field_az_event_date'];
+        foreach ($entity->field_az_event_date as $item) {
+          unset($accordionBody[$item->getName()]);
+        }
+        $itemIndex = 0;
+
+        // Add any upcoming instances to the accordion body.
+        $nextRecurringInstance = NULL;
+        $now = \Drupal::time()->getCurrentTime();
+        foreach ($entity->field_az_event_date as $item) {
+          if ($item->end_value > $now) {
+            if (!isset($nextRecurringInstance)) {
+              $nextRecurringInstance = $item->getName();
+              $accordionBody[$itemIndex++] = [
+                '#markup' => '<h3 class="h5 mt-0">Upcoming Events</h3>',
+              ];
+            }
+            $accordionBody[$itemIndex++] = $build['field_az_event_date'][$item->getName()];
+          }
+        }
+
+        // Add any past instances to the accordion body.
+        if (!isset($nextRecurringInstance) || $nextRecurringInstance !== 0) {
+          $accordionBody[$itemIndex++] = [
+            '#markup' => "<h3 class='h5" . (isset($nextRecurringInstance) ? '' : ' mt-0') . "'>Past Events</h3>",
+          ];
+          for ($i = 0; $i < ($nextRecurringInstance ?? $numberOfRecurringInstances); $i++) {
+            $accordionBody[$itemIndex++] = $build['field_az_event_date'][$i];
+          }
+        }
+
+        // Replace/unset the items in the build array for the field.
+        $itemIndex = 0;
+        if (isset($nextRecurringInstance)) {
+          $build['field_az_event_date'][$itemIndex] = $build['field_az_event_date'][$nextRecurringInstance];
+          $build['field_az_event_date'][$itemIndex]['#prefix'] = '<div class="mb-3"><strong>Next event:</strong> ';
+          $build['field_az_event_date'][$itemIndex]['#suffix'] = '</div>';
+          $itemIndex++;
+        }
+        $build['field_az_event_date'][$itemIndex++] = [
+          '#theme' => 'az_accordion',
+          '#title' => "View " . (isset($nextRecurringInstance) ? 'all' : 'past') . " event instances",
path: modules/custom/az_event/az_event.module
user: login: bberndt-uaz
body: ```suggestion
              $upcomingEventsHeading = t("Upcoming Events");
              $accordionBody[$itemIndex++] = [
                '#markup' => "<h3 class='h5 mt-0'>{$upcomingEventsHeading}</h3>",
              ];
            }
            $accordionBody[$itemIndex++] = $build['field_az_event_date'][$item->getName()];
          }
        }

        // Add any past instances to the accordion body.
        if (!isset($nextRecurringInstance) || $nextRecurringInstance !== 0) {
          $pastEventsHeading = t("Past Events");
          $accordionBody[$itemIndex++] = [
            '#markup' => "<h3 class='h5" . (isset($nextRecurringInstance) ? '' : ' mt-0') . "'>{$pastEventsHeading}</h3>",
          ];
          for ($i = 0; $i < ($nextRecurringInstance ?? $numberOfRecurringInstances); $i++) {
            $accordionBody[$itemIndex++] = $build['field_az_event_date'][$i];
          }
        }

        // Replace/unset the items in the build array for the field.
        $itemIndex = 0;
        if (isset($nextRecurringInstance)) {
          $build['field_az_event_date'][$itemIndex] = $build['field_az_event_date'][$nextRecurringInstance];
          $nextEventText = t("Next event");
          $build['field_az_event_date'][$itemIndex]['#prefix'] = "<div class='mb-3'><strong>{$nextEventText}:</strong> ";
          $build['field_az_event_date'][$itemIndex]['#suffix'] = '</div>';
          $itemIndex++;
        }
        $build['field_az_event_date'][$itemIndex++] = [
          '#theme' => 'az_accordion',
          '#title' => t("View all event instances"),
```
created_at: 2025-04-02T17:26:51Z
author_association: CONTRIBUTOR
path: modules/custom/az_event/az_event.module
user: login: joeparsons
body: Annoying nitpick:  I think we shouldn't be mixing snake case and camel case variable names.

From [Drupal coding standards documentation](https://www.drupal.org/docs/develop/standards/php/php-coding-standards#s-functions-and-variables):

>Variables should be named using lowercase, and words should be separated either with uppercase characters (example: $lowerCamelCase) or with an underscore (example: $snake_case). **Be consistent; do not mix camelCase and snake_case variable naming inside a file**.
created_at: 2025-04-02T18:36:35Z
author_association: MEMBER
diff_hunk: @@ -107,6 +107,71 @@ function az_event_node_view(array &$build, EntityInterface $entity, EntityViewDi
       }
     }
 
+    // For the full content view mode, modify the display of recurring events.
+    if (($view_mode === 'full') && ($entity instanceof NodeInterface)) {
+      /** @var \Drupal\Core\Field\FieldItemList $event_dates */
+      $event_dates = $entity->field_az_event_date;
+      $number_of_occurrences = $event_dates->count();
+      if ($number_of_occurrences > 1) {
+        // Initialize the accordion body build array.
+        $accordion_body = $build['field_az_event_date'];
+        foreach ($entity->field_az_event_date as $item) {
+          unset($accordion_body[$item->getName()]);
+        }
+        $item_index = 0;
+
+        // Add any upcoming occurrences to the accordion body.
+        $next_occurrence = NULL;
+        $now = \Drupal::time()->getCurrentTime();
+        /** @var \Drupal\smart_date\Plugin\Field\FieldType\SmartDateItem $item */
+        foreach ($entity->field_az_event_date as $item) {
+          if (!empty($item->end_value) && $item->end_value > $now) {
+            if (!isset($next_occurrence)) {
+              $next_occurrence = $item->getName();
+              $upcoming_events_heading = t("Upcoming Events");
+              $accordion_body[$item_index++] = [
+                '#markup' => "<h3 class='h5 mt-0'>{$upcoming_events_heading}</h3>",
+              ];
+            }
+            $accordion_body[$item_index++] = $build['field_az_event_date'][$item->getName()];
+          }
+        }
+
+        // Add any past occurrences to the accordion body.
+        if (!isset($next_occurrence) || $next_occurrence !== 0) {
+          $past_events_heading = t("Past Events");
+          $accordion_body[$item_index++] = [
+            '#markup' => "<h3 class='h5" . (isset($next_occurrence) ? '' : ' mt-0') . "'>{$past_events_heading}</h3>",
+          ];
+          for ($i = 0; $i < ($next_occurrence ?? $number_of_occurrences); $i++) {
+            $accordion_body[$item_index++] = $build['field_az_event_date'][$i];
+          }
+        }
+
+        // Replace/unset the items in the build array for the field.
+        $item_index = 0;
+        if (isset($next_occurrence)) {
+          $build['field_az_event_date'][$item_index] = $build['field_az_event_date'][$next_occurrence];
+          $next_event_text = t("Next event");
+          $build['field_az_event_date'][$item_index]['#prefix'] = "<div class='mb-3'><strong>{$next_event_text}:</strong> ";
+          $build['field_az_event_date'][$item_index]['#suffix'] = '</div>';
+          $item_index++;
+        }
+        $build['field_az_event_date'][$item_index++] = [
+          '#theme' => 'az_accordion',
+          '#title' => t("View all occurrences"),
+          '#body' => $accordion_body,
+          '#attributes' => ['class' => 'accordion'],
+          '#accordion_item_id' => "event-" . $entity->id(),
+          '#collapsed' => 'collapse',
+          '#aria_expanded' => 'false',
+        ];
+        for ($i = $item_index; $i < $number_of_occurrences; $i++) {
+          unset($build['field_az_event_date'][$i]);
+        }
+      }
+    }
+    
path: modules/custom/az_event/az_event.module
user: login: joeparsons
body: ```suggestion

```
created_at: 2025-04-02T20:44:07Z
author_association: MEMBER

PR comments:
user: login: bberndt-uaz
created_at: 2025-03-28T18:23:08Z
body: Per discussion in AZ Digital:

1. We can apply the new presentation to any event with recurring instances (more than 1).
2. (Done) We'll add an "Upcoming Event Instances" heading in the accordion to match the "Past Event Instances" heading. The "Next" badge can be removed.
3. (Done) I'll look into changing "View all instances" to "View past instances" when all are in the past (since nothing will be displayed above the accordion).
4. As a future improvement, Troy recommended looking for a php library to generate a description of the recurrence rule for the event. This description could be added above the next instance line. If the recurring events have been edited (at least one overridden or removed), a notice could be added.
author_association: CONTRIBUTOR
user: login: alyssakw31
created_at: 2025-04-02T00:00:55Z
body: Looks ok to me for right now, in the future I wonder if adding something like "Repeats weekly (Month day)" like this to give more clarifying information to the user on why we have the accordion with future event dates: 
<img width="415" alt="Screenshot 2025-04-01 at 4 59 32 PM" src="https://github.com/user-attachments/assets/47cf9651-226f-416f-a195-b2c7ea090a99" />

I also wonder if having the list of past occurrences of the event is necessary/helpful to users? 

author_association: NONE
user: login: bberndt-uaz
created_at: 2025-04-02T00:14:49Z
body: > Looks ok to me for right now, in the future I wonder if adding something like "Repeats weekly (Month day)" like this to give more clarifying information to the user on why we have the accordion with future event dates: 
> 
> I also wonder if having the list of past occurrences of the event is necessary/helpful to users?
> 
> Looks good to me, I approve

Thanks @alyssakw31! Yep, we are planning to add a line describing the recurrence as a follow-up: I wasn't able to get it ready in time for the alpha release. As for the past occurrences, I do think they are sometimes helpful, but perhaps another follow-up could allow site owners to choose whether or not to display them.
author_association: CONTRIBUTOR