diff --git a/src/BlazorUI/Bit.BlazorUI/Components/Inputs/FileInput/BitFileInput.razor b/src/BlazorUI/Bit.BlazorUI/Components/Inputs/FileInput/BitFileInput.razor
index c303c838653..42155a74e64 100644
--- a/src/BlazorUI/Bit.BlazorUI/Components/Inputs/FileInput/BitFileInput.razor
+++ b/src/BlazorUI/Bit.BlazorUI/Components/Inputs/FileInput/BitFileInput.razor
@@ -13,14 +13,30 @@
}
else if (HideLabel is false)
{
-
}
@@ -56,7 +72,16 @@
var removeTitle = RemoveButtonTitle ?? "Remove";
var removeIcon = ShowRemoveButton ? BitIconInfo.From(RemoveButtonIcon, RemoveButtonIconName ?? "Delete") : null;
-
+ @* the list is named so that it is announced as the selected files rather than as one more
+ unlabelled list of the page when a screen reader user walks the landmarks and the lists. *@
+ @* a file list given a ceiling scrolls, and a scrollable box a keyboard has no way into is
+ unreachable for anyone who cannot use a pointer. The remove buttons are that way in whenever
+ they are rendered; without them - and without a template that brings its own - the list takes
+ the tab stop itself so the arrow keys can scroll it. *@
+
@foreach (var file in Files)
{
if (FileViewTemplate is not null)
@@ -68,45 +93,83 @@
}
else
{
+ var folder = GetFolder(file);
+
- @if (ShowPreview && file.PreviewUrl is not null)
+ @if (ShowPreview)
{
- @* draggable is off so that dragging a thumbnail does not start a drag over the drop zone itself.
- the alt is empty on purpose: the thumbnail is decorative and its name is already
- right next to it, so announcing it again would just repeat the same text. *@
-
+ if (file.PreviewUrl is not null)
+ {
+ @* draggable is off so that dragging a thumbnail does not start a drag over the drop zone itself.
+ the alt is empty on purpose: the thumbnail is decorative and its name is already
+ right next to it, so announcing it again would just repeat the same text. *@
+ @* lazily, since a folder selection can hold thousands of images and only
+ the handful in view is worth decoding. *@
+
+ }
+ else
+ {
+ var fileIcon = GetFileIcon(file);
+
+ @* the glyph stands in the thumbnail's slot for everything that has no preview, so the
+ names of a mixed list stay in one column instead of stepping in and out. *@
+ if (fileIcon is not null)
+ {
+
+
+
+ }
+ }
}
-
+
+ @* the extension is split off so that it survives the ellipsis: a long name
+ truncated as one string ends in "..." and stops saying what kind of file it is,
+ which is the one thing a glance at a file list is for. *@
@(FileSizeFormatter is null ? FileSizeHumanizer.Humanize(file.Size) : FileSizeFormatter(file.Size))
+ @if (folder.HasValue())
+ {
+ @* a folder selection flattens into one list, where two files can only be told
+ apart by where they came from, so the folder is printed beside the size. *@
+ @folder
+ }
@if (file.IsValid is false)
{
-
+
@file.Message
}
@if (ShowRemoveButton)
{
-
-
+
- Color picks the theme color of the browse button and the drag-and-drop indicator, with
- Primary as the default. The semantic colors Success, Warning, Error and
- friends tie the input to what the selection means, while the Background, Foreground
- and Border families keep it legible on non-default surfaces like the inverted panel below.
+ ShowDropZone renders the browse button as a full-width dashed panel with a glyph, still reached with
+ Tab and opened with Enter or Space, and defaults it to Outline. DropZoneIconName swaps the
+ CloudUpload glyph (an empty string removes it); DropZoneIcon takes an external one.
+
+
+
+
+
+
+
+
+
+
+
+
+
+ BitParams hands a BitFileInputParams to every file input under it. Its values are defaults,
+ not overrides: an input keeps each parameter it sets itself.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Color sets the theme color of the browse button, its focus ring and the drop indicator
+ (Primary by default).
@@ -620,13 +518,10 @@
-
+
- The remove button icon is not limited to the built-in icon font: RemoveButtonIcon takes a
- BitIconInfo or a raw CSS class string that can point at any icon library whose stylesheet is
- loaded, like FontAwesome or Bootstrap Icons below. Use BitIconInfo.Fa(), BitIconInfo.Bi()
- or BitIconInfo.Css() to build one, or stick with the built-in font and just pick a different
- icon via RemoveButtonIconName.
+ RemoveButtonIcon, and whatever FileIconSelector returns, accept any icon library through
+ BitIconInfo.Fa(), BitIconInfo.Bi() or BitIconInfo.Css().
@@ -641,8 +536,9 @@
-
+
@@ -656,37 +552,33 @@
-
+
-
+
- Size scales the browse button, the file list items and their preview thumbnails together:
- Small for dense layouts, Medium (the default) for regular forms, and Large where
- the input is a primary control or aimed at touch.
+ Size scales the browse button, the file items, their thumbnails and their remove buttons together.
Small:
-
+
Medium:
-
+
Large:
-
+
-
+
- Style and Class land on the root element, which inline styles and a single class cover
- well. For anything deeper, Styles and Classes reach the individual parts the browse
- button, the file list, each file item, its name, size, error message and remove button and their
- Dragging entry applies to the root element only while files are being dragged over the
- component, which is how the examples below restyle the drop state without touching the idle look.
+ Style and Class target the root, Styles and Classes each part. Their
+ Dragging entry applies to the root only while files hover over it.
+ The public CSS variables inherit, so they can be set on
+ :root, on an ancestor or on the Style of one instance.
+
+
+
CSS variables:
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Set once on an ancestor, inherited by every file input inside it:
+
+
+
+
+
-
+
- Setting Dir to BitDir.Rtl mirrors the whole layout for right-to-left languages the
- browse button, the file names, sizes and remove buttons all follow so the BitFileInput reads
- naturally inside an RTL page like the Farsi example below.
+ Setting Dir to BitDir.Rtl mirrors the whole layout.
-
+
diff --git a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Inputs/FileInput/BitFileInputDemo.razor.cs b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Inputs/FileInput/BitFileInputDemo.razor.cs
index 4515b3155f0..7a6290368e5 100644
--- a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Inputs/FileInput/BitFileInputDemo.razor.cs
+++ b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Inputs/FileInput/BitFileInputDemo.razor.cs
@@ -23,7 +23,7 @@ public partial class BitFileInputDemo
Name = "AllowDuplicates",
Type = "bool",
DefaultValue = "true",
- Description = "Whether a file that is already in the file list can be selected again. When disabled, a newly selected file matching an existing one by name, size and last modified time is marked as invalid with the DuplicateErrorMessage instead of being added as a second entry, becoming valid again once the file it duplicates is removed.",
+ Description = "Whether a file that is already in the file list can be selected again. When disabled, a newly selected file matching an existing one by folder, name, size and last modified time is marked as invalid with the DuplicateErrorMessage instead of being added as a second entry, becoming valid again once the file it duplicates is removed.",
},
new()
{
@@ -109,6 +109,22 @@ public partial class BitFileInputDemo
Description = "Whether to select folders (directories) instead of files, rendered as the webkitdirectory attribute. All files inside the selected folder and its subfolders will be added to the file list. It also makes a dropped folder expand into its contents instead of being ignored."
},
new()
+ {
+ Name = "DropZoneIcon",
+ Type = "BitIconInfo?",
+ DefaultValue = "null",
+ Description = "The glyph of the drop zone panel, using custom CSS classes for external icon libraries. Takes precedence over DropZoneIconName when both are set, and is only rendered while ShowDropZone is enabled.",
+ LinkType = LinkType.Link,
+ Href = "#bit-icon-info"
+ },
+ new()
+ {
+ Name = "DropZoneIconName",
+ Type = "string?",
+ DefaultValue = "null",
+ Description = "The name of the drop zone panel's glyph from the built-in Fluent UI icons. Defaults to \"CloudUpload\", and an empty string leaves the panel without a glyph at all."
+ },
+ new()
{
Name = "DuplicateErrorMessage",
Type = "string?",
@@ -117,12 +133,19 @@ public partial class BitFileInputDemo
},
new()
{
- Name = "FileValidator",
- Type = "Func?",
+ Name = "FileIconSelector",
+ Type = "Func?",
DefaultValue = "null",
- Description = "Custom validation function called for each newly selected file after the built-in validations pass. Return an error message to mark the file as invalid, or null to accept it.",
+ Description = "Custom provider of the glyph shown in the thumbnail's place for a file that has no image preview, which is rendered while ShowPreview is enabled. Receives the file and returns the icon to draw, or null to leave that file without one. When not set, the icon is picked from the file's MIME type and extension.",
LinkType = LinkType.Link,
- Href = "#file-input-info"
+ Href = "#bit-icon-info"
+ },
+ new()
+ {
+ Name = "FileListAriaLabel",
+ Type = "string?",
+ DefaultValue = "null",
+ Description = "The accessible name of the file list, which tells a screen reader user walking the lists of the page what this one holds. Defaults to \"Selected files\"."
},
new()
{
@@ -132,6 +155,15 @@ public partial class BitFileInputDemo
Description = "Custom formatter of the file size shown under the name of each file item. Receives the size of the file in bytes and returns the text to display, which is the place to localize the units or to switch between the binary and the decimal bases. When not set, a built-in humanizer is used.",
},
new()
+ {
+ Name = "FileValidator",
+ Type = "Func?",
+ DefaultValue = "null",
+ Description = "Custom validation function called for each newly selected file after the built-in validations pass. Return an error message to mark the file as invalid, or null to accept it.",
+ LinkType = LinkType.Link,
+ Href = "#file-input-info"
+ },
+ new()
{
Name = "FileViewTemplate",
Type = "RenderFragment?",
@@ -297,11 +329,18 @@ public partial class BitFileInputDemo
Description = "The tooltip of the remove button, which is also used as the prefix of its accessible label (e.g., \"Remove report.pdf\"). Defaults to \"Remove\"."
},
new()
+ {
+ Name = "ShowDropZone",
+ Type = "bool",
+ DefaultValue = "false",
+ Description = "Whether to render the browse area as a full width drop zone panel - a dashed rule around a glyph and the label - instead of an ordinary button. It is the same button underneath, so it is still reached with Tab and activated with Enter or Space, and it carries the drag indicator exactly as the button does. It also makes Outline the default variant; set Variant to take that back."
+ },
+ new()
{
Name = "ShowPreview",
Type = "bool",
DefaultValue = "false",
- Description = "Whether to display a preview thumbnail for image files in the file list."
+ Description = "Whether to display a preview thumbnail for image files in the file list, and a file type glyph in the same place for every file that has no preview."
},
new()
{
@@ -329,6 +368,13 @@ public partial class BitFileInputDemo
Href = "#class-styles"
},
new()
+ {
+ Name = "Title",
+ Type = "string?",
+ DefaultValue = "null",
+ Description = "The tooltip of the browse button, rendered as its title attribute."
+ },
+ new()
{
Name = "Variant",
Type = "BitVariant?",
@@ -367,9 +413,18 @@ public partial class BitFileInputDemo
new()
{
Name = "ReadContentAsync",
- Type = "(BitFileInputInfo? fileInfo = null) => Task",
+ Type = "(BitFileInputInfo? fileInfo = null, CancellationToken cancellationToken = default) => Task",
DefaultValue = "",
- Description = "Reads the content of the specified file from the browser and populates its Content property with the byte array, or reads every valid file of the file list when no file is specified. Only reads valid files and only while the component is enabled.",
+ Description = "Reads the content of the specified file from the browser and populates its Content property with the byte array, or reads every valid file of the file list when no file is specified. Only reads valid files and only while the component is enabled. The whole file crosses the interop boundary as one message, which on Blazor Server the circuit caps (SignalR's MaximumReceiveMessageSize, 32 KB by default), so anything larger is read with OpenReadStreamAsync instead.",
+ LinkType = LinkType.Link,
+ Href = "#file-input-info"
+ },
+ new()
+ {
+ Name = "OpenReadStreamAsync",
+ Type = "(BitFileInputInfo fileInfo, long? maxAllowedSize = null, CancellationToken cancellationToken = default) => Task",
+ DefaultValue = "",
+ Description = "Opens a forward-only stream over the content of the specified file, which the runtime reads from the browser in chunks instead of materializing the whole file in memory the way ReadContentAsync does - which is also what gets a file past a Blazor Server circuit's message size cap. Unlike ReadContentAsync it also reads a file the validations rejected. maxAllowedSize defaults to the size the browser reported for the file, and the stream must be disposed by the caller.",
LinkType = LinkType.Link,
Href = "#file-input-info"
},
@@ -415,6 +470,13 @@ public partial class BitFileInputDemo
Description = "The name of the file including its extension (e.g., \"document.pdf\")."
},
new()
+ {
+ Name = "RelativePath",
+ Type = "string",
+ DefaultValue = "string.Empty",
+ Description = "The path of the file relative to the selected folder, including the folder's own name (e.g., \"photos/2024/summer.jpg\"). It is only reported by the browser for a folder selection or a dropped folder, and is an empty string for a file picked or dropped on its own."
+ },
+ new()
{
Name = "Size",
Type = "long",
@@ -440,6 +502,12 @@ public partial class BitFileInputDemo
Description = "The last modified time of the file reported by the browser, in milliseconds since the Unix epoch."
},
new()
+ {
+ Name = "Extension",
+ Type = "string",
+ Description = "The extension of the file including its leading dot, lowercased (e.g. \".pdf\"), or an empty string for a file whose name carries none, a dotfile (\".gitignore\") included."
+ },
+ new()
{
Name = "LastModifiedDate",
Type = "DateTimeOffset",
@@ -517,6 +585,13 @@ public partial class BitFileInputDemo
Description = "Custom CSS classes/styles for the browse button (label) of the BitFileInput."
},
new()
+ {
+ Name = "DropZoneIcon",
+ Type = "string?",
+ DefaultValue = "null",
+ Description = "Custom CSS classes/styles for the glyph of the drop zone panel, which is only rendered while ShowDropZone is enabled."
+ },
+ new()
{
Name = "Description",
Type = "string?",
@@ -545,6 +620,13 @@ public partial class BitFileInputDemo
Description = "Custom CSS classes/styles for the image preview thumbnail of each file item of the BitFileInput."
},
new()
+ {
+ Name = "FileIcon",
+ Type = "string?",
+ DefaultValue = "null",
+ Description = "Custom CSS classes/styles for the file type glyph shown in the thumbnail's place of each file item that has no image preview."
+ },
+ new()
{
Name = "FileName",
Type = "string?",
@@ -552,6 +634,13 @@ public partial class BitFileInputDemo
Description = "Custom CSS classes/styles for the file name of each file item of the BitFileInput."
},
new()
+ {
+ Name = "FilePath",
+ Type = "string?",
+ DefaultValue = "null",
+ Description = "Custom CSS classes/styles for the folder of each file item that came from a folder selection."
+ },
+ new()
{
Name = "FileSize",
Type = "string?",
@@ -668,6 +757,294 @@ public partial class BitFileInputDemo
+ private readonly List componentCssVariables =
+ [
+ new()
+ {
+ Name = "--bit-FileInput-max-width",
+ DefaultValue = "21.875rem",
+ Description = "The widest the whole component gets. Set it to 100% for a file input that fills its column.",
+ },
+ new()
+ {
+ Name = "--bit-FileInput-color",
+ DefaultValue = "The Color role's main color",
+ Description = "The role color: the fill of a Fill browse button, the rule and the text of an Outline or Text one.",
+ },
+ new()
+ {
+ Name = "--bit-FileInput-text-color",
+ DefaultValue = "The Color role's on-color",
+ Description = "Text drawn on top of the role color.",
+ },
+ new()
+ {
+ Name = "--bit-FileInput-hover-color",
+ DefaultValue = "The Color role's hover color",
+ Description = "Role color while the browse button is hovered (pointer devices only).",
+ },
+ new()
+ {
+ Name = "--bit-FileInput-active-color",
+ DefaultValue = "The Color role's active color",
+ Description = "Role color while the browse button is pressed.",
+ },
+ new()
+ {
+ Name = "--bit-FileInput-focus-color",
+ DefaultValue = "The Color role's focus color",
+ Description = "Color of the keyboard focus ring of the browse button and of each remove button.",
+ },
+ new()
+ {
+ Name = "--bit-FileInput-disabled-color",
+ DefaultValue = "--bit-clr-fg-dis",
+ Description = "Foreground when IsEnabled is false.",
+ },
+ new()
+ {
+ Name = "--bit-FileInput-disabled-background",
+ DefaultValue = "--bit-clr-bg-dis",
+ Description = "Background of the browse button and of the remove buttons when IsEnabled is false.",
+ },
+ new()
+ {
+ Name = "--bit-FileInput-disabled-border-color",
+ DefaultValue = "--bit-clr-brd-dis",
+ Description = "Border color of the browse button when IsEnabled is false.",
+ },
+ new()
+ {
+ Name = "--bit-FileInput-label-height",
+ DefaultValue = "--bit-siz-ctrl-sm / -md / -lg per Size",
+ Description = "Smallest height of the browse button, which is what lines it up with the other controls of its size.",
+ },
+ new()
+ {
+ Name = "--bit-FileInput-label-padding",
+ DefaultValue = "--bit-siz-ctrl-pad-y-* --bit-siz-ctrl-pad-x-* per Size",
+ Description = "Padding of the browse button.",
+ },
+ new()
+ {
+ Name = "--bit-FileInput-label-font-size",
+ DefaultValue = "Per Size, from the type ramp",
+ Description = "Text size of the browse button.",
+ },
+ new()
+ {
+ Name = "--bit-FileInput-label-font-weight",
+ DefaultValue = "--bit-tpg-font-weight",
+ Description = "Text weight of the browse button, which defaults to the weight the theme gives every control label.",
+ },
+ new()
+ {
+ Name = "--bit-FileInput-label-radius",
+ DefaultValue = "--bit-shp-radius-button",
+ Description = "Corner radius of the browse button, which its focus ring follows.",
+ },
+ new()
+ {
+ Name = "--bit-FileInput-drop-zone-height",
+ DefaultValue = "Per Size, 4.5rem / 5.5rem / 6.5rem",
+ Description = "The smallest height of the drop zone panel rendered by ShowDropZone.",
+ },
+ new()
+ {
+ Name = "--bit-FileInput-drop-zone-padding",
+ DefaultValue = "Per Size, from the spacing rhythm",
+ Description = "Padding of the drop zone panel.",
+ },
+ new()
+ {
+ Name = "--bit-FileInput-drop-zone-radius",
+ DefaultValue = "--bit-shp-radius-surface",
+ Description = "Corner radius of the drop zone panel, which its focus ring follows.",
+ },
+ new()
+ {
+ Name = "--bit-FileInput-drop-zone-border-width",
+ DefaultValue = "--bit-shp-brd-width-thick",
+ Description = "Rule thickness of the drop zone panel.",
+ },
+ new()
+ {
+ Name = "--bit-FileInput-drop-zone-border-style",
+ DefaultValue = "dashed",
+ Description = "Rule style of the drop zone panel at rest. Set it to solid for a panel that reads as a surface rather than as a target.",
+ },
+ new()
+ {
+ Name = "--bit-FileInput-drop-zone-icon-size",
+ DefaultValue = "Per Size, 1.5rem / 2rem / 2.5rem",
+ Description = "Glyph size inside the drop zone panel.",
+ },
+ new()
+ {
+ Name = "--bit-FileInput-description-color",
+ DefaultValue = "--bit-clr-fg-sec",
+ Description = "Color of the hint under the browse button.",
+ },
+ new()
+ {
+ Name = "--bit-FileInput-description-font-size",
+ DefaultValue = "Per Size, from the type ramp",
+ Description = "Text size of that hint, of the file size and of the folder.",
+ },
+ new()
+ {
+ Name = "--bit-FileInput-drop-color",
+ DefaultValue = "--bit-FileInput-text-color",
+ Description = "Rule and text of the browse button while files are dragged over the component.",
+ },
+ new()
+ {
+ Name = "--bit-FileInput-drop-background",
+ DefaultValue = "--bit-FileInput-hover-color",
+ Description = "Fill of the browse button while files are dragged over the component.",
+ },
+ new()
+ {
+ Name = "--bit-FileInput-drop-border-style",
+ DefaultValue = "dashed",
+ Description = "Border style of the drop indicator. Set it to solid for a drop state that does not change the shape of the button.",
+ },
+ new()
+ {
+ Name = "--bit-FileInput-file-list-max-height",
+ DefaultValue = "none",
+ Description = "The tallest the file list gets before it scrolls, which is what keeps a folder selection of thousands of files from pushing the rest of the page away.",
+ },
+ new()
+ {
+ Name = "--bit-FileInput-item-background",
+ DefaultValue = "--bit-clr-bg-sec",
+ Description = "Background of a file item, valid or not.",
+ },
+ new()
+ {
+ Name = "--bit-FileInput-item-hover-background",
+ DefaultValue = "--bit-FileInput-item-background",
+ Description = "Background of a hovered valid file item (pointer devices only).",
+ },
+ new()
+ {
+ Name = "--bit-FileInput-item-border-color",
+ DefaultValue = "--bit-clr-brd-pri",
+ Description = "Border of a file item at rest.",
+ },
+ new()
+ {
+ Name = "--bit-FileInput-item-hover-border-color",
+ DefaultValue = "--bit-FileInput-item-border-color, then --bit-clr-brd-pri-hover",
+ Description = "Border of a hovered file item (pointer devices only). It falls back to the resting border color first, so repainting an item's rule covers its hover without a second variable.",
+ },
+ new()
+ {
+ Name = "--bit-FileInput-item-radius",
+ DefaultValue = "--bit-shp-radius-surface",
+ Description = "Corner radius of a file item.",
+ },
+ new()
+ {
+ Name = "--bit-FileInput-item-padding",
+ DefaultValue = "0.5rem",
+ Description = "Padding around the name, the size and the error message of a file item.",
+ },
+ new()
+ {
+ Name = "--bit-FileInput-item-gap",
+ DefaultValue = "0.1875rem",
+ Description = "Room between two file items, and above the first one.",
+ },
+ new()
+ {
+ Name = "--bit-FileInput-item-font-size",
+ DefaultValue = "Per Size, from the type ramp",
+ Description = "Text size of the file name.",
+ },
+ new()
+ {
+ Name = "--bit-FileInput-file-name-color",
+ DefaultValue = "--bit-clr-fg-pri",
+ Description = "Color of the file name.",
+ },
+ new()
+ {
+ Name = "--bit-FileInput-file-size-color",
+ DefaultValue = "--bit-clr-fg-sec",
+ Description = "Color of the file size and of the folder beside it.",
+ },
+ new()
+ {
+ Name = "--bit-FileInput-error-color",
+ DefaultValue = "--bit-clr-err",
+ Description = "Message and border of an invalid file item.",
+ },
+ new()
+ {
+ Name = "--bit-FileInput-invalid-background",
+ DefaultValue = "--bit-FileInput-item-background",
+ Description = "Background of an invalid file item. It matches a valid one by default so a rejected file still reads as a row of the list; a tint of the error color is the other reasonable choice.",
+ },
+ new()
+ {
+ Name = "--bit-FileInput-preview-size",
+ DefaultValue = "2rem / 2.5rem / 3.25rem per Size",
+ Description = "Side of the image thumbnail and of the file type glyph standing in for it.",
+ },
+ new()
+ {
+ Name = "--bit-FileInput-preview-radius",
+ DefaultValue = "--bit-shp-radius-sm",
+ Description = "Corner radius of the image thumbnail.",
+ },
+ new()
+ {
+ Name = "--bit-FileInput-file-icon-color",
+ DefaultValue = "--bit-clr-fg-sec",
+ Description = "Color of the file type glyph.",
+ },
+ new()
+ {
+ Name = "--bit-FileInput-remove-button-size",
+ DefaultValue = "--bit-siz-ctrl-sm / -md / -lg per Size",
+ Description = "Side of the square remove button.",
+ },
+ new()
+ {
+ Name = "--bit-FileInput-remove-button-color",
+ DefaultValue = "--bit-clr-fg-pri",
+ Description = "Glyph color of the remove button.",
+ },
+ new()
+ {
+ Name = "--bit-FileInput-remove-button-background",
+ DefaultValue = "--bit-clr-bg-sec",
+ Description = "Background of the remove button at rest.",
+ },
+ new()
+ {
+ Name = "--bit-FileInput-remove-button-hover-background",
+ DefaultValue = "--bit-clr-bg-sec-hover",
+ Description = "Background of a hovered remove button (pointer devices only).",
+ },
+ new()
+ {
+ Name = "--bit-FileInput-remove-button-active-background",
+ DefaultValue = "--bit-clr-bg-sec-active",
+ Description = "Background of a pressed remove button.",
+ },
+ new()
+ {
+ Name = "--bit-FileInput-remove-icon-size",
+ DefaultValue = "--bit-siz-icon-sm / -md / -lg per Size",
+ Description = "Glyph size of the remove button.",
+ },
+ ];
+
+
+
private BitFileInput bitFileInput = default!;
@@ -694,6 +1071,21 @@ public partial class BitFileInputDemo
}
+ private BitIconInfo? SelectFileIcon(BitFileInputInfo file)
+ {
+ // anything the app knows nothing about is left without a glyph rather than given a generic one.
+ if (file.ContentType.StartsWith("video/", StringComparison.OrdinalIgnoreCase)) return BitIconInfo.Bit("MyMoviesTV");
+ if (file.ContentType.StartsWith("audio/", StringComparison.OrdinalIgnoreCase)) return BitIconInfo.Bit("Volume3");
+
+ return Path.GetExtension(file.Name).ToLowerInvariant() switch
+ {
+ ".pdf" => BitIconInfo.Bit("PDF"),
+ ".zip" or ".rar" or ".7z" => BitIconInfo.Bit("ZipFolder"),
+ _ => null
+ };
+ }
+
+
private static readonly string[] farsiUnits = ["بایت", "کیلوبایت", "مگابایت", "گیگابایت"];
private string FormatFileSizeInFarsi(long size)
@@ -755,16 +1147,47 @@ private void AddEventLog(string log)
private BitFileInput publicApiFileInput = default!;
+ private string? streamHash;
+
+ private async Task HashTheFirstFile()
+ {
+ streamHash = null;
+
+ var file = publicApiFileInput.Files.FirstOrDefault(f => f.IsValid);
+
+ if (file is null) return;
+
+ // nothing of the file is ever held whole: the stream is read in chunks and folded into the hash.
+ await using var stream = await publicApiFileInput.OpenReadStreamAsync(file);
+
+ streamHash = Convert.ToHexString(await System.Security.Cryptography.SHA256.HashDataAsync(stream));
+ }
- private string? AnnounceAttachments(IReadOnlyList files)
+ private string? AnnounceInFarsi(IReadOnlyList files)
{
- if (files.Count == 0) return "No attachment yet.";
+ if (files.Count == 0) return "فایلی انتخاب نشده است.";
var rejected = files.Count(f => f.IsValid is false);
return rejected == 0
- ? $"{files.Count} attachment(s) ready to send."
- : $"{files.Count - rejected} attachment(s) ready to send, {rejected} rejected as too large.";
+ ? $"{files.Count} فایل انتخاب شد."
+ : $"{files.Count} فایل انتخاب شد، {rejected} مورد نامعتبر است.";
}
+
+
+ private readonly BitFileInputParams[] fileInputParams =
+ [
+ new()
+ {
+ Multiple = true,
+ ShowPreview = true,
+ ShowRemoveButton = true,
+ Label = "Attach a file",
+ MaxSize = 1024 * 1024 * 1,
+ MaxSizeErrorMessage = "Attachments are limited to 1 MB.",
+ Description = "Anything up to 1 MB. Drop it here or browse.",
+ FileSizeFormatter = size => $"{size:N0} bytes"
+ }
+ ];
}
diff --git a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Inputs/FileInput/BitFileInputDemo.razor.samples.cs b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Inputs/FileInput/BitFileInputDemo.razor.samples.cs
index 8a8501a147b..0c1ee88d890 100644
--- a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Inputs/FileInput/BitFileInputDemo.razor.samples.cs
+++ b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Inputs/FileInput/BitFileInputDemo.razor.samples.cs
@@ -1,9 +1,9 @@
-namespace Bit.BlazorUI.Demo.Client.Core.Pages.Components.Inputs.FileInput;
+namespace Bit.BlazorUI.Demo.Client.Core.Pages.Components.Inputs.FileInput;
public partial class BitFileInputDemo
{
private readonly string example1RazorCode = @"
-
+";
@@ -30,59 +30,59 @@ public partial class BitFileInputDemo
";
private readonly string example4RazorCode = @"
-";
+
+
+";
private readonly string example5RazorCode = @"
-";
+
- private readonly string example6RazorCode = @"
-";
+";
- private readonly string example7RazorCode = @"
+ private readonly string example6RazorCode = @"
";
-
- private readonly string example8RazorCode = @"
-
+ DuplicateErrorMessage=""This file has already been picked."" />
-
-
-
+";
+ private readonly string example7RazorCode = @"
";
+ MinSize=""1024""
+ MaxSizeErrorMessage=""The file is too big! Please select a file smaller than 1 MB."" />
- private readonly string example9RazorCode = @"
-
+";
-
+ private readonly string example8RazorCode = @"
+
-";
+
- private readonly string example10RazorCode = @"
-";
+";
- private readonly string example11RazorCode = @"
-";
- private readonly string example11CsharpCode = @"
-private string? ValidateEmptyFile(BitFileInputInfo file)
-{
- return file.Size == 0 ? ""Empty files are not allowed"" : null;
-}";
+ private readonly string example9RazorCode = @"
+
- private readonly string example12RazorCode = @"
";
- private readonly string example12CsharpCode = @"
+ private readonly string example9CsharpCode = @"
+private string? ValidateEmptyFile(BitFileInputInfo file)
+{
+ return file.Size == 0 ? ""Empty files are not allowed"" : null;
+}
+
private string? ValidateImageDimensions(BitFileInputInfo file)
{
// dropped and pasted files bypass the accept filter, so non-image files reach the validator as well.
@@ -95,27 +95,37 @@ public partial class BitFileInputDemo
: null;
}";
- private readonly string example13RazorCode = @"
-";
-
- private readonly string example14RazorCode = @"
-";
-
- private readonly string example15RazorCode = @"
-";
-
- private readonly string example16RazorCode = @"
-
+ private readonly string example10RazorCode = @"
+
-
+";
+ private readonly string example10CsharpCode = @"
+private BitIconInfo? SelectFileIcon(BitFileInputInfo file)
+{
+ // anything the app knows nothing about is left without a glyph rather than given a generic one.
+ if (file.ContentType.StartsWith(""video/"", StringComparison.OrdinalIgnoreCase)) return BitIconInfo.Bit(""MyMoviesTV"");
+ if (file.ContentType.StartsWith(""audio/"", StringComparison.OrdinalIgnoreCase)) return BitIconInfo.Bit(""Volume3"");
-";
+ return Path.GetExtension(file.Name).ToLowerInvariant() switch
+ {
+ "".pdf"" => BitIconInfo.Bit(""PDF""),
+ "".zip"" or "".rar"" or "".7z"" => BitIconInfo.Bit(""ZipFolder""),
+ _ => null
+ };
+}";
- private readonly string example17RazorCode = @"
-
+ private readonly string example11RazorCode = @"
+ $""{size:N0} bytes"")"" />";
- private readonly string example17CsharpCode = @"
+ private readonly string example11CsharpCode = @"
private static readonly string[] farsiUnits = [""بایت"", ""کیلوبایت"", ""مگابایت"", ""گیگابایت""];
private string FormatFileSizeInFarsi(long size)
@@ -130,9 +140,20 @@ private string FormatFileSizeInFarsi(long size)
}
return $""{Math.Round(value, 1)} {farsiUnits[unit]}"";
+}
+
+private string? AnnounceInFarsi(IReadOnlyList files)
+{
+ if (files.Count == 0) return ""فایلی انتخاب نشده است."";
+
+ var rejected = files.Count(f => f.IsValid is false);
+
+ return rejected == 0
+ ? $""{files.Count} فایل انتخاب شد.""
+ : $""{files.Count} فایل انتخاب شد، {rejected} مورد نامعتبر است."";
}";
- private readonly string example18RazorCode = @"
+ private readonly string example12RazorCode = @"