Skip to content

Commit

Permalink
feat: hackday multiselect shows selected options in field
Browse files Browse the repository at this point in the history
  • Loading branch information
apattersonATX-HB committed May 17, 2024
1 parent 62bf485 commit 44167b8
Show file tree
Hide file tree
Showing 4 changed files with 82 additions and 7 deletions.
14 changes: 14 additions & 0 deletions src/inputs/MultiSelectField.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(<TestMultiSelectField values={["1", "2", "3"] as string[]} options={options} />);

// 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(<TestMultiSelectField values={[]} options={options} />);
Expand Down
27 changes: 27 additions & 0 deletions src/inputs/TextFieldBase.test.tsx
Original file line number Diff line number Diff line change
@@ -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 () => {
Expand All @@ -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(
<TextFieldBase
inputProps={{}}
unfocusedPlaceholder={"Unfocused placeholder text"}
label="Test"
errorMsg="Error"
helperText="Helper"
/>,
);

// 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");
});
});
25 changes: 25 additions & 0 deletions src/inputs/TextFieldBase.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ export interface TextFieldBaseProps<X>
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
Expand Down Expand Up @@ -92,6 +93,7 @@ export function TextFieldBase<X extends Only<TextFieldXss, X>>(props: TextFieldB
hideErrorMessage = false,
alwaysShowHelperText = false,
fullWidth = fieldProps?.fullWidth ?? false,
unfocusedPlaceholder,
} = props;

const typeScale = fieldProps?.typeScale ?? (inputProps.readOnly && labelStyle !== "hidden" ? "smMd" : "sm");
Expand Down Expand Up @@ -176,6 +178,12 @@ export function TextFieldBase<X extends Only<TextFieldXss, X>>(props: TextFieldB
e.target.select();
}, onFocus);

// Simulate clicking `ElementType` when using an unfocused placeholder
function handleUnfocusedPlaceholderClick(e: React.MouseEvent<HTMLDivElement>) {
e.stopPropagation();
fieldRef.current?.click();
}

const showFocus = (isFocused && !inputProps.readOnly) || forceFocus;
const showHover = (isHovered && !inputProps.disabled && !inputProps.readOnly && !isFocused) || forceHover;

Expand Down Expand Up @@ -237,11 +245,23 @@ export function TextFieldBase<X extends Only<TextFieldXss, X>>(props: TextFieldB
}}
{...hoverProps}
ref={inputWrapRef as any}
onClick={unfocusedPlaceholder ? handleUnfocusedPlaceholderClick : undefined}
>
{labelStyle === "inline" && label && (
<InlineLabel multiline={multiline} labelProps={labelProps} label={label} {...tid.label} />
)}
{startAdornment && <span css={Css.df.aic.asc.fs0.br4.pr1.$}>{startAdornment}</span>}
{unfocusedPlaceholder && (
<div
{...tid.unfocusedPlaceholderContainer}
css={{
...Css.df.fdc.w100.pyPx(2).maxh100.overflowAuto.$,
...(isFocused && visuallyHidden),
}}
>
{unfocusedPlaceholder}
</div>
)}
<ElementType
{...mergeProps(
inputProps,
Expand All @@ -255,6 +275,7 @@ export function TextFieldBase<X extends Only<TextFieldXss, X>>(props: TextFieldB
...fieldStyles.input,
...(inputProps.disabled ? fieldStyles.disabled : {}),
...(showHover ? fieldStyles.hover : {}),
...(unfocusedPlaceholder && !isFocused && visuallyHidden),
...xss,
}}
{...tid}
Expand Down Expand Up @@ -310,3 +331,7 @@ export function TextFieldBase<X extends Only<TextFieldXss, X>>(props: TextFieldB
</>
);
}

// Css that would be applied if using react-aria <VisuallyHidden />
const visuallyHidden = Css.add("clip", "inset(50%)").add("clipPath", "").add("border", 0).hPx(1).mPx(-1).wPx(1).nowrap
.p0.overflowHidden.absolute.$;
23 changes: 16 additions & 7 deletions src/inputs/internal/ComboBoxInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -92,23 +92,28 @@ export function ComboBoxInput<O, V extends Value>(props: ComboBoxInputProps<O, V
const multilineProps = allowWrap ? { textAreaMinHeight: 0, multiline: true } : {};
useGrowingTextField({ disabled: !allowWrap, inputRef, inputWrapRef, value: inputProps.value });

const chipLabels = isTree ? selectedOptionsLabels || [] : selectedOptions.map((o) => getOptionLabel(o));

return (
<TextFieldBase
{...otherProps}
{...multilineProps}
unfocusedPlaceholder={showNumSelection && <Chips compact={otherProps.compact} values={chipLabels} />}
inputRef={inputRef}
inputWrapRef={inputWrapRef}
errorMsg={errorMsg}
contrast={contrast}
xss={otherProps.labelStyle !== "inline" && !inputProps.readOnly ? Css.fw5.$ : {}}
startAdornment={
(showNumSelection && (
<span
css={Css.wPx(16).hPx(16).fs0.br100.bgBlue700.white.tinySb.df.aic.jcc.$}
data-testid="selectedOptionsCount"
>
{isTree ? selectedOptionsLabels?.length : state.selectionManager.selectedKeys.size}
</span>
<Tooltip title={<SelectedOptionBullets labels={chipLabels} />}>
<span
css={Css.wPx(16).hPx(16).fs0.br100.bgBlue700.white.tinySb.df.aic.jcc.$}
data-testid="selectedOptionsCount"
>
{isTree ? selectedOptionsLabels?.length : state.selectionManager.selectedKeys.size}
</span>
</Tooltip>
)) ||
(showFieldDecoration && fieldDecoration(selectedOptions[0]))
}
Expand Down Expand Up @@ -250,3 +255,7 @@ export function ComboBoxInput<O, V extends Value>(props: ComboBoxInputProps<O, V
/>
);
}

function SelectedOptionBullets({ labels = [] }: { labels: string[] | undefined }) {
return <div>{labels?.map((label) => <li key={label}>{label}</li>)}</div>;
}

0 comments on commit 44167b8

Please sign in to comment.