Skip to content

feat(context-menu): implementa po-context-menu, adiciona activatedTab/focusTab ao po-tabs#2780

Closed
anderson-gregorio-totvs wants to merge 18 commits intomasterfrom
po-context-menu/devin
Closed

feat(context-menu): implementa po-context-menu, adiciona activatedTab/focusTab ao po-tabs#2780
anderson-gregorio-totvs wants to merge 18 commits intomasterfrom
po-context-menu/devin

Conversation

@anderson-gregorio-totvs
Copy link
Contributor

@anderson-gregorio-totvs anderson-gregorio-totvs commented Mar 19, 2026

feat(context-menu): implementa po-context-menu + melhorias no po-tabs

Summary

po-context-menu (new component)

Adds a new po-context-menu sidebar component for contextual module navigation, inspired by po-menu. The component supports:

  • Expand/collapse with two-way binding on p-expanded (via model())
  • Item selection with single-selection enforcement (sanitizes multiple selected: true via effect)
  • Overflow-based tooltip detection on item labels
  • p-item-selected output event to notify parent on selection
  • Keyboard navigation (Enter/Space with preventDefault) and ARIA attributes
  • ChangeDetectionStrategy.OnPush with Angular Signals (input(), model(), signal(), output())

New files: base component, component, template, interface, module, barrel index, spec, sample-basic, sample-labs
Modified files: components.module.ts, components/index.ts, po-icon-dictionary.ts (added ICON_SIDEBAR, ICON_SIDEBAR_SIMPLES)

po-tabs enhancements

  • activatedTab output on PoTabBaseComponent: new output<PoTabBaseComponent>({ alias: 'p-activated-tab' }) signal emitted when a tab becomes active
  • focusTab(id) method on PoTabsComponent: programmatically activates a tab by its id, with guard against non-matching IDs and JSDoc including @ViewChild usage example
  • id property changed from plain optional property to @Input('id') to support external identification
  • NG0203 fix in po-tab.base.component.spec.ts: replaced direct new PoTabComponent() with TestBed.runInInjectionContext() to support the new output() signal
  • Test mock fix in po-tabs.component.spec.ts: added activatedTab mock to onTabActive and onTabActiveByDropdown tests

Portal docs processor

  • Refactored inferTypeFromInputinferTypeFromCode to handle input(), input.required(), output(), and model() signal patterns
  • Removed custom @Input/@Output JSDoc tags from configuration.js — processor now infers input/output status directly from code
  • Added output signal handling: properties using output() are now correctly marked as isDirectiveOutput with type: 'EventEmitter'
  • Bug fix: empty-argument signals (e.g., input() with no generic/value) no longer incorrectly inferred as number type

