Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion DESCRIPTION
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ License: LGPL-3
Encoding: UTF-8
LazyData: true
Roxygen: list(markdown = TRUE)
RoxygenNote: 7.3.1
RoxygenNote: 7.3.2
Depends:
R (>= 2.10)
Imports:
Expand Down
2 changes: 2 additions & 0 deletions NAMESPACE
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ export(DocumentCardTitle)
export(Dropdown)
export(Dropdown.shinyInput)
export(Facepile)
export(FileUploadButton.shinyInput)
export(FocusTrapCallout)
export(FocusTrapZone)
export(FocusZone)
Expand Down Expand Up @@ -136,6 +137,7 @@ export(updateCompoundButton.shinyInput)
export(updateDatePicker.shinyInput)
export(updateDefaultButton.shinyInput)
export(updateDropdown.shinyInput)
export(updateFileUploadButton.shinyInput)
export(updateIconButton.shinyInput)
export(updateNormalPeoplePicker.shinyInput)
export(updatePrimaryButton.shinyInput)
Expand Down
45 changes: 45 additions & 0 deletions R/documentation.R
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,51 @@ NULL
#' @name Button
NULL

#' FileUploadButton
#'
#' @description
#' A Safari-compatible file upload button that combines Fluent UI styling with cross-browser file selection functionality. This component handles Safari blocking programmatic file input clicks by using React-based file input handling.
#'
#' For more details about Fluent UI buttons visit [the official docs](https://developer.microsoft.com/en-us/fluentui#/controls/web/Button).
#'
#' @param inputId ID of the component.
#' @param value Starting value.
#' @param session Object passed as the `session` argument to Shiny server.
#' @param ... Props to pass to the component.
#' The allowed props are listed below in the \bold{Details} section.
#'
#' @section Best practices:
#' ### Usage
#' - Use FileUploadButton when you need Safari-compatible file uploads
#' - Choose appropriate buttonType for visual hierarchy (primary for main actions)
#' - Use `accept` prop to filter allowed file extensions
#' - Use `multiple=TRUE` for bulk file uploads
#'
#' ### Content
#' - Use clear, action-oriented `text` (e.g., "Upload Files", "Select Document")
#' - Consider using relevant icons such as: Upload, Attach, or FolderOpen
#' - Keep button text concise but descriptive
#'
#' ### Accessibility
#' - Button follows Fluent UI accessibility standards
#' - Supports keyboard navigation (Space/Enter to activate)
#' - Compatible with screen readers and assistive technologies
#'
#' @details
#'
#' * \bold{ text } `string` \cr Text to display on the button.
#' * \bold{ buttonType } `string` \cr Type of Fluent button: "primary", "default", "compound", "action", "command", "commandBar", or "icon". Defaults to "default".
#' * \bold{ icon } `string` \cr Optional Fluent UI icon name (e.g., "Upload", "Attach", "FolderOpen").
#' * \bold{ accept } `string` \cr A comma-separated string of file extensions to accept (e.g., ".xlsx,.csv", ".pdf"). Passed to underlying file input.
#' * \bold{ multiple } `boolean` \cr Whether to allow multiple file selection. Defaults to FALSE.
#' * \bold{ disabled } `boolean` \cr Whether the button is disabled.
#' * \bold{ className } `string` \cr Additional CSS class name for the button.
#' * \bold{ style } `string` \cr Inline CSS styles for the button.
#'
#' @md
#' @name FileUploadButton
NULL

#' Calendar
#'
#' @description
Expand Down
8 changes: 8 additions & 0 deletions R/inputs.R
Original file line number Diff line number Diff line change
Expand Up @@ -198,3 +198,11 @@ Toggle.shinyInput <- input("Toggle", FALSE)
#' @rdname Toggle
#' @export
updateToggle.shinyInput <- shiny.react::updateReactInput

#' @rdname FileUploadButton
#' @export
FileUploadButton.shinyInput <- input("FileUploadButton", NULL)

#' @rdname FileUploadButton
#' @export
updateFileUploadButton.shinyInput <- shiny.react::updateReactInput
131 changes: 104 additions & 27 deletions inst/examples/Button3.R
Original file line number Diff line number Diff line change
@@ -1,50 +1,127 @@

# Example 3
# Example 3: File Upload with Fluent UI Buttons
library(shiny)
library(shiny.fluent)
library(shinyjs)

# This example app shows how to use a Fluent UI Button to trigger a file upload.
# File upload is not natively supported by shiny.fluent so shinyjs is used
# to trigger the file upload input.
# This example demonstrates FileUploadButton - a native Fluent UI file upload component
# that works across all browsers, including Safari.

