mirror of
https://github.com/rstudio/shiny.git
synced 2026-01-14 09:28:02 -05:00
Allow update input labels with HTML (#3996)
* fix: allow update input labels with HTML fixes #3995 * refactor: use processDeps and renderContent * fix: formatting on lists * fix: put spaces between infix * chore: generated files * fix: update input tests * revert: generated javascript and sourcemaps * fix: empty label check * Remove package-lock * Undo unintended change when merging * Update news * Simplify --------- Co-authored-by: Carson <cpsievert1@gmail.com>
This commit is contained in:
2
NEWS.md
2
NEWS.md
@@ -6,6 +6,8 @@
|
||||
|
||||
* `textAreaInput()` gains a `autoresize` option, which automatically resizes the text area to fit its content. (#4210)
|
||||
|
||||
* The family of `update*Input()` functions can now render HTML content passed to the `label` argument (e.g., `updateInputText(label = tags$b("New label"))`). (#3996)
|
||||
|
||||
## Changes
|
||||
|
||||
* Shiny no longer suspends input changes when _any_ `<input type="submit">` or `<button type="submit">` is on the page. Instead, it now only suspends when a `submitButton()` is present. If you have reason for creating a submit button from custom HTML, add a CSS class of `shiny-submit-button` to the button. (#4209)
|
||||
|
||||
@@ -37,7 +37,11 @@
|
||||
updateTextInput <- function(session = getDefaultReactiveDomain(), inputId, label = NULL, value = NULL, placeholder = NULL) {
|
||||
validate_session_object(session)
|
||||
|
||||
message <- dropNulls(list(label=label, value=value, placeholder=placeholder))
|
||||
message <- dropNulls(list(
|
||||
label = processDeps(label, session),
|
||||
value = value,
|
||||
placeholder = placeholder
|
||||
))
|
||||
session$sendInputMessage(inputId, message)
|
||||
}
|
||||
|
||||
@@ -111,7 +115,10 @@ updateTextAreaInput <- updateTextInput
|
||||
updateCheckboxInput <- function(session = getDefaultReactiveDomain(), inputId, label = NULL, value = NULL) {
|
||||
validate_session_object(session)
|
||||
|
||||
message <- dropNulls(list(label=label, value=value))
|
||||
message <- dropNulls(list(
|
||||
label = processDeps(label, session),
|
||||
value = value
|
||||
))
|
||||
session$sendInputMessage(inputId, message)
|
||||
}
|
||||
|
||||
@@ -175,13 +182,17 @@ updateActionButton <- function(session = getDefaultReactiveDomain(), inputId, la
|
||||
validate_session_object(session)
|
||||
|
||||
if (!is.null(icon)) icon <- as.character(validateIcon(icon))
|
||||
message <- dropNulls(list(label=label, icon=icon, disabled=disabled))
|
||||
message <- dropNulls(list(
|
||||
label = processDeps(label, session),
|
||||
icon = icon,
|
||||
disabled = disabled
|
||||
))
|
||||
session$sendInputMessage(inputId, message)
|
||||
}
|
||||
#' @rdname updateActionButton
|
||||
#' @export
|
||||
updateActionLink <- function(session = getDefaultReactiveDomain(), inputId, label = NULL, icon = NULL) {
|
||||
updateActionButton(session, inputId=inputId, label=label, icon=icon)
|
||||
updateActionButton(session, inputId = inputId, label = processDeps(label, session), icon = icon)
|
||||
}
|
||||
|
||||
|
||||
@@ -225,7 +236,12 @@ updateDateInput <- function(session = getDefaultReactiveDomain(), inputId, label
|
||||
min <- dateYMD(min, "min")
|
||||
max <- dateYMD(max, "max")
|
||||
|
||||
message <- dropNulls(list(label=label, value=value, min=min, max=max))
|
||||
message <- dropNulls(list(
|
||||
label = processDeps(label, session),
|
||||
value = value,
|
||||
min = min,
|
||||
max = max
|
||||
))
|
||||
session$sendInputMessage(inputId, message)
|
||||
}
|
||||
|
||||
@@ -275,7 +291,7 @@ updateDateRangeInput <- function(session = getDefaultReactiveDomain(), inputId,
|
||||
max <- dateYMD(max, "max")
|
||||
|
||||
message <- dropNulls(list(
|
||||
label = label,
|
||||
label = processDeps(label, session),
|
||||
value = dropNulls(list(start = start, end = end)),
|
||||
min = min,
|
||||
max = max
|
||||
@@ -374,13 +390,16 @@ updateNavlistPanel <- updateTabsetPanel
|
||||
#' }
|
||||
#' @export
|
||||
updateNumericInput <- function(session = getDefaultReactiveDomain(), inputId, label = NULL, value = NULL,
|
||||
min = NULL, max = NULL, step = NULL) {
|
||||
min = NULL, max = NULL, step = NULL) {
|
||||
|
||||
validate_session_object(session)
|
||||
|
||||
message <- dropNulls(list(
|
||||
label = label, value = formatNoSci(value),
|
||||
min = formatNoSci(min), max = formatNoSci(max), step = formatNoSci(step)
|
||||
label = processDeps(label, session),
|
||||
value = formatNoSci(value),
|
||||
min = formatNoSci(min),
|
||||
max = formatNoSci(max),
|
||||
step = formatNoSci(step)
|
||||
))
|
||||
session$sendInputMessage(inputId, message)
|
||||
}
|
||||
@@ -460,7 +479,7 @@ updateSliderInput <- function(session = getDefaultReactiveDomain(), inputId, lab
|
||||
}
|
||||
|
||||
message <- dropNulls(list(
|
||||
label = label,
|
||||
label = processDeps(label, session),
|
||||
value = formatNoSci(value),
|
||||
min = formatNoSci(min),
|
||||
max = formatNoSci(max),
|
||||
@@ -491,7 +510,11 @@ updateInputOptions <- function(session, inputId, label = NULL, choices = NULL,
|
||||
))
|
||||
}
|
||||
|
||||
message <- dropNulls(list(label = label, options = options, value = selected))
|
||||
message <- dropNulls(list(
|
||||
label = processDeps(label, session),
|
||||
options = options,
|
||||
value = selected
|
||||
))
|
||||
|
||||
session$sendInputMessage(inputId, message)
|
||||
}
|
||||
@@ -644,7 +667,11 @@ updateSelectInput <- function(session = getDefaultReactiveDomain(), inputId, lab
|
||||
choices <- if (!is.null(choices)) choicesWithNames(choices)
|
||||
if (!is.null(selected)) selected <- as.character(selected)
|
||||
options <- if (!is.null(choices)) selectOptions(choices, selected, inputId, FALSE)
|
||||
message <- dropNulls(list(label = label, options = options, value = selected))
|
||||
message <- dropNulls(list(
|
||||
label = processDeps(label, session),
|
||||
options = options,
|
||||
value = selected
|
||||
))
|
||||
session$sendInputMessage(inputId, message)
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
28
inst/www/shared/shiny.min.js
vendored
28
inst/www/shared/shiny.min.js
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -113,10 +113,10 @@ class CheckboxGroupInputBinding extends InputBinding {
|
||||
options: options,
|
||||
};
|
||||
}
|
||||
receiveMessage(
|
||||
async receiveMessage(
|
||||
el: CheckboxGroupHTMLElement,
|
||||
data: CheckboxGroupReceiveMessageData
|
||||
): void {
|
||||
): Promise<void> {
|
||||
const $el = $(el);
|
||||
|
||||
// This will replace all the options
|
||||
@@ -132,7 +132,7 @@ class CheckboxGroupInputBinding extends InputBinding {
|
||||
this.setValue(el, data.value);
|
||||
}
|
||||
|
||||
updateLabel(data.label, getLabelNode(el));
|
||||
await updateLabel(data.label, getLabelNode(el));
|
||||
|
||||
$(el).trigger("change");
|
||||
}
|
||||
|
||||
@@ -292,10 +292,13 @@ class DateInputBinding extends DateInputBindingBase {
|
||||
startview: startview,
|
||||
};
|
||||
}
|
||||
receiveMessage(el: HTMLElement, data: DateReceiveMessageData): void {
|
||||
async receiveMessage(
|
||||
el: HTMLElement,
|
||||
data: DateReceiveMessageData
|
||||
): Promise<void> {
|
||||
const $input = $(el).find("input");
|
||||
|
||||
updateLabel(data.label, this._getLabelNode(el));
|
||||
await updateLabel(data.label, this._getLabelNode(el));
|
||||
|
||||
if (hasDefinedProperty(data, "min")) this._setMin($input[0], data.min);
|
||||
|
||||
|
||||
@@ -106,13 +106,16 @@ class DateRangeInputBinding extends DateInputBindingBase {
|
||||
startview: startview,
|
||||
};
|
||||
}
|
||||
receiveMessage(el: HTMLElement, data: DateRangeReceiveMessageData): void {
|
||||
async receiveMessage(
|
||||
el: HTMLElement,
|
||||
data: DateRangeReceiveMessageData
|
||||
): Promise<void> {
|
||||
const $el = $(el);
|
||||
const $inputs = $el.find("input");
|
||||
const $startinput = $inputs.eq(0);
|
||||
const $endinput = $inputs.eq(1);
|
||||
|
||||
updateLabel(data.label, getLabelNode(el));
|
||||
await updateLabel(data.label, getLabelNode(el));
|
||||
|
||||
if (hasDefinedProperty(data, "min")) {
|
||||
this._setMin($startinput[0], data.min);
|
||||
|
||||
@@ -51,7 +51,10 @@ class NumberInputBinding extends TextInputBindingBase {
|
||||
return "shiny.number";
|
||||
el;
|
||||
}
|
||||
receiveMessage(el: NumberHTMLElement, data: NumberReceiveMessageData): void {
|
||||
async receiveMessage(
|
||||
el: NumberHTMLElement,
|
||||
data: NumberReceiveMessageData
|
||||
): Promise<void> {
|
||||
// Setting values to `""` will remove the attribute value from the DOM element.
|
||||
// The attr key will still remain, but there is not value... ex: `<input id="foo" type="number" min max/>`
|
||||
if (hasDefinedProperty(data, "value")) el.value = data.value ?? "";
|
||||
@@ -59,7 +62,7 @@ class NumberInputBinding extends TextInputBindingBase {
|
||||
if (hasDefinedProperty(data, "max")) el.max = data.max ?? "";
|
||||
if (hasDefinedProperty(data, "step")) el.step = data.step ?? "";
|
||||
|
||||
updateLabel(data.label, getLabelNode(el));
|
||||
await updateLabel(data.label, getLabelNode(el));
|
||||
|
||||
$(el).trigger("change");
|
||||
}
|
||||
|
||||
@@ -103,7 +103,10 @@ class RadioInputBinding extends InputBinding {
|
||||
options: options,
|
||||
};
|
||||
}
|
||||
receiveMessage(el: RadioHTMLElement, data: RadioReceiveMessageData): void {
|
||||
async receiveMessage(
|
||||
el: RadioHTMLElement,
|
||||
data: RadioReceiveMessageData
|
||||
): Promise<void> {
|
||||
const $el = $(el);
|
||||
// This will replace all the options
|
||||
|
||||
@@ -122,7 +125,7 @@ class RadioInputBinding extends InputBinding {
|
||||
this.setValue(el, data.value);
|
||||
}
|
||||
|
||||
updateLabel(data.label, getLabelNode(el));
|
||||
await updateLabel(data.label, getLabelNode(el));
|
||||
|
||||
$(el).trigger("change");
|
||||
}
|
||||
|
||||
@@ -102,10 +102,10 @@ class SelectInputBinding extends InputBinding {
|
||||
options: options,
|
||||
};
|
||||
}
|
||||
receiveMessage(
|
||||
async receiveMessage(
|
||||
el: SelectHTMLElement,
|
||||
data: SelectInputReceiveMessageData
|
||||
): void {
|
||||
): Promise<void> {
|
||||
const $el = $(el);
|
||||
|
||||
// This will replace all the options
|
||||
@@ -205,7 +205,7 @@ class SelectInputBinding extends InputBinding {
|
||||
this.setValue(el, data.value);
|
||||
}
|
||||
|
||||
updateLabel(data.label, getLabelNode(el));
|
||||
await updateLabel(data.label, getLabelNode(el));
|
||||
|
||||
$(el).trigger("change");
|
||||
}
|
||||
|
||||
@@ -179,7 +179,10 @@ class SliderInputBinding extends TextInputBindingBase {
|
||||
unsubscribe(el: HTMLElement): void {
|
||||
$(el).off(".sliderInputBinding");
|
||||
}
|
||||
receiveMessage(el: HTMLElement, data: SliderReceiveMessageData): void {
|
||||
async receiveMessage(
|
||||
el: HTMLElement,
|
||||
data: SliderReceiveMessageData
|
||||
): Promise<void> {
|
||||
const $el = $(el);
|
||||
const slider = $el.data("ionRangeSlider");
|
||||
const msg: {
|
||||
@@ -226,7 +229,7 @@ class SliderInputBinding extends TextInputBindingBase {
|
||||
}
|
||||
}
|
||||
|
||||
updateLabel(data.label, getLabelNode(el));
|
||||
await updateLabel(data.label, getLabelNode(el));
|
||||
|
||||
// (maybe) update data elements
|
||||
const domElements: Array<"data-type" | "time-format" | "timezone"> = [
|
||||
|
||||
@@ -126,10 +126,13 @@ class TextInputBinding extends TextInputBindingBase {
|
||||
placeholder: el.placeholder,
|
||||
};
|
||||
}
|
||||
receiveMessage(el: TextHTMLElement, data: TextReceiveMessageData): void {
|
||||
async receiveMessage(
|
||||
el: TextHTMLElement,
|
||||
data: TextReceiveMessageData
|
||||
): Promise<void> {
|
||||
if (hasDefinedProperty(data, "value")) this.setValue(el, data.value);
|
||||
|
||||
updateLabel(data.label, getLabelNode(el));
|
||||
await updateLabel(data.label, getLabelNode(el));
|
||||
|
||||
if (hasDefinedProperty(data, "placeholder"))
|
||||
el.placeholder = data.placeholder;
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import $ from "jquery";
|
||||
import type { HtmlDep } from "../shiny/render";
|
||||
import { renderContent } from "../shiny/render";
|
||||
import { windowDevicePixelRatio } from "../window/pixelRatio";
|
||||
import type { MapValuesUnion, MapWithResult } from "./extraTypes";
|
||||
import { hasDefinedProperty, hasOwnProperty } from "./object";
|
||||
@@ -351,23 +353,27 @@ const compareVersion = function (
|
||||
else throw `Unknown operator: ${op}`;
|
||||
};
|
||||
|
||||
function updateLabel(
|
||||
labelTxt: string | undefined,
|
||||
async function updateLabel(
|
||||
labelContent: string | { html: string; deps: HtmlDep[] } | undefined,
|
||||
labelNode: JQuery<HTMLElement>
|
||||
): void {
|
||||
): Promise<void> {
|
||||
// Only update if label was specified in the update method
|
||||
if (typeof labelTxt === "undefined") return;
|
||||
if (typeof labelContent === "undefined") return;
|
||||
if (labelNode.length !== 1) {
|
||||
throw new Error("labelNode must be of length 1");
|
||||
}
|
||||
|
||||
// Should the label be empty?
|
||||
const emptyLabel = Array.isArray(labelTxt) && labelTxt.length === 0;
|
||||
if (typeof labelContent === "string") {
|
||||
labelContent = {
|
||||
html: labelContent,
|
||||
deps: [],
|
||||
};
|
||||
}
|
||||
|
||||
if (emptyLabel) {
|
||||
if (labelContent.html === "") {
|
||||
labelNode.addClass("shiny-label-null");
|
||||
} else {
|
||||
labelNode.text(labelTxt);
|
||||
await renderContent(labelNode, labelContent);
|
||||
labelNode.removeClass("shiny-label-null");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ declare class CheckboxGroupInputBinding extends InputBinding {
|
||||
value: ReturnType<CheckboxGroupInputBinding["getValue"]>;
|
||||
options: ValueLabelObject[];
|
||||
};
|
||||
receiveMessage(el: CheckboxGroupHTMLElement, data: CheckboxGroupReceiveMessageData): void;
|
||||
receiveMessage(el: CheckboxGroupHTMLElement, data: CheckboxGroupReceiveMessageData): Promise<void>;
|
||||
subscribe(el: CheckboxGroupHTMLElement, callback: (x: boolean) => void): void;
|
||||
unsubscribe(el: CheckboxGroupHTMLElement): void;
|
||||
}
|
||||
|
||||
2
srcts/types/src/bindings/input/date.d.ts
vendored
2
srcts/types/src/bindings/input/date.d.ts
vendored
@@ -52,7 +52,7 @@ declare class DateInputBinding extends DateInputBindingBase {
|
||||
format: string;
|
||||
startview: DatepickerViewModes;
|
||||
};
|
||||
receiveMessage(el: HTMLElement, data: DateReceiveMessageData): void;
|
||||
receiveMessage(el: HTMLElement, data: DateReceiveMessageData): Promise<void>;
|
||||
}
|
||||
export { DateInputBinding, DateInputBindingBase };
|
||||
export type { DateReceiveMessageData };
|
||||
|
||||
@@ -27,7 +27,7 @@ declare class DateRangeInputBinding extends DateInputBindingBase {
|
||||
language: string;
|
||||
startview: string;
|
||||
};
|
||||
receiveMessage(el: HTMLElement, data: DateRangeReceiveMessageData): void;
|
||||
receiveMessage(el: HTMLElement, data: DateRangeReceiveMessageData): Promise<void>;
|
||||
initialize(el: HTMLElement): void;
|
||||
subscribe(el: HTMLElement, callback: (x: boolean) => void): void;
|
||||
unsubscribe(el: HTMLElement): void;
|
||||
|
||||
2
srcts/types/src/bindings/input/number.d.ts
vendored
2
srcts/types/src/bindings/input/number.d.ts
vendored
@@ -12,7 +12,7 @@ declare class NumberInputBinding extends TextInputBindingBase {
|
||||
getValue(el: NumberHTMLElement): string[] | number | string | null | undefined;
|
||||
setValue(el: NumberHTMLElement, value: number): void;
|
||||
getType(el: NumberHTMLElement): string;
|
||||
receiveMessage(el: NumberHTMLElement, data: NumberReceiveMessageData): void;
|
||||
receiveMessage(el: NumberHTMLElement, data: NumberReceiveMessageData): Promise<void>;
|
||||
getState(el: NumberHTMLElement): {
|
||||
label: string;
|
||||
value: ReturnType<NumberInputBinding["getValue"]>;
|
||||
|
||||
2
srcts/types/src/bindings/input/radio.d.ts
vendored
2
srcts/types/src/bindings/input/radio.d.ts
vendored
@@ -18,7 +18,7 @@ declare class RadioInputBinding extends InputBinding {
|
||||
value: ReturnType<RadioInputBinding["getValue"]>;
|
||||
options: ValueLabelObject[];
|
||||
};
|
||||
receiveMessage(el: RadioHTMLElement, data: RadioReceiveMessageData): void;
|
||||
receiveMessage(el: RadioHTMLElement, data: RadioReceiveMessageData): Promise<void>;
|
||||
subscribe(el: RadioHTMLElement, callback: (x: boolean) => void): void;
|
||||
unsubscribe(el: RadioHTMLElement): void;
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ declare class SelectInputBinding extends InputBinding {
|
||||
label: string;
|
||||
}>;
|
||||
};
|
||||
receiveMessage(el: SelectHTMLElement, data: SelectInputReceiveMessageData): void;
|
||||
receiveMessage(el: SelectHTMLElement, data: SelectInputReceiveMessageData): Promise<void>;
|
||||
subscribe(el: SelectHTMLElement, callback: (x: boolean) => void): void;
|
||||
unsubscribe(el: HTMLElement): void;
|
||||
initialize(el: SelectHTMLElement): void;
|
||||
|
||||
2
srcts/types/src/bindings/input/slider.d.ts
vendored
2
srcts/types/src/bindings/input/slider.d.ts
vendored
@@ -26,7 +26,7 @@ declare class SliderInputBinding extends TextInputBindingBase {
|
||||
setValue(el: HTMLElement, value: number | string | [number | string, number | string]): void;
|
||||
subscribe(el: HTMLElement, callback: (x: boolean) => void): void;
|
||||
unsubscribe(el: HTMLElement): void;
|
||||
receiveMessage(el: HTMLElement, data: SliderReceiveMessageData): void;
|
||||
receiveMessage(el: HTMLElement, data: SliderReceiveMessageData): Promise<void>;
|
||||
getRatePolicy(el: HTMLElement): {
|
||||
policy: "debounce";
|
||||
delay: 250;
|
||||
|
||||
2
srcts/types/src/bindings/input/text.d.ts
vendored
2
srcts/types/src/bindings/input/text.d.ts
vendored
@@ -27,7 +27,7 @@ declare class TextInputBinding extends TextInputBindingBase {
|
||||
value: string;
|
||||
placeholder: string;
|
||||
};
|
||||
receiveMessage(el: TextHTMLElement, data: TextReceiveMessageData): void;
|
||||
receiveMessage(el: TextHTMLElement, data: TextReceiveMessageData): Promise<void>;
|
||||
}
|
||||
export { TextInputBinding, TextInputBindingBase };
|
||||
export type { TextHTMLElement, TextReceiveMessageData };
|
||||
|
||||
6
srcts/types/src/utils/index.d.ts
vendored
6
srcts/types/src/utils/index.d.ts
vendored
@@ -1,3 +1,4 @@
|
||||
import type { HtmlDep } from "../shiny/render";
|
||||
import type { MapValuesUnion, MapWithResult } from "./extraTypes";
|
||||
import { hasDefinedProperty, hasOwnProperty } from "./object";
|
||||
declare function escapeHTML(str: string): string;
|
||||
@@ -26,7 +27,10 @@ declare function isnan(x: unknown): boolean;
|
||||
declare function _equal(x: unknown, y: unknown): boolean;
|
||||
declare function equal(...args: unknown[]): boolean;
|
||||
declare const compareVersion: (a: string, op: "<" | "<=" | "==" | ">" | ">=", b: string) => boolean;
|
||||
declare function updateLabel(labelTxt: string | undefined, labelNode: JQuery<HTMLElement>): void;
|
||||
declare function updateLabel(labelContent: string | {
|
||||
html: string;
|
||||
deps: HtmlDep[];
|
||||
} | undefined, labelNode: JQuery<HTMLElement>): Promise<void>;
|
||||
declare function getComputedLinkColor(el: HTMLElement): string;
|
||||
declare function isBS3(): boolean;
|
||||
declare function toLowerCase<T extends string>(str: T): Lowercase<T>;
|
||||
|
||||
@@ -16,18 +16,19 @@ test_that("Radio buttons and checkboxes work with modules", {
|
||||
resultA <- sessA$lastInputMessage
|
||||
|
||||
expect_equal(resultA$id, "test1")
|
||||
expect_equal(resultA$message$label, "Label")
|
||||
expect_equal(as.character(resultA$message$label$html), "Label")
|
||||
expect_equal(resultA$message$value, "a")
|
||||
expect_match(resultA$message$options, '"modA-test1"')
|
||||
expect_no_match(resultA$message$options, '"test1"')
|
||||
|
||||
sessB <- createModuleSession("modB")
|
||||
|
||||
updateCheckboxGroupInput(sessB, "test2", label = "Label", choices = LETTERS[1:5])
|
||||
updateCheckboxGroupInput(sessB, "test2", label = icon("eye"), choices = LETTERS[1:5])
|
||||
resultB <- sessB$lastInputMessage
|
||||
|
||||
expect_equal(resultB$id, "test2")
|
||||
expect_equal(resultB$message$label, "Label")
|
||||
expect_length(resultB$message$label, 2)
|
||||
expect_s3_class(resultB$message$label$html, "html")
|
||||
expect_null(resultB$message$value)
|
||||
expect_match(resultB$message$options, '"modB-test2"')
|
||||
expect_no_match(resultB$message$options, '"test2"')
|
||||
|
||||
Reference in New Issue
Block a user