feat(components): add Textarea component#313
Conversation
Move the inner <input> markup behind a protected virtual RenderInputElement method exposed as a RenderFragment field, mirroring the existing _renderMainWrapper / _renderInputWrapper / _renderHelperWrapper pattern. Promote _inputType, InputClass, HasValue, and JSRuntime to protected so subclasses can override the renderer. No behavior change for existing inputs (Textbox, Numbox, Datebox).
LumexTextarea<>: multi-line input mirroring LumexTextbox, inheriting from LumexDebouncedInputBase<string?>. Overrides RenderInputElement to emit <textarea>; exposes MinRows (3), MaxRows (8), and DisableAutosize. Companion JS module textarea.js handles HeroUI-style autosize: clamps height between minRows and maxRows line-heights on input, toggles data-has-multiple-rows. Multi-line slot tweaks (h-auto wrapper, resize-none input, top-right clear button, etc.) live in InputField.GetMultilineStyles, gated on input is LumexTextarea so LumexNumbox / LumexDatebox stay untouched and gain no "multiline" concept.
19 facts mirroring TextboxTests structure: <textarea> element rendering, slot wiring, clear button, focus behavior, OnInput vs OnChange, debounce delay, and rows attribute matching MinRows. Stubs both input.js and textarea.js modules.
Top-level Textarea.razor with 16 examples mirroring Textbox plus a new Autosize section demonstrating MinRows/MaxRows/DisableAutosize. Registers LumexTextarea in the sidebar (with PageStatus.New) and API navigation, and adds an entry to llms.txt.
Rendering @CurrentValueAsString as textarea content only sets the default value (per HTML spec); once the user types, subsequent re-renders don't sync back to the DOM. Switch to value="@x" attribute, matching Microsoft's official InputTextArea, so Blazor's renderer pushes value updates to the textarea's .value DOM property.
The inherited LumexInputFieldBase layout puts clear-button inside inner-wrapper and wraps everything in main-wrapper when the label is outside. HeroUI's textarea is flatter: clear-button is a sibling of inner-wrapper, the outside label sits as a sibling of input-wrapper, and there is no main-wrapper. Mirror that exactly so the rendered DOM matches HeroUI's textarea component. LumexTextarea now renders its own complete tree (no longer routes through the base's _renderInputElement hook). Textbox / Numbox / Datebox still inherit the existing base layout and are unaffected. Promotes LabelClass, InputWrapperClass, InnerWrapperClass, ClearButtonClass, HelperWrapperClass, DescriptionClass, ErrorMessageClass, ClearButtonVisible, HasHelper, FocusInputAsync, and ClearAsync to protected so subclasses with custom rendering can reuse the existing class composition and event handlers.
The hook was added so LumexTextarea could swap the inner element through the base wrapper, but textarea now renders its own complete DOM, leaving the hook without overrides. Inline the <input> back into RenderInputWrapper to keep the base lean. InputClass / HasValue / JSRuntime stay protected because LumexTextarea's custom render reuses them.
HeroUI's input.ts defines four `isMultiline`-aware compound variants that depend on labelPlacement (py-2 on inputWrapper, pb-1.5 / pb-0.5 on label, pt-0 on input). Apply them in GetMultilineStyles so the rendered classes match HeroUI's textarea for both inside and outside labels.
…line HeroUI's `labelPlacement: "outside"` absolute-positioning bundle for the label and the `has-[label]:mt-X` baseline on the base wrapper are scoped to the `isMultiline: false` compound. Lumex applied both unconditionally for outside-label inputs, so textarea + outside label ended up with conflicting `relative` (from multiline) and `top-1/2 -translate-y-1/2` (from outside) classes, plus extra top-margin sized for a floating label. Gate `GetLabelPlacementOutsideBySizeStyles(_, _base|_label)` and the outside-label branch of `GetLabelPlacementStyles(_, _label)` on `input is not LumexTextarea` so the label flows naturally above the textarea with just `relative` + `pb-1.5` (multiline) styling.
…de label The inside-label scale-down (`group-data-[filled-focused=true]:scale-[0.85]`) and the size-based translate-up (`group-data-[filled-focused=true]:-translate-y-[calc(...)]`) are designed for an absolutely-positioned floating label. Textarea forces the label to `relative` (HeroUI's isMultiline base rule), so those effects shrink and shift a flow-positioned element in place — the wrong behavior. Gate the placement-driven `_label` styling on `input is not LumexTextarea` and move `cursor-text` into GetMultilineStyles so inside-textarea retains the click-to-focus cursor without the floating-label transitions.
Reverting the scale suppression from the previous commit. The inside-label `scale-[0.85]` on focus/fill is a desired animation — it works for a `relative`-positioned textarea label, shrinking in place. Only the size-based translate-up stays suppressed (that one shifts a flow-positioned element to nowhere useful).
Now that GetMultilineStyles applies `items-start` to inner-wrapper, the manual top-margin on icons isn't needed — the flex container aligns start-content / end-content to the top naturally.
Add the existing `scrollbar-hide` utility to the multiline input slot so the scrollbar stays out of sight even when content overflows past MaxRows. The JS autosize logic still sets overflow:auto at the row limit, so wheel / keyboard scrolling continues to work — only the visual scrollbar is hidden.
Earlier I gated GetLabelPlacementInsideBySizeStyles / OutsideBySizeStyles entirely off for textarea labels to skip the floating-label translate. That also dropped the per-size `text-X` rule, so Size.Large textareas (and the other sizes too) rendered the label at the base `text-small` instead of the size-appropriate text size. Un-gate the size helpers for the `_label` slot and instead neutralise just the parts that conflict with a `relative` flow label in GetMultilineStyles: `group-data-[filled-focused=true]:translate-y-0` resets the focus-translate, `left-auto` resets the outside-by-size `left-X` offset. Text size now comes from the size helper for both inside and outside placements.
Lost in an earlier `git reset --hard` that reverted the data-autosize-disabled commit and also discarded an uncommitted local edit on the file. Restoring to the intended baseline.
Render `data-autosize-disabled` on the textarea element reflecting the
DisableAutosize parameter. Gate both `resize-none` and the height transition
on `data-[autosize-disabled=false]` so:
- With autosize on: JS controls height, `resize-none` blocks the native
resize handle, and `transition-[height] duration-100` smooths the
JS-driven grow/shrink.
- With autosize off: native resize handle is available and there's no
transition fighting the drag — the lag was the transition catching
every drag frame.
textarea.js: when disableAutosize is set, skip the input listener and the
adjust() call entirely so user-initiated resizes aren't clobbered on the
next keystroke. The `rows` attribute still provides the initial height.
Keep `resize-none` unconditional and gate only the height transition on `data-[autosize-disabled=false]`. With DisableAutosize the textarea is locked at MinRows — no JS-driven autogrow, no native resize handle, no transition. The transition gating still keeps drag-lag away in case any height change does happen.
Autosize: put the three demos on a single flex row (default / custom range / autosize disabled) instead of stacking the disabled one separately. StartEndContent: make the outer grid `w-full` so it stretches across the preview container.
|
Caution Review failedThe pull request is closed. ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (2)
📝 WalkthroughWalkthroughAdds a new LumexTextarea component (multiline, autosize, debounce), JS autosize controller, styling adjustments for multiline inputs, base-class visibility changes to support inheritance, docs/examples/previews, navigation updates, and comprehensive tests. ChangesLumexTextarea Component
Sequence DiagramsequenceDiagram
participant LumexTextarea as LumexTextarea (Render)
participant IJSModule as IJSRuntime (module)
participant TextareaJS as textarea.js
LumexTextarea->>IJSModule: import module (OnAfterRenderAsync)
IJSModule->>TextareaJS: initialize(element, options)
TextareaJS->>TextareaJS: adjust height, register input listener
LumexTextarea->>IJSModule: update(element, options) (when MinRows/MaxRows/DisableAutosize change)
IJSModule->>TextareaJS: update(element, options)
LumexTextarea->>IJSModule: DisposeAsync -> destroy(element)
IJSModule->>TextareaJS: destroy(element)
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Suggested labels
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Tip 💬 Introducing Slack Agent: The best way for teams to turn conversations into code.Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.
Built for teams:
One agent for your entire SDLC. Right inside Slack. 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. Comment |
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #313 +/- ##
==========================================
- Coverage 96.95% 92.71% -4.24%
==========================================
Files 70 168 +98
Lines 1542 2811 +1269
Branches 150 416 +266
==========================================
+ Hits 1495 2606 +1111
- Misses 28 109 +81
- Partials 19 96 +77 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Actionable comments posted: 5
🧹 Nitpick comments (3)
tests/LumexUI.Tests/Components/Textarea/TextareaTests.razor (1)
90-98: ⚡ Quick winConsider adding test coverage for
MaxRowsandDisableAutosizeparameters.The tests verify
MinRowsmaps to therowsattribute, but the PR objectives describeMaxRowsandDisableAutosizeas key parameters. Adding tests for these would improve coverage:
- A test verifying that
MaxRowsis accepted (it may not directly affect DOM since autosize constraints are JS-side, but you could verify the component renders without throwing).- A test verifying that
DisableAutosize="true"sets thedata-autosize-disabledattribute (mentioned in PR objectives).📝 Example test stubs
[Fact] public void ShouldAcceptMaxRowsParameter() { var action = () => Render(@<LumexTextarea Label="Test" MinRows="3" MaxRows="10" />); action.Should().NotThrow(); } [Fact] public void ShouldSetDataAutosizeDisabledWhenDisableAutosizeTrue() { var cut = Render(@<LumexTextarea Label="Test" DisableAutosize="true" />); var textarea = cut.Find( "textarea" ); textarea.GetAttribute( "data-autosize-disabled" ).Should().Be( "true" ); }🤖 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 `@tests/LumexUI.Tests/Components/Textarea/TextareaTests.razor` around lines 90 - 98, Add two tests to TextareaTests.razor: one that renders LumexTextarea with MaxRows set (e.g., Render(@<LumexTextarea Label="Test" MinRows="3" MaxRows="10" />)) and asserts that rendering does not throw (use an Action and Should().NotThrow()), and another that renders LumexTextarea with DisableAutosize="true", finds the "textarea" element and asserts its data-autosize-disabled attribute equals "true"; reference the existing ShouldHaveRowsAttributeMatchingMinRows test for style and use the same Render and cut.Find("textarea") patterns.docs/LumexUI.Docs.Client/Pages/Components/Textarea/Examples/_Radius.razor (1)
5-5: 💤 Low valueConsider using variant-specific labels for clarity.
All textareas share the same hardcoded
Label="Radius", making it difficult to distinguish between variants at a glance. Consider using the radius value in the label (e.g.,Label="@($"Radius: {radius}")) to improve visual differentiation in the documentation.📝 Suggested improvement
- <LumexTextarea Radius="@radius" Label="Radius" Placeholder="@radius.ToString()" /> + <LumexTextarea Radius="@radius" Label="@($"Radius: {radius}")" Placeholder="Enter text here..." />🤖 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 `@docs/LumexUI.Docs.Client/Pages/Components/Textarea/Examples/_Radius.razor` at line 5, The Label for the example LumexTextarea is hardcoded as "Radius", making variants indistinguishable; update the LumexTextarea usage (the component instance using the Radius parameter and Label attribute) to interpolate the radius value into the label (e.g., use string interpolation to set Label to include the radius value) so each variant shows a distinct label like "Radius: {radius}".src/LumexUI/Components/Textarea/LumexTextarea.razor.cs (1)
23-31: ⚡ Quick winConsider validating MinRows ≤ MaxRows.
The component allows
MinRowsandMaxRowsto be set independently without validation. IfMaxRows < MinRows, the JS autosize logic will computeMath.max(contentHeight, minHeight)followed byMath.min(..., maxHeight), which could clamp the textarea toMaxRowseven when content is less thanMinRows, producing unexpected behavior.🛡️ Suggested validation in SetParametersAsync or OnParametersSet
+/// <inheritdoc /> +protected override void OnParametersSet() +{ + base.OnParametersSet(); + + if( MaxRows < MinRows ) + { + throw new ArgumentException( $"{nameof(MaxRows)} ({MaxRows}) must be greater than or equal to {nameof(MinRows)} ({MinRows})." ); + } +}🤖 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/LumexUI/Components/Textarea/LumexTextarea.razor.cs` around lines 23 - 31, Validate that MinRows is not greater than MaxRows in the LumexTextarea component (LumexTextarea.razor.cs) by adding a check in SetParametersAsync or OnParametersSet: if MinRows > MaxRows, either swap them or clamp MinRows to MaxRows (or throw/Log an ArgumentException) and ensure any computed minHeight/maxHeight logic uses the corrected values; reference the MinRows and MaxRows properties and the SetParametersAsync/OnParametersSet lifecycle methods to locate where to perform this validation.
🤖 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
`@docs/LumexUI.Docs.Client/Pages/Components/Textarea/Examples/ErrorMessage.razor`:
- Around line 24-27: The OnBioChange handler in the Textarea example currently
declares a non-nullable parameter (string) but must accept a nullable string to
match LumexTextarea's ValueChanged contract (LumexDebouncedInputBase<string?>);
change the signature of OnBioChange to take string? (nullable) and update any
null-safe assignments inside (e.g., assign _user.Bio = value ?? string.Empty or
handle null appropriately) so the handler aligns with ValueChanged and avoids
nullable-mismatch warnings.
In
`@docs/LumexUI.Docs.Client/Pages/Components/Textarea/Examples/TwoWayDataBinding.razor`:
- Around line 28-31: The handler OnValueChanged currently accepts a non-nullable
string but LumexTextarea is used with TValue = string? and its ValueChanged is
EventCallback<string?>; update the OnValueChanged signature to accept string?
(nullable) so it matches the EventCallback<string?>, and ensure any assignment
to _valueTwo accepts null (e.g., _valueTwo = value) without forcing non-null;
reference the OnValueChanged method, the _valueTwo field, and the LumexTextarea
TValue/ValueChanged types when making the change.
In `@docs/LumexUI.Docs.Client/Pages/Components/Textarea/Textarea.razor`:
- Line 180: The docs mistakenly list InputFieldSlots.MainWrapper for
LumexTextarea even though LumexTextarea does not render a main-wrapper; remove
the entry new(nameof(InputFieldSlots.MainWrapper), ...) from the Textarea slot
documentation so the slot API accurately reflects LumexTextarea's rendered slots
and only documents the actual slots exposed by LumexTextarea.
In `@docs/LumexUI.Docs/wwwroot/llms.txt`:
- Around line 9-13: The Key Features list contains mojibake characters ("�") in
the bullet lines; open the Key Features text block in llms.txt (the list entries
starting with "Tailwind CSS�based" etc.) and replace each corrupted character
with the correct punctuation/spacing (e.g., change "Tailwind CSS�based" to
"Tailwind CSS-based", "Clean by default � Professional" to "Clean by default —
Professional" or a normal hyphen/emdash as consistent with other bullets),
ensuring all instances of "�" are removed and proper ASCII/Unicode punctuation
and spacing are used so the bullets read cleanly and indexable.
In `@tests/LumexUI.Tests/Components/Textarea/TextareaTests.razor`:
- Line 15: The JS interop setup uses
inputModule.Setup<string>("input.getValidationMessage", _ => true) but the
lambda returns a boolean, causing a type mismatch; fix by either (a) if the
interop should return a string, change the lambda to return the appropriate
message string (e.g., _ => "validation message") and keep Setup<string>, (b) if
it should return a boolean change the setup to
Setup<bool>("input.getValidationMessage", _ => true), or (c) if the return value
is unused adjust the generic to match the actual expectation (e.g.,
Setup<object> or Setup<string> with _ => string.Empty) so the delegate return
type matches the generic used in inputModule.Setup.
---
Nitpick comments:
In `@docs/LumexUI.Docs.Client/Pages/Components/Textarea/Examples/_Radius.razor`:
- Line 5: The Label for the example LumexTextarea is hardcoded as "Radius",
making variants indistinguishable; update the LumexTextarea usage (the component
instance using the Radius parameter and Label attribute) to interpolate the
radius value into the label (e.g., use string interpolation to set Label to
include the radius value) so each variant shows a distinct label like "Radius:
{radius}".
In `@src/LumexUI/Components/Textarea/LumexTextarea.razor.cs`:
- Around line 23-31: Validate that MinRows is not greater than MaxRows in the
LumexTextarea component (LumexTextarea.razor.cs) by adding a check in
SetParametersAsync or OnParametersSet: if MinRows > MaxRows, either swap them or
clamp MinRows to MaxRows (or throw/Log an ArgumentException) and ensure any
computed minHeight/maxHeight logic uses the corrected values; reference the
MinRows and MaxRows properties and the SetParametersAsync/OnParametersSet
lifecycle methods to locate where to perform this validation.
In `@tests/LumexUI.Tests/Components/Textarea/TextareaTests.razor`:
- Around line 90-98: Add two tests to TextareaTests.razor: one that renders
LumexTextarea with MaxRows set (e.g., Render(@<LumexTextarea Label="Test"
MinRows="3" MaxRows="10" />)) and asserts that rendering does not throw (use an
Action and Should().NotThrow()), and another that renders LumexTextarea with
DisableAutosize="true", finds the "textarea" element and asserts its
data-autosize-disabled attribute equals "true"; reference the existing
ShouldHaveRowsAttributeMatchingMinRows test for style and use the same Render
and cut.Find("textarea") patterns.
🪄 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: e33aaada-2168-4f7e-8441-5dabb2e87867
📒 Files selected for processing (44)
docs/LumexUI.Docs.Client/Common/Navigation/NavigationStore.csdocs/LumexUI.Docs.Client/Pages/Components/Textarea/Examples/Autosize.razordocs/LumexUI.Docs.Client/Pages/Components/Textarea/Examples/ClearButton.razordocs/LumexUI.Docs.Client/Pages/Components/Textarea/Examples/Colors.razordocs/LumexUI.Docs.Client/Pages/Components/Textarea/Examples/CustomStyles.razordocs/LumexUI.Docs.Client/Pages/Components/Textarea/Examples/DebounceDelay.razordocs/LumexUI.Docs.Client/Pages/Components/Textarea/Examples/Description.razordocs/LumexUI.Docs.Client/Pages/Components/Textarea/Examples/Disabled.razordocs/LumexUI.Docs.Client/Pages/Components/Textarea/Examples/ErrorMessage.razordocs/LumexUI.Docs.Client/Pages/Components/Textarea/Examples/LabelPlacements.razordocs/LumexUI.Docs.Client/Pages/Components/Textarea/Examples/ReadOnly.razordocs/LumexUI.Docs.Client/Pages/Components/Textarea/Examples/Required.razordocs/LumexUI.Docs.Client/Pages/Components/Textarea/Examples/Sizes.razordocs/LumexUI.Docs.Client/Pages/Components/Textarea/Examples/StartEndContent.razordocs/LumexUI.Docs.Client/Pages/Components/Textarea/Examples/TwoWayDataBinding.razordocs/LumexUI.Docs.Client/Pages/Components/Textarea/Examples/Usage.razordocs/LumexUI.Docs.Client/Pages/Components/Textarea/Examples/Variants.razordocs/LumexUI.Docs.Client/Pages/Components/Textarea/Examples/_Radius.razordocs/LumexUI.Docs.Client/Pages/Components/Textarea/PreviewCodes/Autosize.razordocs/LumexUI.Docs.Client/Pages/Components/Textarea/PreviewCodes/ClearButton.razordocs/LumexUI.Docs.Client/Pages/Components/Textarea/PreviewCodes/Colors.razordocs/LumexUI.Docs.Client/Pages/Components/Textarea/PreviewCodes/CustomStyles.razordocs/LumexUI.Docs.Client/Pages/Components/Textarea/PreviewCodes/DebounceDelay.razordocs/LumexUI.Docs.Client/Pages/Components/Textarea/PreviewCodes/Description.razordocs/LumexUI.Docs.Client/Pages/Components/Textarea/PreviewCodes/Disabled.razordocs/LumexUI.Docs.Client/Pages/Components/Textarea/PreviewCodes/ErrorMessage.razordocs/LumexUI.Docs.Client/Pages/Components/Textarea/PreviewCodes/LabelPlacements.razordocs/LumexUI.Docs.Client/Pages/Components/Textarea/PreviewCodes/Radius.razordocs/LumexUI.Docs.Client/Pages/Components/Textarea/PreviewCodes/ReadOnly.razordocs/LumexUI.Docs.Client/Pages/Components/Textarea/PreviewCodes/Required.razordocs/LumexUI.Docs.Client/Pages/Components/Textarea/PreviewCodes/Sizes.razordocs/LumexUI.Docs.Client/Pages/Components/Textarea/PreviewCodes/StartEndContent.razordocs/LumexUI.Docs.Client/Pages/Components/Textarea/PreviewCodes/TwoWayDataBinding.razordocs/LumexUI.Docs.Client/Pages/Components/Textarea/PreviewCodes/Usage.razordocs/LumexUI.Docs.Client/Pages/Components/Textarea/PreviewCodes/Variants.razordocs/LumexUI.Docs.Client/Pages/Components/Textarea/Textarea.razordocs/LumexUI.Docs/wwwroot/llms.txtsrc/LumexUI/Components/Bases/InputFieldSlots.cssrc/LumexUI/Components/Bases/LumexInputFieldBase.razor.cssrc/LumexUI/Components/Textarea/LumexTextarea.razorsrc/LumexUI/Components/Textarea/LumexTextarea.razor.cssrc/LumexUI/Styles/InputField.cssrc/LumexUI/wwwroot/js/components/textarea.jstests/LumexUI.Tests/Components/Textarea/TextareaTests.razor
Closes #117
Description
Adds
LumexTextarea— the multi-line counterpart ofLumexTextbox.What's been done?
LumexTextareawith new parameters:MinRows(default3),MaxRows(default8),DisableAutosize. Renders<textarea>with value bound via thevalueattribute (matches Microsoft'sInputTextArea— content-as-value desyncs on re-render).wwwroot/js/components/textarea.jsclamps height betweenminRowsandmaxRowsline-heights on each input event. Skipped entirely whenDisableAutosizeis set so the user owns sizing without JS interference.data-autosize-disabledattribute. Drives a conditionaltransition-[height]so dragging the native resize handle (when overridden in user CSS) isn't laggy.input is LumexTextareatype check rather than aMultilineflag on the base —LumexTextbox/LumexNumbox/LumexDateboxget no new members and no "multiline" concept on their surface.tests/LumexUI.Tests/Components/Textarea/TextareaTests.razor: rendering,<textarea>element + value-attribute binding, slot wiring, clear button, debounce delay & behavior, OnInput vs OnChange semantics,MinRowson therowsattribute, label-outside-input-wrapper placement.docs/LumexUI.Docs.Client/Pages/Components/Textarea/(16 examples) plus a newAutosizeexample demonstratingMinRows/MaxRows/DisableAutosize. Registered inNavigationStore(sidebar + API) andllms.txt.Checklist
Additional Notes
Summary by CodeRabbit
New Features
LumexTextareacomponent with autosizing, customizable rows, clear button, and validation support.Documentation