ui <- function(id) {
ns <- NS(id)
fluentPage(
useShinyjs(),
h3("File Upload with Fluent UI"),
p("Native file upload components with full cross-browser support:"),

Stack(
tokens = list(
childrenGap = 10L
tokens = list(childrenGap = 20),
horizontal = FALSE,

# Primary button for important uploads
div(
Text(variant = "mediumPlus", "Upload Data Files"),
FileUploadButton.shinyInput(
inputId = ns("data_files"),
text = "Choose Data Files",
buttonType = "primary",
icon = "Upload",
accept = ".xlsx,.csv,.json",
multiple = TRUE
)
),
horizontal = TRUE,
DefaultButton.shinyInput(
inputId = ns("uploadFileButton"),
text = "Upload File",
iconProps = list(iconName = "Upload")

# Default button for documents
div(
Text(variant = "mediumPlus", "Upload Document"),
FileUploadButton.shinyInput(
inputId = ns("document"),
text = "Choose Document",
buttonType = "default",
icon = "TextDocument",
accept = ".pdf,.docx,.txt"
)
),

# Compound button with description
div(
style = "
visibility: hidden;
height: 0;
width: 0;
",
fileInput(
inputId = ns("uploadFile"),
label = NULL
Text(variant = "mediumPlus", "Upload Images"),
FileUploadButton.shinyInput(
inputId = ns("images"),
text = "Choose Images",
buttonType = "compound",
icon = "Photo2",
accept = ".png,.jpg,.jpeg,.gif",
multiple = TRUE
)
)
),
textOutput(ns("file_path"))

br(),
h4("Upload Status:"),
div(
uiOutput(ns("data_files_status")),
uiOutput(ns("document_status")),
uiOutput(ns("images_status"))
)
)
}

server <- function(id) {
moduleServer(id, function(input, output, session) {
observeEvent(input$uploadFileButton, {
click("uploadFile")

# Handle data files upload
observeEvent(input$data_files, {
req(input$data_files)
# Handle multiple files (array) or single file (object)
if (is.list(input$data_files) && !is.null(names(input$data_files))) {
# Single file object with named elements
files <- input$data_files$name
} else if (is.list(input$data_files)) {
# Array of file objects
files <- paste(sapply(input$data_files, function(f) f$name), collapse = ", ")
} else {
# Fallback
files <- "Unknown file format"
}
output$data_files_status <- renderUI({
MessageBar(
messageBarType = 1, # success
paste("✅ Data files uploaded:", files)
)
})
})

output$file_path <- renderText({
input$uploadFile$name

# Handle document upload
observeEvent(input$document, {
req(input$document)
output$document_status <- renderUI({
MessageBar(
messageBarType = 1, # success
paste("📄 Document uploaded:", input$document$name)
)
})
})

# Handle images upload
observeEvent(input$images, {
req(input$images)
# Handle multiple files (array) or single file (object)
if (is.list(input$images) && !is.null(names(input$images))) {
# Single file object
files <- paste("Image uploaded:", input$images$name)
} else if (is.list(input$images)) {
# Array of file objects
files <- paste(length(input$images), "images:",
paste(sapply(input$images, function(f) f$name), collapse = ", "))
} else {
# Fallback
files <- "Unknown file format"
}
output$images_status <- renderUI({
MessageBar(
messageBarType = 1, # success
paste("🖼️", files)
)
})
})
})
}
Expand Down
2 changes: 1 addition & 1 deletion inst/www/shiny.fluent/shiny-fluent.js

Large diffs are not rendered by default.

84 changes: 84 additions & 0 deletions js/src/inputs.jsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import * as React from 'react';
import * as Fluent from '@fluentui/react';
import { ButtonAdapter, InputAdapter, debounce } from '@/shiny.react';

Expand Down Expand Up @@ -107,3 +108,86 @@ export const Toggle = InputAdapter(Fluent.Toggle, (value, setValue) => ({
checked: value,
onChange: (e, v) => setValue(v),
}));

export const FileUploadButton = InputAdapter(
({
onChange,
buttonType = 'default',
icon,
text,
accept,
multiple,
disabled,
className,
style,
ariaLabel,
title,
}) => {
const fileInputRef = React.useRef(null);

const handleClick = () => {
if (fileInputRef.current) {
fileInputRef.current.click();
}
};

const handleFileChange = (event) => {
const { files } = event.target;
if (files && files.length > 0) {
// Convert FileList to format Shiny expects
const fileData = Array.from(files).map((file) => ({
name: file.name,
size: file.size,
type: file.type,
lastModified: file.lastModified,
}));
onChange(multiple ? fileData : fileData[0]);
}
};

// Select the appropriate button component
let ButtonComponent;
if (buttonType === 'primary') {
ButtonComponent = Fluent.PrimaryButton;
} else if (buttonType === 'compound') {
ButtonComponent = Fluent.CompoundButton;
} else if (buttonType === 'action') {
ButtonComponent = Fluent.ActionButton;
} else if (buttonType === 'command') {
ButtonComponent = Fluent.CommandButton;
} else if (buttonType === 'commandBar') {
ButtonComponent = Fluent.CommandBarButton;
} else if (buttonType === 'icon') {
ButtonComponent = Fluent.IconButton;
} else {
ButtonComponent = Fluent.DefaultButton;
}

return (
<div>
<ButtonComponent

Choose a reason for hiding this comment

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

Issue: it looks like it's not possible to pass props to the button than the hard-coded handful.

Choose a reason for hiding this comment

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

@R3myG this is still an issue. Although, I see the list of supported attributed extended.

onClick={handleClick}
text={text}
iconProps={icon ? { iconName: icon } : undefined}
disabled={disabled}
className={className}
style={style}
ariaLabel={ariaLabel}
title={title}
/>
<input
ref={fileInputRef}
type="file"
accept={accept}
multiple={multiple}
onChange={handleFileChange}
style={{ display: 'none' }}
/>
</div>
);
},
(value, setValue) => ({
value,
onChange: setValue,
}),
);
Loading
Loading