Updates since last revision

  • Documentation: Replaced @ToDo placeholders with full JSDoc for activatedTab output and focusTab method; fixed stray backtick and missing closing quote in po-context-menu-base.component.ts
  • Test fix: Resolved NG0203: output() can only be used within an injection context by wrapping component instantiation in TestBed.runInInjectionContext(); added activatedTab mocks to po-tabs test suite
  • Sample files: Added sample-po-context-menu-basic and sample-po-context-menu-basic-labs examples
  • focusTab guard: Added if (tab) check to prevent TypeError when no tab matches the given ID
  • inferTypeFromCode fix: Added value !== '' check to prevent empty-string values from being incorrectly typed as number
  • Keyboard accessibility: Added $event.preventDefault() to (keydown.space) in po-context-menu to prevent page scroll
  • Portal docs regex: Updated to support input.required<T>() pattern via input(?:\.required)? regex
  • Model output event name fix: Corrected documentation example from (p-expandChange) to (p-expandedChange) — Angular's model() with alias p-expanded generates the output event p-expandedChange (convention: <alias>Change)
  • Nested generics regex fix: Updated inferTypeFromCode regex patterns from [^>]+ to .+ with lookahead (?=\s*\() so nested generic types like Array<PoContextMenuItem> and string | TemplateRef<void> are captured correctly instead of truncated

CI Status: All 8682 unit tests passing ✅

Review & Testing Checklist for Human

  • Portal docs processor regex patterns — The regex now uses greedy .+ with lookahead (?=\s*\() to handle nested generics. While this should work for standard signal declarations, test doc generation with edge cases: multi-line declarations, unusual formatting/whitespace, deeply nested generics (e.g., Map<string, Array<T>>), and union types with generics.
  • processPropertyDoc behavior change — Previously, explicit @Input/@Output JSDoc tags took absolute priority. Now, decorator-based detection runs first, with signal-based detection as fallback (|| operator). If any component in the codebase used explicit JSDoc tags to override decorator detection, those docs may now be incorrect.
  • id property changed to @Input('id') in PoTabBaseComponent (was id?: string = uuid()). This makes id bindable from templates, which changes behavior — verify no existing consumers rely on the old non-input behavior and that Angular's own element id attribute binding doesn't conflict.
  • activatedTab.emit(tab) fires before deactivateTab() (line 219 of po-tabs.component.ts) — verify this ordering is intentional and doesn't cause issues with tab lifecycle or event listeners.
  • Test end-to-end:
    • Activate tabs programmatically using focusTab(id) and verify (p-activated-tab) event fires with correct PoTabBaseComponent instance
    • Test focusTab with non-existent IDs (should silently do nothing, not crash)
    • Bind id to a po-tab from parent template and verify it's accessible via focusTab
    • Test po-context-menu keyboard navigation (Space key should select item without scrolling page)
    • Test po-context-menu samples in browser to verify expand/collapse, item selection, tooltips, and two-way binding work correctly
    • Generate portal docs and verify that signal-based properties (input(), input.required(), output(), model()) display correct types, especially nested generics

Notes

  • Sample components not module-registered: SamplePoContextMenuBasicComponent and SamplePoContextMenuBasicLabsComponent need to be declared in a documentation module (e.g., DocPoContextMenuModule) for portal build.
  • PoContextMenuBaseComponent not exported: The barrel index.ts doesn't export the base class — add it if consumers need to extend.
  • Commitlint CI failure: One commit message exceeds 72 chars (74 chars) — will be resolved on squash merge.
  • All unit tests pass locally and in CI (8682 tests ✅); only commitlint fails due to message length.
  • Session: https://totvs.devinenterprise.com/sessions/00c60bac99f14fa8b48672f9d4202754
  • Requested by: @anderson-gregorio-totvs

Open with Devin

anderson-gregorio-totvs and others added 2 commits March 19, 2026 17:05
- corrige effect que sobrescrevia sanitizacao (dead code)
- adiciona output itemSelected para propagar selecao ao parent
- corrige comparacao por referencia para comparacao por label
- corrige handlerTooltip para atualizar via signal update
- torna action opcional na interface PoContextMenuItem
- simplifica sanitizeSelection com findIndex
- adiciona tipo de retorno void ao handlerTooltip
- remove import nao utilizado (tick)
- adiciona PoTooltipModule ao TestBed
- corrige typo handlerExpeanded e formato @default
- remove chamada ngOnInit inexistente dos testes
- usa setInput ao inves de items.set() nos testes
- implementa testes unitarios abrangentes para cobertura completa

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@devin-ai-integration
Copy link
Contributor

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment and CI monitoring

devin-ai-integration[bot]

This comment was marked as resolved.

Copy link
Contributor

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 new potential issue.

View 9 additional findings in Devin Review.

Open in Devin Review


const label = value.firstElementChild as HTMLSpanElement;
if (label.scrollHeight > label.offsetHeight) {
this._items.update(items => items.map(i => (i === item ? { ...i, tooltip: item.label } : i)));
Copy link
Contributor

@devin-ai-integration devin-ai-integration bot Mar 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 handlerTooltip silently fails to match item after selectItem due to reference equality on new spread objects

In handlerTooltip at line 72, the code uses reference equality (i === item) to find the matching item in _items. However, selectItem (line 52-55) replaces ALL items in _items with new spread copies ({ ...i, selected: ... }). While Angular's change detection typically re-renders the template before a new mouseenter event fires, this creates an inconsistency with selectItem which uses label-based equality (i.label === item.label) for the same purpose. If for any reason the template hasn't re-rendered yet (e.g., during the same microtask), the reference check will silently fail and no tooltip will be set. Using label-based matching (consistent with selectItem) would be more robust.

Inconsistency between selectItem and handlerTooltip matching strategies

selectItem at po-context-menu.component.ts:54 uses i.label === item.label (label equality), while handlerTooltip at po-context-menu.component.ts:72 uses i === item (reference equality). After selectItem creates entirely new objects via spread at lines 52-55, any stale reference from the template would fail the === check.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Contributor

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 new potential issue.

View 10 additional findings in Devin Review.

Open in Devin Review

@if (expanded()) {
<nav class="po-context-menu-body">
<ul class="po-context-menu-list" role="menu">
@for (item of _items(); track item.label) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Using item.label as both @for track key and selection identifier breaks with duplicate labels

The template at po-context-menu.component.html:27 uses track item.label in the @for loop, and selectItem at po-context-menu.component.ts:59 matches items by i.label === item.label. If a consumer provides two items with the same label (e.g., [{label: 'Settings', action: fn1}, {label: 'Settings', action: fn2}]), two things break: (1) Angular's @for requires unique track values and will produce incorrect DOM diffing or runtime errors with duplicate keys, and (2) selectItem will mark ALL items sharing that label as selected: true, violating the single-selection invariant the component enforces elsewhere via sanitizeSelection.

Prompt for agents
In projects/ui/src/lib/components/po-context-menu/po-context-menu.component.html line 27, change `track item.label` to `track $index` (or add a unique `id` field to the PoContextMenuItem interface and use `track item.id`). Additionally, in projects/ui/src/lib/components/po-context-menu/po-context-menu.component.ts line 59 inside `selectItem`, change the matching from label-based (`i.label === item.label`) to index-based or identity-based matching to avoid selecting multiple items with the same label.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Contributor

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 new potential issue.

View 11 additional findings in Devin Review.

Open in Devin Review

const inferTypeFromCode = codeLine => {
// Extrai tipo genérico
const genericMatch = codeLine.match(/input<([^>]+)>/);
const genericMatch = codeLine.match(/(?:input|output|model)<([^>]+)>/);
Copy link
Contributor

@devin-ai-integration devin-ai-integration bot Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Greedy .+ in generic type regex truncates nested generic types like Array<PoContextMenuItem>

The regex /(?:input(?:\.required)?|output|model)<(.+)>(?=\s*\()/ at projects/portal/docs/processors/helpers/functions.js:125 uses greedy .+ to capture the generic type parameter. For nested generics, the greedy quantifier backtracks to find the > immediately before ( (due to the lookahead), which is the outermost closing > — causing the captured group to miss the inner closing >. This directly impacts the new PoContextMenuBaseComponent's items property defined as input<Array<PoContextMenuItem>>([], { alias: 'p-items' }): the type captured would be Array<PoContextMenuItem instead of Array<PoContextMenuItem>. The fix is to use non-greedy .+?, which correctly finds the first > that satisfies the (?=\s*\() lookahead.

Detailed trace for input<Array<PoContextMenuItem>>([])

With greedy .+:

  • .+ = Array<PoContextMenuItem>> matches second > → lookahead sees ( → capture: Array<PoContextMenuItem

With non-greedy .+?:

  • .+? = Array<PoContextMenuItem> matches first > → lookahead sees > not ( → fail, continue
  • .+? = Array<PoContextMenuItem>> matches second > → lookahead sees ( → capture: Array<PoContextMenuItem>
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 18ec322 — updated both regex patterns to use .+ with lookahead (?=\s*\() instead of [^>]+, so nested generics like Array<PoContextMenuItem> and string | TemplateRef<void> are captured correctly.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The greedy .+ actually works correctly here. The lookahead (?=\s*\() constrains the match — the regex engine backtracks from the right until it finds a > followed by (, which is always the outermost closing >.\n\nTrace for input<Array<PoContextMenuItem>>([], ...):\n- .+ greedily matches everything, then backtracks\n- Finds > at position right before ( (the second >)\n- .+ = Array<PoContextMenuItem>> matches second > → lookahead sees ( → capture: Array<PoContextMenuItem> ✅\n\nBoth greedy and non-greedy produce identical results for all signal patterns because the >(?=\s*\() anchor forces the correct match point. No change needed.

Copy link
Contributor

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 2 new potential issues.

View 12 additional findings in Devin Review.

Open in Devin Review

Comment on lines 157 to +166
property.isOptional = true;
}

if (property.type.includes('input') && property.Input !== undefined) {
const { type, alias } = inferTypeFromInput(property.type);
// Tratamento input e model no Signal
if ((property.type.includes('input') || property.type.includes('model'))) {
const { type, alias } = inferTypeFromCode(property.type);
properties[index].type = type;
properties[index].directiveInputAlias = alias;
properties[index].isDirectiveInput = true;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Docs processor does not classify model() signal properties as outputs

Angular's model() signal creates a two-way binding that acts as both an input and an output. In resolveProperties at projects/portal/docs/processors/helpers/functions.js:161-166, properties whose type includes model are only classified as inputs (isDirectiveInput = true), but isDirectiveOutput is never set. The output check at line 169 (property.type.includes('output')) does not match model. This means the expanded property in PoContextMenuBaseComponent (which uses model<boolean>) will not be documented as supporting two-way binding output events.

Suggested change
property.isOptional = true;
}
if (property.type.includes('input') && property.Input !== undefined) {
const { type, alias } = inferTypeFromInput(property.type);
// Tratamento input e model no Signal
if ((property.type.includes('input') || property.type.includes('model'))) {
const { type, alias } = inferTypeFromCode(property.type);
properties[index].type = type;
properties[index].directiveInputAlias = alias;
properties[index].isDirectiveInput = true;
}
// Tratamento input e model no Signal
if ((property.type.includes('input') || property.type.includes('model'))) {
const { type, alias } = inferTypeFromCode(property.type);
properties[index].type = type;
properties[index].directiveInputAlias = alias;
properties[index].isDirectiveInput = true;
}
// model() also acts as an output for two-way binding
if (property.type.includes('model')) {
const { alias } = inferTypeFromCode(property.type);
properties[index].directiveOutputAlias = alias ? alias + 'Change' : null;
properties[index].isDirectiveOutput = true;
}
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@devin-ai-integration devin-ai-integration bot changed the title feat(context-menu): implementa componente po-context-menu feat(context-menu): implementa po-context-menu, adiciona activatedTab/focusTab ao po-tabs Mar 23, 2026
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
devin-ai-integration[bot]

This comment was marked as resolved.

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
devin-ai-integration[bot]

This comment was marked as resolved.

…a doc

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
devin-ai-integration[bot]

This comment was marked as resolved.

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant