Class: LcpRuby::Metadata::MenuItem

Inherits:
Object
  • Object
show all
Defined in:
lib/lcp_ruby/metadata/menu_item.rb

Constant Summary collapse

TYPES =
%i[view_group link group separator sidebar_toggle].freeze
POSITION_BOTTOM =
"bottom"
POSITION_RIGHT =
"right"
POSITION_TOP =
"top"
POSITION_LEFT =
"left"
POSITIONS =
[ POSITION_BOTTOM, POSITION_RIGHT, POSITION_TOP, POSITION_LEFT ].freeze
MAX_NESTING_DEPTH =
2
BUILTIN_ACTIONS =

Built-in CRUD actions implicit on every presenter. Used by ‘meets_visibility?` to pick `can?(action)` (built-in) vs `can_execute_action?(action)` (custom) for permission gating of `presenter:` items, and by `MenuItemResolver` to pick the correct route helper.

%w[show edit new destroy index].freeze
DESTINATION_KEYS =

Destination keys — exactly one must appear on every non-widget item. ‘widget:` (PR #2b) accepts `provider:` only; that mutex is layered on top in PR #2b’s MenuItem extensions.

%w[url presenter view_group provider children separator sidebar_toggle].freeze
SEPARATOR_ALLOWED_KEYS =

Keys allowed on a ‘:separator` item. Anything else is a configurator footgun — a separator with a `label:` is invisible (no element renders it); a separator with `disable_when:` makes no sense (nothing to disable). Validator rejects at boot.

%w[separator position visible_when].freeze
%w[
  sidebar_toggle label label_key icon aria_label aria_label_key
  position visible_when
].freeze

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(type:, view_group_name: nil, label: nil, label_key: nil, icon: nil, url: nil, children: [], visible_when: {}, disable_when: {}, position: nil, badge: nil, aria_label: nil, aria_label_key: nil, http_method: nil, presenter_slug: nil, alias_name: nil, action_name: nil, defaults: nil, render_partial: nil, render_panel_partial: nil, widget: nil, provider_name: nil, provider_options: nil, panel_provider_name: nil, label_source_loc: nil, aria_label_source_loc: nil, responsive_priority: nil, pin: nil, hide_below: nil, show_below: nil, collapse_label_below: nil) ⇒ MenuItem

Returns a new instance of MenuItem.



32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
# File 'lib/lcp_ruby/metadata/menu_item.rb', line 32

def initialize(type:, view_group_name: nil, label: nil, label_key: nil, icon: nil,
               url: nil, children: [], visible_when: {}, disable_when: {},
               position: nil, badge: nil, aria_label: nil, aria_label_key: nil,
               http_method: nil, presenter_slug: nil, alias_name: nil, action_name: nil,
               defaults: nil,
               render_partial: nil, render_panel_partial: nil, widget: nil,
               provider_name: nil, provider_options: nil, panel_provider_name: nil,
               label_source_loc: nil, aria_label_source_loc: nil,
               responsive_priority: nil, pin: nil, hide_below: nil,
               show_below: nil, collapse_label_below: nil)
  @type = type
  @view_group_name = view_group_name
  # `label` may be a String, `false` (explicit opt-out from any
  # resolved fallback — relevant for view_group items where the
  # presenter would otherwise supply a label), or nil/omitted.
  @label = label
  @label_key = label_key
  @icon = icon
  @url = url
  @children = children
  @visible_when = HashUtils.stringify_deep(visible_when || {})
  @disable_when = HashUtils.stringify_deep(disable_when || {})
  @position = position
  @badge = badge.is_a?(Hash) ? HashUtils.stringify_deep(badge) : nil
  @aria_label = aria_label
  @aria_label_key = aria_label_key
  # `method:` (YAML key) is stored as `http_method` to avoid
  # shadowing `Object#method(name)`. Normalized to a lowercase
  # symbol (`:get`, `:post`, `:put`, `:patch`, `:delete`); `nil`
  # means "unspecified" (renderer picks a default — `:get` for
  # url:/view_group:/presenter+show, etc.).
  @http_method = http_method&.to_s&.downcase&.to_sym
  @presenter_slug = presenter_slug&.to_s
  @alias_name = alias_name&.to_s
  @action_name = action_name&.to_s
  @defaults = defaults.is_a?(Hash) ? HashUtils.stringify_deep(defaults) : nil
  # PR #2b modifiers: `render:` partial path, `render_panel:`
  # partial path, `widget:` flag, `provider:` and `panel_provider:`
  # service names, and the shared `options:` hash both providers
  # consume (mutex with each other so no key collision).
  @render_partial = render_partial&.to_s
  @render_panel_partial = render_panel_partial&.to_s
  @widget = widget == true
  @provider_name = provider_name&.to_s
  @provider_options = provider_options.is_a?(Hash) ? HashUtils.stringify_deep(provider_options) : {}
  @panel_provider_name = panel_provider_name&.to_s
  # i18n_check Phase 3a — populated when a menu DSL builder is
  # added (none exists today; menu is YAML-only). YAML-loaded
  # menus carry nil and Pass 3 covers them.
  @label_source_loc = label_source_loc
  @aria_label_source_loc = aria_label_source_loc

  # Responsive per-item keys (all optional; nil = no effect at
  # runtime). Type coercion only — depth/layout-aware warnings
  # live in ConfigurationValidator.
  @responsive_priority = responsive_priority&.to_i
  @pin = pin&.to_s
  @hide_below = hide_below&.to_i
  @show_below = show_below&.to_i
  @collapse_label_below = collapse_label_below&.to_i

  validate!
end

Instance Attribute Details

#action_nameObject (readonly)

Returns the value of attribute action_name.



21
22
23
# File 'lib/lcp_ruby/metadata/menu_item.rb', line 21

def action_name
  @action_name
end

#alias_nameObject (readonly)

Returns the value of attribute alias_name.



21
22
23
# File 'lib/lcp_ruby/metadata/menu_item.rb', line 21

def alias_name
  @alias_name
end

#aria_labelObject (readonly)

Returns the value of attribute aria_label.



21
22
23
# File 'lib/lcp_ruby/metadata/menu_item.rb', line 21

def aria_label
  @aria_label
end

#aria_label_keyObject (readonly)

Returns the value of attribute aria_label_key.



21
22
23
# File 'lib/lcp_ruby/metadata/menu_item.rb', line 21

def aria_label_key
  @aria_label_key
end

#aria_label_source_locObject (readonly)

Returns the value of attribute aria_label_source_loc.



21
22
23
# File 'lib/lcp_ruby/metadata/menu_item.rb', line 21

def aria_label_source_loc
  @aria_label_source_loc
end

#badgeObject (readonly)

Returns the value of attribute badge.



21
22
23
# File 'lib/lcp_ruby/metadata/menu_item.rb', line 21

def badge
  @badge
end

#childrenObject (readonly)

Returns the value of attribute children.



21
22
23
# File 'lib/lcp_ruby/metadata/menu_item.rb', line 21

def children
  @children
end

#collapse_label_belowObject (readonly)

Returns the value of attribute collapse_label_below.



21
22
23
# File 'lib/lcp_ruby/metadata/menu_item.rb', line 21

def collapse_label_below
  @collapse_label_below
end

#defaultsObject (readonly)

Returns the value of attribute defaults.



21
22
23
# File 'lib/lcp_ruby/metadata/menu_item.rb', line 21

def defaults
  @defaults
end

#disable_whenObject (readonly)

Returns the value of attribute disable_when.



21
22
23
# File 'lib/lcp_ruby/metadata/menu_item.rb', line 21

def disable_when
  @disable_when
end

#hide_belowObject (readonly)

Returns the value of attribute hide_below.



21
22
23
# File 'lib/lcp_ruby/metadata/menu_item.rb', line 21

def hide_below
  @hide_below
end

#http_methodObject (readonly)

Returns the value of attribute http_method.



21
22
23
# File 'lib/lcp_ruby/metadata/menu_item.rb', line 21

def http_method
  @http_method
end

#iconObject (readonly)

Returns the value of attribute icon.



21
22
23
# File 'lib/lcp_ruby/metadata/menu_item.rb', line 21

def icon
  @icon
end

#labelObject (readonly)

Returns the value of attribute label.



21
22
23
# File 'lib/lcp_ruby/metadata/menu_item.rb', line 21

def label
  @label
end

#label_keyObject (readonly)

Returns the value of attribute label_key.



21
22
23
# File 'lib/lcp_ruby/metadata/menu_item.rb', line 21

def label_key
  @label_key
end

#label_source_locObject (readonly)

Returns the value of attribute label_source_loc.



21
22
23
# File 'lib/lcp_ruby/metadata/menu_item.rb', line 21

def label_source_loc
  @label_source_loc
end

#panel_provider_nameObject (readonly)

Returns the value of attribute panel_provider_name.



21
22
23
# File 'lib/lcp_ruby/metadata/menu_item.rb', line 21

def panel_provider_name
  @panel_provider_name
end

#pinObject (readonly)

Returns the value of attribute pin.



21
22
23
# File 'lib/lcp_ruby/metadata/menu_item.rb', line 21

def pin
  @pin
end

#positionObject (readonly)

Returns the value of attribute position.



21
22
23
# File 'lib/lcp_ruby/metadata/menu_item.rb', line 21

def position
  @position
end

#presenter_slugObject (readonly)

Returns the value of attribute presenter_slug.



21
22
23
# File 'lib/lcp_ruby/metadata/menu_item.rb', line 21

def presenter_slug
  @presenter_slug
end

#provider_nameObject (readonly)

Returns the value of attribute provider_name.



21
22
23
# File 'lib/lcp_ruby/metadata/menu_item.rb', line 21

def provider_name
  @provider_name
end

#provider_optionsObject (readonly)

Returns the value of attribute provider_options.



21
22
23
# File 'lib/lcp_ruby/metadata/menu_item.rb', line 21

def provider_options
  @provider_options
end

#render_panel_partialObject (readonly)

Returns the value of attribute render_panel_partial.



21
22
23
# File 'lib/lcp_ruby/metadata/menu_item.rb', line 21

def render_panel_partial
  @render_panel_partial
end

#render_partialObject (readonly)

Returns the value of attribute render_partial.



21
22
23
# File 'lib/lcp_ruby/metadata/menu_item.rb', line 21

def render_partial
  @render_partial
end

#responsive_priorityObject (readonly)

Returns the value of attribute responsive_priority.



21
22
23
# File 'lib/lcp_ruby/metadata/menu_item.rb', line 21

def responsive_priority
  @responsive_priority
end

#show_belowObject (readonly)

Returns the value of attribute show_below.



21
22
23
# File 'lib/lcp_ruby/metadata/menu_item.rb', line 21

def show_below
  @show_below
end

#typeObject (readonly)

Returns the value of attribute type.



21
22
23
# File 'lib/lcp_ruby/metadata/menu_item.rb', line 21

def type
  @type
end

#urlObject (readonly)

Returns the value of attribute url.



21
22
23
# File 'lib/lcp_ruby/metadata/menu_item.rb', line 21

def url
  @url
end

#view_group_nameObject (readonly)

Returns the value of attribute view_group_name.



21
22
23
# File 'lib/lcp_ruby/metadata/menu_item.rb', line 21

def view_group_name
  @view_group_name
end

#visible_whenObject (readonly)

Returns the value of attribute visible_when.



21
22
23
# File 'lib/lcp_ruby/metadata/menu_item.rb', line 21

def visible_when
  @visible_when
end

#widgetObject (readonly)

Returns the value of attribute widget.



21
22
23
# File 'lib/lcp_ruby/metadata/menu_item.rb', line 21

def widget
  @widget
end

Class Method Details

.from_hash(hash) ⇒ Object

Build a MenuItem from a parsed YAML hash. Detection priority: separator > view_group > children > url|presenter Existing call style preserved: ‘from_hash(“view_group” => “x”, …)` works because the method takes a single positional Hash. Any incoming `:type` / `“type”` key is silently ignored — type is always re-derived from the destination keys present (so merging `to_kwargs` with provider returns works without colliding on type).



111
112
113
# File 'lib/lcp_ruby/metadata/menu_item.rb', line 111

def self.from_hash(hash)
  build_from_hash(hash, 0)
end

.from_hash_at_depth(hash, depth) ⇒ Object

Variant that threads an explicit depth budget through static ‘:children:` recursion AND provider expansion (PR #2b). Public so `LayoutHelper#expand_provider` can pass the running depth without re-implementing the construction logic. The leading positional `hash` keeps Ruby 3 from misinterpreting string-key hash literals as kwargs.



121
122
123
# File 'lib/lcp_ruby/metadata/menu_item.rb', line 121

def self.from_hash_at_depth(hash, depth)
  build_from_hash(hash, depth)
end

Instance Method Details

#badge_formObject



529
530
531
532
533
534
535
536
537
# File 'lib/lcp_ruby/metadata/menu_item.rb', line 529

def badge_form
  if badge&.dig("renderer")
    :renderer
  elsif badge&.dig("partial")
    :partial
  elsif badge&.dig("template")
    :template
  end
end

#badge_optionsObject



551
552
553
# File 'lib/lcp_ruby/metadata/menu_item.rb', line 551

def badge_options
  badge&.dig("options") || {}
end

#badge_partialObject



543
544
545
# File 'lib/lcp_ruby/metadata/menu_item.rb', line 543

def badge_partial
  badge&.dig("partial")
end

#badge_providerObject



521
522
523
# File 'lib/lcp_ruby/metadata/menu_item.rb', line 521

def badge_provider
  badge&.dig("provider")
end

#badge_rendererObject



539
540
541
# File 'lib/lcp_ruby/metadata/menu_item.rb', line 539

def badge_renderer
  badge&.dig("renderer")
end

#badge_templateObject



547
548
549
# File 'lib/lcp_ruby/metadata/menu_item.rb', line 547

def badge_template
  badge&.dig("template")
end

#bottom?Boolean

Returns:

  • (Boolean)


555
556
557
# File 'lib/lcp_ruby/metadata/menu_item.rb', line 555

def bottom?
  position.to_s == POSITION_BOTTOM
end

#contains_slug?(slug, loader) ⇒ Boolean

Recursively check if this item or any descendant contains the given slug

Returns:

  • (Boolean)


414
415
416
417
418
419
420
421
422
423
424
425
426
# File 'lib/lcp_ruby/metadata/menu_item.rb', line 414

def contains_slug?(slug, loader)
  if view_group?
    vg = loader.view_group_definitions[view_group_name]
    return false unless vg

    all_slugs = vg.page_names.filter_map do |name|
      loader.page_definitions[name]&.slug
    end
    return true if all_slugs.include?(slug)
  end

  children.any? { |child| child.contains_slug?(slug, loader) }
end

#group?Boolean

Returns:

  • (Boolean)


582
583
584
# File 'lib/lcp_ruby/metadata/menu_item.rb', line 582

def group?
  type == :group
end

#has_badge?Boolean

Returns:

  • (Boolean)


525
526
527
# File 'lib/lcp_ruby/metadata/menu_item.rb', line 525

def has_badge?
  badge_provider.present?
end

#left?Boolean

Returns:

  • (Boolean)


570
571
572
# File 'lib/lcp_ruby/metadata/menu_item.rb', line 570

def left?
  position.to_s == POSITION_LEFT
end

#link?Boolean

Returns:

  • (Boolean)


578
579
580
# File 'lib/lcp_ruby/metadata/menu_item.rb', line 578

def link?
  type == :link
end

#meets_enabled?(user, _helper = nil) ⇒ Boolean

Mirror of ‘meets_visibility?` but for `disable_when:`. Returns true when the item should render in its enabled (clickable) state. Returns false when `disable_when:` evaluates true →rendering applies `aria-disabled` / `<button disabled>` plus the `lcp-menu-item–disabled` CSS class.

On condition errors: dev/test re-raises, production records and returns true (item enabled). The “enabled by default on error” bias mirrors today’s failure-modes table — disable_when is a UI affordance, not a security boundary, so its failure should not silently block users from a working endpoint.

Returns:

  • (Boolean)


459
460
461
462
463
464
465
466
467
468
469
470
471
# File 'lib/lcp_ruby/metadata/menu_item.rb', line 459

def meets_enabled?(user, _helper = nil)
  return true if disable_when.blank?

  !LcpRuby::ConditionEvaluator.evaluate_any(
    nil,
    disable_when,
    context: { current_user: user }
  )
rescue LcpRuby::ConditionError => e
  raise unless defined?(Rails) && Rails.respond_to?(:env) && Rails.env.production?
  LcpRuby.record_error(e, subsystem: "menu_disable", item: label || view_group_name || url)
  true
end

#meets_visibility?(user, helper = nil) ⇒ Boolean

Menu items evaluate visibility from two stacks: the ‘visible_when:` condition tree (record-free; supports service: and compound all/any/not — `field:`/`collection:` are rejected by the validator) AND, for `presenter:` items, the same permission gate the presenter toolbar runs (`can_access_presenter?` plus `can?(action)` for built-in CRUD or `can_execute_action?` for custom actions).

The optional ‘helper` arg lets the layout helper share its per-render `@permission_cache` so N siblings with the same presenter pay the evaluator construction cost once. When `helper` is nil (specs and any non-helper context) the method falls back to constructing a fresh evaluator per call —functionally identical, just slower.

Returns:

  • (Boolean)


442
443
444
445
446
# File 'lib/lcp_ruby/metadata/menu_item.rb', line 442

def meets_visibility?(user, helper = nil)
  return false unless meets_condition_visibility?(user)
  return true unless presenter_slug.present?
  meets_presenter_permission?(user, helper)
end

#render_panel_partial_pathObject

‘<namespace>/menu_renderers/<name>` — Rails partial path for the panel renderer. Same naming convention as `render_partial_path`; the role distinguishing `render:` from `render_panel:` lives at the YAML key level.



689
690
691
692
693
694
# File 'lib/lcp_ruby/metadata/menu_item.rb', line 689

def render_panel_partial_path
  return nil if render_panel_partial.blank?
  ns, name = render_panel_partial.split("/", 2)
  return nil if name.blank?
  "#{ns}/menu_renderers/#{name}"
end

#render_partial_pathObject

‘<namespace>/menu_renderers/<name>` — Rails partial path for the trigger renderer. Returns nil when `render:` is unset so callers can guard with `if render_partial.present?`.



678
679
680
681
682
683
# File 'lib/lcp_ruby/metadata/menu_item.rb', line 678

def render_partial_path
  return nil if render_partial.blank?
  ns, name = render_partial.split("/", 2)
  return nil if name.blank?
  "#{ns}/menu_renderers/#{name}"
end

#resolved_aria_label(_loader = nil) ⇒ Object

Resolve the accessible name for the link/button. Required when no visible label is rendered (icon-only items). Mirrors ‘resolved_label` priority: explicit aria_label_key > literal aria_label with i18n auto-lookup. Namespace `lcp_ruby.menu.aria.<slug>` keeps it separate from labels so the same literal used in both roles does not collide.



382
383
384
385
386
387
388
389
390
391
# File 'lib/lcp_ruby/metadata/menu_item.rb', line 382

def resolved_aria_label(_loader = nil)
  if @aria_label_key.present?
    fallback = @aria_label.is_a?(String) && @aria_label.present? ? @aria_label : @aria_label_key.split(".").last.humanize
    return I18n.t(@aria_label_key, default: fallback)
  end

  return nil unless @aria_label.is_a?(String) && @aria_label.present?

  I18n.t("lcp_ruby.menu.aria.#{@aria_label.parameterize(separator: '_')}", default: nil) || @aria_label
end

#resolved_icon(loader) ⇒ Object

Resolve icon from view group’s primary presenter when not explicitly set



394
395
396
397
398
399
400
# File 'lib/lcp_ruby/metadata/menu_item.rb', line 394

def resolved_icon(loader)
  return @icon if @icon.present?
  return nil unless view_group?

  presenter = primary_presenter(loader)
  presenter&.icon
end

#resolved_label(loader) ⇒ Object

Resolve label with i18n support. Priority: explicit ‘label: false` short-circuit > label_key > explicit label > view_group’s primary presenter resolved_label > ‘presenter:` referenced presenter resolved_label. Returns nil when the item should render no visible label —icon-only items, separator items, or items whose underlying presenter exposes no resolvable label.



348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
# File 'lib/lcp_ruby/metadata/menu_item.rb', line 348

def resolved_label(loader)
  return nil if @label == false

  if @label_key.present?
    fallback = @label.is_a?(String) && @label.present? ? @label : @label_key.split(".").last.humanize
    return I18n.t(@label_key, default: fallback)
  end

  if @label.is_a?(String) && @label.present?
    result = I18n.t("lcp_ruby.menu.#{@label.parameterize(separator: '_')}", default: nil)
    return result || @label
  end

  # No `view_group_name.humanize` fallback — a view_group whose
  # presenter has no resolvable label is a misconfiguration that
  # the validator surfaces, not something we silently humanize.
  return primary_presenter(loader)&.resolved_label if view_group?

  # `presenter:` items inherit the referenced presenter's resolved
  # label as the fallback — keeps `validate_resolved_content!`
  # happy without forcing the configurator to repeat the label.
  if presenter_slug.present?
    return loader.presenter_definitions[presenter_slug]&.resolved_label
  end

  nil
end

#resolved_slug(loader) ⇒ Object

Resolve slug from view group’s primary page



403
404
405
406
407
408
409
410
411
# File 'lib/lcp_ruby/metadata/menu_item.rb', line 403

def resolved_slug(loader)
  return nil unless view_group?

  vg = loader.view_group_definitions[view_group_name]
  return nil unless vg

  page = loader.page_definitions[vg.primary_page]
  page&.slug
end

#right?Boolean

Returns:

  • (Boolean)


559
560
561
# File 'lib/lcp_ruby/metadata/menu_item.rb', line 559

def right?
  position.to_s == POSITION_RIGHT
end

#separator?Boolean

Returns:

  • (Boolean)


586
587
588
# File 'lib/lcp_ruby/metadata/menu_item.rb', line 586

def separator?
  type == :separator
end

Returns:

  • (Boolean)


590
591
592
# File 'lib/lcp_ruby/metadata/menu_item.rb', line 590

def sidebar_toggle?
  type == :sidebar_toggle
end

#to_kwargsObject

Snapshot of the keyword arguments needed to reconstruct this item via ‘MenuItem.new(**kwargs)`. Powers `#with` and keeps the rebuild path in `LayoutHelper#visible_menu_items` from drifting away from `initialize` whenever a new optional key is added. Public so provider expansion (PR #2b) can reconstruct items via `to_kwargs.merge(provider_returned_keys)`.



639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
# File 'lib/lcp_ruby/metadata/menu_item.rb', line 639

def to_kwargs
  {
    type: type,
    view_group_name: view_group_name,
    label: label,
    label_key: label_key,
    icon: icon,
    url: url,
    children: children,
    visible_when: visible_when,
    disable_when: disable_when,
    position: position,
    badge: badge,
    aria_label: aria_label,
    aria_label_key: aria_label_key,
    http_method: http_method,
    presenter_slug: presenter_slug,
    alias_name: alias_name,
    action_name: action_name,
    defaults: defaults,
    render_partial: render_partial,
    render_panel_partial: render_panel_partial,
    widget: widget,
    provider_name: provider_name,
    provider_options: provider_options,
    panel_provider_name: panel_provider_name,
    label_source_loc: label_source_loc,
    aria_label_source_loc: aria_label_source_loc,
    responsive_priority: responsive_priority,
    pin: pin,
    hide_below: hide_below,
    show_below: show_below,
    collapse_label_below: collapse_label_below
  }
end

#top?Boolean

Forward-compat predicates for ‘position: top` / `position: left`. Reserved for future sidebar alignments — no consumer paths exist today, so these always return false unless the YAML opts in.

Returns:

  • (Boolean)


566
567
568
# File 'lib/lcp_ruby/metadata/menu_item.rb', line 566

def top?
  position.to_s == POSITION_TOP
end

#validate_resolved_content!(loader) ⇒ Object

Loader-aware boot-time validation. Called by ‘ConfigurationValidator#validate_menu_items` after the loader is fully populated so view_group fallbacks resolve. Raises with a descriptor that points at the offending entry.



598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
# File 'lib/lcp_ruby/metadata/menu_item.rb', line 598

def validate_resolved_content!(loader)
  return if separator?
  # Platform-owned UI chrome — no destination, no privilege
  # escalation surface, stable aria_label.
  return if sidebar_toggle?
  # widget: items drop the wrapper entirely — the render: partial
  # owns all visible content + accessibility semantics. The
  # validator can't introspect the partial's HTML.
  return if widget?
  # provider: items are computed at render time — the provider
  # supplies label/icon/url in its return hash. Static YAML
  # content is optional (and usually empty); the validator
  # cannot introspect provider returns at boot.
  return if provider_name.present?

  content = visible_content(loader)

  # render: items provide visible content via the renderer
  # partial; label/icon are not required. aria_label/aria_label_key
  # is still strongly recommended (the renderer's HTML may not
  # include accessible text); enforce when neither label nor
  # aria_label is set.
  if content.empty? && render_partial.blank?
    raise MetadataError, "Menu item #{descriptor} has no visible content (label or icon)"
  end

  # Items with no visible label (icon-only OR render: producing
  # opaque HTML) need an explicit aria_label/aria_label_key. We
  # cannot introspect a render: partial's accessible text, so we
  # require the aria attribute regardless when no YAML label resolves.
  if !content.include?(:label) && @aria_label.blank? && @aria_label_key.blank?
    raise MetadataError, "Icon-only menu item #{descriptor} requires `aria_label` or `aria_label_key`"
  end
end

#view_group?Boolean

Returns:

  • (Boolean)


574
575
576
# File 'lib/lcp_ruby/metadata/menu_item.rb', line 574

def view_group?
  type == :view_group
end

#widget?Boolean

Predicate sugar — clearer at call sites than ‘widget == true`.

Returns:

  • (Boolean)


697
698
699
# File 'lib/lcp_ruby/metadata/menu_item.rb', line 697

def widget?
  @widget == true
end

#with(**overrides) ⇒ Object

Returns a copy of this item with selected attributes overridden. Used by ‘LayoutHelper#visible_menu_items` to rebuild a `:group` item with filtered children without re-enumerating every attribute by hand.



100
101
102
# File 'lib/lcp_ruby/metadata/menu_item.rb', line 100

def with(**overrides)
  self.class.new(**to_kwargs.merge(overrides))
end