diff --git a/BlazorBootstrap.Demo.RCL/Components/Layout/DemosMainLayout.razor.cs b/BlazorBootstrap.Demo.RCL/Components/Layout/DemosMainLayout.razor.cs index d1e6f1d92..e6b838d5b 100644 --- a/BlazorBootstrap.Demo.RCL/Components/Layout/DemosMainLayout.razor.cs +++ b/BlazorBootstrap.Demo.RCL/Components/Layout/DemosMainLayout.razor.cs @@ -22,6 +22,8 @@ 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 = "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 9df45dc94..4d441e1ad 100644 --- a/BlazorBootstrap.Demo.RCL/Components/Layout/DocsMainLayout.razor.cs +++ b/BlazorBootstrap.Demo.RCL/Components/Layout/DocsMainLayout.razor.cs @@ -21,6 +21,8 @@ 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 = "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 0a5391509..1402be6ea 100644 --- a/BlazorBootstrap.Demo.RCL/Components/Layout/MainLayout.razor.cs +++ b/BlazorBootstrap.Demo.RCL/Components/Layout/MainLayout.razor.cs @@ -22,6 +22,8 @@ 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 = "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/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/DragAndDropFileInput/DragAndDropFileInputDocumentation.razor b/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Form/DragAndDropFileInput/DragAndDropFileInputDocumentation.razor new file mode 100644 index 000000000..d15a2f0ce --- /dev/null +++ b/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Form/DragAndDropFileInput/DragAndDropFileInputDocumentation.razor @@ -0,0 +1,37 @@ +@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. Configure validation-message templates with {FileName}, {MaxFileSize}, {AllowedFileTypes}, or {MaxFileCount} where applicable; the single-file message is static.
+ +
+ +
+
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"; + 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..ad0490057 --- /dev/null +++ b/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Form/DragAndDropFileInput/DragAndDropFileInput_Demo_02_Validation.razor @@ -0,0 +1,13 @@ + + +@code { + private readonly string[] allowedFileTypes = [".pdf", "png"]; +} 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 new file mode 100644 index 000000000..10e6abf37 --- /dev/null +++ b/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Form/FileInput/FileInputDocumentation.razor @@ -0,0 +1,52 @@ +@page "/file-input" + +@attribute [Route(pageUrl)] +@layout DemosMainLayout + + + + + +
+
FileInput uses Bootstrap's standard visible file-control presentation. Use DragAndDropFileInput when a drop area is needed.
+ +
+ +
+
Set Multiple to allow a selected file to be removed without clearing the others.
+ +
+ +
+
Use Size to apply Bootstrap's form-control-lg and form-control-sm file-control sizes.
+ +
+ +
+
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.
+ +
+ +
+
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.
+ +
+ +@code { + private const string pageUrl = DemoRouteConstants.Demos_URL_FileInput; + private const string pageTitle = "Blazor FileInput"; + 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 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 new file mode 100644 index 000000000..5ac7324fe --- /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..42e456842 --- /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..0278bb949 --- /dev/null +++ b/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Form/FileInput/FileInput_Demo_04_Validation.razor @@ -0,0 +1,13 @@ + + +@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/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.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.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/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/Components/Pages/Home/Index.razor b/BlazorBootstrap.Demo.RCL/Components/Pages/Home/Index.razor index bc6747647..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

+ + + +

Number Input

diff --git a/BlazorBootstrap.Demo.RCL/Constants/DemoRouteConstants.cs b/BlazorBootstrap.Demo.RCL/Constants/DemoRouteConstants.cs index 3dccaee09..ece6c25ad 100644 --- a/BlazorBootstrap.Demo.RCL/Constants/DemoRouteConstants.cs +++ b/BlazorBootstrap.Demo.RCL/Constants/DemoRouteConstants.cs @@ -30,7 +30,9 @@ 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"; public const string Demos_URL_OTPInput = Demos_URL_Forms_Prefix + "/otp-input"; public const string Demos_URL_PasswordInput = Demos_URL_Forms_Prefix + "/password-input"; @@ -144,7 +146,9 @@ 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"; 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/DragAndDropFileInput/DragAndDropFileInput.razor b/blazorbootstrap/Components/Form/DragAndDropFileInput/DragAndDropFileInput.razor new file mode 100644 index 000000000..6206d7c19 --- /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..1a4cc0f7a --- /dev/null +++ b/blazorbootstrap/Components/Form/DragAndDropFileInput/DragAndDropFileInput.razor.cs @@ -0,0 +1,55 @@ +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 + + /// + /// 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), + (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..16dfd3ba7 --- /dev/null +++ b/blazorbootstrap/Components/Form/DragAndDropFileInput/DragAndDropFileInput.razor.css @@ -0,0 +1,43 @@ +.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); +} + +::deep .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; +} diff --git a/blazorbootstrap/Components/Form/FileInput/FileInput.razor b/blazorbootstrap/Components/Form/FileInput/FileInput.razor new file mode 100644 index 000000000..cbd8cab3e --- /dev/null +++ b/blazorbootstrap/Components/Form/FileInput/FileInput.razor @@ -0,0 +1,40 @@ +@namespace BlazorBootstrap +@inherits FileInputBase + +
+ + + @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..6d0c1c891 --- /dev/null +++ b/blazorbootstrap/Components/Form/FileInput/FileInput.razor.cs @@ -0,0 +1,22 @@ +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), + (Size.ToFileInputSizeClass(), Size != FileInputSize.Default)); + + #endregion +} diff --git a/blazorbootstrap/Components/Form/FileInput/FileInputBase.cs b/blazorbootstrap/Components/Form/FileInput/FileInputBase.cs new file mode 100644 index 000000000..b292199c1 --- /dev/null +++ b/blazorbootstrap/Components/Form/FileInput/FileInputBase.cs @@ -0,0 +1,227 @@ +namespace BlazorBootstrap; + +public abstract class FileInputBase : BlazorBootstrapComponentBase +{ + #region Fields and Constants + + private const string AllowedFileTypesToken = "{AllowedFileTypes}"; + private const string DefaultInvalidFileTypeErrorMessage = "{FileName}: the file extension is not allowed."; + private const string DefaultMaxFileCountErrorMessage = "{FileName}: the maximum number of files has been reached."; + private const string DefaultMaxFileSizeErrorMessage = "{FileName}: the file exceeds the maximum allowed size."; + private const string DefaultSingleFileErrorMessage = "Only one file can be selected."; + private const string FileNameToken = "{FileName}"; + private const string MaxFileCountToken = "{MaxFileCount}"; + private const string MaxFileSizeToken = "{MaxFileSize}"; + + 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 FormatValidationMessage(MaxFileSizeErrorMessage, + (FileNameToken, file.Name), + (MaxFileSizeToken, MaxFileSize.Value.ToString())); + + if (NormalizedAllowedFileTypes.Count > 0 && !NormalizedAllowedFileTypes.Contains(NormalizeExtension(Path.GetExtension(file.Name)))) + return FormatValidationMessage(InvalidFileTypeErrorMessage, + (AllowedFileTypesToken, Accept), + (FileNameToken, file.Name)); + + 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(SingleFileErrorMessage); + } + + foreach (var selectedFile in selectedFiles) + { + if (MaxFileCount.HasValue && files.Count >= MaxFileCount.Value) + { + validationErrors.Add(FormatValidationMessage(MaxFileCountErrorMessage, + (FileNameToken, selectedFile.Name), + (MaxFileCountToken, MaxFileCount.Value.ToString()))); + 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 + + #region Private Methods + + private static string FormatValidationMessage(string template, params (string Token, string Value)[] tokens) + { + foreach (var token in tokens) + template = template.Replace(token.Token, token.Value, StringComparison.Ordinal); + + return template; + } + + #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"; + + /// + /// 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 invalid file type validation message template. + /// Supported tokens: {FileName} and {AllowedFileTypes}. + /// + [AddedVersion("4.0.0")] + [DefaultValue(DefaultInvalidFileTypeErrorMessage)] + [Description("Gets or sets the invalid file type validation message template.")] + [Parameter] + public string InvalidFileTypeErrorMessage { get; set; } = DefaultInvalidFileTypeErrorMessage; + + /// + /// 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 file count validation message template. + /// Supported tokens: {FileName} and {MaxFileCount}. + /// + [AddedVersion("4.0.0")] + [DefaultValue(DefaultMaxFileCountErrorMessage)] + [Description("Gets or sets the maximum file count validation message template.")] + [Parameter] + public string MaxFileCountErrorMessage { get; set; } = DefaultMaxFileCountErrorMessage; + + /// + /// 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 the maximum file size validation message template. + /// Supported tokens: {FileName} and {MaxFileSize}. + /// + [AddedVersion("4.0.0")] + [DefaultValue(DefaultMaxFileSizeErrorMessage)] + [Description("Gets or sets the maximum file size validation message template.")] + [Parameter] + public string MaxFileSizeErrorMessage { get; set; } = DefaultMaxFileSizeErrorMessage; + + /// + /// 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 single-file selection validation message template. + /// + [AddedVersion("4.0.0")] + [DefaultValue(DefaultSingleFileErrorMessage)] + [Description("Gets or sets the single-file selection validation message template.")] + [Parameter] + public string SingleFileErrorMessage { get; set; } = DefaultSingleFileErrorMessage; + + #endregion +} 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/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 { diff --git a/blazorbootstrap/wwwroot/blazor.bootstrap.css b/blazorbootstrap/wwwroot/blazor.bootstrap.css index 0e43e9aad..4df091d2b 100644 --- a/blazorbootstrap/wwwroot/blazor.bootstrap.css +++ b/blazorbootstrap/wwwroot/blazor.bootstrap.css @@ -60,6 +60,12 @@ /* callout */ --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: 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; @@ -212,6 +218,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 +832,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; } 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..e0a0e8c58 --- /dev/null +++ b/docs/docs/04-forms/drag-and-drop-file-input.mdx @@ -0,0 +1,91 @@ +--- +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 | +| 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 + +| 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) + +### 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. + +```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 new file mode 100644 index 000000000..c68961db5 --- /dev/null +++ b/docs/docs/04-forms/file-input.mdx @@ -0,0 +1,121 @@ +--- +title: Blazor File Input Component +description: The Blazor Bootstrap FileInput component provides a standard Bootstrap file control with validation, feedback, and removal. +sidebar_label: File Input +sidebar_position: 6 +--- + +import CarbonAd from '/carbon-ad.mdx' + +# Blazor FileInput + +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. + + + +## 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 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 + +| Name | Description | Added Version | +|:--|:--|:--| +| FilesChanged | Fires when the accepted selected-file list changes. | 4.0.0 | + +## Examples + +### Basic usage + +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) + +### 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) + +### 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. + +```cshtml showLineNumbers + + +@code { + private readonly string[] allowedExtensions = []; +} +``` + +[See demo here](https://demos.blazorbootstrap.com/form/file-input#hint-text-and-unrestricted-extensions) + +### Sizes + +Use `Size` to apply Bootstrap's `form-control-lg` or `form-control-sm` classes. + +```cshtml showLineNumbers + +``` + +[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. For drag-and-drop with a touch-friendly native-picker fallback, use [DragAndDropFileInput](./drag-and-drop-file-input).