Thanks Aaron for this module, the picker is a real improvement over a long select list.

Problem/Motivation

Declare enum and x-canvas-icon on the same prop and the picker never appears. You get a plain select list, and no error.

The hook sets only fieldWidget. Canvas has already chosen list_string / options_select from the enum, so that shape stays and the widget is ignored.

This hits any theme with an enum-constrained icon prop, on Drupal core with Drupal Canvas, Drupal CMS included. Today the only option is to delete the prop's enum, which loses per-component icon curation for every site using that theme.

Still present in 1.0.3. Verified on Drupal core 11.4.5, PHP 8.4.20, Drupal Canvas 1.10.1, UI Icons 1.1.2, Canvas Icon Picker 1.0.2, against a Vartheme BS5 component's icon prop.

Steps to reproduce

  1. Enable Drupal Canvas, UI Icons and Canvas Icon Picker.
  2. Give a component a string prop declaring both an enum of icon ids and x-canvas-icon.
  3. Place the component on a Canvas page and open its props form.
  4. A select list renders, not the picker.

Proposed resolution

1. Force the string field type. The widget only supports string, so the hook must set the type, not just the widget:

$storable_prop_shape->fieldTypeProp = new FieldTypePropExpression('string', 'value');
$storable_prop_shape->fieldStorageSettings = NULL;

2. Honour the enum. That alone shows the picker, but it stores pack:icon_id while the enum lists bare ids, so every pick fails with Does not have a value in the enumeration. When a prop declares an enum:

  • pass its values as an allowed_icon_ids instance setting (43, 50 and 51 ids on the components tested);
  • send them to the index and sprite endpoints as an ids query parameter, filtering the grid server-side; no JavaScript change;
  • strip the pack: prefix in massageFormValues(), so the stored value matches the enum. IconRepository::resolve() already takes a bare id.

Tested: the index drops from 2078 icons to 5, the sprite returns 5 symbol elements, and both endpoints gain the url.query_args:ids cache context.

Remaining tasks

  • ✅ File an issue
  • ✅ Addition/Change/Update/Fix
  • ❌ Testing to ensure no regression
  • ❌ Automated unit/functional testing coverage
  • ➖ Developer Documentation support
  • ➖ User Guide Documentation support
  • ➖ UX/UI designer responsibilities
  • ➖ Accessibility and Readability
  • ❌ Reviewed by a human
  • ❌ Code review by maintainers
  • ❌ Full testing and approval
  • ❌ Credit contributors
  • ❌ Review with the product owner
  • ❌ Update Release Notes
  • ❌ Release

User interface changes

  • A prop declaring both enum and x-canvas-icon gets the picker instead of a select list, showing only the icons the enum lists.

API changes

  • New optional ids query parameter on the icon index and sprite routes.
  • New allowed_icon_ids field instance setting.

Data model changes

  • field.field_settings.string gains an allowed_icon_ids sequence.

Release notes snippet

  • The icon picker now works on a prop that also declares an enum, and offers only the icons that enum lists.

AI-Generated: Yes

Command icon Show commands

Start within a Git clone of the project using the version control instructions.

Or, if you do not have SSH keys set up on git.drupalcode.org:

Comments

rajab natshah created an issue. See original summary.

rajab natshah’s picture

Issue summary: View changes
rajab natshah’s picture

Issue summary: View changes

rajab natshah’s picture

Issue summary: View changes
rajab natshah’s picture

StatusFileSize
new76.37 KB

Example 1 of 4: what an enum-constrained icon prop looks like today. Reproduced on a stock Drupal CMS install, so this is not distribution-specific.

Environment: Drupal CMS with the Haven site template, Drupal core 11.4.5, PHP 8.4.20, Drupal Canvas 1.10.1, UI Icons 1.1.2, Canvas Icon Picker 1.0.3 unpatched. Icon pack is Haven's own phosphor, 1512 icons.

A component with a curated icon set, declaring both enum and x-canvas-icon:

icon:
  type: string
  title: Icon
  default: info
  enum:
    - info
    - warning
    - check-circle
    - x-circle
  'meta:enum':
    info: Information
    warning: Warning
    check-circle: Success
    x-circle: Error
  x-canvas-icon: ['phosphor']

Canvas derives field_type: list_string with field_widget: canvas_icon_picker. The widget only supports string, so it is dropped and a plain select renders. No error, no warning.

What the editor shows: an Icon select with _none, info, warning, check-circle, x-circle, and zero picker elements in the DOM.

Drupal Canvas editor on Drupal CMS with the Haven site template. The Notice component is selected and its Icon field renders as a plain select list, not the icon picker.

The next three comments show the same site with the merge request applied.

AI-Generated: Yes

rajab natshah’s picture

StatusFileSize
new95.23 KB

Example 2 of 4: a curated set that ships in Drupal CMS today. This one needs no invented component. The Haven site template's own Button already curates its icon prop:

icon:
  type: string
  title: Icon
  enum:
    - arrow-right
    - arrow-left
    - caret-right
    - caret-left
    - download
    - user-plus
  'meta:enum':
    arrow-right: 'Arrow right'
    arrow-left: 'Arrow left'
    caret-right: 'Caret right'
    caret-left: 'Caret left'
    download: Download
    user-plus: 'User plus'

Six icons, with labels, chosen by the theme's designers. Haven does not yet opt into this module, so for the test I added x-canvas-icon: ['phosphor'] to that prop and nothing else.

Unpatched, that combination gives the select from the previous comment. With the merge request applied, the picker renders and the grid holds exactly those six:

Drupal Canvas editor with Haven's Button component selected. The Icon field shows the Canvas Icon Picker with a search box and a grid of exactly six icons, labelled 6 icons.

The drawer reports 6 icons, and the element requests only those ids:

data-canvas-icon-picker-index-url="/canvas-icon-picker/index?packs=phosphor&ids=arrow-right,arrow-left,caret-right,caret-left,download,user-plus"

So the theme keeps its curated set and the editor still gets a visual picker.

AI-Generated: Yes

rajab natshah’s picture

StatusFileSize
new100.91 KB

Example 3 of 4: a share component, where only social icons make sense. This is the case the Webshare module covers: buttons that share the current page. Webshare itself ships no Single Directory Components, so this is a small component modelled on it.

name: Share button
props:
  type: object
  required:
    - network
  properties:
    network:
      type: string
      title: Network
    icon:
      type: string
      title: Icon
      default: link-simple
      enum:
        - facebook-logo
        - x-logo
        - linkedin-logo
        - instagram-logo
        - whatsapp-logo
        - telegram-logo
        - reddit-logo
        - envelope
        - link-simple
      'meta:enum':
        facebook-logo: Facebook
        x-logo: X
        linkedin-logo: LinkedIn
        instagram-logo: Instagram
        whatsapp-logo: WhatsApp
        telegram-logo: Telegram
        reddit-logo: Reddit
        envelope: Email
        link-simple: 'Copy link'
      x-canvas-icon: ['phosphor']
      examples:
        - facebook-logo

Nine icons out of the pack's 1512. An editor picking a share icon should not be offered 1512 options, and the component should not accept an arbitrary one.

Drupal Canvas editor with the Share button component selected. The icon picker drawer shows a grid of exactly nine social network icons, labelled 9 icons.

The drawer reports 9 icons, and the grid tiles are exactly the enum values: link-simple, linkedin-logo, telegram-logo, instagram-logo, x-logo, envelope, whatsapp-logo, facebook-logo, reddit-logo.

AI-Generated: Yes

rajab natshah’s picture

StatusFileSize
new114.69 KB

Example 4 of 4: no enum, nothing changes. Haven's own Icon card has an icon prop with no enum, so the whole pack stays available. This is the regression check: the patch must not narrow a prop that was never curated.

icon:
  type: string
  title: Icon
  x-canvas-icon: ['phosphor']

Drupal Canvas editor with Haven's Icon card selected. The icon picker drawer shows a large scrolling grid, labelled 1512 icons.

The drawer reports 1512 icons and the request carries no ids parameter:

