From dcfb78911aad3baf2194b112b2b78e306e6939f1 Mon Sep 17 00:00:00 2001 From: Vikram Reddy Date: Sun, 12 Jul 2026 11:37:25 +0530 Subject: [PATCH 01/13] Add FileInput component with demos and documentation Introduced a new FileInput component to the Blazor Bootstrap library, supporting file selection via click or drag-and-drop. - Added validation for file size, count, and extensions. - Enabled multiple file selection and hint text display. - Styled the component with CSS for drop zones and file lists. - Updated navigation menus and routing constants for the new demo. - Created demo pages showcasing various FileInput features. - Added documentation detailing parameters and events. --- .../Layout/DemosMainLayout.razor.cs | 1 + .../Components/Layout/DocsMainLayout.razor.cs | 1 + .../Components/Layout/MainLayout.razor.cs | 1 + .../FileInput/FileInputDocumentation.razor | 46 ++++ .../FileInput_Demo_01_Basic_Usage.razor | 1 + ...t_Demo_02_Multiple_Files_And_Removal.razor | 1 + .../FileInput_Demo_03_Colors_And_Sizes.razor | 4 + .../FileInput_Demo_04_Validation.razor | 9 + ..._05_Hint_And_Unrestricted_Extensions.razor | 8 + .../FileInput_Doc_01_Documentation.razor | 29 +++ .../Constants/DemoRouteConstants.cs | 2 + .../Components/Form/FileInput/FileInput.razor | 58 +++++ .../Form/FileInput/FileInput.razor.cs | 229 ++++++++++++++++++ .../Form/FileInput/FileInput.razor.css | 68 ++++++ blazorbootstrap/Enums/FileInputSize.cs | 11 + blazorbootstrap/Extensions/EnumExtensions.cs | 13 + 16 files changed, 482 insertions(+) create mode 100644 BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Form/FileInput/FileInputDocumentation.razor create mode 100644 BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Form/FileInput/FileInput_Demo_01_Basic_Usage.razor create mode 100644 BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Form/FileInput/FileInput_Demo_02_Multiple_Files_And_Removal.razor create mode 100644 BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Form/FileInput/FileInput_Demo_03_Colors_And_Sizes.razor create mode 100644 BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Form/FileInput/FileInput_Demo_04_Validation.razor create mode 100644 BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Form/FileInput/FileInput_Demo_05_Hint_And_Unrestricted_Extensions.razor create mode 100644 BlazorBootstrap.Demo.RCL/Components/Pages/Docs/Form/FileInput/FileInput_Doc_01_Documentation.razor create mode 100644 blazorbootstrap/Components/Form/FileInput/FileInput.razor create mode 100644 blazorbootstrap/Components/Form/FileInput/FileInput.razor.cs create mode 100644 blazorbootstrap/Components/Form/FileInput/FileInput.razor.css create mode 100644 blazorbootstrap/Enums/FileInputSize.cs diff --git a/BlazorBootstrap.Demo.RCL/Components/Layout/DemosMainLayout.razor.cs b/BlazorBootstrap.Demo.RCL/Components/Layout/DemosMainLayout.razor.cs index d1e6f1d92..3299bf266 100644 --- a/BlazorBootstrap.Demo.RCL/Components/Layout/DemosMainLayout.razor.cs +++ b/BlazorBootstrap.Demo.RCL/Components/Layout/DemosMainLayout.razor.cs @@ -22,6 +22,7 @@ internal override IEnumerable GetNavItems() new (){ Id = "402", Text = "Currency Input", Href = DemoRouteConstants.Demos_URL_CurrencyInput, IconName = IconName.CurrencyDollar, ParentId = "4" }, new (){ Id = "403", Text = "Date Input", Href = DemoRouteConstants.Demos_URL_DateInput, IconName = IconName.CalendarDate, ParentId = "4" }, new (){ Id = "404", Text = "Enum Input", Href = DemoRouteConstants.Demos_URL_EnumInput, IconName = IconName.MenuButtonWideFill, ParentId = "4" }, + new (){ Id = "414", Text = "File Input", Href = DemoRouteConstants.Demos_URL_FileInput, IconName = IconName.FileEarmarkArrowUp, ParentId = "4" }, new (){ Id = "405", Text = "Number Input", Href = DemoRouteConstants.Demos_URL_NumberInput, IconName = IconName.InputCursor, ParentId = "4" }, new (){ Id = "406", Text = "OTP Input", Href = DemoRouteConstants.Demos_URL_OTPInput, IconName = IconName.Asterisk, ParentId = "4" }, new (){ Id = "407", Text = "Password Input", Href = DemoRouteConstants.Demos_URL_PasswordInput, IconName = IconName.EyeSlashFill, ParentId = "4" }, diff --git a/BlazorBootstrap.Demo.RCL/Components/Layout/DocsMainLayout.razor.cs b/BlazorBootstrap.Demo.RCL/Components/Layout/DocsMainLayout.razor.cs index 9df45dc94..c6d578627 100644 --- a/BlazorBootstrap.Demo.RCL/Components/Layout/DocsMainLayout.razor.cs +++ b/BlazorBootstrap.Demo.RCL/Components/Layout/DocsMainLayout.razor.cs @@ -21,6 +21,7 @@ internal override IEnumerable GetNavItems() new (){ Id = "402", Text = "Currency Input", Href = DemoRouteConstants.Docs_URL_CurrencyInput, IconName = IconName.CurrencyDollar, ParentId = "4" }, new (){ Id = "403", Text = "Date Input", Href = DemoRouteConstants.Docs_URL_DateInput, IconName = IconName.CalendarDate, ParentId = "4" }, new (){ Id = "404", Text = "Enum Input", Href = DemoRouteConstants.Docs_URL_EnumInput, IconName = IconName.MenuButtonWideFill, ParentId = "4" }, + new (){ Id = "414", Text = "File Input", Href = DemoRouteConstants.Docs_URL_FileInput, IconName = IconName.FileEarmarkArrowUp, ParentId = "4" }, new (){ Id = "405", Text = "Number Input", Href = DemoRouteConstants.Docs_URL_NumberInput, IconName = IconName.InputCursor, ParentId = "4" }, new (){ Id = "406", Text = "OTP Input", Href = DemoRouteConstants.Docs_URL_OTPInput, IconName = IconName.Asterisk, ParentId = "4" }, new (){ Id = "407", Text = "Password Input", Href = DemoRouteConstants.Docs_URL_PasswordInput, IconName = IconName.EyeSlashFill, ParentId = "4" }, diff --git a/BlazorBootstrap.Demo.RCL/Components/Layout/MainLayout.razor.cs b/BlazorBootstrap.Demo.RCL/Components/Layout/MainLayout.razor.cs index 0a5391509..b82ad99a3 100644 --- a/BlazorBootstrap.Demo.RCL/Components/Layout/MainLayout.razor.cs +++ b/BlazorBootstrap.Demo.RCL/Components/Layout/MainLayout.razor.cs @@ -22,6 +22,7 @@ internal override IEnumerable GetNavItems() new (){ Id = "402", Text = "Currency Input", Href = DemoRouteConstants.Demos_URL_CurrencyInput, IconName = IconName.CurrencyDollar, ParentId = "4" }, new (){ Id = "403", Text = "Date Input", Href = DemoRouteConstants.Demos_URL_DateInput, IconName = IconName.CalendarDate, ParentId = "4" }, new (){ Id = "404", Text = "Enum Input", Href = DemoRouteConstants.Demos_URL_EnumInput, IconName = IconName.MenuButtonWideFill, ParentId = "4" }, + new (){ Id = "413", Text = "File Input", Href = DemoRouteConstants.Demos_URL_FileInput, IconName = IconName.FileEarmarkArrowUp, ParentId = "4" }, new (){ Id = "405", Text = "Number Input", Href = DemoRouteConstants.Demos_URL_NumberInput, IconName = IconName.InputCursor, ParentId = "4" }, new (){ Id = "406", Text = "Password Input", Href = DemoRouteConstants.Demos_URL_PasswordInput, IconName = IconName.EyeSlashFill, ParentId = "4" }, new (){ Id = "407", Text = "Radio Input", Href = DemoRouteConstants.Demos_URL_RadioInput, IconName = IconName.RecordCircle, ParentId = "4" }, diff --git a/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Form/FileInput/FileInputDocumentation.razor b/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Form/FileInput/FileInputDocumentation.razor new file mode 100644 index 000000000..addbf0391 --- /dev/null +++ b/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Form/FileInput/FileInputDocumentation.razor @@ -0,0 +1,46 @@ +@page "/file-input" + +@attribute [Route(pageUrl)] +@layout DemosMainLayout + + + + + +
+ +
+ +
+
Set Multiple to allow a selected file to be removed without clearing the others.
+ +
+ +
+
Use BackgroundColor and Size to match the surrounding Bootstrap UI.
+ +
+ +
+
FileInput validates sizes, counts, and extensions for file-dialog and drag-and-drop selection.
+ +
+ +
+
An empty AllowedFileTypes collection accepts every extension while other configured rules remain active.
+ +
+ +@code { + private const string pageUrl = DemoRouteConstants.Demos_URL_FileInput; + private const string pageTitle = "Blazor FileInput"; + private const string pageDescription = "The Blazor Bootstrap FileInput component supports click and drag-and-drop file selection with validation and removal."; + private const string metaTitle = "Blazor FileInput Component"; + private const string metaDescription = "The Blazor Bootstrap FileInput component supports click and drag-and-drop file selection with validation and removal."; + private const string imageUrl = DemoScreenshotSrcConstants.Demos_URL_Forms_Prefix; +} diff --git a/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Form/FileInput/FileInput_Demo_01_Basic_Usage.razor b/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Form/FileInput/FileInput_Demo_01_Basic_Usage.razor new file mode 100644 index 000000000..693821704 --- /dev/null +++ b/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Form/FileInput/FileInput_Demo_01_Basic_Usage.razor @@ -0,0 +1 @@ + diff --git a/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Form/FileInput/FileInput_Demo_02_Multiple_Files_And_Removal.razor b/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Form/FileInput/FileInput_Demo_02_Multiple_Files_And_Removal.razor new file mode 100644 index 000000000..cfc68955a --- /dev/null +++ b/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Form/FileInput/FileInput_Demo_02_Multiple_Files_And_Removal.razor @@ -0,0 +1 @@ + diff --git a/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Form/FileInput/FileInput_Demo_03_Colors_And_Sizes.razor b/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Form/FileInput/FileInput_Demo_03_Colors_And_Sizes.razor new file mode 100644 index 000000000..261a78ec4 --- /dev/null +++ b/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Form/FileInput/FileInput_Demo_03_Colors_And_Sizes.razor @@ -0,0 +1,4 @@ +
+ + +
diff --git a/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Form/FileInput/FileInput_Demo_04_Validation.razor b/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Form/FileInput/FileInput_Demo_04_Validation.razor new file mode 100644 index 000000000..6a63858f8 --- /dev/null +++ b/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Form/FileInput/FileInput_Demo_04_Validation.razor @@ -0,0 +1,9 @@ + + +@code { + private readonly string[] allowedFileTypes = [".pdf", "png"]; +} diff --git a/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Form/FileInput/FileInput_Demo_05_Hint_And_Unrestricted_Extensions.razor b/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Form/FileInput/FileInput_Demo_05_Hint_And_Unrestricted_Extensions.razor new file mode 100644 index 000000000..32a834d6e --- /dev/null +++ b/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Form/FileInput/FileInput_Demo_05_Hint_And_Unrestricted_Extensions.razor @@ -0,0 +1,8 @@ + + +@code { + private readonly string[] allowedFileTypes = []; +} diff --git a/BlazorBootstrap.Demo.RCL/Components/Pages/Docs/Form/FileInput/FileInput_Doc_01_Documentation.razor b/BlazorBootstrap.Demo.RCL/Components/Pages/Docs/Form/FileInput/FileInput_Doc_01_Documentation.razor new file mode 100644 index 000000000..d272d3d6c --- /dev/null +++ b/BlazorBootstrap.Demo.RCL/Components/Pages/Docs/Form/FileInput/FileInput_Doc_01_Documentation.razor @@ -0,0 +1,29 @@ +@attribute [Route(pageUrl)] +@layout DocsMainLayout + + + + + +
+ +
+ +
+ +
+ +@code { + private const string componentName = nameof(FileInput); + private const string pageUrl = DemoRouteConstants.Docs_URL_FileInput; + private const string pageTitle = componentName; + private const string pageDescription = $"This documentation provides a comprehensive reference for the {componentName} component, guiding you through its configuration options."; + private const string metaTitle = $"Blazor {componentName} Component"; + private const string metaDescription = $"This documentation provides a comprehensive reference for the {componentName} component, guiding you through its configuration options."; + private const string imageUrl = DemoScreenshotSrcConstants.Demos_URL_Forms_Prefix; +} diff --git a/BlazorBootstrap.Demo.RCL/Constants/DemoRouteConstants.cs b/BlazorBootstrap.Demo.RCL/Constants/DemoRouteConstants.cs index 3dccaee09..735f810fa 100644 --- a/BlazorBootstrap.Demo.RCL/Constants/DemoRouteConstants.cs +++ b/BlazorBootstrap.Demo.RCL/Constants/DemoRouteConstants.cs @@ -31,6 +31,7 @@ public static class DemoRouteConstants public const string Demos_URL_CurrencyInput = Demos_URL_Forms_Prefix + "/currency-input"; public const string Demos_URL_DateInput = Demos_URL_Forms_Prefix + "/date-input"; public const string Demos_URL_EnumInput = Demos_URL_Forms_Prefix + "/enum-input"; + public const string Demos_URL_FileInput = Demos_URL_Forms_Prefix + "/file-input"; public const string Demos_URL_NumberInput = Demos_URL_Forms_Prefix + "/number-input"; public const string Demos_URL_OTPInput = Demos_URL_Forms_Prefix + "/otp-input"; public const string Demos_URL_PasswordInput = Demos_URL_Forms_Prefix + "/password-input"; @@ -145,6 +146,7 @@ public static class DemoRouteConstants public const string Docs_URL_CurrencyInput = Docs_URL_Forms_Prefix + "/currency-input"; public const string Docs_URL_DateInput = Docs_URL_Forms_Prefix + "/date-input"; public const string Docs_URL_EnumInput = Docs_URL_Forms_Prefix + "/enum-input"; + public const string Docs_URL_FileInput = Docs_URL_Forms_Prefix + "/file-input"; public const string Docs_URL_NumberInput = Docs_URL_Forms_Prefix + "/number-input"; public const string Docs_URL_OTPInput = Docs_URL_Forms_Prefix + "/otp-input"; public const string Docs_URL_PasswordInput = Docs_URL_Forms_Prefix + "/password-input"; diff --git a/blazorbootstrap/Components/Form/FileInput/FileInput.razor b/blazorbootstrap/Components/Form/FileInput/FileInput.razor new file mode 100644 index 000000000..2eec95e44 --- /dev/null +++ b/blazorbootstrap/Components/Form/FileInput/FileInput.razor @@ -0,0 +1,58 @@ +@namespace BlazorBootstrap +@inherits BlazorBootstrapComponentBase + +
+
+ + +
+ + @if (files.Any()) + { +
    + @foreach (var file in files) + { +
  • + @file.Name + +
  • + } +
+ } + + @if (validationErrors.Any()) + { + + } + + @if (!string.IsNullOrWhiteSpace(HintText)) + { +
@HintText
+ } +
diff --git a/blazorbootstrap/Components/Form/FileInput/FileInput.razor.cs b/blazorbootstrap/Components/Form/FileInput/FileInput.razor.cs new file mode 100644 index 000000000..0767215e2 --- /dev/null +++ b/blazorbootstrap/Components/Form/FileInput/FileInput.razor.cs @@ -0,0 +1,229 @@ +namespace BlazorBootstrap; + +public partial class FileInput : BlazorBootstrapComponentBase +{ + #region Fields and Constants + + private int dragDepth; + + private readonly List files = new(); + + private readonly List validationErrors = new(); + + #endregion + + #region Methods + + #region Private Methods + + private string GetFileValidationError(IBrowserFile file) + { + if (MaxFileSize.HasValue && file.Size > MaxFileSize.Value) + return $"{file.Name}: the file exceeds the maximum allowed size."; + + var allowedFileTypes = NormalizedAllowedFileTypes; + if (allowedFileTypes.Count > 0 && !allowedFileTypes.Contains(NormalizeExtension(Path.GetExtension(file.Name)))) + return $"{file.Name}: the file extension is not allowed."; + + return string.Empty; + } + + private string NormalizeExtension(string? extension) => extension?.Trim().TrimStart('.').ToUpperInvariant() ?? string.Empty; + + private void OnDragEnter(DragEventArgs _) => dragDepth++; + + private void OnDragLeave(DragEventArgs _) + { + if (dragDepth > 0) + dragDepth--; + } + + private async Task OnDrop(DragEventArgs _) + { + dragDepth = 0; + await Task.CompletedTask; + } + + private async Task OnInputFileChangedAsync(InputFileChangeEventArgs e) + { + validationErrors.Clear(); + + var selectedFiles = Multiple ? e.GetMultipleFiles(int.MaxValue) : new List { e.File }; + var hasChanged = false; + + if (!Multiple) + { + files.Clear(); + hasChanged = true; + + if (e.FileCount > 1) + validationErrors.Add("Only one file can be selected."); + } + + foreach (var selectedFile in selectedFiles) + { + if (MaxFileCount.HasValue && files.Count >= MaxFileCount.Value) + { + validationErrors.Add($"{selectedFile.Name}: the maximum number of files has been reached."); + continue; + } + + var validationError = GetFileValidationError(selectedFile); + if (!string.IsNullOrEmpty(validationError)) + { + validationErrors.Add(validationError); + continue; + } + + files.Add(selectedFile); + hasChanged = true; + } + + if (hasChanged) + await FilesChanged.InvokeAsync(files.AsReadOnly()); + } + + private async Task RemoveFileAsync(IBrowserFile file) + { + if (!files.Remove(file)) + return; + + await FilesChanged.InvokeAsync(files.AsReadOnly()); + } + + #endregion + + #endregion + + #region Properties, Indexers + + protected override string? ClassNames => + BuildClassNames(Class, + ("bb-file-input", true), + (BackgroundColor.ToBackgroundAndTextClass(), BackgroundColor != BackgroundColor.None), + (Size.ToFileInputSizeClass(), Size != FileInputSize.Default)); + + private string Accept => string.Join(',', NormalizedAllowedFileTypes.Select(fileType => $".{fileType.ToLowerInvariant()}")); + + private string DropZoneClassNames => BuildClassNames( + ("bb-file-input__drop-zone", true), + ("bb-file-input__drop-zone--drag-over", dragDepth > 0)); + + private HashSet NormalizedAllowedFileTypes => + AllowedFileTypes? + .Select(NormalizeExtension) + .Where(fileType => !string.IsNullOrWhiteSpace(fileType)) + .ToHashSet(StringComparer.OrdinalIgnoreCase) + ?? new HashSet(StringComparer.OrdinalIgnoreCase); + + /// + /// Gets or sets the allowed file extensions. + /// + /// An empty collection allows all extensions. Values may include a leading dot and are matched without regard to case. + /// + /// + [AddedVersion("4.0.0")] + [DefaultValue(null)] + [Description("Gets or sets the allowed file extensions.")] + [Parameter] + public IEnumerable? AllowedFileTypes { get; set; } + + /// + /// Gets or sets the accessible label for the file selection area. + /// + /// Default value is Select files. + /// + /// + [AddedVersion("4.0.0")] + [DefaultValue("Select files")] + [Description("Gets or sets the accessible label for the file selection area.")] + [Parameter] + public string AriaLabel { get; set; } = "Select files"; + + /// + /// Gets or sets the contextual background color. + /// + /// Default value is . + /// + /// + [AddedVersion("4.0.0")] + [DefaultValue(BackgroundColor.None)] + [Description("Gets or sets the contextual background color.")] + [Parameter] + public BackgroundColor BackgroundColor { get; set; } = BackgroundColor.None; + + /// + /// This event fires when the selected files change. + /// + [AddedVersion("4.0.0")] + [Description("This event fires when the selected files change.")] + [Parameter] + public EventCallback> FilesChanged { get; set; } + + /// + /// Gets or sets the hint text displayed below the component. + /// + /// Default value is . + /// + /// + [AddedVersion("4.0.0")] + [DefaultValue(null)] + [Description("Gets or sets the hint text displayed below the component.")] + [Parameter] + public string? HintText { get; set; } + + /// + /// Gets or sets the maximum number of selected files. + /// + /// Default value is . + /// + /// + [AddedVersion("4.0.0")] + [DefaultValue(null)] + [Description("Gets or sets the maximum number of selected files.")] + [Parameter] + public int? MaxFileCount { get; set; } + + /// + /// Gets or sets the maximum allowed file size in bytes. + /// + /// Default value is . + /// + /// + [AddedVersion("4.0.0")] + [DefaultValue(null)] + [Description("Gets or sets the maximum allowed file size in bytes.")] + [Parameter] + public long? MaxFileSize { get; set; } + + /// + /// Gets or sets whether multiple files can be selected. + /// + /// Default value is false. + /// + /// + [AddedVersion("4.0.0")] + [DefaultValue(false)] + [Description("Gets or sets whether multiple files can be selected.")] + [Parameter] + public bool Multiple { get; set; } + + /// + /// Gets the currently selected files. + /// + public IReadOnlyList SelectedFiles => files.AsReadOnly(); + + /// + /// Gets or sets the component size. + /// + /// Default value is . + /// + /// + [AddedVersion("4.0.0")] + [DefaultValue(FileInputSize.Default)] + [Description("Gets or sets the component size.")] + [Parameter] + public FileInputSize Size { get; set; } = FileInputSize.Default; + + #endregion +} diff --git a/blazorbootstrap/Components/Form/FileInput/FileInput.razor.css b/blazorbootstrap/Components/Form/FileInput/FileInput.razor.css new file mode 100644 index 000000000..ab59029b8 --- /dev/null +++ b/blazorbootstrap/Components/Form/FileInput/FileInput.razor.css @@ -0,0 +1,68 @@ +.bb-file-input { + --bb-file-input-border-color: var(--bs-border-color); + --bb-file-input-border-radius: var(--bs-border-radius); + --bb-file-input-drag-border-color: var(--bs-primary); + --bb-file-input-drag-background-color: rgba(var(--bs-primary-rgb), .08); + --bb-file-input-error-color: var(--bs-danger-text-emphasis); +} + +.bb-file-input__drop-zone { + border: 2px dashed var(--bb-file-input-border-color); + border-radius: var(--bb-file-input-border-radius); + min-height: 8rem; + padding: 1.5rem; + position: relative; + transition: background-color .15s ease-in-out, border-color .15s ease-in-out; +} + +.bb-file-input__drop-zone--drag-over { + background-color: var(--bb-file-input-drag-background-color); + border-color: var(--bb-file-input-drag-border-color); +} + +.bb-file-input__native { + cursor: pointer; + height: 100%; + inset: 0; + opacity: 0; + position: absolute; + width: 100%; + z-index: 1; +} + +.bb-file-input__content { + align-items: center; + display: flex; + flex-direction: column; + gap: .25rem; + justify-content: center; + min-height: 5rem; + text-align: center; +} + +.bb-file-input__title { + font-weight: 600; +} + +.bb-file-input__description { + font-size: .875em; +} + +.bb-file-input__files { + list-style: none; + margin: .75rem 0 0; + padding: 0; +} + +.bb-file-input__file { + align-items: center; + display: flex; + gap: .75rem; + justify-content: space-between; + padding: .25rem 0; +} + +.bb-file-input__errors { + color: var(--bb-file-input-error-color); + margin-top: .75rem; +} diff --git a/blazorbootstrap/Enums/FileInputSize.cs b/blazorbootstrap/Enums/FileInputSize.cs new file mode 100644 index 000000000..b7cfb5090 --- /dev/null +++ b/blazorbootstrap/Enums/FileInputSize.cs @@ -0,0 +1,11 @@ +namespace BlazorBootstrap; + +/// +/// Defines the supported sizes for a . +/// +public enum FileInputSize +{ + Default, + Large, + Small +} diff --git a/blazorbootstrap/Extensions/EnumExtensions.cs b/blazorbootstrap/Extensions/EnumExtensions.cs index d36cfec3a..b65115569 100644 --- a/blazorbootstrap/Extensions/EnumExtensions.cs +++ b/blazorbootstrap/Extensions/EnumExtensions.cs @@ -310,6 +310,19 @@ public static string ToEnumIputSizeClass(this EnumInputSize enumInputSize) => _ => "" }; + /// + /// Converts a value to its Bootstrap CSS class. + /// + [AddedVersion("4.0.0")] + [Description("Converts a FileInputSize value to its Bootstrap CSS class.")] + public static string ToFileInputSizeClass(this FileInputSize fileInputSize) => + fileInputSize switch + { + FileInputSize.Large => "form-control-lg", + FileInputSize.Small => "form-control-sm", + _ => "" + }; + public static string ToIconColorClass(this IconColor iconColor) => iconColor switch { From ac9b314669ac9df2e9646f611c62c4e3d0ddc0ed Mon Sep 17 00:00:00 2001 From: Vikram Reddy Date: Sun, 12 Jul 2026 11:55:35 +0530 Subject: [PATCH 02/13] Add documentation for Blazor FileInput component Added `file-input.mdx` to document the Blazor Bootstrap `FileInput` component. Included metadata, parameters, events, and accessibility details. Provided usage examples for drag-and-drop, validation, multiple file selection, and styling. Linked to relevant demos for better understanding. --- docs/docs/04-forms/file-input.mdx | 114 ++++++++++++++++++++++++++++++ 1 file changed, 114 insertions(+) create mode 100644 docs/docs/04-forms/file-input.mdx diff --git a/docs/docs/04-forms/file-input.mdx b/docs/docs/04-forms/file-input.mdx new file mode 100644 index 000000000..73376e5c9 --- /dev/null +++ b/docs/docs/04-forms/file-input.mdx @@ -0,0 +1,114 @@ +--- +title: Blazor File Input Component +description: The Blazor Bootstrap FileInput component supports click and drag-and-drop file selection, validation, feedback, and removal. +sidebar_label: File Input +sidebar_position: 6 +--- + +import CarbonAd from '/carbon-ad.mdx' + +# Blazor FileInput + +The Blazor Bootstrap `FileInput` component provides a drop area that users can click or drag files onto. It supports single or multiple files, per-file removal, validation, contextual colors, sizes, and accessible feedback. + + + +## Parameters + +| Name | Type | Default | Description | Added Version | +|:--|:--|:--|:--|:--| +| AllowedFileTypes | IEnumerable<string> | null | Allowed file extensions. Values can use `.pdf` or `pdf` and match case-insensitively. An empty collection allows every extension. | 3.5.0 | +| AriaLabel | string | Select files | Accessible label for the selection area. | 3.5.0 | +| BackgroundColor | BackgroundColor | BackgroundColor.None | Bootstrap contextual background color. | 3.5.0 | +| HintText | string | null | Help text displayed below the component. | 3.5.0 | +| MaxFileCount | int | null | Maximum number of selected files. | 3.5.0 | +| MaxFileSize | long | null | Maximum file size in bytes. | 3.5.0 | +| Multiple | bool | false | Allows multiple files to be selected. | 3.5.0 | +| Size | FileInputSize | FileInputSize.Default | Component size. | 3.5.0 | + +## Events + +| Name | Description | Added Version | +|:--|:--|:--| +| FilesChanged | Fires when the accepted selected-file list changes. | 3.5.0 | + +## Examples + +### Basic usage and drag-and-drop + +Click the drop area to open the native file picker, or drag a file onto it. The same validation rules apply to either path. + +```cshtml showLineNumbers + +``` + +[See demo here](https://demos.blazorbootstrap.com/form/file-input#basic-usage) + +### Multiple files and removal + +Set `Multiple` to show every accepted file. Each file has its own keyboard-accessible remove button, so removing one does not clear the others. + +```cshtml showLineNumbers + +``` + +[See demo here](https://demos.blazorbootstrap.com/form/file-input#multiple-files-and-removal) + +### Validation + +`AllowedFileTypes` accepts extensions only. Values with or without a leading dot are equivalent, and matching is case-insensitive. Files that fail an extension, size, or count rule show a file-specific error; valid files from a mixed selection remain selected. + +```cshtml showLineNumbers + + +@code { + private readonly string[] allowedExtensions = [".pdf", "png"]; +} +``` + +[See demo here](https://demos.blazorbootstrap.com/form/file-input#validation) + +### Unrestricted extensions + +Use an empty `AllowedFileTypes` collection when every extension is permitted. Other rules, such as `MaxFileSize` and `MaxFileCount`, still apply. + +```cshtml showLineNumbers + + +@code { + private readonly string[] allowedExtensions = []; +} +``` + +[See demo here](https://demos.blazorbootstrap.com/form/file-input#hint-text-and-unrestricted-extensions) + +### Colors, sizes, and styling + +Use `BackgroundColor` and `Size` to match the surrounding Bootstrap interface. Consumer CSS can override the component variables for borders, drag feedback, and validation colors. + +```cshtml showLineNumbers + +``` + +```css +.bb-file-input { + --bb-file-input-drag-border-color: var(--bs-success); + --bb-file-input-drag-background-color: rgba(var(--bs-success-rgb), .1); +} +``` + +[See demo here](https://demos.blazorbootstrap.com/form/file-input#colors-and-sizes) + +## Accessibility + +Provide a clear `AriaLabel` when the default label does not describe the expected files. FileInput announces validation errors through an alert region, and each selected file has an accessible, keyboard-operable remove button. From 750fd96380cd118d8d03f9b6e798f3272e40abba Mon Sep 17 00:00:00 2001 From: Vikram Reddy Date: Sun, 12 Jul 2026 12:59:54 +0530 Subject: [PATCH 03/13] Refactor FileInput and add DragAndDropFileInput Refactored `FileInput` to separate drag-and-drop functionality into a new `DragAndDropFileInput` component. Introduced `FileInputBase` to centralize shared logic and properties. Simplified `FileInput.razor` to focus on basic file input handling. Added drag-and-drop-specific logic and styles to `DragAndDropFileInput.razor` and `DragAndDropFileInput.razor.css`. Enhanced accessibility with ARIA attributes and improved maintainability by separating concerns between components. --- .../DragAndDropFileInput.razor | 50 ++++ .../DragAndDropFileInput.razor.cs | 48 ++++ .../DragAndDropFileInput.razor.css | 62 +++++ .../Components/Form/FileInput/FileInput.razor | 38 +-- .../Form/FileInput/FileInput.razor.cs | 220 +----------------- .../Form/FileInput/FileInput.razor.css | 52 +---- .../Form/FileInput/FileInputBase.cs | 182 +++++++++++++++ 7 files changed, 355 insertions(+), 297 deletions(-) create mode 100644 blazorbootstrap/Components/Form/DragAndDropFileInput/DragAndDropFileInput.razor create mode 100644 blazorbootstrap/Components/Form/DragAndDropFileInput/DragAndDropFileInput.razor.cs create mode 100644 blazorbootstrap/Components/Form/DragAndDropFileInput/DragAndDropFileInput.razor.css create mode 100644 blazorbootstrap/Components/Form/FileInput/FileInputBase.cs diff --git a/blazorbootstrap/Components/Form/DragAndDropFileInput/DragAndDropFileInput.razor b/blazorbootstrap/Components/Form/DragAndDropFileInput/DragAndDropFileInput.razor new file mode 100644 index 000000000..349160215 --- /dev/null +++ b/blazorbootstrap/Components/Form/DragAndDropFileInput/DragAndDropFileInput.razor @@ -0,0 +1,50 @@ +@namespace BlazorBootstrap +@inherits FileInputBase + +
+
+ + +
+ + @if (files.Any()) + { +
    + @foreach (var file in files) + { +
  • + @file.Name + +
  • + } +
+ } + + @if (validationErrors.Any()) + { + + } +
diff --git a/blazorbootstrap/Components/Form/DragAndDropFileInput/DragAndDropFileInput.razor.cs b/blazorbootstrap/Components/Form/DragAndDropFileInput/DragAndDropFileInput.razor.cs new file mode 100644 index 000000000..72ba7730f --- /dev/null +++ b/blazorbootstrap/Components/Form/DragAndDropFileInput/DragAndDropFileInput.razor.cs @@ -0,0 +1,48 @@ +using Microsoft.AspNetCore.Components.Web; + +namespace BlazorBootstrap; + +public partial class DragAndDropFileInput : FileInputBase +{ + #region Fields and Constants + + private int dragDepth; + + #endregion + + #region Methods + + #region Protected Methods + + protected void OnDragEnter(DragEventArgs _) + { + dragDepth++; + StateHasChanged(); + } + + protected void OnDragLeave(DragEventArgs _) + { + dragDepth = Math.Max(0, dragDepth - 1); + StateHasChanged(); + } + + protected void OnDrop(DragEventArgs _) + { + dragDepth = 0; + StateHasChanged(); + } + + #endregion + + #endregion + + #region Properties, Indexers + + protected override string? ClassNames => + BuildClassNames(Class, + ("bb-drag-and-drop-file-input", true), + (BackgroundColor.ToBackgroundClass(), BackgroundColor != BackgroundColor.None), + ("bb-drag-and-drop-file-input--dragging", dragDepth > 0)); + + #endregion +} diff --git a/blazorbootstrap/Components/Form/DragAndDropFileInput/DragAndDropFileInput.razor.css b/blazorbootstrap/Components/Form/DragAndDropFileInput/DragAndDropFileInput.razor.css new file mode 100644 index 000000000..c3bbae342 --- /dev/null +++ b/blazorbootstrap/Components/Form/DragAndDropFileInput/DragAndDropFileInput.razor.css @@ -0,0 +1,62 @@ +.bb-drag-and-drop-file-input { + position: relative; + min-height: 9rem; + overflow: hidden; + border: 2px dashed var(--bs-border-color); + border-radius: var(--bs-border-radius); +} + +.bb-drag-and-drop-file-input--dragging { + border-color: var(--bs-primary); + background-color: var(--bs-primary-bg-subtle); +} + +.bb-drag-and-drop-file-input__native-input { + position: absolute; + inset: 0; + z-index: 1; + width: 100%; + height: 100%; + cursor: pointer; + opacity: 0; +} + +.bb-drag-and-drop-file-input__content { + display: flex; + min-height: 9rem; + flex-direction: column; + align-items: center; + justify-content: center; + gap: .25rem; + padding: 1rem; + text-align: center; + pointer-events: none; +} + +.bb-drag-and-drop-file-input__title { + font-weight: 500; +} + +.bb-drag-and-drop-file-input__hint { + color: var(--bs-secondary-color); + font-size: .875em; +} + +.bb-file-input__files { + list-style: none; + margin: .75rem 0 0; + padding: 0; +} + +.bb-file-input__file { + align-items: center; + display: flex; + gap: .75rem; + justify-content: space-between; + padding: .25rem 0; +} + +.bb-file-input__errors { + color: var(--bs-danger-text-emphasis); + margin-top: .75rem; +} diff --git a/blazorbootstrap/Components/Form/FileInput/FileInput.razor b/blazorbootstrap/Components/Form/FileInput/FileInput.razor index 2eec95e44..9c8cb9e07 100644 --- a/blazorbootstrap/Components/Form/FileInput/FileInput.razor +++ b/blazorbootstrap/Components/Form/FileInput/FileInput.razor @@ -1,29 +1,14 @@ @namespace BlazorBootstrap -@inherits BlazorBootstrapComponentBase +@inherits FileInputBase -
-
- - -
+
+ @if (files.Any()) { @@ -32,10 +17,7 @@ {
  • @file.Name - +
  • } diff --git a/blazorbootstrap/Components/Form/FileInput/FileInput.razor.cs b/blazorbootstrap/Components/Form/FileInput/FileInput.razor.cs index 0767215e2..bd1c2193c 100644 --- a/blazorbootstrap/Components/Form/FileInput/FileInput.razor.cs +++ b/blazorbootstrap/Components/Form/FileInput/FileInput.razor.cs @@ -1,229 +1,13 @@ namespace BlazorBootstrap; -public partial class FileInput : BlazorBootstrapComponentBase +public partial class FileInput : FileInputBase { - #region Fields and Constants - - private int dragDepth; - - private readonly List files = new(); - - private readonly List validationErrors = new(); - - #endregion - - #region Methods - - #region Private Methods - - private string GetFileValidationError(IBrowserFile file) - { - if (MaxFileSize.HasValue && file.Size > MaxFileSize.Value) - return $"{file.Name}: the file exceeds the maximum allowed size."; - - var allowedFileTypes = NormalizedAllowedFileTypes; - if (allowedFileTypes.Count > 0 && !allowedFileTypes.Contains(NormalizeExtension(Path.GetExtension(file.Name)))) - return $"{file.Name}: the file extension is not allowed."; - - return string.Empty; - } - - private string NormalizeExtension(string? extension) => extension?.Trim().TrimStart('.').ToUpperInvariant() ?? string.Empty; - - private void OnDragEnter(DragEventArgs _) => dragDepth++; - - private void OnDragLeave(DragEventArgs _) - { - if (dragDepth > 0) - dragDepth--; - } - - private async Task OnDrop(DragEventArgs _) - { - dragDepth = 0; - await Task.CompletedTask; - } - - private async Task OnInputFileChangedAsync(InputFileChangeEventArgs e) - { - validationErrors.Clear(); - - var selectedFiles = Multiple ? e.GetMultipleFiles(int.MaxValue) : new List { e.File }; - var hasChanged = false; - - if (!Multiple) - { - files.Clear(); - hasChanged = true; - - if (e.FileCount > 1) - validationErrors.Add("Only one file can be selected."); - } - - foreach (var selectedFile in selectedFiles) - { - if (MaxFileCount.HasValue && files.Count >= MaxFileCount.Value) - { - validationErrors.Add($"{selectedFile.Name}: the maximum number of files has been reached."); - continue; - } - - var validationError = GetFileValidationError(selectedFile); - if (!string.IsNullOrEmpty(validationError)) - { - validationErrors.Add(validationError); - continue; - } - - files.Add(selectedFile); - hasChanged = true; - } - - if (hasChanged) - await FilesChanged.InvokeAsync(files.AsReadOnly()); - } - - private async Task RemoveFileAsync(IBrowserFile file) - { - if (!files.Remove(file)) - return; - - await FilesChanged.InvokeAsync(files.AsReadOnly()); - } - - #endregion - - #endregion - #region Properties, Indexers protected override string? ClassNames => BuildClassNames(Class, - ("bb-file-input", true), - (BackgroundColor.ToBackgroundAndTextClass(), BackgroundColor != BackgroundColor.None), + (BootstrapClass.FormControl, true), (Size.ToFileInputSizeClass(), Size != FileInputSize.Default)); - private string Accept => string.Join(',', NormalizedAllowedFileTypes.Select(fileType => $".{fileType.ToLowerInvariant()}")); - - private string DropZoneClassNames => BuildClassNames( - ("bb-file-input__drop-zone", true), - ("bb-file-input__drop-zone--drag-over", dragDepth > 0)); - - private HashSet NormalizedAllowedFileTypes => - AllowedFileTypes? - .Select(NormalizeExtension) - .Where(fileType => !string.IsNullOrWhiteSpace(fileType)) - .ToHashSet(StringComparer.OrdinalIgnoreCase) - ?? new HashSet(StringComparer.OrdinalIgnoreCase); - - /// - /// Gets or sets the allowed file extensions. - /// - /// An empty collection allows all extensions. Values may include a leading dot and are matched without regard to case. - /// - /// - [AddedVersion("4.0.0")] - [DefaultValue(null)] - [Description("Gets or sets the allowed file extensions.")] - [Parameter] - public IEnumerable? AllowedFileTypes { get; set; } - - /// - /// Gets or sets the accessible label for the file selection area. - /// - /// Default value is Select files. - /// - /// - [AddedVersion("4.0.0")] - [DefaultValue("Select files")] - [Description("Gets or sets the accessible label for the file selection area.")] - [Parameter] - public string AriaLabel { get; set; } = "Select files"; - - /// - /// Gets or sets the contextual background color. - /// - /// Default value is . - /// - /// - [AddedVersion("4.0.0")] - [DefaultValue(BackgroundColor.None)] - [Description("Gets or sets the contextual background color.")] - [Parameter] - public BackgroundColor BackgroundColor { get; set; } = BackgroundColor.None; - - /// - /// This event fires when the selected files change. - /// - [AddedVersion("4.0.0")] - [Description("This event fires when the selected files change.")] - [Parameter] - public EventCallback> FilesChanged { get; set; } - - /// - /// Gets or sets the hint text displayed below the component. - /// - /// Default value is . - /// - /// - [AddedVersion("4.0.0")] - [DefaultValue(null)] - [Description("Gets or sets the hint text displayed below the component.")] - [Parameter] - public string? HintText { get; set; } - - /// - /// Gets or sets the maximum number of selected files. - /// - /// Default value is . - /// - /// - [AddedVersion("4.0.0")] - [DefaultValue(null)] - [Description("Gets or sets the maximum number of selected files.")] - [Parameter] - public int? MaxFileCount { get; set; } - - /// - /// Gets or sets the maximum allowed file size in bytes. - /// - /// Default value is . - /// - /// - [AddedVersion("4.0.0")] - [DefaultValue(null)] - [Description("Gets or sets the maximum allowed file size in bytes.")] - [Parameter] - public long? MaxFileSize { get; set; } - - /// - /// Gets or sets whether multiple files can be selected. - /// - /// Default value is false. - /// - /// - [AddedVersion("4.0.0")] - [DefaultValue(false)] - [Description("Gets or sets whether multiple files can be selected.")] - [Parameter] - public bool Multiple { get; set; } - - /// - /// Gets the currently selected files. - /// - public IReadOnlyList SelectedFiles => files.AsReadOnly(); - - /// - /// Gets or sets the component size. - /// - /// Default value is . - /// - /// - [AddedVersion("4.0.0")] - [DefaultValue(FileInputSize.Default)] - [Description("Gets or sets the component size.")] - [Parameter] - public FileInputSize Size { get; set; } = FileInputSize.Default; - #endregion } diff --git a/blazorbootstrap/Components/Form/FileInput/FileInput.razor.css b/blazorbootstrap/Components/Form/FileInput/FileInput.razor.css index ab59029b8..55c4e2789 100644 --- a/blazorbootstrap/Components/Form/FileInput/FileInput.razor.css +++ b/blazorbootstrap/Components/Form/FileInput/FileInput.razor.css @@ -1,53 +1,3 @@ -.bb-file-input { - --bb-file-input-border-color: var(--bs-border-color); - --bb-file-input-border-radius: var(--bs-border-radius); - --bb-file-input-drag-border-color: var(--bs-primary); - --bb-file-input-drag-background-color: rgba(var(--bs-primary-rgb), .08); - --bb-file-input-error-color: var(--bs-danger-text-emphasis); -} - -.bb-file-input__drop-zone { - border: 2px dashed var(--bb-file-input-border-color); - border-radius: var(--bb-file-input-border-radius); - min-height: 8rem; - padding: 1.5rem; - position: relative; - transition: background-color .15s ease-in-out, border-color .15s ease-in-out; -} - -.bb-file-input__drop-zone--drag-over { - background-color: var(--bb-file-input-drag-background-color); - border-color: var(--bb-file-input-drag-border-color); -} - -.bb-file-input__native { - cursor: pointer; - height: 100%; - inset: 0; - opacity: 0; - position: absolute; - width: 100%; - z-index: 1; -} - -.bb-file-input__content { - align-items: center; - display: flex; - flex-direction: column; - gap: .25rem; - justify-content: center; - min-height: 5rem; - text-align: center; -} - -.bb-file-input__title { - font-weight: 600; -} - -.bb-file-input__description { - font-size: .875em; -} - .bb-file-input__files { list-style: none; margin: .75rem 0 0; @@ -63,6 +13,6 @@ } .bb-file-input__errors { - color: var(--bb-file-input-error-color); + color: var(--bs-danger-text-emphasis); margin-top: .75rem; } diff --git a/blazorbootstrap/Components/Form/FileInput/FileInputBase.cs b/blazorbootstrap/Components/Form/FileInput/FileInputBase.cs new file mode 100644 index 000000000..c14867d89 --- /dev/null +++ b/blazorbootstrap/Components/Form/FileInput/FileInputBase.cs @@ -0,0 +1,182 @@ +using System.ComponentModel; +using Microsoft.AspNetCore.Components.Forms; + +namespace BlazorBootstrap; + +public abstract class FileInputBase : BlazorBootstrapComponentBase +{ + #region Fields and Constants + + protected readonly List files = new(); + + protected readonly List validationErrors = new(); + + #endregion + + #region Methods + + #region Protected Methods + + protected string GetFileValidationError(IBrowserFile file) + { + if (MaxFileSize.HasValue && file.Size > MaxFileSize.Value) + return $"{file.Name}: the file exceeds the maximum allowed size."; + + if (NormalizedAllowedFileTypes.Count > 0 && !NormalizedAllowedFileTypes.Contains(NormalizeExtension(Path.GetExtension(file.Name)))) + return $"{file.Name}: the file extension is not allowed."; + + return string.Empty; + } + + protected async Task OnInputFileChangedAsync(InputFileChangeEventArgs e) + { + validationErrors.Clear(); + + var selectedFiles = Multiple ? e.GetMultipleFiles(int.MaxValue) : new List { e.File }; + var hasChanged = false; + + if (!Multiple) + { + files.Clear(); + hasChanged = true; + + if (e.FileCount > 1) + validationErrors.Add("Only one file can be selected."); + } + + foreach (var selectedFile in selectedFiles) + { + if (MaxFileCount.HasValue && files.Count >= MaxFileCount.Value) + { + validationErrors.Add($"{selectedFile.Name}: the maximum number of files has been reached."); + continue; + } + + var validationError = GetFileValidationError(selectedFile); + if (!string.IsNullOrEmpty(validationError)) + { + validationErrors.Add(validationError); + continue; + } + + files.Add(selectedFile); + hasChanged = true; + } + + if (hasChanged) + await FilesChanged.InvokeAsync(files.AsReadOnly()); + } + + protected string NormalizeExtension(string? extension) => extension?.Trim().TrimStart('.').ToUpperInvariant() ?? string.Empty; + + protected async Task RemoveFileAsync(IBrowserFile file) + { + if (!files.Remove(file)) + return; + + await FilesChanged.InvokeAsync(files.AsReadOnly()); + } + + #endregion + + #endregion + + #region Properties, Indexers + + protected string Accept => string.Join(',', NormalizedAllowedFileTypes.Select(fileType => $".{fileType.ToLowerInvariant()}")); + + protected HashSet NormalizedAllowedFileTypes => + AllowedFileTypes? + .Select(NormalizeExtension) + .Where(fileType => !string.IsNullOrWhiteSpace(fileType)) + .ToHashSet(StringComparer.OrdinalIgnoreCase) + ?? new HashSet(StringComparer.OrdinalIgnoreCase); + + /// + /// Gets or sets the allowed file extensions. + /// An empty collection allows all extensions. Values may include a leading dot and are matched without regard to case. + /// + [AddedVersion("4.0.0")] + [DefaultValue(null)] + [Description("Gets or sets the allowed file extensions.")] + [Parameter] + public IEnumerable? AllowedFileTypes { get; set; } + + /// + /// Gets or sets the accessible label for the file selection area. + /// + [AddedVersion("4.0.0")] + [DefaultValue("Select files")] + [Description("Gets or sets the accessible label for the file selection area.")] + [Parameter] + public string AriaLabel { get; set; } = "Select files"; + + /// + /// Gets or sets the contextual background color used by DragAndDropFileInput. + /// + [AddedVersion("4.0.0")] + [DefaultValue(BackgroundColor.None)] + [Description("Gets or sets the contextual background color used by DragAndDropFileInput.")] + [Parameter] + public BackgroundColor BackgroundColor { get; set; } = BackgroundColor.None; + + /// + /// This event fires when the selected files change. + /// + [AddedVersion("4.0.0")] + [Description("This event fires when the selected files change.")] + [Parameter] + public EventCallback> FilesChanged { get; set; } + + /// + /// Gets or sets the hint text displayed below the component. + /// + [AddedVersion("4.0.0")] + [DefaultValue(null)] + [Description("Gets or sets the hint text displayed below the component.")] + [Parameter] + public string? HintText { get; set; } + + /// + /// Gets or sets the maximum number of selected files. + /// + [AddedVersion("4.0.0")] + [DefaultValue(null)] + [Description("Gets or sets the maximum number of selected files.")] + [Parameter] + public int? MaxFileCount { get; set; } + + /// + /// Gets or sets the maximum allowed file size in bytes. + /// + [AddedVersion("4.0.0")] + [DefaultValue(null)] + [Description("Gets or sets the maximum allowed file size in bytes.")] + [Parameter] + public long? MaxFileSize { get; set; } + + /// + /// Gets or sets whether multiple files can be selected. + /// + [AddedVersion("4.0.0")] + [DefaultValue(false)] + [Description("Gets or sets whether multiple files can be selected.")] + [Parameter] + public bool Multiple { get; set; } + + /// + /// Gets the currently selected files. + /// + public IReadOnlyList SelectedFiles => files.AsReadOnly(); + + /// + /// Gets or sets the component size. + /// + [AddedVersion("4.0.0")] + [DefaultValue(FileInputSize.Default)] + [Description("Gets or sets the component size.")] + [Parameter] + public FileInputSize Size { get; set; } = FileInputSize.Default; + + #endregion +} From 0d5b6b4068eae95dfca9f115bd3ee7f3ab566153 Mon Sep 17 00:00:00 2001 From: Vikram Reddy Date: Sun, 12 Jul 2026 14:05:37 +0530 Subject: [PATCH 04/13] Add DragAndDropFileInput component and related updates Introduced a new `DragAndDropFileInput` component to support drag-and-drop file selection and touch file-picker functionality. - Updated layouts to include `DragAndDropFileInput` in navigation. - Added routes for the new component in `DemoRouteConstants` and `DocsRouteConstants`. - Created documentation and demos for `DragAndDropFileInput`. - Refined `FileInput` to focus on standard Bootstrap file control. - Removed `BackgroundColor` from `FileInputBase` and added it to `DragAndDropFileInput`. - Updated `Index.razor` to link to new demos. --- .../Layout/DemosMainLayout.razor.cs | 1 + .../Components/Layout/DocsMainLayout.razor.cs | 1 + .../Components/Layout/MainLayout.razor.cs | 1 + .../DragAndDropFileInputDocumentation.razor | 32 +++++++++++++++++++ ...AndDropFileInput_Demo_01_Basic_Usage.razor | 1 + ...gAndDropFileInput_Demo_02_Validation.razor | 9 ++++++ .../FileInput/FileInputDocumentation.razor | 11 ++++--- .../FileInput_Demo_01_Basic_Usage.razor | 2 +- .../FileInput_Demo_03_Colors_And_Sizes.razor | 4 +-- ...ndDropFileInput_Doc_01_Documentation.razor | 29 +++++++++++++++++ .../Components/Pages/Home/Index.razor | 10 ++++++ .../Constants/DemoRouteConstants.cs | 2 ++ .../DragAndDropFileInput.razor.cs | 10 ++++++ .../Form/FileInput/FileInputBase.cs | 9 ------ 14 files changed, 105 insertions(+), 17 deletions(-) create mode 100644 BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Form/DragAndDropFileInput/DragAndDropFileInputDocumentation.razor create mode 100644 BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Form/DragAndDropFileInput/DragAndDropFileInput_Demo_01_Basic_Usage.razor create mode 100644 BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Form/DragAndDropFileInput/DragAndDropFileInput_Demo_02_Validation.razor create mode 100644 BlazorBootstrap.Demo.RCL/Components/Pages/Docs/Form/DragAndDropFileInput/DragAndDropFileInput_Doc_01_Documentation.razor diff --git a/BlazorBootstrap.Demo.RCL/Components/Layout/DemosMainLayout.razor.cs b/BlazorBootstrap.Demo.RCL/Components/Layout/DemosMainLayout.razor.cs index 3299bf266..e6b838d5b 100644 --- a/BlazorBootstrap.Demo.RCL/Components/Layout/DemosMainLayout.razor.cs +++ b/BlazorBootstrap.Demo.RCL/Components/Layout/DemosMainLayout.razor.cs @@ -23,6 +23,7 @@ internal override IEnumerable GetNavItems() new (){ Id = "403", Text = "Date Input", Href = DemoRouteConstants.Demos_URL_DateInput, IconName = IconName.CalendarDate, ParentId = "4" }, new (){ Id = "404", Text = "Enum Input", Href = DemoRouteConstants.Demos_URL_EnumInput, IconName = IconName.MenuButtonWideFill, ParentId = "4" }, new (){ Id = "414", Text = "File Input", Href = DemoRouteConstants.Demos_URL_FileInput, IconName = IconName.FileEarmarkArrowUp, ParentId = "4" }, + new (){ Id = "415", Text = "Drag and Drop File Input", Href = DemoRouteConstants.Demos_URL_DragAndDropFileInput, IconName = IconName.FileEarmarkArrowUp, ParentId = "4" }, new (){ Id = "405", Text = "Number Input", Href = DemoRouteConstants.Demos_URL_NumberInput, IconName = IconName.InputCursor, ParentId = "4" }, new (){ Id = "406", Text = "OTP Input", Href = DemoRouteConstants.Demos_URL_OTPInput, IconName = IconName.Asterisk, ParentId = "4" }, new (){ Id = "407", Text = "Password Input", Href = DemoRouteConstants.Demos_URL_PasswordInput, IconName = IconName.EyeSlashFill, ParentId = "4" }, diff --git a/BlazorBootstrap.Demo.RCL/Components/Layout/DocsMainLayout.razor.cs b/BlazorBootstrap.Demo.RCL/Components/Layout/DocsMainLayout.razor.cs index c6d578627..4d441e1ad 100644 --- a/BlazorBootstrap.Demo.RCL/Components/Layout/DocsMainLayout.razor.cs +++ b/BlazorBootstrap.Demo.RCL/Components/Layout/DocsMainLayout.razor.cs @@ -22,6 +22,7 @@ internal override IEnumerable GetNavItems() new (){ Id = "403", Text = "Date Input", Href = DemoRouteConstants.Docs_URL_DateInput, IconName = IconName.CalendarDate, ParentId = "4" }, new (){ Id = "404", Text = "Enum Input", Href = DemoRouteConstants.Docs_URL_EnumInput, IconName = IconName.MenuButtonWideFill, ParentId = "4" }, new (){ Id = "414", Text = "File Input", Href = DemoRouteConstants.Docs_URL_FileInput, IconName = IconName.FileEarmarkArrowUp, ParentId = "4" }, + new (){ Id = "415", Text = "Drag and Drop File Input", Href = DemoRouteConstants.Docs_URL_DragAndDropFileInput, IconName = IconName.FileEarmarkArrowUp, ParentId = "4" }, new (){ Id = "405", Text = "Number Input", Href = DemoRouteConstants.Docs_URL_NumberInput, IconName = IconName.InputCursor, ParentId = "4" }, new (){ Id = "406", Text = "OTP Input", Href = DemoRouteConstants.Docs_URL_OTPInput, IconName = IconName.Asterisk, ParentId = "4" }, new (){ Id = "407", Text = "Password Input", Href = DemoRouteConstants.Docs_URL_PasswordInput, IconName = IconName.EyeSlashFill, ParentId = "4" }, diff --git a/BlazorBootstrap.Demo.RCL/Components/Layout/MainLayout.razor.cs b/BlazorBootstrap.Demo.RCL/Components/Layout/MainLayout.razor.cs index b82ad99a3..1402be6ea 100644 --- a/BlazorBootstrap.Demo.RCL/Components/Layout/MainLayout.razor.cs +++ b/BlazorBootstrap.Demo.RCL/Components/Layout/MainLayout.razor.cs @@ -23,6 +23,7 @@ internal override IEnumerable GetNavItems() new (){ Id = "403", Text = "Date Input", Href = DemoRouteConstants.Demos_URL_DateInput, IconName = IconName.CalendarDate, ParentId = "4" }, new (){ Id = "404", Text = "Enum Input", Href = DemoRouteConstants.Demos_URL_EnumInput, IconName = IconName.MenuButtonWideFill, ParentId = "4" }, new (){ Id = "413", Text = "File Input", Href = DemoRouteConstants.Demos_URL_FileInput, IconName = IconName.FileEarmarkArrowUp, ParentId = "4" }, + new (){ Id = "414", Text = "Drag and Drop File Input", Href = DemoRouteConstants.Demos_URL_DragAndDropFileInput, IconName = IconName.FileEarmarkArrowUp, ParentId = "4" }, new (){ Id = "405", Text = "Number Input", Href = DemoRouteConstants.Demos_URL_NumberInput, IconName = IconName.InputCursor, ParentId = "4" }, new (){ Id = "406", Text = "Password Input", Href = DemoRouteConstants.Demos_URL_PasswordInput, IconName = IconName.EyeSlashFill, ParentId = "4" }, new (){ Id = "407", Text = "Radio Input", Href = DemoRouteConstants.Demos_URL_RadioInput, IconName = IconName.RecordCircle, ParentId = "4" }, diff --git a/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Form/DragAndDropFileInput/DragAndDropFileInputDocumentation.razor b/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Form/DragAndDropFileInput/DragAndDropFileInputDocumentation.razor new file mode 100644 index 000000000..ce7eb5236 --- /dev/null +++ b/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Form/DragAndDropFileInput/DragAndDropFileInputDocumentation.razor @@ -0,0 +1,32 @@ +@page "/drag-and-drop-file-input" + +@attribute [Route(pageUrl)] +@layout DemosMainLayout + + + + + +
    +
    Drop files onto the area on desktop, or tap it on touch devices to open the native file picker.
    + +
    + +
    +
    Drop and picker selection share the same extension, size, and count validation behavior.
    + +
    + +@code { + private const string pageUrl = DemoRouteConstants.Demos_URL_DragAndDropFileInput; + private const string pageTitle = "Blazor DragAndDropFileInput"; + private const string pageDescription = "The Blazor Bootstrap DragAndDropFileInput component supports desktop drag and drop and touch file-picker selection."; + private const string metaTitle = "Blazor DragAndDropFileInput Component"; + private const string metaDescription = "The Blazor Bootstrap DragAndDropFileInput component supports desktop drag and drop and touch file-picker selection."; + private const string imageUrl = DemoScreenshotSrcConstants.Demos_URL_Forms_Prefix; +} diff --git a/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Form/DragAndDropFileInput/DragAndDropFileInput_Demo_01_Basic_Usage.razor b/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Form/DragAndDropFileInput/DragAndDropFileInput_Demo_01_Basic_Usage.razor new file mode 100644 index 000000000..91a16521c --- /dev/null +++ b/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Form/DragAndDropFileInput/DragAndDropFileInput_Demo_01_Basic_Usage.razor @@ -0,0 +1 @@ + diff --git a/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Form/DragAndDropFileInput/DragAndDropFileInput_Demo_02_Validation.razor b/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Form/DragAndDropFileInput/DragAndDropFileInput_Demo_02_Validation.razor new file mode 100644 index 000000000..3197357e4 --- /dev/null +++ b/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Form/DragAndDropFileInput/DragAndDropFileInput_Demo_02_Validation.razor @@ -0,0 +1,9 @@ + + +@code { + private readonly string[] allowedFileTypes = [".pdf", "png"]; +} diff --git a/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Form/FileInput/FileInputDocumentation.razor b/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Form/FileInput/FileInputDocumentation.razor index addbf0391..f559ffbe9 100644 --- a/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Form/FileInput/FileInputDocumentation.razor +++ b/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Form/FileInput/FileInputDocumentation.razor @@ -13,6 +13,7 @@
    +
    FileInput uses Bootstrap's standard visible file-control presentation. Use DragAndDropFileInput when a drop area is needed.
    @@ -21,13 +22,13 @@ -
    -
    Use BackgroundColor and Size to match the surrounding Bootstrap UI.
    +
    +
    Use Size to apply Bootstrap's form-control-lg and form-control-sm file-control sizes.
    -
    FileInput validates sizes, counts, and extensions for file-dialog and drag-and-drop selection.
    +
    FileInput validates sizes, counts, and extensions selected from the file dialog.
    @@ -39,8 +40,8 @@ @code { private const string pageUrl = DemoRouteConstants.Demos_URL_FileInput; private const string pageTitle = "Blazor FileInput"; - private const string pageDescription = "The Blazor Bootstrap FileInput component supports click and drag-and-drop file selection with validation and removal."; + private const string pageDescription = "The Blazor Bootstrap FileInput component provides a standard Bootstrap file control with validation and removal."; private const string metaTitle = "Blazor FileInput Component"; - private const string metaDescription = "The Blazor Bootstrap FileInput component supports click and drag-and-drop file selection with validation and removal."; + private const string metaDescription = "The Blazor Bootstrap FileInput component provides a standard Bootstrap file control with validation and removal."; private const string imageUrl = DemoScreenshotSrcConstants.Demos_URL_Forms_Prefix; } diff --git a/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Form/FileInput/FileInput_Demo_01_Basic_Usage.razor b/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Form/FileInput/FileInput_Demo_01_Basic_Usage.razor index 693821704..5ac7324fe 100644 --- a/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Form/FileInput/FileInput_Demo_01_Basic_Usage.razor +++ b/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Form/FileInput/FileInput_Demo_01_Basic_Usage.razor @@ -1 +1 @@ - + diff --git a/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Form/FileInput/FileInput_Demo_03_Colors_And_Sizes.razor b/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Form/FileInput/FileInput_Demo_03_Colors_And_Sizes.razor index 261a78ec4..42e456842 100644 --- a/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Form/FileInput/FileInput_Demo_03_Colors_And_Sizes.razor +++ b/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Form/FileInput/FileInput_Demo_03_Colors_And_Sizes.razor @@ -1,4 +1,4 @@
    - - + +
    diff --git a/BlazorBootstrap.Demo.RCL/Components/Pages/Docs/Form/DragAndDropFileInput/DragAndDropFileInput_Doc_01_Documentation.razor b/BlazorBootstrap.Demo.RCL/Components/Pages/Docs/Form/DragAndDropFileInput/DragAndDropFileInput_Doc_01_Documentation.razor new file mode 100644 index 000000000..2dfc38b51 --- /dev/null +++ b/BlazorBootstrap.Demo.RCL/Components/Pages/Docs/Form/DragAndDropFileInput/DragAndDropFileInput_Doc_01_Documentation.razor @@ -0,0 +1,29 @@ +@attribute [Route(pageUrl)] +@layout DocsMainLayout + + + + + +
    + +
    + +
    + +
    + +@code { + private const string componentName = nameof(DragAndDropFileInput); + private const string pageUrl = DemoRouteConstants.Docs_URL_DragAndDropFileInput; + private const string pageTitle = componentName; + private const string pageDescription = $"This documentation provides a comprehensive reference for the {componentName} component, guiding you through its configuration options."; + private const string metaTitle = $"Blazor {componentName} Component"; + private const string metaDescription = $"This documentation provides a comprehensive reference for the {componentName} component, guiding you through its configuration options."; + private const string imageUrl = DemoScreenshotSrcConstants.Demos_URL_Forms_Prefix; +} diff --git a/BlazorBootstrap.Demo.RCL/Components/Pages/Home/Index.razor b/BlazorBootstrap.Demo.RCL/Components/Pages/Home/Index.razor index bc6747647..d7289291c 100644 --- a/BlazorBootstrap.Demo.RCL/Components/Pages/Home/Index.razor +++ b/BlazorBootstrap.Demo.RCL/Components/Pages/Home/Index.razor @@ -319,6 +319,16 @@

    Enum Input New

    + +

    Number Input

    diff --git a/BlazorBootstrap.Demo.RCL/Constants/DemoRouteConstants.cs b/BlazorBootstrap.Demo.RCL/Constants/DemoRouteConstants.cs index 735f810fa..ece6c25ad 100644 --- a/BlazorBootstrap.Demo.RCL/Constants/DemoRouteConstants.cs +++ b/BlazorBootstrap.Demo.RCL/Constants/DemoRouteConstants.cs @@ -30,6 +30,7 @@ public static class DemoRouteConstants public const string Demos_URL_CheckboxInput = Demos_URL_Forms_Prefix + "/checkbox-input"; public const string Demos_URL_CurrencyInput = Demos_URL_Forms_Prefix + "/currency-input"; public const string Demos_URL_DateInput = Demos_URL_Forms_Prefix + "/date-input"; + public const string Demos_URL_DragAndDropFileInput = Demos_URL_Forms_Prefix + "/drag-and-drop-file-input"; public const string Demos_URL_EnumInput = Demos_URL_Forms_Prefix + "/enum-input"; public const string Demos_URL_FileInput = Demos_URL_Forms_Prefix + "/file-input"; public const string Demos_URL_NumberInput = Demos_URL_Forms_Prefix + "/number-input"; @@ -145,6 +146,7 @@ public static class DemoRouteConstants public const string Docs_URL_CheckboxInput = Docs_URL_Forms_Prefix + "/checkbox-input"; public const string Docs_URL_CurrencyInput = Docs_URL_Forms_Prefix + "/currency-input"; public const string Docs_URL_DateInput = Docs_URL_Forms_Prefix + "/date-input"; + public const string Docs_URL_DragAndDropFileInput = Docs_URL_Forms_Prefix + "/drag-and-drop-file-input"; public const string Docs_URL_EnumInput = Docs_URL_Forms_Prefix + "/enum-input"; public const string Docs_URL_FileInput = Docs_URL_Forms_Prefix + "/file-input"; public const string Docs_URL_NumberInput = Docs_URL_Forms_Prefix + "/number-input"; diff --git a/blazorbootstrap/Components/Form/DragAndDropFileInput/DragAndDropFileInput.razor.cs b/blazorbootstrap/Components/Form/DragAndDropFileInput/DragAndDropFileInput.razor.cs index 72ba7730f..e24f87449 100644 --- a/blazorbootstrap/Components/Form/DragAndDropFileInput/DragAndDropFileInput.razor.cs +++ b/blazorbootstrap/Components/Form/DragAndDropFileInput/DragAndDropFileInput.razor.cs @@ -1,3 +1,4 @@ +using System.ComponentModel; using Microsoft.AspNetCore.Components.Web; namespace BlazorBootstrap; @@ -38,6 +39,15 @@ protected void OnDrop(DragEventArgs _) #region Properties, Indexers + /// + /// Gets or sets the contextual background color of the drag-and-drop area. + /// + [AddedVersion("4.0.0")] + [DefaultValue(BackgroundColor.None)] + [Description("Gets or sets the contextual background color of the drag-and-drop area.")] + [Parameter] + public BackgroundColor BackgroundColor { get; set; } = BackgroundColor.None; + protected override string? ClassNames => BuildClassNames(Class, ("bb-drag-and-drop-file-input", true), diff --git a/blazorbootstrap/Components/Form/FileInput/FileInputBase.cs b/blazorbootstrap/Components/Form/FileInput/FileInputBase.cs index c14867d89..cad5476e0 100644 --- a/blazorbootstrap/Components/Form/FileInput/FileInputBase.cs +++ b/blazorbootstrap/Components/Form/FileInput/FileInputBase.cs @@ -111,15 +111,6 @@ protected async Task RemoveFileAsync(IBrowserFile file) [Parameter] public string AriaLabel { get; set; } = "Select files"; - /// - /// Gets or sets the contextual background color used by DragAndDropFileInput. - /// - [AddedVersion("4.0.0")] - [DefaultValue(BackgroundColor.None)] - [Description("Gets or sets the contextual background color used by DragAndDropFileInput.")] - [Parameter] - public BackgroundColor BackgroundColor { get; set; } = BackgroundColor.None; - /// /// This event fires when the selected files change. /// From 0ba3221b431ffb82a6b9564a630037f153ebd536 Mon Sep 17 00:00:00 2001 From: Vikram Reddy Date: Sun, 12 Jul 2026 16:51:53 +0530 Subject: [PATCH 05/13] Add form validation and improve file input docs Added a "Form validation" section to `DragAndDropFileInputDocumentation.razor` and `FileInputDocumentation.razor` to demonstrate `FilesChanged` usage. Introduced a `Size` parameter in `FileInput.razor.cs` and removed it from `FileInputBase.cs` for better encapsulation. Updated `DragAndDropFileInput.razor.css` to use `::deep` for proper CSS scoping. Refined `file-input.mdx` and created `drag-and-drop-file-input.mdx` to enhance documentation with examples, parameters, and accessibility details. Added new form validation examples in `DragAndDropFileInput_Demo_03_Form_Validation.razor` and `FileInput_Demo_06_Form_Validation.razor`. Improved accessibility and clarified usage of `DragAndDropFileInput`. --- .../DragAndDropFileInputDocumentation.razor | 5 ++ ...ropFileInput_Demo_03_Form_Validation.razor | 50 +++++++++++++ .../FileInput/FileInputDocumentation.razor | 5 ++ .../FileInput_Demo_06_Form_Validation.razor | 50 +++++++++++++ .../DragAndDropFileInput.razor.css | 2 +- .../Form/FileInput/FileInput.razor.cs | 11 +++ .../Form/FileInput/FileInputBase.cs | 9 --- .../04-forms/drag-and-drop-file-input.mdx | 75 +++++++++++++++++++ docs/docs/04-forms/file-input.mdx | 47 +++++------- 9 files changed, 216 insertions(+), 38 deletions(-) create mode 100644 BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Form/DragAndDropFileInput/DragAndDropFileInput_Demo_03_Form_Validation.razor create mode 100644 BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Form/FileInput/FileInput_Demo_06_Form_Validation.razor create mode 100644 docs/docs/04-forms/drag-and-drop-file-input.mdx diff --git a/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Form/DragAndDropFileInput/DragAndDropFileInputDocumentation.razor b/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Form/DragAndDropFileInput/DragAndDropFileInputDocumentation.razor index ce7eb5236..dfc77d228 100644 --- a/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Form/DragAndDropFileInput/DragAndDropFileInputDocumentation.razor +++ b/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Form/DragAndDropFileInput/DragAndDropFileInputDocumentation.razor @@ -22,6 +22,11 @@ +
    +
    Use FilesChanged to update a form model while retaining the drag-and-drop area and touch picker fallback.
    + +
    + @code { private const string pageUrl = DemoRouteConstants.Demos_URL_DragAndDropFileInput; private const string pageTitle = "Blazor DragAndDropFileInput"; diff --git a/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Form/DragAndDropFileInput/DragAndDropFileInput_Demo_03_Form_Validation.razor b/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Form/DragAndDropFileInput/DragAndDropFileInput_Demo_03_Form_Validation.razor new file mode 100644 index 000000000..2b3473f48 --- /dev/null +++ b/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Form/DragAndDropFileInput/DragAndDropFileInput_Demo_03_Form_Validation.razor @@ -0,0 +1,50 @@ +@using System.ComponentModel.DataAnnotations +@using Microsoft.AspNetCore.Components.Forms + + + + + +
    + + + +
    + + + + @if (submitted) + { +
    Form submitted with @upload.DocumentName.
    + } +
    + +@code { + private readonly string[] allowedFileTypes = ["pdf"]; + private readonly UploadModel upload = new(); + private EditContext editContext = null!; + private bool submitted; + + protected override void OnInitialized() + { + editContext = new EditContext(upload); + } + + private void HandleValidSubmit() => submitted = true; + + private void OnFilesChanged(IReadOnlyList files) + { + upload.DocumentName = files.FirstOrDefault()?.Name; + submitted = false; + editContext.NotifyFieldChanged(new FieldIdentifier(upload, nameof(upload.DocumentName))); + } + + private sealed class UploadModel + { + [Required(ErrorMessage = "A supporting document is required.")] + public string? DocumentName { get; set; } + } +} diff --git a/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Form/FileInput/FileInputDocumentation.razor b/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Form/FileInput/FileInputDocumentation.razor index f559ffbe9..0ee9f9da3 100644 --- a/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Form/FileInput/FileInputDocumentation.razor +++ b/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Form/FileInput/FileInputDocumentation.razor @@ -32,6 +32,11 @@ +
    +
    Use FilesChanged to update a form model and combine file-selection rules with standard form validation.
    + +
    +
    An empty AllowedFileTypes collection accepts every extension while other configured rules remain active.
    diff --git a/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Form/FileInput/FileInput_Demo_06_Form_Validation.razor b/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Form/FileInput/FileInput_Demo_06_Form_Validation.razor new file mode 100644 index 000000000..d510e4f17 --- /dev/null +++ b/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Form/FileInput/FileInput_Demo_06_Form_Validation.razor @@ -0,0 +1,50 @@ +@using System.ComponentModel.DataAnnotations +@using Microsoft.AspNetCore.Components.Forms + + + + + +
    + + + +
    + + + + @if (submitted) + { +
    Form submitted with @upload.DocumentName.
    + } +
    + +@code { + private readonly string[] allowedFileTypes = ["pdf"]; + private readonly UploadModel upload = new(); + private EditContext editContext = null!; + private bool submitted; + + protected override void OnInitialized() + { + editContext = new EditContext(upload); + } + + private void HandleValidSubmit() => submitted = true; + + private void OnFilesChanged(IReadOnlyList files) + { + upload.DocumentName = files.FirstOrDefault()?.Name; + submitted = false; + editContext.NotifyFieldChanged(new FieldIdentifier(upload, nameof(upload.DocumentName))); + } + + private sealed class UploadModel + { + [Required(ErrorMessage = "A supporting document is required.")] + public string? DocumentName { get; set; } + } +} diff --git a/blazorbootstrap/Components/Form/DragAndDropFileInput/DragAndDropFileInput.razor.css b/blazorbootstrap/Components/Form/DragAndDropFileInput/DragAndDropFileInput.razor.css index c3bbae342..f26b0a18a 100644 --- a/blazorbootstrap/Components/Form/DragAndDropFileInput/DragAndDropFileInput.razor.css +++ b/blazorbootstrap/Components/Form/DragAndDropFileInput/DragAndDropFileInput.razor.css @@ -11,7 +11,7 @@ background-color: var(--bs-primary-bg-subtle); } -.bb-drag-and-drop-file-input__native-input { +::deep .bb-drag-and-drop-file-input__native-input { position: absolute; inset: 0; z-index: 1; diff --git a/blazorbootstrap/Components/Form/FileInput/FileInput.razor.cs b/blazorbootstrap/Components/Form/FileInput/FileInput.razor.cs index bd1c2193c..d55637840 100644 --- a/blazorbootstrap/Components/Form/FileInput/FileInput.razor.cs +++ b/blazorbootstrap/Components/Form/FileInput/FileInput.razor.cs @@ -1,9 +1,20 @@ +using System.ComponentModel; + namespace BlazorBootstrap; public partial class FileInput : FileInputBase { #region Properties, Indexers + /// + /// Gets or sets the component size. + /// + [AddedVersion("4.0.0")] + [DefaultValue(FileInputSize.Default)] + [Description("Gets or sets the component size.")] + [Parameter] + public FileInputSize Size { get; set; } = FileInputSize.Default; + protected override string? ClassNames => BuildClassNames(Class, (BootstrapClass.FormControl, true), diff --git a/blazorbootstrap/Components/Form/FileInput/FileInputBase.cs b/blazorbootstrap/Components/Form/FileInput/FileInputBase.cs index cad5476e0..fdb6722f4 100644 --- a/blazorbootstrap/Components/Form/FileInput/FileInputBase.cs +++ b/blazorbootstrap/Components/Form/FileInput/FileInputBase.cs @@ -160,14 +160,5 @@ protected async Task RemoveFileAsync(IBrowserFile file) /// public IReadOnlyList SelectedFiles => files.AsReadOnly(); - /// - /// Gets or sets the component size. - /// - [AddedVersion("4.0.0")] - [DefaultValue(FileInputSize.Default)] - [Description("Gets or sets the component size.")] - [Parameter] - public FileInputSize Size { get; set; } = FileInputSize.Default; - #endregion } diff --git a/docs/docs/04-forms/drag-and-drop-file-input.mdx b/docs/docs/04-forms/drag-and-drop-file-input.mdx new file mode 100644 index 000000000..fb7f240d6 --- /dev/null +++ b/docs/docs/04-forms/drag-and-drop-file-input.mdx @@ -0,0 +1,75 @@ +--- +title: Blazor Drag and Drop File Input Component +description: The Blazor Bootstrap DragAndDropFileInput component supports desktop drag and drop with a touch-friendly native file-picker fallback. +sidebar_label: Drag and Drop File Input +sidebar_position: 7 +--- + +import CarbonAd from '/carbon-ad.mdx' + +# Blazor DragAndDropFileInput + +The Blazor Bootstrap `DragAndDropFileInput` component displays a file drop area. On desktop browsers, users can drop files onto the area. On touch browsers, tapping the area opens the native file picker. It supports the same selection, validation, feedback, and removal behavior as `FileInput`. + + + +## Parameters + +| Name | Type | Default | Description | Added Version | +|:--|:--|:--|:--|:--| +| AllowedFileTypes | IEnumerable<string> | null | Allowed file extensions. Values can use `.pdf` or `pdf` and match case-insensitively. An empty collection allows every extension. | 4.0.0 | +| AriaLabel | string | Select files | Accessible label for the drop area and native file picker. | 4.0.0 | +| BackgroundColor | BackgroundColor | BackgroundColor.None | Bootstrap contextual background color for the drop area. | 4.0.0 | +| HintText | string | null | Help text displayed in the drop area. | 4.0.0 | +| MaxFileCount | int | null | Maximum number of selected files. | 4.0.0 | +| MaxFileSize | long | null | Maximum file size in bytes. | 4.0.0 | +| Multiple | bool | false | Allows multiple files to be selected. | 4.0.0 | + +## Events + +| Name | Description | Added Version | +|:--|:--|:--| +| FilesChanged | Fires when the accepted selected-file list changes. | 4.0.0 | + +## Examples + +### Desktop drag-and-drop and touch selection + +Drop a file onto the area in desktop browsers. On tablet and mobile touch browsers, tap the area to open the platform's native file picker. + +```cshtml showLineNumbers + +``` + +[See demo here](https://demos.blazorbootstrap.com/form/drag-and-drop-file-input#basic-usage) + +### Validation + +`AllowedFileTypes` accepts extensions only. Values with or without a leading dot are equivalent, and matching is case-insensitive. Files that fail an extension, size, or count rule show a file-specific error; valid files from a mixed selection remain selected. + +```cshtml showLineNumbers + + +@code { + private readonly string[] allowedExtensions = [".pdf", "png"]; +} +``` + +[See demo here](https://demos.blazorbootstrap.com/form/drag-and-drop-file-input#validation) + +### Background color + +Use `BackgroundColor` to match the drop area to the surrounding Bootstrap interface. + +```cshtml showLineNumbers + +``` + +## Accessibility + +The native file input is visually hidden within the drop area but remains the selection control. It is therefore operable through touch, pointer, and keyboard activation. DragAndDropFileInput announces validation errors through an alert region, and each selected file has an accessible, keyboard-operable remove button. diff --git a/docs/docs/04-forms/file-input.mdx b/docs/docs/04-forms/file-input.mdx index 73376e5c9..85a24f513 100644 --- a/docs/docs/04-forms/file-input.mdx +++ b/docs/docs/04-forms/file-input.mdx @@ -1,6 +1,6 @@ --- title: Blazor File Input Component -description: The Blazor Bootstrap FileInput component supports click and drag-and-drop file selection, validation, feedback, and removal. +description: The Blazor Bootstrap FileInput component provides a standard Bootstrap file control with validation, feedback, and removal. sidebar_label: File Input sidebar_position: 6 --- @@ -9,7 +9,7 @@ import CarbonAd from '/carbon-ad.mdx' # Blazor FileInput -The Blazor Bootstrap `FileInput` component provides a drop area that users can click or drag files onto. It supports single or multiple files, per-file removal, validation, contextual colors, sizes, and accessible feedback. +The Blazor Bootstrap `FileInput` component follows the standard Bootstrap file-control presentation. It supports single or multiple files, per-file removal, validation, sizes, and accessible feedback. Use `DragAndDropFileInput` when a drop area is required. @@ -17,29 +17,28 @@ The Blazor Bootstrap `FileInput` component provides a drop area that users can c | Name | Type | Default | Description | Added Version | |:--|:--|:--|:--|:--| -| AllowedFileTypes | IEnumerable<string> | null | Allowed file extensions. Values can use `.pdf` or `pdf` and match case-insensitively. An empty collection allows every extension. | 3.5.0 | -| AriaLabel | string | Select files | Accessible label for the selection area. | 3.5.0 | -| BackgroundColor | BackgroundColor | BackgroundColor.None | Bootstrap contextual background color. | 3.5.0 | -| HintText | string | null | Help text displayed below the component. | 3.5.0 | -| MaxFileCount | int | null | Maximum number of selected files. | 3.5.0 | -| MaxFileSize | long | null | Maximum file size in bytes. | 3.5.0 | -| Multiple | bool | false | Allows multiple files to be selected. | 3.5.0 | -| Size | FileInputSize | FileInputSize.Default | Component size. | 3.5.0 | +| AllowedFileTypes | IEnumerable<string> | null | Allowed file extensions. Values can use `.pdf` or `pdf` and match case-insensitively. An empty collection allows every extension. | 4.0.0 | +| AriaLabel | string | Select files | Accessible label for the file control. | 4.0.0 | +| HintText | string | null | Help text displayed below the component. | 4.0.0 | +| MaxFileCount | int | null | Maximum number of selected files. | 4.0.0 | +| MaxFileSize | long | null | Maximum file size in bytes. | 4.0.0 | +| Multiple | bool | false | Allows multiple files to be selected. | 4.0.0 | +| Size | FileInputSize | FileInputSize.Default | Applies Bootstrap `form-control-lg` or `form-control-sm` sizing. | 4.0.0 | ## Events | Name | Description | Added Version | |:--|:--|:--| -| FilesChanged | Fires when the accepted selected-file list changes. | 3.5.0 | +| FilesChanged | Fires when the accepted selected-file list changes. | 4.0.0 | ## Examples -### Basic usage and drag-and-drop +### Basic usage -Click the drop area to open the native file picker, or drag a file onto it. The same validation rules apply to either path. +Use the visible Bootstrap file control to open the native file picker. ```cshtml showLineNumbers - + ``` [See demo here](https://demos.blazorbootstrap.com/form/file-input#basic-usage) @@ -90,25 +89,17 @@ Use an empty `AllowedFileTypes` collection when every extension is permitted. Ot [See demo here](https://demos.blazorbootstrap.com/form/file-input#hint-text-and-unrestricted-extensions) -### Colors, sizes, and styling +### Sizes -Use `BackgroundColor` and `Size` to match the surrounding Bootstrap interface. Consumer CSS can override the component variables for borders, drag feedback, and validation colors. +Use `Size` to apply Bootstrap's `form-control-lg` or `form-control-sm` classes. ```cshtml showLineNumbers - + ``` -```css -.bb-file-input { - --bb-file-input-drag-border-color: var(--bs-success); - --bb-file-input-drag-background-color: rgba(var(--bs-success-rgb), .1); -} -``` - -[See demo here](https://demos.blazorbootstrap.com/form/file-input#colors-and-sizes) +[See demo here](https://demos.blazorbootstrap.com/form/file-input#sizes) ## Accessibility -Provide a clear `AriaLabel` when the default label does not describe the expected files. FileInput announces validation errors through an alert region, and each selected file has an accessible, keyboard-operable remove button. +Provide a clear `AriaLabel` when the default label does not describe the expected files. FileInput announces validation errors through an alert region, and each selected file has an accessible, keyboard-operable remove button. For drag-and-drop with a touch-friendly native-picker fallback, use [DragAndDropFileInput](./drag-and-drop-file-input). From f096dc54baf9be564fb22b706ed8025617ee6f7f Mon Sep 17 00:00:00 2001 From: Vikram Reddy Date: Sun, 12 Jul 2026 16:52:41 +0530 Subject: [PATCH 06/13] Clean up unused using directives Removed unused `using` directives from `DragAndDropFileInput.razor.cs`, `FileInput.razor.cs`, and `FileInputBase.cs` to improve code maintainability and reduce unnecessary dependencies. --- .../Form/DragAndDropFileInput/DragAndDropFileInput.razor.cs | 3 --- blazorbootstrap/Components/Form/FileInput/FileInput.razor.cs | 2 -- blazorbootstrap/Components/Form/FileInput/FileInputBase.cs | 3 --- 3 files changed, 8 deletions(-) diff --git a/blazorbootstrap/Components/Form/DragAndDropFileInput/DragAndDropFileInput.razor.cs b/blazorbootstrap/Components/Form/DragAndDropFileInput/DragAndDropFileInput.razor.cs index e24f87449..1a4cc0f7a 100644 --- a/blazorbootstrap/Components/Form/DragAndDropFileInput/DragAndDropFileInput.razor.cs +++ b/blazorbootstrap/Components/Form/DragAndDropFileInput/DragAndDropFileInput.razor.cs @@ -1,6 +1,3 @@ -using System.ComponentModel; -using Microsoft.AspNetCore.Components.Web; - namespace BlazorBootstrap; public partial class DragAndDropFileInput : FileInputBase diff --git a/blazorbootstrap/Components/Form/FileInput/FileInput.razor.cs b/blazorbootstrap/Components/Form/FileInput/FileInput.razor.cs index d55637840..6d0c1c891 100644 --- a/blazorbootstrap/Components/Form/FileInput/FileInput.razor.cs +++ b/blazorbootstrap/Components/Form/FileInput/FileInput.razor.cs @@ -1,5 +1,3 @@ -using System.ComponentModel; - namespace BlazorBootstrap; public partial class FileInput : FileInputBase diff --git a/blazorbootstrap/Components/Form/FileInput/FileInputBase.cs b/blazorbootstrap/Components/Form/FileInput/FileInputBase.cs index fdb6722f4..b14ac0cb0 100644 --- a/blazorbootstrap/Components/Form/FileInput/FileInputBase.cs +++ b/blazorbootstrap/Components/Form/FileInput/FileInputBase.cs @@ -1,6 +1,3 @@ -using System.ComponentModel; -using Microsoft.AspNetCore.Components.Forms; - namespace BlazorBootstrap; public abstract class FileInputBase : BlazorBootstrapComponentBase From 22dc0f2bba454ca62aa70efc68666fea14a1d46a Mon Sep 17 00:00:00 2001 From: Vikram Reddy Date: Sun, 12 Jul 2026 22:14:35 +0530 Subject: [PATCH 07/13] Refactor FileInput to use Bootstrap classes Replaced custom classes in FileInput.razor with Bootstrap classes for `
      ` and `
    • ` elements to improve styling consistency. Removed unused CSS styles for `.bb-file-input__files` and `.bb-file-input__file` from FileInput.razor.css. --- .../Components/Form/FileInput/FileInput.razor | 4 ++-- .../Components/Form/FileInput/FileInput.razor.css | 14 -------------- 2 files changed, 2 insertions(+), 16 deletions(-) diff --git a/blazorbootstrap/Components/Form/FileInput/FileInput.razor b/blazorbootstrap/Components/Form/FileInput/FileInput.razor index 9c8cb9e07..245ae82cb 100644 --- a/blazorbootstrap/Components/Form/FileInput/FileInput.razor +++ b/blazorbootstrap/Components/Form/FileInput/FileInput.razor @@ -12,10 +12,10 @@ @if (files.Any()) { -
        +
          @foreach (var file in files) { -
        • +
        • @file.Name
        • diff --git a/blazorbootstrap/Components/Form/FileInput/FileInput.razor.css b/blazorbootstrap/Components/Form/FileInput/FileInput.razor.css index 55c4e2789..13ffb971e 100644 --- a/blazorbootstrap/Components/Form/FileInput/FileInput.razor.css +++ b/blazorbootstrap/Components/Form/FileInput/FileInput.razor.css @@ -1,17 +1,3 @@ -.bb-file-input__files { - list-style: none; - margin: .75rem 0 0; - padding: 0; -} - -.bb-file-input__file { - align-items: center; - display: flex; - gap: .75rem; - justify-content: space-between; - padding: .25rem 0; -} - .bb-file-input__errors { color: var(--bs-danger-text-emphasis); margin-top: .75rem; From 5a0599f694823b2a7c4f99a1a1df63cb60d2e1b6 Mon Sep 17 00:00:00 2001 From: Vikram Reddy Date: Sun, 12 Jul 2026 22:44:47 +0530 Subject: [PATCH 08/13] Refactor file input components to use Bootstrap classes Replaced custom classes with Bootstrap classes for styling in `DragAndDropFileInput.razor` and `FileInput.razor`. Updated `ul` and `li` elements to use `list-group` classes. Replaced validation error `div` with `text-danger`. Removed unused CSS styles. --- .../DragAndDropFileInput.razor | 6 +++--- .../DragAndDropFileInput.razor.css | 19 ------------------- .../Components/Form/FileInput/FileInput.razor | 2 +- .../Form/FileInput/FileInput.razor.css | 4 ---- 4 files changed, 4 insertions(+), 27 deletions(-) delete mode 100644 blazorbootstrap/Components/Form/FileInput/FileInput.razor.css diff --git a/blazorbootstrap/Components/Form/DragAndDropFileInput/DragAndDropFileInput.razor b/blazorbootstrap/Components/Form/DragAndDropFileInput/DragAndDropFileInput.razor index 349160215..6206d7c19 100644 --- a/blazorbootstrap/Components/Form/DragAndDropFileInput/DragAndDropFileInput.razor +++ b/blazorbootstrap/Components/Form/DragAndDropFileInput/DragAndDropFileInput.razor @@ -27,10 +27,10 @@ @if (files.Any()) { -
            +
              @foreach (var file in files) { -
            • +
            • @file.Name
            • @@ -40,7 +40,7 @@ @if (validationErrors.Any()) { -
    -
    Drop and picker selection share the same extension, size, and count validation behavior.
    +
    Drop and picker selection share the same extension, size, and count validation behavior. Configure validation-message templates with {FileName}, {MaxFileSize}, {AllowedFileTypes}, or {MaxFileCount} where applicable; the single-file message is static.
    diff --git a/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Form/DragAndDropFileInput/DragAndDropFileInput_Demo_02_Validation.razor b/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Form/DragAndDropFileInput/DragAndDropFileInput_Demo_02_Validation.razor index 3197357e4..ad0490057 100644 --- a/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Form/DragAndDropFileInput/DragAndDropFileInput_Demo_02_Validation.razor +++ b/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Form/DragAndDropFileInput/DragAndDropFileInput_Demo_02_Validation.razor @@ -2,6 +2,10 @@ MaxFileCount="3" MaxFileSize="1048576" AllowedFileTypes="@allowedFileTypes" + InvalidFileTypeErrorMessage="{FileName} must use one of: {AllowedFileTypes}." + MaxFileCountErrorMessage="{FileName} would exceed the {MaxFileCount}-file limit." + MaxFileSizeErrorMessage="{FileName} exceeds the {MaxFileSize}-byte limit." + SingleFileErrorMessage="Select only one file." HintText="Select up to three PDF or PNG files, each no larger than 1 MB." /> @code { diff --git a/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Form/FileInput/FileInputDocumentation.razor b/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Form/FileInput/FileInputDocumentation.razor index 0ee9f9da3..10e6abf37 100644 --- a/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Form/FileInput/FileInputDocumentation.razor +++ b/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Form/FileInput/FileInputDocumentation.razor @@ -28,7 +28,7 @@
    -
    FileInput validates sizes, counts, and extensions selected from the file dialog.
    +
    FileInput validates sizes, counts, and extensions selected from the file dialog. Configure validation-message templates with {FileName}, {MaxFileSize}, {AllowedFileTypes}, or {MaxFileCount} where applicable; the single-file message is static.
    diff --git a/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Form/FileInput/FileInput_Demo_04_Validation.razor b/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Form/FileInput/FileInput_Demo_04_Validation.razor index 6a63858f8..0278bb949 100644 --- a/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Form/FileInput/FileInput_Demo_04_Validation.razor +++ b/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Form/FileInput/FileInput_Demo_04_Validation.razor @@ -2,6 +2,10 @@ MaxFileCount="3" MaxFileSize="1048576" AllowedFileTypes="@allowedFileTypes" + InvalidFileTypeErrorMessage="{FileName} must use one of: {AllowedFileTypes}." + MaxFileCountErrorMessage="{FileName} would exceed the {MaxFileCount}-file limit." + MaxFileSizeErrorMessage="{FileName} exceeds the {MaxFileSize}-byte limit." + SingleFileErrorMessage="Select only one file." HintText="Select up to three PDF or PNG files, each no larger than 1 MB." /> @code { diff --git a/docs/docs/04-forms/drag-and-drop-file-input.mdx b/docs/docs/04-forms/drag-and-drop-file-input.mdx index fb7f240d6..e0a0e8c58 100644 --- a/docs/docs/04-forms/drag-and-drop-file-input.mdx +++ b/docs/docs/04-forms/drag-and-drop-file-input.mdx @@ -21,9 +21,13 @@ The Blazor Bootstrap `DragAndDropFileInput` component displays a file drop area. | AriaLabel | string | Select files | Accessible label for the drop area and native file picker. | 4.0.0 | | BackgroundColor | BackgroundColor | BackgroundColor.None | Bootstrap contextual background color for the drop area. | 4.0.0 | | HintText | string | null | Help text displayed in the drop area. | 4.0.0 | +| InvalidFileTypeErrorMessage | string | `{FileName}: the file extension is not allowed.` | Invalid-extension message template. Supports `{FileName}` and `{AllowedFileTypes}`. | 4.0.0 | | MaxFileCount | int | null | Maximum number of selected files. | 4.0.0 | +| MaxFileCountErrorMessage | string | `{FileName}: the maximum number of files has been reached.` | Maximum-count message template. Supports `{FileName}` and `{MaxFileCount}`. | 4.0.0 | | MaxFileSize | long | null | Maximum file size in bytes. | 4.0.0 | +| MaxFileSizeErrorMessage | string | `{FileName}: the file exceeds the maximum allowed size.` | Maximum-size message template. Supports `{FileName}` and `{MaxFileSize}`. | 4.0.0 | | Multiple | bool | false | Allows multiple files to be selected. | 4.0.0 | +| SingleFileErrorMessage | string | Only one file can be selected. | Single-file selection message template; no dynamic token applies. | 4.0.0 | ## Events @@ -61,6 +65,18 @@ Drop a file onto the area in desktop browsers. On tablet and mobile touch browse [See demo here](https://demos.blazorbootstrap.com/form/drag-and-drop-file-input#validation) +### Validation message templates + +DragAndDropFileInput inherits the same validation-message templates as FileInput. The current messages remain the defaults. Use `{FileName}` for file-specific errors, `{MaxFileSize}` for size errors, `{AllowedFileTypes}` for extension errors, and `{MaxFileCount}` for count errors. `SingleFileErrorMessage` is static and has no dynamic token. + +~~~cshtml showLineNumbers + +~~~ + ### Background color Use `BackgroundColor` to match the drop area to the surrounding Bootstrap interface. diff --git a/docs/docs/04-forms/file-input.mdx b/docs/docs/04-forms/file-input.mdx index 85a24f513..c68961db5 100644 --- a/docs/docs/04-forms/file-input.mdx +++ b/docs/docs/04-forms/file-input.mdx @@ -20,9 +20,13 @@ The Blazor Bootstrap `FileInput` component follows the standard Bootstrap file-c | AllowedFileTypes | IEnumerable<string> | null | Allowed file extensions. Values can use `.pdf` or `pdf` and match case-insensitively. An empty collection allows every extension. | 4.0.0 | | AriaLabel | string | Select files | Accessible label for the file control. | 4.0.0 | | HintText | string | null | Help text displayed below the component. | 4.0.0 | +| InvalidFileTypeErrorMessage | string | `{FileName}: the file extension is not allowed.` | Invalid-extension message template. Supports `{FileName}` and `{AllowedFileTypes}`. | 4.0.0 | | MaxFileCount | int | null | Maximum number of selected files. | 4.0.0 | +| MaxFileCountErrorMessage | string | `{FileName}: the maximum number of files has been reached.` | Maximum-count message template. Supports `{FileName}` and `{MaxFileCount}`. | 4.0.0 | | MaxFileSize | long | null | Maximum file size in bytes. | 4.0.0 | +| MaxFileSizeErrorMessage | string | `{FileName}: the file exceeds the maximum allowed size.` | Maximum-size message template. Supports `{FileName}` and `{MaxFileSize}`. | 4.0.0 | | Multiple | bool | false | Allows multiple files to be selected. | 4.0.0 | +| SingleFileErrorMessage | string | Only one file can be selected. | Single-file selection message template; no dynamic token applies. | 4.0.0 | | Size | FileInputSize | FileInputSize.Default | Applies Bootstrap `form-control-lg` or `form-control-sm` sizing. | 4.0.0 | ## Events @@ -72,6 +76,18 @@ Set `Multiple` to show every accepted file. Each file has its own keyboard-acces [See demo here](https://demos.blazorbootstrap.com/form/file-input#validation) +### Validation message templates + +Each validation message can be customized. The current messages remain the defaults. Use only the tokens that apply to the condition: `{FileName}` with file-specific errors, `{MaxFileSize}` for size errors, `{AllowedFileTypes}` for extension errors, and `{MaxFileCount}` for count errors. `SingleFileErrorMessage` is static and has no dynamic token. + +~~~cshtml showLineNumbers + +~~~ + ### Unrestricted extensions Use an empty `AllowedFileTypes` collection when every extension is permitted. Other rules, such as `MaxFileSize` and `MaxFileCount`, still apply. From 82f3fe6e1c2a79800ab1acb5b93c8d637dbdfcfb Mon Sep 17 00:00:00 2001 From: Vikram Reddy Date: Sun, 12 Jul 2026 23:19:13 +0530 Subject: [PATCH 11/13] Add new sections for file input demos Added a "Drag and Drop File Input" section with a link to `@DemoRouteConstants.Demos_URL_DragAndDropFileInput`, including an icon and a "New" badge. Added a "File Input" section with a link to `@DemoRouteConstants.Demos_URL_FileInput`, also including an icon and a "New" badge. --- .../Components/Pages/Home/Index.razor | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/BlazorBootstrap.Demo.RCL/Components/Pages/Home/Index.razor b/BlazorBootstrap.Demo.RCL/Components/Pages/Home/Index.razor index d7289291c..a785c2d7c 100644 --- a/BlazorBootstrap.Demo.RCL/Components/Pages/Home/Index.razor +++ b/BlazorBootstrap.Demo.RCL/Components/Pages/Home/Index.razor @@ -110,6 +110,11 @@

    Date Input

    + +

    Google Map

    From b912ea7911784ac0b8e22f9eded98fc1b75cbdea Mon Sep 17 00:00:00 2001 From: Vikram Reddy Date: Mon, 13 Jul 2026 12:47:25 +0530 Subject: [PATCH 12/13] Centralize validation styles and update SortableList Centralized validation styles into `blazor.bootstrap.css` using new CSS variables for outlines and messages. Removed inline styles from `.razor` files and replaced them with the new centralized classes. Updated `SortableList.razor` to use a new class `sortable-list-group-item` for better styling control. Adjusted related templates and CSS to reflect this change. --- .../AutoComplete_Demo_04_Validations.razor | 14 ------------- .../CurrencyInput_Demo_11_Validations.razor | 14 ------------- .../DateInput_Demo_05_Validations.razor | 14 ------------- ...d_On_The_Entry_In_Another_Date_Field.razor | 14 ------------- .../NumberInput_Demo_08_Validations.razor | 14 ------------- .../PasswordInput_Demo_03_Validations.razor | 14 ------------- .../Form/Switch/Switch_Demo_05_Form.razor | 14 ------------- .../TextAreaInput_Demo_05_Validations.razor | 14 ------------- .../TextInput_Demo_05_Validations.razor | 14 ------------- .../TimeInput_Demo_05_Validations.razor | 14 ------------- ...d_On_The_Entry_In_Another_Time_Field.razor | 14 ------------- .../SortableList/SortableList.razor | 10 +++++----- blazorbootstrap/wwwroot/blazor.bootstrap.css | 20 ++++++++++++++++++- 13 files changed, 24 insertions(+), 160 deletions(-) diff --git a/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Form/AutoComplete/AutoComplete_Demo_04_Validations.razor b/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Form/AutoComplete/AutoComplete_Demo_04_Validations.razor index 376eb2fe7..37b6a2291 100644 --- a/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Form/AutoComplete/AutoComplete_Demo_04_Validations.razor +++ b/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Form/AutoComplete/AutoComplete_Demo_04_Validations.razor @@ -1,19 +1,5 @@ @using System.ComponentModel.DataAnnotations - - diff --git a/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Form/CurrencyInput/CurrencyInput_Demo_11_Validations.razor b/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Form/CurrencyInput/CurrencyInput_Demo_11_Validations.razor index 44912cd46..fde5114b6 100644 --- a/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Form/CurrencyInput/CurrencyInput_Demo_11_Validations.razor +++ b/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Form/CurrencyInput/CurrencyInput_Demo_11_Validations.razor @@ -1,19 +1,5 @@ @using System.ComponentModel.DataAnnotations - - diff --git a/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Form/DateInput/DateInput_Demo_05_Validations.razor b/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Form/DateInput/DateInput_Demo_05_Validations.razor index e94f3616e..4992c4c27 100644 --- a/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Form/DateInput/DateInput_Demo_05_Validations.razor +++ b/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Form/DateInput/DateInput_Demo_05_Validations.razor @@ -1,19 +1,5 @@ @using System.ComponentModel.DataAnnotations - - diff --git a/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Form/DateInput/DateInput_Demo_07_Restrict_The_Date_Field_Based_On_The_Entry_In_Another_Date_Field.razor b/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Form/DateInput/DateInput_Demo_07_Restrict_The_Date_Field_Based_On_The_Entry_In_Another_Date_Field.razor index a00d2c9c6..294cc816c 100644 --- a/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Form/DateInput/DateInput_Demo_07_Restrict_The_Date_Field_Based_On_The_Entry_In_Another_Date_Field.razor +++ b/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Form/DateInput/DateInput_Demo_07_Restrict_The_Date_Field_Based_On_The_Entry_In_Another_Date_Field.razor @@ -1,19 +1,5 @@ @using System.ComponentModel.DataAnnotations - - diff --git a/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Form/NumberInput/NumberInput_Demo_08_Validations.razor b/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Form/NumberInput/NumberInput_Demo_08_Validations.razor index c0a09f30d..30eef6b86 100644 --- a/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Form/NumberInput/NumberInput_Demo_08_Validations.razor +++ b/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Form/NumberInput/NumberInput_Demo_08_Validations.razor @@ -1,19 +1,5 @@ @using System.ComponentModel.DataAnnotations - - diff --git a/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Form/PasswordInput/PasswordInput_Demo_03_Validations.razor b/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Form/PasswordInput/PasswordInput_Demo_03_Validations.razor index 92dd7d08b..f2b1b42b1 100644 --- a/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Form/PasswordInput/PasswordInput_Demo_03_Validations.razor +++ b/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Form/PasswordInput/PasswordInput_Demo_03_Validations.razor @@ -1,19 +1,5 @@ @using System.ComponentModel.DataAnnotations - - diff --git a/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Form/Switch/Switch_Demo_05_Form.razor b/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Form/Switch/Switch_Demo_05_Form.razor index 4c9fec7d7..3b8fe39ec 100644 --- a/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Form/Switch/Switch_Demo_05_Form.razor +++ b/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Form/Switch/Switch_Demo_05_Form.razor @@ -1,19 +1,5 @@ @using System.ComponentModel.DataAnnotations - - diff --git a/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Form/TextAreaInput/TextAreaInput_Demo_05_Validations.razor b/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Form/TextAreaInput/TextAreaInput_Demo_05_Validations.razor index 3c4c8f77d..d2f667ef2 100644 --- a/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Form/TextAreaInput/TextAreaInput_Demo_05_Validations.razor +++ b/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Form/TextAreaInput/TextAreaInput_Demo_05_Validations.razor @@ -1,19 +1,5 @@ @using System.ComponentModel.DataAnnotations - - diff --git a/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Form/TextInput/TextInput_Demo_05_Validations.razor b/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Form/TextInput/TextInput_Demo_05_Validations.razor index 9a6b848e4..ca2e58cf1 100644 --- a/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Form/TextInput/TextInput_Demo_05_Validations.razor +++ b/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Form/TextInput/TextInput_Demo_05_Validations.razor @@ -1,19 +1,5 @@ @using System.ComponentModel.DataAnnotations - - diff --git a/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Form/TimeInput/TimeInput_Demo_05_Validations.razor b/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Form/TimeInput/TimeInput_Demo_05_Validations.razor index d5f0b6901..3f72576f9 100644 --- a/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Form/TimeInput/TimeInput_Demo_05_Validations.razor +++ b/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Form/TimeInput/TimeInput_Demo_05_Validations.razor @@ -1,19 +1,5 @@ @using System.ComponentModel.DataAnnotations - - diff --git a/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Form/TimeInput/TimeInput_Demo_07_Restrict_The_Time_Field_Based_On_The_Entry_In_Another_Time_Field.razor b/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Form/TimeInput/TimeInput_Demo_07_Restrict_The_Time_Field_Based_On_The_Entry_In_Another_Time_Field.razor index 9a97fff3f..24f51cd3a 100644 --- a/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Form/TimeInput/TimeInput_Demo_07_Restrict_The_Time_Field_Based_On_The_Entry_In_Another_Time_Field.razor +++ b/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Form/TimeInput/TimeInput_Demo_07_Restrict_The_Time_Field_Based_On_The_Entry_In_Another_Time_Field.razor @@ -1,19 +1,5 @@ @using System.ComponentModel.DataAnnotations - - diff --git a/blazorbootstrap/Components/SortableList/SortableList.razor b/blazorbootstrap/Components/SortableList/SortableList.razor index 720146a57..a9ee343cc 100644 --- a/blazorbootstrap/Components/SortableList/SortableList.razor +++ b/blazorbootstrap/Components/SortableList/SortableList.razor @@ -7,7 +7,7 @@ { if (LoadingTemplate is not null) { -
    @LoadingTemplate
    +
    @LoadingTemplate
    } else { @@ -23,11 +23,11 @@ var disableItem = DisableItem?.Invoke(item) ?? false; if (disableItem) // disable item { -
    @ItemTemplate(item)
    +
    @ItemTemplate(item)
    } else { -
    @ItemTemplate(item)
    +
    @ItemTemplate(item)
    } } } @@ -36,11 +36,11 @@ { if (EmptyDataTemplate is not null) { -
    @EmptyDataTemplate
    +
    @EmptyDataTemplate
    } else { -
    @EmptyText
    +
    @EmptyText
    } }
    diff --git a/blazorbootstrap/wwwroot/blazor.bootstrap.css b/blazorbootstrap/wwwroot/blazor.bootstrap.css index 0e43e9aad..dd2bb6a6f 100644 --- a/blazorbootstrap/wwwroot/blazor.bootstrap.css +++ b/blazorbootstrap/wwwroot/blazor.bootstrap.css @@ -60,6 +60,11 @@ /* callout */ --bb-callout-link: 10, 88, 202; --bb-callout-code-color: #ab296a; + /* validation */ + --bb-validation-outline-width: 1px; + --bb-validation-valid-outline-color: #26b050; + --bb-validation-invalid-outline-color: red; + --bb-validation-message-color: red; /* sidebar */ --bb-violet: #9461fb; --bb-violet-bg: #712cf9; @@ -212,6 +217,19 @@ table button.dropdown-toggle.bb-grid-filter::after { padding: .375rem; } +/* forms - validation */ +.valid.modified:not([type=checkbox]) { + outline: var(--bb-validation-outline-width) solid var(--bb-validation-valid-outline-color); +} + +.invalid { + outline: var(--bb-validation-outline-width) solid var(--bb-validation-invalid-outline-color); +} + +.validation-message { + color: var(--bb-validation-message-color); +} + /* split view */ .bb-split-view { display: flex; @@ -813,7 +831,7 @@ main { } /* sortable-list */ -.list-group-item:not(:has(.bb-sortable-list-handle)) { +.sortable-list-group-item:not(:has(.bb-sortable-list-handle)) { cursor: grab !important; } From 429f55eeefd3cf81cf8738d9168034738c2066d1 Mon Sep 17 00:00:00 2001 From: Vikram Reddy Date: Mon, 13 Jul 2026 12:55:51 +0530 Subject: [PATCH 13/13] Use dynamic Bootstrap variables for validation colors Replaced hardcoded validation color values with dynamic `rgba` values based on Bootstrap's `--bs-success-rgb`, `--bs-danger-rgb`, and the new `--bs-text-opacity` variable. This enhances consistency and supports easier theme customization. --- blazorbootstrap/wwwroot/blazor.bootstrap.css | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/blazorbootstrap/wwwroot/blazor.bootstrap.css b/blazorbootstrap/wwwroot/blazor.bootstrap.css index dd2bb6a6f..4df091d2b 100644 --- a/blazorbootstrap/wwwroot/blazor.bootstrap.css +++ b/blazorbootstrap/wwwroot/blazor.bootstrap.css @@ -61,10 +61,11 @@ --bb-callout-link: 10, 88, 202; --bb-callout-code-color: #ab296a; /* validation */ + --bs-text-opacity: 1; --bb-validation-outline-width: 1px; - --bb-validation-valid-outline-color: #26b050; - --bb-validation-invalid-outline-color: red; - --bb-validation-message-color: red; + --bb-validation-valid-outline-color: rgba(var(--bs-success-rgb), var(--bs-text-opacity)); + --bb-validation-invalid-outline-color: rgba(var(--bs-danger-rgb), var(--bs-text-opacity)); + --bb-validation-message-color: rgba(var(--bs-danger-rgb), var(--bs-text-opacity)); /* sidebar */ --bb-violet: #9461fb; --bb-violet-bg: #712cf9;