Closes #5813: Add Ranking-related SDC's and Refactor az_ranking to use them - #5834
Closes #5813: Add Ranking-related SDC's and Refactor az_ranking to use them#5834kevdevlu wants to merge 23 commits into
Conversation
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.
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
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>
|
Troy suggested at Jul 29 PR review: What if we used an internal placeholder generating service , like what the external placehold.co does? |
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>
…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>
|
Asked on
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>
|
Tugboat has finished building the preview for this pull request! Link: Dashboard: |
|
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:
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 |
Closes #5813, MAR-259 , and fixes a (not super related) ranking issue (#5156) as well
Description
Goal:
az_flexible_pageusing the az_ranking paragraph typeor
AZRankingDefaultFormatter.phpto use the above SDC's.Key Decisions from Discussion with Chris:
Use a new
componentsfolder in ouraz_quickstartprofile 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.Instead of using
canvas:image($ref: json-schema-definitions://canvas.module/imagein 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
componentsdirectory has the new SDC's:ranking,ranking-deck, andranking-imageFrom
az_ranking, we moved rankings relatedtwig,js, andcssto the SDC'sRefactored
az_rankingto use aAZRankingComponentBuilderfor both form widget previews (live form state) and the formatter (displaying stored items)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.Added to
az_media: anaz_media_image_styletwig filter that applies an image style in twigaz_media_image_style()works whether the prop was built from paragraphs page builder or from Canvas.Deleted the
az_ranking_responsiveimage style. After testing, I realized upscaling (which is whataz_ranking_responsivedoes) is not needed for focal point to work. This PR is now usingmax_1300x1300image 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.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, orAdd 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
Tugboat Testing:
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/
Types of changes
Arizona Quickstart (install profile, custom modules, custom theme)
Drupal core
Drupal contrib projects
Checklist