data-canvas-icon-picker-index-url="/canvas-icon-picker/index?packs=phosphor"

Summary of the four examples

Same site, same pack, canvas_icon_picker 1.0.3 versus 1.0.3 plus the merge request:

component                        enum   1.0.3                      with MR !1
Notice (demo)                    4      list_string, select        string, picker, 4 icons
Haven Button (ships in Haven)    6      list_string, select        string, picker, 6 icons
Share button (Webshare case)     9      list_string, select        string, picker, 9 icons
Haven Icon card (ships)          none   string, picker             string, picker, 1512 icons

All six Haven components with an icon prop were opted in (Button, Badge, Card, Icon card, Feature card, Icon). Front page stayed HTTP 200 throughout, and the patch applies cleanly to the 1.0.3 tarball from ftp.drupal.org.

One note on adoption, since it surprised me: opting a theme in by adding x-canvas-icon to its manifests is safe on a site with existing content. Canvas derives a new component version and existing instances keep resolving against the stored one.

AI-Generated: Yes

rajab natshah’s picture

A second merge request, as an alternative to the first. Same outcome, smaller surface. They are mutually exclusive, so pick whichever fits the module better.

  • MR !1 forces the field to string and carries the enum forward in a new allowed_icon_ids field instance setting, which needs a config/schema addition. 7 files.
  • MR !2 leaves the field as list_string and reads the constraint where Canvas already keeps it. No new setting, no schema change, and canvasStorablePropShapeAlter() is untouched. 5 files.

The idea behind !2 is that the module does not need to be told about the enum, it can just notice it. A prop declaring enum already becomes a list field whose allowed values are those enum members, so the constraint exists on the field before the picker gets involved. Three small changes are enough:

  • the widget declares field_types: ['string', 'list_string'], so Canvas stops discarding it for an enum-constrained prop;
  • allowed ids come from that field's own allowed_values_function, which is Canvas's canvas_load_allowed_values_for_component_prop;
  • massageFormValues() strips the pack: prefix on a list field, so the stored value stays one of the allowed values.

Verified on the same Drupal CMS and Haven site as the examples above, with Canvas Icon Picker 1.0.3. A prop with a curated enum of four goes from a plain select to the picker reporting 4 icons, requesting /canvas-icon-picker/index?packs=phosphor&ids=info,warning,check-circle,x-circle. A prop with no enum is unaffected, still 1512 icons and no ids parameter.

One part of !2 I am not confident about: it calls allowed_values_function directly, because getSettableOptions() came back empty in the widget context. If there is a better-supported way to reach a list field's allowed values from a widget, that is the line to change.

AI-Generated: Yes

rajab natshah’s picture

MR !2 on Drupal CMS with the Byte site template. Fresh install, template picked in the installer, then MR !2 applied to Canvas Icon Picker 1.0.3.

Environment: Drupal core 11.4.5, PHP 8.4.20, Drupal Canvas 1.10.1, UI Icons 1.1.2. Icon pack is Byte's own phosphor, 1512 icons. Byte does not opt into the picker yet, so x-canvas-icon: ['phosphor'] was added to its four icon props and nothing else.

Byte's Button ships a curated enum of six, the same set Haven ships:

icon:
  type: string
  title: Icon
  enum:
    - arrow-right
    - arrow-left
    - caret-right
    - caret-left
    - download
    - user-plus

With MR !2 the field stays list_string, the widget is no longer discarded, and the grid holds exactly those six. The drawer reports 6 icons, the request carries ids=arrow-right,arrow-left,caret-right,caret-left,download,user-plus, and no select renders.

Drupal Canvas editor on Drupal CMS with the Byte site template. Byte's Button component is selected and the icon picker drawer shows a grid of exactly six icons, labelled 6 icons.

Byte's Icon card has no enum, and is unaffected: 1512 icons, no ids parameter.

Same editor with Byte's Icon card selected. The icon picker drawer shows a large scrolling grid, labelled 1512 icons.

Config for the four opted-in components, after the patch:

component   enum   field_type    widget
button      6      list_string   canvas_icon_picker
badge       none   string        canvas_icon_picker
card-icon   none   string        canvas_icon_picker
icon        none   string        canvas_icon_picker

Front page stayed HTTP 200 throughout.

AI-Generated: Yes

rajab natshah’s picture

MR !2 on Drupal CMS with the Mercury Demo site template. A second fresh install, Mercury Demo picked in the installer, then MR !2 applied to Canvas Icon Picker 1.0.3.

Same environment as the Byte comment: Drupal core 11.4.5, PHP 8.4.20, Drupal Canvas 1.10.1, UI Icons 1.1.2, Mercury's own phosphor pack of 1512 icons. Mercury does not opt into the picker yet, so x-canvas-icon: ['phosphor'] was added to its four icon props and nothing else.

Mercury's Button curates the same six icons Haven and Byte curate, which is worth noting on its own: this shape is standard across the Drupal CMS site templates, not a one-off.

Drawer reports 6 icons, request carries ids=arrow-right,arrow-left,caret-right,caret-left,download,user-plus, no select:

Drupal Canvas editor on Drupal CMS with the Mercury Demo site template. Mercury's Button component is selected and the icon picker drawer shows a grid of exactly six icons, labelled 6 icons.

Mercury's Icon card has no enum and is unaffected: 1512 icons, no ids parameter.

Same editor with Mercury's Icon card selected. The icon picker drawer shows a large scrolling grid, labelled 1512 icons.

Coverage so far for MR !2

platform / template          component     enum   result
Drupal CMS + Haven           button        6      picker, 6 icons
Drupal CMS + Byte            button        6      picker, 6 icons
Drupal CMS + Mercury Demo    button        6      picker, 6 icons
Drupal CMS + Byte            card-icon     none   picker, 1512 icons
Drupal CMS + Mercury Demo    card-icon     none   picker, 1512 icons
Varbase 11 + Vartheme BS5    badge         43     picker, 42 icons

The Varbase row is the one that differs: its enum of 43 includes none, which is a valid prop value but not an icon, so the grid shows 42 tiles for 43 requested ids. Worth knowing if you want the count label to match the enum length exactly.

One limitation of MR !2 to record: it keys off the field being list_string. A component instance pinned to a stored version created while MR !1 was applied has field type string, so it renders the picker unrestricted. That only affects a site that ran !1 first.

AI-Generated: Yes

  • aaronchristian committed 493c1b09 on 1.0.x
    Issue #3619510: Store enum-valid icon ids client-side, add tests and...

  • rajab natshah committed 22554657 on 1.0.x
    fix: #3619510 Force the string field type and honour the prop enum in...
aaronchristian’s picture

Thanks Rajab, good catch. Forcing the string field type was definitely the right call. I merged that into `1.0.x` with a follow-up commit (`493c1b0`).

I also used AI to help review the MR and test the different paths, and that surfaced another issue with enum props. The picker would show up correctly, but selecting an icon wouldn’t actually stick.

Canvas validates the value in the browser with Ajv before anything reaches the server. The picker was writing `pack:icon`, so if the enum only allowed bare IDs like `umbrella`, Canvas rejected it immediately. That meant massageFormValues() never even ran. MR !2 had the same problem.

What I landed instead is a bit simpler. The allowed enum values are passed into the JS, and the picker writes the value in the same format the enum already uses, either the bare icon ID or the full `pack:icon` value.

I dropped massageFormValues() since it wasn’t doing anything in this flow, added kernel coverage, and added a short README section for enum usage.

Tested it on a live Canvas page as well. Picking an icon now stores `icon: "umbrella"` and survives a reload.

No update hook needed for existing sites either. A cache rebuild will regenerate the Canvas component version, and existing stored values are already fine.

Closing both MRs as merged/superseded. Thanks again for the report and the patch.

aaronchristian’s picture

Status: Active » Fixed

Now that this issue is closed, review the contribution record.

As a contributor, attribute any organization that helped you, or if you volunteered your own time.

Maintainers, credit people who helped resolve this issue.

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.