Skip to content

Port FormGroup to Bootstrap 5#38

Merged
Vitexus merged 5 commits into
mainfrom
fix/formgroup-bootstrap5
Jun 9, 2026
Merged

Port FormGroup to Bootstrap 5#38
Vitexus merged 5 commits into
mainfrom
fix/formgroup-bootstrap5

Conversation

@Vitexus

@Vitexus Vitexus commented Jun 9, 2026

Copy link
Copy Markdown
Member

The Ease\TWB5\FormGroup widget was a stale carry-over from the Bootstrap 4 package and broke many callers.

Changes

  • Bootstrap 5 markup: .mb-3 wrapper, .form-label label, .form-text helper text.
  • Restore the flexible ($label, $content, $placeholder, $helptext, $addTagClass) signature. The previous strict 3-arg, Input-typed constructor threw on label-only/no-arg calls, non-Input content (ATag, SelectTag), the 4-arg helptext form, and referenced a non-existent Ease\TWB5\DivTag.

Testing

Rendered the widget for textarea+helptext, 2/3-arg, ATag, no-arg and label-only cases, and smoke-tested 60+ call sites in the consuming app (login, profile, credential-type, event-source forms) — all render with correct BS5 markup and no errors.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Refactor
    • Updated FormGroup for more flexible Bootstrap 5 form controls, improved labeling, placeholder/help text handling and accessibility.
  • New Features
    • Navbar can now present its menu as an offcanvas drawer with configurable placement.
    • Offcanvas component enhanced with structured header/body, placement options, and improved trigger integration.

Replace the stale Bootstrap 4 carry-over with a proper Bootstrap 5
widget: .mb-3 wrapper, .form-label label and .form-text helper text.

Restore the flexible ($label, $content, $placeholder, $helptext,
$addTagClass) signature used across callers. The previous strict
3-arg, Input-typed constructor threw on label-only/no-arg calls,
non-Input content (ATag, SelectTag) and the 4-arg helptext form,
and referenced a non-existent Ease\TWB5\DivTag.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 9, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@Vitexus, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 47 minutes and 32 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 4c7bd660-c216-44c7-93d7-6af87d98e078

📥 Commits

Reviewing files that changed from the base of the PR and between b77c410 and 4d4678a.

