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 12 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.
| x-allowed-schemes: | ||
| - public | ||
| examples: | ||
| - public://canyon_running.jpg |
There was a problem hiding this comment.
| - public://canyon_running.jpg | |
| - https://placehold.co/600x400?font=Proxima+Nova |
There was a problem hiding this comment.
I think we should use placeholder images for this, preferably with a suggested size printed on them.
There was a problem hiding this comment.
The problem is we're trying to match this image shape, which requires public:// (and we're using this shape because compared to using $ref, this one doesn't crash if we don't have Canvas enabled):
(
isset($storable_prop_shape->shape->schema['contentMediaType'])
&& $storable_prop_shape->shape->schema['contentMediaType'] === 'image/*'
// Stream wrapper URIs can only be `format: uri|iri`.
&& JsonSchemaStringFormat::from($storable_prop_shape->shape->schema['format'])->allowsOnlyAbsoluteUri()
// @see json-schema-definitions://canvas.module/stream-wrapper-image-uri
&& ($storable_prop_shape->shape->schema['x-allowed-schemes'] ?? []) === ['public']
)https://git.drupalcode.org/project/canvas/-/blob/1.x/src/Hook/ShapeMatchingHooks.php#L282
Which, x-allowed-schemes is only public, so we can have public://___________ but not https://_____________
But i totally get having a placeholder image (from an editor's perspective), so this commit is what i am suggesting: 00b6552
Open to any other ideas
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
| type: string | ||
| title: Image | ||
| description: Pick an image from the media library. | ||
| format: uri |
There was a problem hiding this comment.
| format: uri | |
| format: uri-reference |
There was a problem hiding this comment.
This didn't work, but if we really want it to be able to take a https url image, i made a subbranch #5849 basically reproducing the internal mechanisms of canvas onto our az_media module > i don't recommend it because it adds so much complexity / extra maintenance just to allow https://placeholder.co to work
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 placeholder.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>
|
Tugboat has finished building the preview for this pull request! Link: Dashboard: |
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.A Caveat
Since we needed to add images without using canvas:image, claude helped me find inside Canvas:
src/Hook/ShapeMatchingHooks.php'smediaLibraryStorablePropShapeAlter()— a hook Canvas ships itself, gated on core'smedia_librarymodule — which recognizes a plain string prop shaped exactly as:But this is not anything that Canvas talks about in its API and so they might internally change it without telling anybody. In that case, the Editor experience will be broken, but the image will still render on the frontend. This is something we'd need to check every time we upgrade our version of Canvas.
Release notes
Related issues
How to test
az_flexible_pageand using az_ranking paragraph type works as well.Types of changes
Arizona Quickstart (install profile, custom modules, custom theme)
Drupal core
Drupal contrib projects
Checklist