Skip to content

feat(components/modals): add modal banner (#4275)#4280

Merged
Blackbaud-PaulCrowder merged 2 commits intomainfrom
cherry-pick_c6cf820017fcb46232234cba0d06011af620b54d_1773169838236
Mar 12, 2026
Merged

feat(components/modals): add modal banner (#4275)#4280
Blackbaud-PaulCrowder merged 2 commits intomainfrom
cherry-pick_c6cf820017fcb46232234cba0d06011af620b54d_1773169838236

Conversation

@blackbaud-sky-build-user
Copy link
Copy Markdown
Collaborator

@blackbaud-sky-build-user blackbaud-sky-build-user commented Mar 10, 2026

🍒 Cherry picked from #4275 feat(components/modals): add modal banner

Summary by CodeRabbit

  • New Features
    • Modal banner component: Display images and content within modals for enhanced visual layouts.
    • Configurable header visibility: Hide modal headers when needed for cleaner presentations.
    • Extended modal variants with integrated banner support for flexible modal configurations.

@nx-cloud
Copy link
Copy Markdown

nx-cloud bot commented Mar 10, 2026

View your CI Pipeline Execution ↗ for commit 0ecdb4e

Command Status Duration Result
nx build code-examples-playground --baseHref=ht... ✅ Succeeded 7m 20s View ↗
nx build playground --baseHref=https://blackbau... ✅ Succeeded 3m 14s View ↗
nx build integration --baseHref=https://blackba... ✅ Succeeded 1m 31s View ↗

☁️ Nx Cloud last updated this comment at 2026-03-12 21:35:05 UTC

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 10, 2026

📝 Walkthrough

Walkthrough

This PR introduces a new SkyModalBannerComponent for rendering full-width banner content in modals with optional background images, adds a headingHidden input property to SkyModalComponent to control header visibility, and expands test coverage with comprehensive fixtures, harnesses, and code examples demonstrating the new banner functionality.

Changes

Cohort / File(s) Summary
Core Banner Component
libs/components/modals/src/lib/modules/modal/modal-banner.component.ts, modal-banner.component.html, modal-banner.component.scss
Introduces new SkyModalBannerComponent with an imageSrc input signal and computed backgroundImage property. Renders a projection slot for banner content and applies styling for optional background images with aspect ratio and image sizing.
Modal Component Updates
libs/components/modals/src/lib/modules/modal/modal.component.ts, modal.component.html, modal-content.component.scss
Adds headingHidden input property with boolean attribute transform to SkyModalComponent. Updates template to conditionally hide header and project banner content in header/content regions. Adjusts padding between banner and content.
Modal Module Exports
libs/components/modals/src/index.ts, modal.module.ts, documentation.json
Exports SkyModalBannerComponent publicly and adds ModalsModalBasicWithBannerExampleComponent to documentation's code examples list.
Modal Test Fixtures & Context
libs/components/modals/src/lib/modules/modal/fixtures/modal-context.ts, modal-fixtures.module.ts, modal.component.fixture.html, modal-banner.component.fixture.ts, modal-banner.component.fixture.html
Extends ModalTestContext with bannerImageSrc and headingHidden properties. Adds ModalBannerTestComponent fixture and updates fixture template bindings. Replaces RouterTestingModule with RouterModule.
Modal Testing Harnesses
libs/components/modals/testing/src/modules/modal/modal-banner-harness.ts, modal-banner-harness.spec.ts, modal-harness.ts, public-api.ts
Introduces SkyModalBannerHarness with getImageSrc() method to retrieve banner background images and handle URL parsing/unescaping. Adds getBanner() method to SkyModalHarness. Exports harness publicly.
Modal Component Tests
libs/components/modals/src/lib/modules/modal/modal-banner.component.spec.ts, modal.component.spec.ts
Adds comprehensive unit tests for banner content projection, background image handling, CSS escaping, and accessibility. Tests header visibility with headingHidden property via new helper openModalWithHeadingContext().
E2E Tests
apps/e2e/modals-storybook-e2e/src/e2e/modal.component.cy.ts, apps/e2e/modals-storybook/src/app/modal/...
Extends e2e test coverage with five new modal variants: heading-hidden, banner-content, banner-image, banner-content-image, and banner-heading-visible. Adds corresponding HTML buttons and TypeScript handler methods with modal configuration via ModalTestContext factory providers.
Playground Examples
apps/playground/src/app/components/modal/modal-banner-*.component.*, modal-visual.component.*
Introduces ModalBannerContentComponent and ModalBannerImageComponent example components with templates, styles, and modal close handling. Updates ModalVisualComponent with openBannerImageModal() and openBannerContentModal() methods.
Code Examples Library
libs/components/code-examples/src/...
Adds ModalsModalBasicWithBannerExampleComponent example component demonstrating banner modal usage with modal lifecycle management. Exports component and registers lazy-loaded route.

Sequence Diagram

sequenceDiagram
    participant User
    participant ModalVisualComponent
    participant SkyModalService
    participant SkyModalComponent
    participant SkyModalBannerComponent
    participant DOM

    User->>ModalVisualComponent: Click "Open Banner Modal"
    ModalVisualComponent->>SkyModalService: openModal(ModalComponent, {providers: [...]})
    SkyModalService->>SkyModalComponent: Create modal instance with context
    SkyModalComponent->>DOM: Render modal with headingHidden binding
    SkyModalComponent->>SkyModalBannerComponent: Project banner content (ng-content)
    SkyModalBannerComponent->>DOM: Render banner with backgroundImage style
    SkyModalBannerComponent->>DOM: Apply imageSrc to background-image CSS
    DOM->>User: Display modal with banner and content
    User->>SkyModalComponent: Click close button
    SkyModalComponent->>SkyModalService: Close modal instance
    SkyModalService->>DOM: Remove modal from DOM
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • Blackbaud-TrevorBurch
  • Blackbaud-SteveBrush

Poem

🐰 With banners waving, modals gleam,
The headings hide, or so they seem,
Full-width images paint the sky,
While tests and fixtures catch the eye!
A rabbit's gift of UI delight,
Where banners dance and content's bright! 🌟

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat(components/modals): add modal banner (#4275)' clearly and directly describes the main change: adding a modal banner feature to the modals component library.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch cherry-pick_c6cf820017fcb46232234cba0d06011af620b54d_1773169838236

Warning

Review ran into problems

🔥 Problems

Git: Failed to clone repository. Please run the @coderabbitai full review command to re-trigger a full review. If the issue persists, set path_filters to include or exclude specific files.


Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
libs/components/modals/documentation.json (1)

59-65: ⚠️ Potential issue | 🟠 Major

Add the new public banner component to the modal API docs.

ModalsModalBasicWithBannerExampleComponent is registered here, but SkyModalBannerComponent is now part of the public modals API and is still missing from groups.modal.development.docsIds. That leaves the feature discoverable through the example, but not through the API reference.

📝 Suggested docs update
       "development": {
         "docsIds": [
           "SkyModalModule",
           "SkyModalComponent",
+          "SkyModalBannerComponent",
           "SkyModalHeaderComponent",
           "SkyModalContentComponent",
As per coding guidelines, `**/libs/components/*/documentation.json`: "Add class names to the component's `documentation.json` file under `codeExamples` section and keep examples in sync with component and harness changes".
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@libs/components/modals/documentation.json` around lines 59 - 65, Update the
modal API docs to include the new public banner component by adding
"SkyModalBannerComponent" to the documentation JSON array used for API grouping;
specifically, add "SkyModalBannerComponent" to the
groups.modal.development.docsIds (alongside the existing
"ModalsModalBasicWithBannerExampleComponent") in the component's
documentation.json so the SkyModalBannerComponent appears in the API reference.
🧹 Nitpick comments (10)
apps/playground/src/app/components/modal/modal-banner-image.component.ts (1)

4-7: Set OnPush on this new component.

This component is stateless, so it should opt into the repo's default change-detection strategy.

Suggested change
-import { Component, inject } from '@angular/core';
+import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
@@
 `@Component`({
+  changeDetection: ChangeDetectionStrategy.OnPush,
   imports: [SkyModalModule],
   templateUrl: './modal-banner-image.component.html',
 })

As per coding guidelines "Set changeDetection: ChangeDetectionStrategy.OnPush in @Component decorator".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/playground/src/app/components/modal/modal-banner-image.component.ts`
around lines 4 - 7, The component decorator for ModalBannerImageComponent should
opt into OnPush change detection: import ChangeDetectionStrategy from
'@angular/core' and add changeDetection: ChangeDetectionStrategy.OnPush to the
`@Component` metadata (the decorator that currently contains imports and
templateUrl) so the stateless component uses the repo default strategy.
apps/playground/src/app/components/modal/modal-banner-content.component.scss (1)

1-11: Add the compat mixin scaffold here too.

This new SCSS file is also missing the required compatMixins import and top-of-file override mixins.

As per coding guidelines "Add compat mixins to SCSS component files: Ensure the import @use 'libs/components/theme/src/lib/styles/compat-tokens-mixins' as compatMixins; is included" and "Include the mixin compatMixins.sky-default-overrides outside of any selectors".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/playground/src/app/components/modal/modal-banner-content.component.scss`
around lines 1 - 11, This SCSS file is missing the compat mixins import and
top-level override mixin; add the import statement for the compat mixins module
using the namespace compatMixins (i.e. `@use`
'libs/components/theme/src/lib/styles/compat-tokens-mixins' as compatMixins) at
the top of modal-banner-content.component.scss and invoke the top-level mixin
compatMixins.sky-default-overrides outside any selector blocks (before
.banner-content) so the overrides apply globally to this component.
apps/playground/src/app/components/modal/modal-banner-content.component.ts (1)

4-8: Set OnPush on this component as well.

This one is also stateless and should follow the standard component change-detection setting.

Suggested change
-import { Component, inject } from '@angular/core';
+import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
@@
 `@Component`({
+  changeDetection: ChangeDetectionStrategy.OnPush,
   imports: [SkyModalModule],
   templateUrl: './modal-banner-content.component.html',
   styleUrl: './modal-banner-content.component.scss',
 })

As per coding guidelines "Set changeDetection: ChangeDetectionStrategy.OnPush in @Component decorator".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/playground/src/app/components/modal/modal-banner-content.component.ts`
around lines 4 - 8, The component decorator for ModalBannerContentComponent
lacks the OnPush change-detection setting; update the `@Component` on
ModalBannerContentComponent to include changeDetection:
ChangeDetectionStrategy.OnPush and add the corresponding import for
ChangeDetectionStrategy from '@angular/core' so the component uses OnPush
semantics. Ensure you update the imports section to include
ChangeDetectionStrategy and leave the rest of the decorator intact.
libs/components/modals/src/lib/modules/modal/modal-content.component.scss (1)

21-25: Expose this new spacing through an override token.

This hard-codes component tokens in a new rule, so consumers can't tune the banner/content spacing through sky-default-overrides. Please add an override variable to the existing mixin and use the current calc(...) as the fallback here.

As per coding guidelines "When using a CSS variable with its current value, find the existing location matching the variable name, assign the new variable that value, and use the new variable in the current location with the given fallback".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@libs/components/modals/src/lib/modules/modal/modal-content.component.scss`
around lines 21 - 25, Add an override CSS variable for the banner/content
spacing and use it as the padding-top fallback: inside the existing mixin that
defines component tokens, create a new variable (e.g.
--sky-comp-modal-banner-content-space-inset) and initialize it to the current
calc(...) expression using the existing tokens
var(--sky-comp-modal-header-space-inset-bottom) and
var(--sky-comp-modal-content-space-inset-top); then replace the hard-coded
padding-top in the sky-modal-banner + sky-modal-content rule with padding-top:
var(--sky-comp-modal-banner-content-space-inset, calc(...)). Ensure the new
variable is exposed so consumers can override via sky-default-overrides and
reference the same tokens used originally.
libs/components/modals/src/lib/modules/modal/modal-banner.component.scss (1)

1-9: Add the required compat mixin scaffold to this new stylesheet.

This component SCSS skips the repo's compat-token import and top-of-file override mixins, so it won't participate in the standard override path. Please add the required compatMixins scaffold before the :host selector.

As per coding guidelines "Add compat mixins to SCSS component files: Ensure the import @use 'libs/components/theme/src/lib/styles/compat-tokens-mixins' as compatMixins; is included" and "Create empty compat mixins ... positioned at the top of the SCSS file directly below imports and before any other selectors".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@libs/components/modals/src/lib/modules/modal/modal-banner.component.scss`
around lines 1 - 9, Add the required compat mixins scaffold at the top of
modal-banner.component.scss by adding the import `@use`
'libs/components/theme/src/lib/styles/compat-tokens-mixins' as compatMixins; and
then invoking/declaring the empty compat mixin hooks (e.g.,
compatMixins.override and compatMixins.override-top or the repo-standard mixin
names) immediately below the import and before the existing :host selector so
the component participates in the standard override path.
apps/e2e/modals-storybook/src/app/modal/modal.component.ts (1)

95-187: Consider extracting a shared context-opening helper.

Lines 95-187 repeat the same providers/useFactory scaffolding five times and only vary a few ModalTestContext fields. Pulling that into one helper would reduce copy/paste drift as more modal variants get added.

♻️ Possible simplification
+  private openContextModal(overrides: Partial<ModalTestContext>): void {
+    this.openModal(ModalBasicComponent, {
+      providers: [
+        {
+          provide: ModalTestContext,
+          useFactory: (): ModalTestContext =>
+            Object.assign(new ModalTestContext(), overrides),
+        },
+      ],
+    });
+  }
+
   protected onOpenHeadingHiddenModalClick(): void {
-    this.openModal(ModalBasicComponent, {
-      providers: [
-        {
-          provide: ModalTestContext,
-          useFactory: (): ModalTestContext => {
-            const context = new ModalTestContext();
-            context.headingHidden = true;
-            context.headingText = 'My heading';
-            context.modalContent = 'Modal content';
-
-            return context;
-          },
-        },
-      ],
+    this.openContextModal({
+      headingHidden: true,
+      headingText: 'My heading',
+      modalContent: 'Modal content',
     });
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/e2e/modals-storybook/src/app/modal/modal.component.ts` around lines 95 -
187, Extract the repeated provider/useFactory block into a single helper (e.g.,
createModalContextProvider or buildModalContext) that accepts partial
ModalTestContext values and returns the providers array used by openModal;
replace each of the methods (onOpenHeadingHiddenModalClick,
onOpenBannerContentModalClick, onOpenBannerImageModalClick,
onOpenBannerContentImageModalClick, onOpenBannerHeadingVisibleModalClick) to
call openModal(ModalBasicComponent, { providers: createModalContextProvider({
... }) }) and pass only the differing fields (bannerContent, bannerImageSrc,
headingHidden, headingText, modalContent) so the scaffolding is centralized and
duplication removed while preserving behavior.
apps/e2e/modals-storybook/src/app/modal/modal.component.html (1)

67-116: Use class bindings instead of [ngClass] on these new buttons.

These additions only toggle one class, so [class.modal-screenshot-buttons-hidden]="buttonsHidden" is the simpler match for the repo template rule.

♻️ Suggested template change
-  [ngClass]="{
-    'modal-screenshot-buttons-hidden': buttonsHidden
-  }"
+  [class.modal-screenshot-buttons-hidden]="buttonsHidden"

As per coding guidelines, "Do NOT use ngClass; use class bindings instead in Angular templates".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/e2e/modals-storybook/src/app/modal/modal.component.html` around lines 67
- 116, These buttons use [ngClass] to toggle a single class; replace each
[ngClass]="{ 'modal-screenshot-buttons-hidden': buttonsHidden }" with a class
binding [class.modal-screenshot-buttons-hidden]="buttonsHidden" on the buttons
that call onOpenHeadingHiddenModalClick, onOpenBannerContentModalClick,
onOpenBannerImageModalClick, onOpenBannerContentImageModalClick, and
onOpenBannerHeadingVisibleModalClick so they follow the repo template (use class
bindings instead of ngClass).
apps/playground/src/app/components/modal/modal-visual.component.html (1)

145-158: Give the new demo buttons unique sky-test-* hooks.

These two launchers reuse .sky-test-modal-required-field, which is already attached to several other modal buttons above. Any automation keyed off that class now has no deterministic way to target the banner variants.

♻️ Suggested update
-    class="sky-btn sky-btn-primary sky-test-modal-required-field"
+    class="sky-btn sky-btn-primary sky-test-modal-banner-image"
...
-    class="sky-btn sky-btn-primary sky-test-modal-required-field"
+    class="sky-btn sky-btn-primary sky-test-modal-banner-content"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/playground/src/app/components/modal/modal-visual.component.html` around
lines 145 - 158, The two buttons that open banner modals reuse the generic test
hook class and should get unique hooks: update the button element for
openBannerImageModal() to use a distinct test hook (e.g., replace or add a class
like "sky-test-modal-banner-image") and update the button for
openBannerContentModal() to use a different distinct test hook (e.g.,
"sky-test-modal-banner-content"); ensure you change the class attribute on the
button elements that call openBannerImageModal() and openBannerContentModal() so
automation can target each launcher deterministically.
apps/playground/src/app/components/modal/modal-banner-content.component.html (1)

1-5: Align the hidden modal title with the visible banner heading.

With headingHidden enabled, assistive tech still gets the modal title from headingText, but sighted users see Banner content in the banner. Keeping those labels in sync makes the demo less confusing.

♿ Suggested update
-<sky-modal headingText="Test" headingHidden>
+<sky-modal headingText="Banner content" headingHidden>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/playground/src/app/components/modal/modal-banner-content.component.html`
around lines 1 - 5, The visible banner heading ("Banner content" inside
<sky-modal-banner> .banner-content h2) is out of sync with the modal's hidden
accessible title (sky-modal headingText used with headingHidden), so either set
the modal's headingText to the same string or wire accessibility to the visible
heading: add a unique id to the h2 (e.g., banner-heading) and set the modal to
reference it via aria-labelledby on <sky-modal> (or update headingText to match
the h2). Ensure the chosen approach keeps <sky-modal headingText/headingHidden>
and the banner h2 content identical for sighted and assistive users.
libs/components/code-examples/src/lib/modules/modals/modal/with-banner/example.component.ts (1)

1-18: Align the example metadata with the new banner variant.

Line 10 still uses the with-controller selector, and this component is also missing OnPush. Both look copied from the controller example.

♻️ Suggested cleanup
-import { Component, OnDestroy, inject } from '@angular/core';
+import {
+  ChangeDetectionStrategy,
+  Component,
+  OnDestroy,
+  inject,
+} from '@angular/core';
@@
 `@Component`({
-  selector: 'app-modals-modal-basic-with-controller-example',
+  selector: 'app-modals-modal-basic-with-banner-example',
+  changeDetection: ChangeDetectionStrategy.OnPush,
   template: `<button
As per coding guidelines, `Set changeDetection: ChangeDetectionStrategy.OnPush` in `@Component` decorator.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@libs/components/code-examples/src/lib/modules/modals/modal/with-banner/example.component.ts`
around lines 1 - 18, Update the `@Component` metadata to use the "with-banner"
variant selector and enable OnPush change detection: change the selector string
from 'app-modals-modal-basic-with-controller-example' to
'app-modals-modal-basic-with-banner-example', import ChangeDetectionStrategy
from '@angular/core', and add changeDetection: ChangeDetectionStrategy.OnPush to
the `@Component` decorator; ensure the component class that currently implements
OnDestroy remains unchanged (no runtime behavior changes required).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/e2e/modals-storybook-e2e/src/e2e/modal.component.cy.ts`:
- Around line 21-25: The shared modal variant list includes 'heading-hidden' but
the shared test flow assumes the header close button is visible and runs
cy.get('.sky-btn-close').should('be.visible').click(), which fails for the
headingHidden() case; remove 'heading-hidden' from the shared matrix and add a
separate test path for the headingHidden() scenario that does not expect or
click the close button (instead assert the header close is hidden or skip that
step), updating references where the shared matrix is iterated and the test that
calls cy.get('.sky-btn-close').should('be.visible').click() to exclude the
headingHidden() case.

In
`@libs/components/code-examples/src/lib/modules/modals/modal/with-banner/modal.component.ts`:
- Around line 1-35: The component is missing ChangeDetectionStrategy.OnPush;
import ChangeDetectionStrategy from '@angular/core' and add changeDetection:
ChangeDetectionStrategy.OnPush to the `@Component` decorator in modal.component.ts
(alongside the existing imports/metadata) so the component uses OnPush change
detection.

In `@libs/components/modals/src/lib/modules/modal/modal-banner.component.ts`:
- Around line 1-16: The component metadata for SkyModalBannerComponent must opt
into OnPush change detection; add an import for ChangeDetectionStrategy from
'@angular/core' and set changeDetection: ChangeDetectionStrategy.OnPush in the
`@Component` decorator (next to selector/host/templateUrl/etc.) so the
signal-backed presentational component uses OnPush change detection; update the
`@Component` metadata for SkyModalBannerComponent accordingly and keep references
to the existing host bindings (e.g., backgroundImage).

In `@libs/components/modals/src/lib/modules/modal/modal.component.html`:
- Around line 23-24: The modal header currently sets aria-labelledby to
headerId.id while that element can be hidden by headingHidden(), risking an
unnamed dialog; update the template so aria-labelledby is only set when the
header is visible (e.g., bind attr.aria-labelledby to headerId.id when
headingHidden() is false) and provide an alternate accessible label when
headingHidden() is true (e.g., set attr.aria-label to dialogLabel or another
explicit label). Also replace usage of [ngStyle] and [ngClass] in this file (and
the noted occurrences) with native Angular style and class bindings (use
[style.someProp] or [style]="..." and [class.someClass] or class="...") so you
remove [ngStyle] and [ngClass] usages while keeping the same styling and
conditional class logic; reference the header element (headerId.id,
headingHidden(), dialogLabel) and each template binding location to update.

In `@libs/components/modals/src/lib/modules/modal/modal.component.spec.ts`:
- Around line 199-220: The tests using openModalWithHeadingContext() are not
exercising the headingText-only rendering path because ModalTestComponent always
projects a <sky-modal-header> slot; add a new test (or modify the helper) to
open a component that does not project any <sky-modal-header> so the modal must
render using ModalTestContext.headingText instead of projected content.
Specifically, create or reuse a lightweight test component (e.g.,
ModalHeadingOnlyTestComponent) that does not include <sky-modal-header>, call
openModal with that component using openModalWithHeadingContext(headingHidden,
async) or a similar helper, and assert the headingText-only DOM path
renders/does not render when headingHidden toggles; keep references to
ModalTestContext, openModalWithHeadingContext, and ModalTestComponent to locate
and update the tests.

In
`@libs/components/modals/testing/src/modules/modal/modal-banner-harness.spec.ts`:
- Around line 137-177: Add a predicate-based test that verifies the public
filter API by using SkyModalBannerHarness.with(...) (e.g., with dataSkyId) when
obtaining the banner from the modal harness: open a modal that includes a banner
with a known dataSkyId, call loader.getHarness(SkyModalHarness) then
modalHarness.getBanner(SkyModalBannerHarness.with({ dataSkyId: 'expected-id' }))
and assert it resolves to a SkyModalBannerHarness instance; also add a negative
case that queries with a non-matching dataSkyId and asserts getBanner(...)
resolves to null. Ensure the tests reference SkyModalBannerHarness.with,
modalHarness.getBanner, and bannerHarness.getImageSrc where appropriate.
- Around line 100-113: The TestBed configuration for the modal harness spec is
missing the TestButtonComponent declaration causing
TestBed.createComponent(TestButtonComponent) to fail; update the
TestBed.configureTestingModule call to include TestButtonComponent in the
declarations array alongside BannerModalComponent, NoBannerModalComponent, and
NonMatchingBannerModalComponent so the test fixture can be created and the
harness can run.

In `@libs/components/modals/testing/src/modules/modal/modal-banner-harness.ts`:
- Around line 6-34: Add a standard static with() entry point and a
SkyModalBannerHarnessFilters interface to the SkyModalBannerHarness so callers
can use predicate-based filtering (e.g., dataSkyId). Define export interface
SkyModalBannerHarnessFilters with optional properties used by other harness
filters (at minimum dataSkyId?: string), and implement public static
with(filters?: SkyModalBannerHarnessFilters) that returns a
HarnessPredicate<SkyModalBannerHarness> configured to filter by the provided
dataSkyId (use HarnessPredicate.stringIncludes and the hostSelector), mirroring
the pattern used by other SKY harnesses.

---

Outside diff comments:
In `@libs/components/modals/documentation.json`:
- Around line 59-65: Update the modal API docs to include the new public banner
component by adding "SkyModalBannerComponent" to the documentation JSON array
used for API grouping; specifically, add "SkyModalBannerComponent" to the
groups.modal.development.docsIds (alongside the existing
"ModalsModalBasicWithBannerExampleComponent") in the component's
documentation.json so the SkyModalBannerComponent appears in the API reference.

---

Nitpick comments:
In `@apps/e2e/modals-storybook/src/app/modal/modal.component.html`:
- Around line 67-116: These buttons use [ngClass] to toggle a single class;
replace each [ngClass]="{ 'modal-screenshot-buttons-hidden': buttonsHidden }"
with a class binding [class.modal-screenshot-buttons-hidden]="buttonsHidden" on
the buttons that call onOpenHeadingHiddenModalClick,
onOpenBannerContentModalClick, onOpenBannerImageModalClick,
onOpenBannerContentImageModalClick, and onOpenBannerHeadingVisibleModalClick so
they follow the repo template (use class bindings instead of ngClass).

In `@apps/e2e/modals-storybook/src/app/modal/modal.component.ts`:
- Around line 95-187: Extract the repeated provider/useFactory block into a
single helper (e.g., createModalContextProvider or buildModalContext) that
accepts partial ModalTestContext values and returns the providers array used by
openModal; replace each of the methods (onOpenHeadingHiddenModalClick,
onOpenBannerContentModalClick, onOpenBannerImageModalClick,
onOpenBannerContentImageModalClick, onOpenBannerHeadingVisibleModalClick) to
call openModal(ModalBasicComponent, { providers: createModalContextProvider({
... }) }) and pass only the differing fields (bannerContent, bannerImageSrc,
headingHidden, headingText, modalContent) so the scaffolding is centralized and
duplication removed while preserving behavior.

In
`@apps/playground/src/app/components/modal/modal-banner-content.component.html`:
- Around line 1-5: The visible banner heading ("Banner content" inside
<sky-modal-banner> .banner-content h2) is out of sync with the modal's hidden
accessible title (sky-modal headingText used with headingHidden), so either set
the modal's headingText to the same string or wire accessibility to the visible
heading: add a unique id to the h2 (e.g., banner-heading) and set the modal to
reference it via aria-labelledby on <sky-modal> (or update headingText to match
the h2). Ensure the chosen approach keeps <sky-modal headingText/headingHidden>
and the banner h2 content identical for sighted and assistive users.

In
`@apps/playground/src/app/components/modal/modal-banner-content.component.scss`:
- Around line 1-11: This SCSS file is missing the compat mixins import and
top-level override mixin; add the import statement for the compat mixins module
using the namespace compatMixins (i.e. `@use`
'libs/components/theme/src/lib/styles/compat-tokens-mixins' as compatMixins) at
the top of modal-banner-content.component.scss and invoke the top-level mixin
compatMixins.sky-default-overrides outside any selector blocks (before
.banner-content) so the overrides apply globally to this component.

In `@apps/playground/src/app/components/modal/modal-banner-content.component.ts`:
- Around line 4-8: The component decorator for ModalBannerContentComponent lacks
the OnPush change-detection setting; update the `@Component` on
ModalBannerContentComponent to include changeDetection:
ChangeDetectionStrategy.OnPush and add the corresponding import for
ChangeDetectionStrategy from '@angular/core' so the component uses OnPush
semantics. Ensure you update the imports section to include
ChangeDetectionStrategy and leave the rest of the decorator intact.

In `@apps/playground/src/app/components/modal/modal-banner-image.component.ts`:
- Around line 4-7: The component decorator for ModalBannerImageComponent should
opt into OnPush change detection: import ChangeDetectionStrategy from
'@angular/core' and add changeDetection: ChangeDetectionStrategy.OnPush to the
`@Component` metadata (the decorator that currently contains imports and
templateUrl) so the stateless component uses the repo default strategy.

In `@apps/playground/src/app/components/modal/modal-visual.component.html`:
- Around line 145-158: The two buttons that open banner modals reuse the generic
test hook class and should get unique hooks: update the button element for
openBannerImageModal() to use a distinct test hook (e.g., replace or add a class
like "sky-test-modal-banner-image") and update the button for
openBannerContentModal() to use a different distinct test hook (e.g.,
"sky-test-modal-banner-content"); ensure you change the class attribute on the
button elements that call openBannerImageModal() and openBannerContentModal() so
automation can target each launcher deterministically.

In
`@libs/components/code-examples/src/lib/modules/modals/modal/with-banner/example.component.ts`:
- Around line 1-18: Update the `@Component` metadata to use the "with-banner"
variant selector and enable OnPush change detection: change the selector string
from 'app-modals-modal-basic-with-controller-example' to
'app-modals-modal-basic-with-banner-example', import ChangeDetectionStrategy
from '@angular/core', and add changeDetection: ChangeDetectionStrategy.OnPush to
the `@Component` decorator; ensure the component class that currently implements
OnDestroy remains unchanged (no runtime behavior changes required).

In `@libs/components/modals/src/lib/modules/modal/modal-banner.component.scss`:
- Around line 1-9: Add the required compat mixins scaffold at the top of
modal-banner.component.scss by adding the import `@use`
'libs/components/theme/src/lib/styles/compat-tokens-mixins' as compatMixins; and
then invoking/declaring the empty compat mixin hooks (e.g.,
compatMixins.override and compatMixins.override-top or the repo-standard mixin
names) immediately below the import and before the existing :host selector so
the component participates in the standard override path.

In `@libs/components/modals/src/lib/modules/modal/modal-content.component.scss`:
- Around line 21-25: Add an override CSS variable for the banner/content spacing
and use it as the padding-top fallback: inside the existing mixin that defines
component tokens, create a new variable (e.g.
--sky-comp-modal-banner-content-space-inset) and initialize it to the current
calc(...) expression using the existing tokens
var(--sky-comp-modal-header-space-inset-bottom) and
var(--sky-comp-modal-content-space-inset-top); then replace the hard-coded
padding-top in the sky-modal-banner + sky-modal-content rule with padding-top:
var(--sky-comp-modal-banner-content-space-inset, calc(...)). Ensure the new
variable is exposed so consumers can override via sky-default-overrides and
reference the same tokens used originally.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e2889815-f020-4e4d-ba39-67c338935b3c

📥 Commits

Reviewing files that changed from the base of the PR and between e2346a0 and c653590.

📒 Files selected for processing (36)
  • apps/e2e/modals-storybook-e2e/src/e2e/modal.component.cy.ts
  • apps/e2e/modals-storybook/src/app/modal/modal.component.html
  • apps/e2e/modals-storybook/src/app/modal/modal.component.ts
  • apps/e2e/modals-storybook/src/app/modal/modals/modal-basic.component.html
  • apps/e2e/modals-storybook/src/app/modal/modals/modal-context.ts
  • apps/playground/src/app/components/modal/modal-banner-content.component.html
  • apps/playground/src/app/components/modal/modal-banner-content.component.scss
  • apps/playground/src/app/components/modal/modal-banner-content.component.ts
  • apps/playground/src/app/components/modal/modal-banner-image.component.html
  • apps/playground/src/app/components/modal/modal-banner-image.component.ts
  • apps/playground/src/app/components/modal/modal-visual.component.html
  • apps/playground/src/app/components/modal/modal-visual.component.ts
  • libs/components/code-examples/routes/src/index.ts
  • libs/components/code-examples/src/index.ts
  • libs/components/code-examples/src/lib/modules/modals/modal/with-banner/example.component.ts
  • libs/components/code-examples/src/lib/modules/modals/modal/with-banner/modal.component.ts
  • libs/components/modals/documentation.json
  • libs/components/modals/src/index.ts
  • libs/components/modals/src/lib/modules/modal/fixtures/modal-banner.component.fixture.html
  • libs/components/modals/src/lib/modules/modal/fixtures/modal-banner.component.fixture.ts
  • libs/components/modals/src/lib/modules/modal/fixtures/modal-context.ts
  • libs/components/modals/src/lib/modules/modal/fixtures/modal-fixtures.module.ts
  • libs/components/modals/src/lib/modules/modal/fixtures/modal.component.fixture.html
  • libs/components/modals/src/lib/modules/modal/modal-banner.component.html
  • libs/components/modals/src/lib/modules/modal/modal-banner.component.scss
  • libs/components/modals/src/lib/modules/modal/modal-banner.component.spec.ts
  • libs/components/modals/src/lib/modules/modal/modal-banner.component.ts
  • libs/components/modals/src/lib/modules/modal/modal-content.component.scss
  • libs/components/modals/src/lib/modules/modal/modal.component.html
  • libs/components/modals/src/lib/modules/modal/modal.component.spec.ts
  • libs/components/modals/src/lib/modules/modal/modal.component.ts
  • libs/components/modals/src/lib/modules/modal/modal.module.ts
  • libs/components/modals/testing/src/modules/modal/modal-banner-harness.spec.ts
  • libs/components/modals/testing/src/modules/modal/modal-banner-harness.ts
  • libs/components/modals/testing/src/modules/modal/modal-harness.ts
  • libs/components/modals/testing/src/public-api.ts

@Blackbaud-PaulCrowder Blackbaud-PaulCrowder enabled auto-merge (squash) March 10, 2026 19:26
@Blackbaud-PaulCrowder Blackbaud-PaulCrowder merged commit b186fc2 into main Mar 12, 2026
131 of 132 checks passed
@Blackbaud-PaulCrowder Blackbaud-PaulCrowder deleted the cherry-pick_c6cf820017fcb46232234cba0d06011af620b54d_1773169838236 branch March 12, 2026 22:00
johnhwhite pushed a commit that referenced this pull request Mar 17, 2026
##
[14.0.0-alpha.9](14.0.0-alpha.8...14.0.0-alpha.9)
(2026-03-17)


### ⚠ BREAKING CHANGES

* **components/indicators:** replace `@angular/animations` in tokens
component with CSS transitions (#4303)
* **components/tabs:** replace `@angular/animations` with CSS
transitions (#4288)
* **components/flyout:** replace `@angular/animations` with CSS
transitions (#4285)

### Features

* **components/lists:** deprecate filter summary
([#4307](#4307))
([b5567d9](b5567d9)),
closes
[AB#3611503](https://dev.azure.com/blackbaud/Products/_workitems/edit/3611503)
* **components/modals:** add modal banner
([#4275](#4275))
([#4280](#4280))
([b186fc2](b186fc2))


### Bug Fixes

* **components/core:** transition handler emits via microtask when
animations disabled
([#4304](#4304))
([3b9cd2d](3b9cd2d))
* **components/data-manager:** address data mutation during view updates
([#4309](#4309))
([#4311](#4311))
([6cadb59](6cadb59)),
closes
[AB#3705639](https://dev.azure.com/blackbaud/Products/_workitems/edit/3705639)
* **components/flyout:** replace `@angular/animations` with CSS
transitions ([#4285](#4285))
([96aa341](96aa341))
* **components/indicators:** replace `@angular/animations` in tokens
component with CSS transitions
([#4303](#4303))
([bcb9d88](bcb9d88))
* **components/tabs:** replace `@angular/animations` with CSS
transitions ([#4288](#4288))
([90b183b](90b183b))

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Breaking Changes**
* Indicators, tabs, and flyout components now use CSS transitions
instead of animations.

* **New Features**
  * Added modal banner component.

* **Deprecations**
  * Filter summary in list components deprecated.

* **Bug Fixes**
* Animation behavior when disabled, data handling during updates, and
transition styling improvements.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

risk level (author): 2 This change has a slight chance of introducing a bug

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants