diff --git a/src/inputs/MultiSelectField.test.tsx b/src/inputs/MultiSelectField.test.tsx index 2c525f0aa..d530d7088 100644 --- a/src/inputs/MultiSelectField.test.tsx +++ b/src/inputs/MultiSelectField.test.tsx @@ -25,6 +25,20 @@ describe("MultiSelectFieldTest", () => { expect(onSelect).toHaveBeenCalledWith(["1", "3"]); }); + it("renders a chip for each selected item", async () => { + // Given a MultiSelectField with 3 selected value + const r = await render(); + + // Then we can see 3 chips rendered + const selectionChips = r.queryAllByTestId("chip"); + expect(selectionChips).toHaveLength(3); + + // And they are rendered with names of the selected options + expect(selectionChips[0]).toHaveTextContent("One"); + expect(selectionChips[1]).toHaveTextContent("Two"); + expect(selectionChips[2]).toHaveTextContent("Three"); + }); + it("has an empty text box not set", async () => { // Given a MultiSelectField with no selected values const r = await render(); diff --git a/src/inputs/TextFieldBase.test.tsx b/src/inputs/TextFieldBase.test.tsx index 0772a1f43..669e9a231 100644 --- a/src/inputs/TextFieldBase.test.tsx +++ b/src/inputs/TextFieldBase.test.tsx @@ -1,5 +1,6 @@ import { TextFieldBase } from "src/inputs/TextFieldBase"; import { render } from "src/utils/rtl"; +import { act } from "@testing-library/react"; describe(TextFieldBase, () => { it("shows error and helper text", async () => { @@ -25,4 +26,30 @@ describe(TextFieldBase, () => { expect(r.query.test_errorMsg).not.toBeInTheDocument(); expect(r.query.test_helperText).not.toBeInTheDocument(); }); + + it("handles unfocusedPlaceholder correctly", async () => { + // When TextFieldBase is first rendered + const r = await render( + , + ); + + // The unfocused placeholder container is rendered + expect(r.test_unfocusedPlaceholderContainer).toBeInTheDocument(); + // And is visible + expect(r.test_unfocusedPlaceholderContainer).not.toHaveStyleRule("position", "absolute"); + + // And when we focus the field + act(() => { + r.test.focus(); + }); + + // Then the unfocused placeholder container is visually hidden + expect(r.test_unfocusedPlaceholderContainer).toHaveStyleRule("position", "absolute"); + }); }); diff --git a/src/inputs/TextFieldBase.tsx b/src/inputs/TextFieldBase.tsx index b81076bf1..b353e54d5 100644 --- a/src/inputs/TextFieldBase.tsx +++ b/src/inputs/TextFieldBase.tsx @@ -58,6 +58,7 @@ export interface TextFieldBaseProps hideErrorMessage?: boolean; // If set, the helper text will always be shown (usually we hide the helper text if read only) alwaysShowHelperText?: boolean; + unfocusedPlaceholder?: ReactNode; } // Used by both TextField and TextArea @@ -92,6 +93,7 @@ export function TextFieldBase>(props: TextFieldB hideErrorMessage = false, alwaysShowHelperText = false, fullWidth = fieldProps?.fullWidth ?? false, + unfocusedPlaceholder, } = props; const typeScale = fieldProps?.typeScale ?? (inputProps.readOnly && labelStyle !== "hidden" ? "smMd" : "sm"); @@ -176,6 +178,12 @@ export function TextFieldBase>(props: TextFieldB e.target.select(); }, onFocus); + // Simulate clicking `ElementType` when using an unfocused placeholder + function handleUnfocusedPlaceholderClick(e: React.MouseEvent) { + e.stopPropagation(); + fieldRef.current?.click(); + } + const showFocus = (isFocused && !inputProps.readOnly) || forceFocus; const showHover = (isHovered && !inputProps.disabled && !inputProps.readOnly && !isFocused) || forceHover; @@ -237,11 +245,23 @@ export function TextFieldBase>(props: TextFieldB }} {...hoverProps} ref={inputWrapRef as any} + onClick={unfocusedPlaceholder ? handleUnfocusedPlaceholderClick : undefined} > {labelStyle === "inline" && label && ( )} {startAdornment && {startAdornment}} + {unfocusedPlaceholder && ( + + {unfocusedPlaceholder} + + )} >(props: TextFieldB ...fieldStyles.input, ...(inputProps.disabled ? fieldStyles.disabled : {}), ...(showHover ? fieldStyles.hover : {}), + ...(unfocusedPlaceholder && !isFocused && visuallyHidden), ...xss, }} {...tid} @@ -310,3 +331,7 @@ export function TextFieldBase>(props: TextFieldB > ); } + +// Css that would be applied if using react-aria +const visuallyHidden = Css.add("clip", "inset(50%)").add("clipPath", "").add("border", 0).hPx(1).mPx(-1).wPx(1).nowrap + .p0.overflowHidden.absolute.$; diff --git a/src/inputs/internal/ComboBoxInput.tsx b/src/inputs/internal/ComboBoxInput.tsx index 28f2e2f70..1d0e6479e 100644 --- a/src/inputs/internal/ComboBoxInput.tsx +++ b/src/inputs/internal/ComboBoxInput.tsx @@ -9,7 +9,7 @@ import React, { } from "react"; import { mergeProps } from "react-aria"; import { ComboBoxState } from "react-stately"; -import { Icon } from "src/components"; +import { Chips, Icon, Tooltip } from "src/components"; import { PresentationFieldProps, usePresentationContext } from "src/components/PresentationContext"; import { Css } from "src/Css"; import { useGrowingTextField } from "src/inputs/hooks/useGrowingTextField"; @@ -92,10 +92,13 @@ export function ComboBoxInput(props: ComboBoxInputProps getOptionLabel(o)); + return ( } inputRef={inputRef} inputWrapRef={inputWrapRef} errorMsg={errorMsg} @@ -103,12 +106,14 @@ export function ComboBoxInput(props: ComboBoxInputProps - {isTree ? selectedOptionsLabels?.length : state.selectionManager.selectedKeys.size} - + }> + + {isTree ? selectedOptionsLabels?.length : state.selectionManager.selectedKeys.size} + + )) || (showFieldDecoration && fieldDecoration(selectedOptions[0])) } @@ -250,3 +255,7 @@ export function ComboBoxInput(props: ComboBoxInputProps ); } + +function SelectedOptionBullets({ labels = [] }: { labels: string[] | undefined }) { + return {labels?.map((label) => {label})}; +}