Skip to content

Closes #5813: Add Ranking-related SDC's and Refactor az_ranking to use them - #5834

Open
kevdevlu wants to merge 23 commits into
mainfrom
issue/5813
Open

Closes #5813: Add Ranking-related SDC's and Refactor az_ranking to use them#5834
kevdevlu wants to merge 23 commits into
mainfrom
issue/5813

Conversation

@kevdevlu

@kevdevlu kevdevlu commented Jul 24, 2026

Copy link
Copy Markdown
Member

Closes #5813, MAR-259 , and fixes a (not super related) ranking issue (#5156) as well

Description

Goal:

  • Make single-source-of-truth SDC's that would be used whether a user is:
    • creating rankings with a traditional az_flexible_page using the az_ranking paragraph type
      or
    • creating rankings with Drupal Canvas
  • Refactor az_ranking's AZRankingDefaultFormatter.php to use the above SDC's.

Key Decisions from Discussion with Chris:

  1. Use a new components folder in our az_quickstart profile as a place to put SDC's.
    Reasoning: we want to avoid adding more components to az_barrio. For this specific component, that would mean making az_ranking (a module) depend on a theme, which we want to avoid.

  2. Instead of using canvas:image ($ref: json-schema-definitions://canvas.module/image in our schema), we want to use something that doesn't depend on canvas and wouldn't break if canvas was not enabled.

High Level Overview of Changes

  1. components directory has the new SDC's: ranking, ranking-deck, and ranking-image

  2. From az_ranking, we moved rankings related twig, js, and css to the SDC's

  3. Refactored az_ranking to use a AZRankingComponentBuilder for both form widget previews (live form state) and the formatter (displaying stored items)

    stored field items ─┐
                        ├─→ AZRankingComponentBuilder ─┐
    live form state ────┘                              │
                                                       ├─→ az_quickstart:ranking
    Canvas prop sources ───────────────────────────────┘      (.twig / .css / .js)
    
  4. ranking-image's image prop is an object ({src, alt, width, height}) matching Canvas's own image shape. This was recommended by penyaskito, a creator of Canvas, in Drupal Slack drupal-canvas channel. See his page here. This provides a media library picker in Canvas for editors just like before. It's matched structurally, with no $ref, which makes it Canvas independent.

  5. Added to az_media: an az_media_image_style twig filter that applies an image style in twig

    <img src="{{ image.src|az_media_image_style('max_1300x1300') }}">
    

    az_media_image_style() works whether the prop was built from paragraphs page builder or from Canvas.

  6. Deleted the az_ranking_responsive image style. After testing, I realized upscaling (which is what az_ranking_responsive does) is not needed for focal point to work. This PR is now using max_1300x1300 image style for ranking image, which comes with Quickstart out of the box and already scales down and converts to WebP.

    • az_ranking_update_1130601() removes the old style on existing sites, and leaves it alone if some other config still depends on it.
  7. Fixed a bunch of old issues with our ranking's when editing using paragraphs page builder

    a. Edit existing rankings page. Reorder a ranking, Click Update Preview, Remove, or Add Another Item. Rankings go all over the place, and widget previews don't match fields anymore.
    b. add a ranking. don't enter any text. save. The page displays it as a blank ranking, when it should just ignore it.
    c. Add some rankings. Then try to remove them all. You can't. Only let's you remove until you reach 2.
    d. In an existing paragraphs page builder with rankings, add a new ranking, then click "update preview" on another existing ranking: the newly added ranking in the form is treated as non-empty so it collapses instead of staying open

    ** Use a local build of main or any other PR's tugboat to see these behaviors. Some of it apply to other paragraphs like cards too. This PR's tugboat should have all of these resolved

Release notes

Related issues

How to test

Flagship Site Testing

I applied this PR to flagship site (the New QS Ranking Components is this PR)
https://ranking2-azs-arizona.pantheonsite.io/about/rankings

  1. Test Everything looks right

Tugboat Testing:

  1. Enable Canvas
  2. Test we can achieve this page's display of rankings using Canvas: https://test-azs-arizona.pantheonsite.io/about/rankings .
  3. Test using the traditional paragraphs page builder (az_flexible_page) and using az_ranking paragraph type works as well.

Testing Existing Sites

I applied this PR to nursing:
https://rankings-azs-nursing.pantheonsite.io/ . compare to: https://live-azs-nursing.pantheonsite.io/

  1. Test Everything looks right. Visual regressions like padding, font size, font weight, etc. are intentional if those changes match flagship's ranking page: https://www.arizona.edu/about/rankings. If a visual regression doesn't match flagship, then that's an error.

Types of changes

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
    • 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

  • My code follows the code style of this project.
  • My change requires a change to the documentation.
  • I have updated the documentation accordingly.
  • I have read the CONTRIBUTING document.
  • I have added tests to cover my changes.
  • All new and existing tests passed.
  • My change requires release notes.

kevdevlu and others added 2 commits July 24, 2026 00:36
Ports the az_ranking paragraph's rendering into three profile-level Single
Directory Components, usable directly in Drupal Canvas as well as from the
Paragraphs pipeline in a later phase:

- ranking: a single text ranking card
- ranking-deck: a responsive CSS grid wrapper for multiple cards/images,
  replacing the Bootstrap row/col composition Canvas has no editor UI for
- image: a layout- and accessibility-aware image, also closing MAR-104
  (decorative image component). Uses a plain string prop shape that Canvas's
  own media_library shape-matching recognizes, giving a real media-library
  picker without requiring the Canvas module to render on Canvas-less sites.

Adds drupal/canvas to composer.json so the Tugboat PR preview build installs
Canvas for review; to be removed in a follow-up commit once review is done.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
AZRankingDefaultFormatter now renders through az_quickstart:ranking,
az_quickstart:image, and az_quickstart:ranking-deck instead of the legacy
#theme => az_ranking render array, so paragraph-authored and Canvas-composed
rankings share the same markup. Legacy stored values (backgrounds, link
styles, per-breakpoint column widths) are mapped to SDC prop tokens via new
class-constant lookup tables; link resolution keeps legacy's exact three-branch
logic, gated on the raw stored link_uri so placeholder '#' links keep their
button. AZRankingWidget's own edit-form preview (#theme => az_ranking) is
untouched, since it's a separate, still-legacy code path.

image's width_span became three per-breakpoint props (desktop/tablet/phone),
replacing a single dynamic value, because CSS Grid has no way to clamp a
span against its container's actual column count (w3c/csswg-drafts#5852) -
the formatter now clamps each breakpoint's span against the sibling
ranking-deck's actual configured columns in PHP instead. ranking-deck and
image also get new default columns/spans (1/2/4 and 1/2/2 phone/tablet/
desktop) for standalone Canvas placement.

ranking's source prop gains a pattern making it Canvas-editable as a
multi-line textarea, matching legacy's line-break-controlled attribution
text; ranking.twig's existing nl2br rendering needed no change.
@kevdevlu kevdevlu self-assigned this Jul 24, 2026
@kevdevlu
kevdevlu marked this pull request as ready for review July 24, 2026 17:08
@kevdevlu
kevdevlu requested review from a team as code owners July 24, 2026 17:08
Comment thread components/ranking-image/ranking-image.component.yml
Comment thread components/image/image.component.yml Outdated
kevdevlu and others added 6 commits July 27, 2026 02:52
The "Image" name was reserved for a future, more broadly-scoped
component; renamed az_quickstart:image to az_quickstart:ranking-image
(directory, component ID, name, CSS/JS) with no change to its actual
capability - it's still a general-purpose, layout-aware image, not
ranking-specific. ranking.component.yml/twig's own cross-references
updated to match.

Also restores WebP conversion and a size cap (lost when the legacy
#theme => image_formatter path was replaced by the plain-URI SDC prop
in an earlier commit): a new Drupal\az_media\Twig\ImageStyleTwigExtension
adds an image_style Twig filter, applied in ranking-image.twig, so both
the paragraph-authored and Canvas-placed pathways get it automatically
regardless of who populates the src prop. The az_ranking_responsive
image style itself moves from az_ranking to az_media (its name, upscale
setting, and WebP conversion left untouched) so a future az_media-only
Canvas site doesn't have to depend on az_ranking to get it - confirmed
via ConfigManager::uninstall() that this was never a deletion-safety
issue, just future-proofing.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QZ6mWfeVEr5qdPLXAEmtqP
az_quickstart:ranking-image's src prop example previously pointed at
canyon_running.jpg, a real demo photo only present on sites that
enable az_demo (explicitly not for production). Adds a dedicated
1000x500 placeholder PNG shipped as a plain az_media asset instead,
copied into public:// via hook_install()/hook_update_N() - a raw
filesystem copy, not a managed file or media entity, so it exists on
every site (including production) without ever showing up as a
selectable option in the Media Library picker.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QZ6mWfeVEr5qdPLXAEmtqP
AZRankingComponentBuilder becomes the single source of truth mapping
ranking item values to az_quickstart:ranking/ranking-image render
arrays, shared by AZRankingDefaultFormatter and AZRankingWidget's live
edit-form preview - previously the widget preview still rendered
through the legacy #theme => 'az_ranking' template/hook_theme path
(az-ranking.html.twig, az_ranking_theme(), plus its own now-dead
az-ranking.css/az-ranking-image.css/az-ranking-focal-point-calc.js),
duplicating the formatter's class-mapping logic and free to drift out
of sync with what actually publishes. Both callers now build a plain
values array (AZRankingComponentBuilder::extractItemValues()) instead
of passing an AZRankingItem directly, since the widget's preview needs
to build that same array from Form API values instead of a field item
(see below) - keeping the builder item-agnostic is what lets it work
for both.

Also fixes az_quickstart#5156: after drag-and-drop reordering a
paragraph's ranking items, clicking "Update Preview"/"Add Another
Item"/"Remove" could leave previews showing stale data for the wrong
row. Root cause: the preview was built from $items[$delta] (stored
array order), while its sibling text/select fields are repopulated by
the Form API from #value (always correct for the current row, since
that's tree-position-based, not stored-order-based). Fixed the way
az_quickstart/az_quickstart#5309 fixed it upstream: build the preview
from the same Form API-populated #value the fields use, instead of the
item, via a new custom Form API element (AZRankingItemElement) whose
#process builds the fields and #after_build rebuilds the preview once
its siblings have resolved - a real, reusable Element plugin instead
of #after_build glue bolted onto Field API's own per-delta wrapper, so
the preview keeps direct access to its sibling fields (an element
scoped to the preview alone would lose that, since #after_build only
sees its own descendants). Also drops the old original_deltas widget-
state remap, which #5309 found actively breaks reorder rather than
protecting it - table-drag already moves whole rows (fields + preview
together) client-side, so no server-side delta remapping is needed.

Verified via a real Drupal form-build cycle (\Drupal::formBuilder()
->doBuildForm()) against real paragraph data, not just isolated calls:
confirmed #process and #after_build both fire in the correct order and
the resulting preview matches the real stored values exactly.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QZ6mWfeVEr5qdPLXAEmtqP
ranking-image.js's object-position formula only ever uses the image's
width/height as a RATIO, never as absolute values, and
az_ranking_responsive's image_scale effect always preserves aspect
ratio (even when upscaling) - so the loaded <img>'s own
naturalWidth/naturalHeight gives the exact same ratio as the true
original, with no need to pass original dimensions down as a prop at
all. Switched to using them directly instead of reading
data-original-width/data-original-height, which also fixes a latent
bug: those attributes, when present with stale values (e.g. a
Canvas-placed instance left at its example defaults), would have fed
wrong data into the calculation instead of ever falling back.

With that dependency gone, original_width/original_height no longer
serve any purpose - their only other use (native <img> width/height
attributes) turned out to be inert too, since ranking-image.css
absolutely-positions the image at 100%/100% of its parent regardless
of intrinsic size. Removed the props entirely, along with the
AZRankingImageHelper computation that read them from the file (a real
file I/O call on every render) and the now-fully-unused ImageFactory
dependency it required.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QZ6mWfeVEr5qdPLXAEmtqP
- ImageStyleTwigExtension: replace the static ImageStyle::load() call
  with an injected EntityTypeManagerInterface
  (DrupalPractice.Objects.GlobalClass.GlobalClass).
- AZRankingItemElement: fix an incorrect import - extended the
  deprecated Drupal\Core\Render\Element\RenderElement (aliased to
  RenderElementBase locally, which doesn't change which class is
  actually extended) instead of the real
  Drupal\Core\Render\Element\RenderElementBase.
- AZRankingItem: merge two adjacent docblocks
  ({@inheritdoc} immediately followed by a bare @todo-only block) into
  one - the second was missing a short description on its own.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QZ6mWfeVEr5qdPLXAEmtqP
Comment thread composer.json Outdated
Comment thread components/ranking-image/ranking-image.component.yml Outdated
@joeparsons joeparsons added enhancement New feature or request minor release Issues/PRs that should only be made as part of a minor release (according to our release policy). developer experience labels Jul 27, 2026
@joeparsons joeparsons moved this from Todo to Needs review in 3.6.0-alpha1 Jul 27, 2026
kevdevlu and others added 2 commits July 28, 2026 15:32
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The legacy image path bubbled the file entity's cache tags via
AZRankingImageHelper::generateImageRenderArray(), which called
renderer->addCacheableDependency($build, $file). The SDC port dropped
that along with the @Renderer service argument.

AZRankingItem stores its media reference as a plain integer property,
not an entity reference, so Drupal contributes no cache metadata for it
automatically and nothing else compensated. Replacing a media entity's
image or moving its focal point would not invalidate an already-cached
ranking.

buildImageComponent() now attaches tags from both the media and file
entities. The media entity is tagged even when it carries no image,
since alt text and both focal point values live there.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@kevdevlu

kevdevlu commented Jul 30, 2026

Copy link
Copy Markdown
Member Author

Troy suggested at Jul 29 PR review: What if we used an internal placeholder generating service , like what the external placehold.co does?

kevdevlu and others added 3 commits July 30, 2026 17:09
An SDC image prop carries only a URI string: Canvas resolves
media -> file -> uri and hands the template the last of those. The alt
text an author typed in the Media Library modal was therefore not
reachable from the component, so an image described once in the library
arrived at the component undescribed.

Add a `media_alt` Twig filter to az_media that walks that chain
backwards, and use it in ranking-image as a fallback: per-placement alt
wins, empty falls back to the media item, decorative overrides both.
The filter takes the media source field name as an argument so any
image-bearing SDC can use it, which is why it lives in az_media rather
than in a consuming module.

Also remove the `examples` value from the `alt` prop. Canvas uses
examples[0] as a prop's stored default value, not as placeholder text,
so "Placeholder image, 1000 by 500 pixels" was shipping verbatim as the
alt text of any real photograph an author did not edit - an
accessibility defect, and one that would also have masked the new
fallback by never being empty. No props on this component are required,
so component metadata stays valid without it.

Retitle both accessibility props to "(this placement)" and say plainly
in their descriptions how they relate to the media item's own values.
The two were being confused for each other, which is what surfaced the
missing carry-over in the first place.

Record in the template's docblock that Canvas independence is a hard
constraint rather than an incidental property, since this component
also renders the legacy az_ranking paragraph type on sites that may
never install Canvas. Canvas's `json-schema-definitions://` $ref scheme
and `apply_image_style` filter are both off limits here; a missing Twig
filter fails at compile time, so there is no graceful degradation.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
kevdevlu and others added 4 commits August 6, 2026 12:05
…dius

Description and source now match the Ranking Card component in az_storybook:
22px/700/30px and 18px/24px. Card body padding is 24px via p-4, and the
description carries mt-1 plus mb-5 on colored backgrounds only - the same
condition the source's mt-auto already used, so transparent cards keep the
spacing they had.

Ranking Image rounds with var(--bs-border-radius), so it follows the site:
1rem on flagship, 0.375rem on vanilla. The img itself inherits that rather
than keeping Bootstrap's flat 0.375rem.

Ranking Deck rows are grid-auto-rows: 1fr, so cards match heights across the
whole deck instead of only within their own row.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@kevdevlu

kevdevlu commented Aug 7, 2026

Copy link
Copy Markdown
Member Author

Asked on #drupal-canvas channel on Drupal Slack Workspace: https://drupal.slack.com/archives/C072JMEPUS1/p1786132948687519

Hi all , i wanted to ask for advice on our situation :thinking_face:
Context: We maintain a large portfolio of sites that we’d like to make Canvas-ready without forcing them onto Canvas yet.

  • Current setup: We use Paragraphs for page building, with many custom paragraph types bundled in one central distribution.
  • Goal: We’ve found we can build SDCs that serve both our Paragraphs rendering and Canvas, so we’d like to start deploying SDCs everywhere now, even on sites where Canvas isn’t installed yet.
  • Challenge: The best-working image component for Canvas seems to be canvas:image, but depending on it means depending on Canvas.

I did a search here and https://drupal.slack.com/archives/C072JMEPUS1/p1772045518951249?thread_ts=1772029243.094249&cid=C072JMEPUS1 seems relevant, but the suggested shape seemed to give no image widget at all 😬 Adding

x-allowed-schemes:
  - http
  - https

gives me the basic image widget but not Media Library…

So my question: Is there an SDC image pattern or approach you guys would recommend that works cleanly in both environments before Canvas is enabled across all our sites?

I got a response from penyaskito: https://penyaskito.com/articles/2026/07/18/canvas-tipstricks-declaring-images-sdcs and have implemented this here in this PR!

…g-image (#5885)

* Adopt Canvas's object image shape for ranking-image

Replaces ranking-image's `src` string prop with an `image` object prop
(src/alt/width/height), structurally matching Canvas's own well-known
image shape. No `$ref` is written, so parsing and validating this
component's schema never depends on Canvas being installed - confirmed
via PropShape::standardize(), which matches the shape by structure, and
Canvas's own image-without-ref test fixture.

Canvas now maps the prop to the media library widget with the full
{src,alt,width,height} expression, while the paragraph path keeps
working with no Canvas present.

Image styling moves out of the component and into az_media's new
AZImageUrlGenerator, behind an `az_media_image_style` Twig filter. The
filter accepts either input form a component prop can arrive in - a
public:// URI (paragraph path) or an already-resolved URL (Canvas's
src_with_alternate_widths) - by reversing the latter back into a URI.
That reversal is what lets one template serve both render paths.

The component now uses the profile's existing max_1300x1300 style,
which already scales and converts to WebP, so az_ranking_responsive is
retired. It deliberately does not upscale: ranking-image.js reads
naturalWidth/naturalHeight only as a ratio, and image_scale preserves
aspect ratio, so the focal point lands identically whatever the
derivative's pixel size, while object-fit: cover does any enlarging in
the browser.

Also deletes the placeholder image generator, controller and route, no
longer needed now that remote `examples` resolve in the Canvas library
preview.

az_ranking_update_1130601() removes the obsolete az_ranking_responsive
style. It lives in az_ranking because that is the module that installed
it on every existing site, and it skips deletion when other config
still depends on the style.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VCwsxHECkN3i67tkMB7Z6T

* Comment refinements

* pass phpcs

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
@trackleft trackleft changed the title Closes 5813: Add Ranking-related SDC's and Refactor az_ranking to use them Closes #5813: Add Ranking-related SDC's and Refactor az_ranking to use them Aug 10, 2026
@az-digital-bot

Copy link
Copy Markdown
Contributor

Tugboat has finished building the preview for this pull request!

Link:

Dashboard:

@kevdevlu

kevdevlu commented Aug 12, 2026

Copy link
Copy Markdown
Member Author

Wednesday - AZ Digital Meeting: Chris said the main thing for this is, we need to make sure editors / power users of quickstart around campus, and people who support users want this kind of structure:

  • 3 simple components: ranking-deck, that can house either a ranking-image or ranking
  1. Dana mentioned that we're going to have focus groups review Canvas's way of building pages to compare to paragraphs page builder with our wider users before putting all our eggs into the canvas 'basket'.

  2. After we reached an agreement that we want Canvas to be a parallel option (or main option) for page building, we can formalize how we want SDC's to be structured, based on the page building experience that everyone wants. For example, in this case specifically, do we want 1 BIG component that can be configured. or 3 small components like what we have now.

Decision: is to leave this until after 1. focus groups review and 2. formalization of a standard of how to structure SDC's for best editor experience in Canvas

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

developer experience enhancement New feature or request minor release Issues/PRs that should only be made as part of a minor release (according to our release policy).

Projects

Status: Needs review

Development

Successfully merging this pull request may close these issues.

Use an SDC to output the rankings paragraph.

4 participants