⛔ Files ignored due to path filters (1)
  • composer.lock is excluded by !**/*.lock
📒 Files selected for processing (1)
  • src/Ease/TWB5/Navbar.php
📝 Walkthrough

Walkthrough

Ease\TWB5\FormGroup constructor redesigned to support flexible content wrappers and Bootstrap 5 markup. Method signature changed from typed Input parameter to optional parameters for label, content, placeholder, helptext, and CSS class. Implementation now resolves control id dynamically, renders conditional label, applies attributes via duck-typing, and wires aria-describedby and helper-text divs for accessibility.

Changes

FormGroup Constructor Redesign

Layer / File(s) Summary
FormGroup constructor redesign
src/Ease/TWB5/FormGroup.php
Class documentation updated to describe Bootstrap 5 .mb-3/.form-label/.form-text markup. Constructor signature changed from ($label, \Ease\Html\Input $input, $desc = '') to ($label = null, $content = null, $placeholder = null, $helptext = null, $addTagClass = 'form-control'). Implementation now resolves control id via $content->getTagID(), label-derived fallback (letters only), or random string; conditionally renders label; applies $addTagClass, placeholder, and id via duck-typed method checks; wires aria-describedby when helptext is provided; and renders helper text in Bootstrap 5 .form-text div with matching id.

Navbar Offcanvas Integration

Layer / File(s) Summary
OffCanvas component refactor
src/Ease/TWB5/OffCanvas.php
OffCanvas adds typed public $header and $body, and the constructor now accepts string $id, $title = null, $bodyContent = null, string $placement = 'start', array $properties = []. Default ARIA attributes are merged, tag id/class set via helpers, header/body assigned to public properties and added via addItem(), and triggerButton() uses $this->getTagID() for targeting.
Navbar offcanvas integration and toggler reuse
src/Ease/TWB5/Navbar.php
Navbar adds public $offcanvas and $offcanvasPlacement, stores a shared toggler instance for reuse, introduces navBarOffcanvas() to build the offcanvas DOM, and updates finalize() to retarget the toggler and append the offcanvas markup when enabled.

🎯 4 (Complex) | ⏱️ ~45 minutes

🐰 A tiny hop, a brand new theme,
FormGroup dances in Bootstrap dream.
Offcanvas doors swing wide with cheer,
Toggler finds the target near.
Code and carrots, fresh and clear.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Port FormGroup to Bootstrap 5' directly and accurately captures the main change: migrating the FormGroup class from Bootstrap 4 to Bootstrap 5 markup and API.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/formgroup-bootstrap5

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/Ease/TWB5/FormGroup.php`:
- Around line 54-64: The current FormGroup logic generates deterministic IDs
from $label via \Ease\Functions::lettersOnly which can produce duplicate DOM ids
and always emits a LabelTag "for" target even when there is no bindable control;
update the code in FormGroup (references: $id, $label, $content,
\Ease\Functions::lettersOnly, \Ease\Functions::randomString, addItem(),
\Ease\Html\LabelTag) to (1) ensure generated ids are unique by appending a short
random suffix when the id is derived from the label (e.g. $id =
lettersOnly($label) . '_' . randomString(6)), and (2) only set the LabelTag
"for" attribute when there is an actual bindable control in $content (detectable
by is_object($content) and either a public id property or a getter like
getId()/getAttribute('id')); if no bindable control exists, emit the label
without a "for" attribute.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 9d704d00-206f-4929-b74a-21b0261aeb00

📥 Commits

Reviewing files that changed from the base of the PR and between fdbaded and ed73412.

📒 Files selected for processing (1)
  • src/Ease/TWB5/FormGroup.php

Comment on lines +54 to +64
if (empty($id) && \is_string($label) && $label !== '') {
$id = \Ease\Functions::lettersOnly($label);
}

if (empty($id)) {
$id = 'formgroup_'.\Ease\Functions::randomString();
}

if ($label !== null && $label !== '') {
$this->addItem(new \Ease\Html\LabelTag((string) $id, $label, ['class' => 'form-label']));
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Ensure control IDs are unique and only referenced when a bindable control exists.

Line 54 creates deterministic IDs from label text, so repeated labels can produce duplicate DOM IDs. Also, Line 63 always emits a for target even when $content is non-object/label-only, which creates broken label associations.

💡 Suggested fix
-        if (empty($id) && \is_string($label) && $label !== '') {
-            $id = \Ease\Functions::lettersOnly($label);
-        }
+        if (empty($id) && \is_string($label) && $label !== '') {
+            $id = 'formgroup_'.\Ease\Functions::lettersOnly($label).'_'.\Ease\Functions::randomString();
+        }

         if (empty($id)) {
             $id = 'formgroup_'.\Ease\Functions::randomString();
         }

-        if ($label !== null && $label !== '') {
-            $this->addItem(new \Ease\Html\LabelTag((string) $id, $label, ['class' => 'form-label']));
-        }
+        $canBindLabel = \is_object($content)
+            && (method_exists($content, 'setTagID') || method_exists($content, 'getTagID'));
+        if ($label !== null && $label !== '') {
+            $this->addItem(new \Ease\Html\LabelTag($canBindLabel ? (string) $id : null, $label, ['class' => 'form-label']));
+        }

Also applies to: 75-86

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/Ease/TWB5/FormGroup.php` around lines 54 - 64, The current FormGroup
logic generates deterministic IDs from $label via \Ease\Functions::lettersOnly
which can produce duplicate DOM ids and always emits a LabelTag "for" target
even when there is no bindable control; update the code in FormGroup
(references: $id, $label, $content, \Ease\Functions::lettersOnly,
\Ease\Functions::randomString, addItem(), \Ease\Html\LabelTag) to (1) ensure
generated ids are unique by appending a short random suffix when the id is
derived from the label (e.g. $id = lettersOnly($label) . '_' . randomString(6)),
and (2) only set the LabelTag "for" attribute when there is an actual bindable
control in $content (detectable by is_object($content) and either a public id
property or a getter like getId()/getAttribute('id')); if no bindable control
exists, emit the label without a "for" attribute.

Vitexus and others added 3 commits June 9, 2026 20:22
OffCanvas no longer renders permanently visible (drop the hardcoded
`show` class), accepts a placement argument (start|end|top|bottom),
exposes public $header/$body, and gains a triggerButton() helper that
mirrors Modal. Navbar gains an opt-in $offcanvas mode that turns the
toggler into an offcanvas trigger and wraps the menu in a drawer
(responsive offcanvas-in-navbar); the default collapse behaviour is
unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/Ease/TWB5/OffCanvas.php`:
- Around line 57-60: The aria-label for the close ButtonTag is hard-coded to
'Close' — update the OffCanvas header construction to use the project's
translation helper (e.g., t(), __(), or the app's i18n function) instead of the
literal string so the label is localized; modify the ButtonTag attribute
'aria-label' to call the translation helper and ensure the OffCanvas class
(where $this->header is built) uses the correct translation function and
namespace so translated text is rendered at runtime.
- Around line 50-60: In the OffCanvas constructor, avoid creating an empty
accessible name when $title is null: only add the 'aria-labelledby' attribute to
parent::__construct(...) when $title is non-null/non-empty, and only create the
H5Tag($title, ...) (the heading node with id $id.'Label') when $title is
provided; if $title is omitted, still render the close ButtonTag but do not
render an empty H5Tag and do not set aria-labelledby so any caller-supplied
aria-label is preserved (adjust the $this->header construction and the
properties passed to parent::__construct accordingly).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 980485d9-0a33-4dd5-9278-887f6994aad9

📥 Commits

Reviewing files that changed from the base of the PR and between ed73412 and b77c410.

📒 Files selected for processing (2)
  • src/Ease/TWB5/Navbar.php
  • src/Ease/TWB5/OffCanvas.php

Comment on lines +50 to +60
parent::__construct(null, array_merge([
'tabindex' => '-1',
'id' => $id,
'aria-labelledby' => $id.'Label',
], $properties));
$this->addTagClass('offcanvas offcanvas-'.$placement);
$this->setTagID($id);

$this->header = new DivTag([
new H5Tag($title, ['class' => 'offcanvas-title', 'id' => $id.'Label']),
new ButtonTag('', ['class' => 'btn-close', 'data-bs-dismiss' => 'offcanvas', 'aria-label' => 'Close']),
], ['class' => 'offcanvas-header']);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Preserve an accessible name when $title is omitted.

$title is nullable, but this constructor always sets aria-labelledby and always renders the heading node. When callers pass null, the offcanvas ends up labelled by an empty element, which leaves it unnamed for assistive tech and can override a caller-supplied aria-label.

Suggested fix
     public function __construct(
         string $id,
         $title = null,
         $bodyContent = null,
         string $placement = 'start',
         array $properties = []
     ) {
-        parent::__construct(null, array_merge([
-            'tabindex' => '-1',
-            'aria-labelledby' => $id.'Label',
-        ], $properties));
+        $defaultProperties = ['tabindex' => '-1'];
+
+        if ($title !== null) {
+            $defaultProperties['aria-labelledby'] = $id.'Label';
+        } elseif (!isset($properties['aria-label'], $properties['aria-labelledby'])) {
+            $defaultProperties['aria-label'] = 'Offcanvas';
+        }
+
+        parent::__construct(null, array_merge($defaultProperties, $properties));
         $this->addTagClass('offcanvas offcanvas-'.$placement);
         $this->setTagID($id);
 
-        $this->header = new DivTag([
-            new H5Tag($title, ['class' => 'offcanvas-title', 'id' => $id.'Label']),
-            new ButtonTag('', ['class' => 'btn-close', 'data-bs-dismiss' => 'offcanvas', 'aria-label' => 'Close']),
-        ], ['class' => 'offcanvas-header']);
+        $headerItems = [];
+
+        if ($title !== null) {
+            $headerItems[] = new H5Tag($title, ['class' => 'offcanvas-title', 'id' => $id.'Label']);
+        }
+
+        $headerItems[] = new ButtonTag('', ['class' => 'btn-close', 'data-bs-dismiss' => 'offcanvas', 'aria-label' => 'Close']);
+
+        $this->header = new DivTag($headerItems, ['class' => 'offcanvas-header']);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/Ease/TWB5/OffCanvas.php` around lines 50 - 60, In the OffCanvas
constructor, avoid creating an empty accessible name when $title is null: only
add the 'aria-labelledby' attribute to parent::__construct(...) when $title is
non-null/non-empty, and only create the H5Tag($title, ...) (the heading node
with id $id.'Label') when $title is provided; if $title is omitted, still render
the close ButtonTag but do not render an empty H5Tag and do not set
aria-labelledby so any caller-supplied aria-label is preserved (adjust the
$this->header construction and the properties passed to parent::__construct
accordingly).

Comment on lines +57 to +60
$this->header = new DivTag([
new H5Tag($title, ['class' => 'offcanvas-title', 'id' => $id.'Label']),
new ButtonTag('', ['class' => 'btn-close', 'data-bs-dismiss' => 'offcanvas', 'aria-label' => 'Close']),
], ['class' => 'offcanvas-header']);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Localize the close button label.

aria-label => 'Close' hard-codes English in a reusable widget. That leaks untranslated UI text into every locale using this component.

Suggested fix
-            new ButtonTag('', ['class' => 'btn-close', 'data-bs-dismiss' => 'offcanvas', 'aria-label' => 'Close']),
+            new ButtonTag('', ['class' => 'btn-close', 'data-bs-dismiss' => 'offcanvas', 'aria-label' => _('Close')]),
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/Ease/TWB5/OffCanvas.php` around lines 57 - 60, The aria-label for the
close ButtonTag is hard-coded to 'Close' — update the OffCanvas header
construction to use the project's translation helper (e.g., t(), __(), or the
app's i18n function) instead of the literal string so the label is localized;
modify the ButtonTag attribute 'aria-label' to call the translation helper and
ensure the OffCanvas class (where $this->header is built) uses the correct
translation function and namespace so translated text is rendered at runtime.

composer update --lock only refreshes the hash; actually re-resolve so
the locked set is installable (composer-normalize ^2.51 pulls
justinrainbow/json-schema 6.9.0).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@Vitexus Vitexus merged commit 7e00ec8 into main Jun 9, 2026
1 of 2 checks passed
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