Skip to content

Commit

Permalink
Merge branch 'main' into sc-58759/update-email-icon
Browse files Browse the repository at this point in the history
  • Loading branch information
Kelsie Besinger Yeh committed Sep 24, 2024
2 parents f0d9555 + 4f3cb49 commit a65c619
Show file tree
Hide file tree
Showing 16 changed files with 156 additions and 69 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@homebound/beam",
"version": "2.365.0",
"version": "2.368.0",
"author": "Homebound",
"license": "MIT",
"main": "dist/index.js",
Expand Down
4 changes: 2 additions & 2 deletions src/components/SuperDrawer/components/SuperDrawerHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ interface SuperDrawerHeaderProps {
}

interface SuperDrawerHeaderStructuredProps {
title: string;
title: string | ReactNode;
left?: ReactNode;
right?: ReactNode;
hideControls?: boolean;
Expand All @@ -32,7 +32,7 @@ export function SuperDrawerHeader(props: SuperDrawerHeaderProps | SuperDrawerHea
{isStructuredProps(props) ? (
<div css={Css.df.jcsb.aic.gap2.fg1.$}>
<div css={Css.fg1.df.aic.gap2.$}>
<h1>{props.title}</h1>
{typeof props.title === "string" ? <h1>{props.title}</h1> : props.title}
{props.left}
</div>
{props.right && <div css={Css.fs0.$}>{props.right}</div>}
Expand Down
1 change: 1 addition & 0 deletions src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export * from "./CssReset";
export * from "./DnDGrid";
export * from "./Filters";
export * from "./Grid";
export * from "./HelperText";
export { HbLoadingSpinner, HbSpinnerProvider, HB_QUIPS_FLAVOR, HB_QUIPS_MISSION } from "./HbLoadingSpinner";
export * from "./Icon";
export * from "./IconButton";
Expand Down
9 changes: 9 additions & 0 deletions src/forms/BoundNumberField.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,14 @@ describe("BoundNumberField", () => {
expect(r.royalties).toHaveValue("$1.00");
});

it("drops the 'in mills' suffix from labels", async () => {
const author = createObjectState(formConfig, { royaltiesInMills: 1_00 });
const r = await render(<BoundNumberField field={author.royaltiesInMills} />);
expect(r.royalties_label).toHaveTextContent("Royalties");
expect(r.royalties_label).not.toHaveTextContent("Cents");
expect(r.royalties).toHaveValue("$0.100");
});

it("retains 0 value", async () => {
const author = createObjectState(formConfig, { royaltiesInCents: 1_00 });
const r = await render(<BoundNumberField field={author.royaltiesInCents} />);
Expand Down Expand Up @@ -117,4 +125,5 @@ describe("BoundNumberField", () => {
const formConfig: ObjectConfig<AuthorInput> = {
heightInInches: { type: "value", rules: [required] },
royaltiesInCents: { type: "value" },
royaltiesInMills: { type: "value" },
};
4 changes: 2 additions & 2 deletions src/forms/BoundNumberField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ export function BoundNumberField(props: BoundNumberFieldProps) {
field,
readOnly,
onChange = (value) => field.set(value),
label = defaultLabel(field.key.replace(/InCents$/, "")),
type = field.key.endsWith("InCents") ? "cents" : undefined,
label = defaultLabel(field.key.replace(/(InCents|InMills)$/, "")),
type = field.key.endsWith("InCents") ? "cents" : field.key.endsWith("InMills") ? "mills" : undefined,
onFocus,
onBlur,
onEnter,
Expand Down
1 change: 1 addition & 0 deletions src/forms/formStateDomain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export interface AuthorInput {
birthday?: Date | null;
heightInInches?: number | null;
royaltiesInCents?: number | null;
royaltiesInMills?: number | null;
books?: BookInput[] | null;
address?: AuthorAddress | null;
favoriteSport?: string | null;
Expand Down
3 changes: 2 additions & 1 deletion src/inputs/MultiSelectField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ import { Value } from "src/inputs";
import { ComboBoxBase, ComboBoxBaseProps } from "src/inputs/internal/ComboBoxBase";
import { HasIdAndName, Optional } from "src/types";

export interface MultiSelectFieldProps<O, V extends Value> extends Exclude<ComboBoxBaseProps<O, V>, "unsetLabel"> {
export interface MultiSelectFieldProps<O, V extends Value>
extends Exclude<ComboBoxBaseProps<O, V>, "unsetLabel" | "addNew"> {
/** Renders `opt` in the dropdown menu, defaults to the `getOptionLabel` prop. */
getOptionMenuLabel?: (opt: O) => string | ReactNode;
getOptionValue: (opt: O) => V;
Expand Down
2 changes: 2 additions & 0 deletions src/inputs/NumberField.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ export function NumberFieldStyles() {
<div css={Css.df.fdc.gap2.$}>
<h1 css={Css.lg.$}>Unit Types</h1>
<TestNumberField label="Percent" type="percent" value={12.55} numFractionDigits={2} truncate />
<TestNumberField label="Mills" type="mills" value={1000} />
<TestNumberField label="Cents" type="cents" value={1000} />
<TestNumberField label="Dollars" type="dollars" value={1000} />
<TestNumberField label="Margin" type="basisPoints" value={1275} />
Expand Down Expand Up @@ -80,6 +81,7 @@ export function NumberFieldReadOnly() {
<TextField label="First Name" value="first" onChange={() => {}} readOnly={true} />
<TestNumberField label="Name" value={100} readOnly={true} />
<TestNumberField label="Name" value={100} labelStyle="hidden" readOnly={true} />
<TestNumberField label="Name" value={100} readOnly={true} type="mills" />
<TestNumberField label="Name" value={100} readOnly={true} type="cents" />
<TestNumberField label="Name" value={100} readOnly={true} type="percent" />
</div>
Expand Down
18 changes: 18 additions & 0 deletions src/inputs/NumberField.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,14 @@ describe("NumberFieldTest", () => {
expect(onChange).toBeCalledTimes(2); // change and blur
});

it("can set mills as dollars", async () => {
const r = await render(<TestNumberField label="Cost" type="mills" value={1200} />);
expect(r.cost).toHaveValue("$1.200");
type(r.cost, "14");
expect(r.cost).toHaveValue("$14.000");
expect(lastSet).toEqual(14000);
});

it("can set cents as dollars", async () => {
const r = await render(<TestNumberField label="Cost" type="cents" value={1200} />);
expect(r.cost).toHaveValue("$12.00");
Expand Down Expand Up @@ -92,6 +100,14 @@ describe("NumberFieldTest", () => {
expect(onChange).toBeCalledTimes(2); // change and blur
});

it("can set mills as mills", async () => {
const r = await render(<TestNumberField label="Cost" type="mills" value={12000} />);
expect(r.cost).toHaveValue("$12.000");
type(r.cost, ".145");
expect(r.cost).toHaveValue("$0.145");
expect(lastSet).toEqual(145);
});

it("can set cents as cents", async () => {
const r = await render(<TestNumberField label="Cost" type="cents" value={1200} />);
expect(r.cost).toHaveValue("$12.00");
Expand Down Expand Up @@ -153,13 +169,15 @@ describe("NumberFieldTest", () => {
const r = await render(
<>
<TestNumberField label="Days" type="days" value={123} displayDirection />
<TestNumberField label="Mills" type="mills" value={456} displayDirection />
<TestNumberField label="Cents" type="cents" value={456} displayDirection />
<TestNumberField label="Basis Points" type="basisPoints" value={789} displayDirection />
<TestNumberField label="Percent" type="percent" value={123} displayDirection />
<TestNumberField label="Zero Percent" type="percent" value={0} displayDirection />
</>,
);
expect(r.days).toHaveValue("+123 days");
expect(r.mills).toHaveValue("+$0.456");
expect(r.cents).toHaveValue("+$4.56");
expect(r.basisPoints).toHaveValue("+7.89%");
expect(r.percent).toHaveValue("+123%");
Expand Down
8 changes: 6 additions & 2 deletions src/inputs/NumberField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { Css, Xss } from "src/Css";
import { maybeCall } from "src/utils";
import { TextFieldBase } from "./TextFieldBase";

export type NumberFieldType = "cents" | "dollars" | "percent" | "basisPoints" | "days";
export type NumberFieldType = "cents" | "dollars" | "percent" | "basisPoints" | "days" | "mills";

// exported for testing purposes
export interface NumberFieldProps extends Pick<PresentationFieldProps, "labelStyle" | "fullWidth"> {
Expand Down Expand Up @@ -85,7 +85,9 @@ export function NumberField(props: NumberFieldProps) {

const isDisabled = !!disabled;
const isReadOnly = !!readOnly;
const factor = type === "percent" || type === "cents" ? 100 : type === "basisPoints" ? 10_000 : 1;
const factor =
type === "percent" ? 100 : type === "cents" ? 100 : type === "mills" ? 1_000 : type === "basisPoints" ? 10_000 : 1;

const signDisplay = displayDirection ? "always" : "auto";
const defaultFormatOptions: Intl.NumberFormatOptions = useMemo(
() => ({
Expand All @@ -111,6 +113,8 @@ export function NumberField(props: NumberFieldProps) {
? { style: "percent", minimumFractionDigits: 2 }
: type === "cents"
? { style: "currency", currency: "USD", minimumFractionDigits: 2 }
: type === "mills"
? { style: "currency", currency: "USD", minimumFractionDigits: 3 }
: type === "dollars"
? { style: "currency", currency: "USD", minimumFractionDigits: numFractionDigits ?? 2 }
: type === "days"
Expand Down
98 changes: 47 additions & 51 deletions src/inputs/RichTextField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,65 +69,61 @@ export function RichTextField(props: RichTextFieldProps) {
onFocusRef.current = onFocus;

// Generate a unique id to be used for matching `trix-initialize` event for this instance.
const id = useMemo(
() => {
if (readOnly) return;

const id = `trix-editor-${trixId}`;
trixId++;

function onEditorInit(e: Event) {
const targetEl = e.target as HTMLElement;
if (targetEl.id === id) {
editorElement.current = targetEl;
const editor = (editorElement.current as any).editor;
setEditor(editor);
if (mergeTags !== undefined) {
attachTributeJs(mergeTags, editorElement.current!);
}
const id = useMemo(() => {
if (readOnly) return;

const id = `trix-editor-${trixId}`;
trixId++;

function onEditorInit(e: Event) {
const targetEl = e.target as HTMLElement;
if (targetEl.id === id) {
editorElement.current = targetEl;
const editor = (editorElement.current as any).editor;
setEditor(editor);
if (mergeTags !== undefined) {
attachTributeJs(mergeTags, editorElement.current!);
}

currentHtml.current = value;
editor.loadHTML(value || "");
// Remove listener once we've initialized
window.removeEventListener("trix-initialize", onEditorInit);

function trixChange(e: ChangeEvent) {
const { textContent, innerHTML } = e.target;
const onChange = onChangeRef.current;
// If the user only types whitespace, treat that as undefined
if ((textContent || "").trim() === "") {
currentHtml.current = undefined;
onChange && onChange(undefined, undefined, []);
} else {
currentHtml.current = innerHTML;
const mentions = extractIdsFromMentions(mergeTags || [], textContent || "");
onChange && onChange(innerHTML, textContent || undefined, mentions);
}
currentHtml.current = value;
editor.loadHTML(value || "");
// Remove listener once we've initialized
window.removeEventListener("trix-initialize", onEditorInit);

function trixChange(e: ChangeEvent) {
const { textContent, innerHTML } = e.target;
const onChange = onChangeRef.current;
// If the user only types whitespace, treat that as undefined
if ((textContent || "").trim() === "") {
currentHtml.current = undefined;
onChange && onChange(undefined, undefined, []);
} else {
currentHtml.current = innerHTML;
const mentions = extractIdsFromMentions(mergeTags || [], textContent || "");
onChange && onChange(innerHTML, textContent || undefined, mentions);
}
}

const trixBlur = () => maybeCall(onBlurRef.current);
const trixFocus = () => maybeCall(onFocusRef.current);
const trixBlur = () => maybeCall(onBlurRef.current);
const trixFocus = () => maybeCall(onFocusRef.current);

// We don't want to allow file attachment for now. In addition to hiding the button, also disable drag-and-drop
// https://github.com/basecamp/trix#storing-attached-files
const preventDefault = (e: any) => e.preventDefault();
window.addEventListener("trix-file-accept", preventDefault);
// We don't want to allow file attachment for now. In addition to hiding the button, also disable drag-and-drop
// https://github.com/basecamp/trix#storing-attached-files
const preventDefault = (e: any) => e.preventDefault();
window.addEventListener("trix-file-accept", preventDefault);

editorElement.current.addEventListener("trix-change", trixChange as any);
editorElement.current.addEventListener("trix-blur", trixBlur);
editorElement.current.addEventListener("trix-focus", trixFocus);
}
editorElement.current.addEventListener("trix-change", trixChange as any);
editorElement.current.addEventListener("trix-blur", trixBlur);
editorElement.current.addEventListener("trix-focus", trixFocus);
}
}

// Attaching listener to the `window` to we're listening prior to render.
// The <trix-editor /> web component's `trix-initialize` event may fire before a `useEffect` hook in the component is executed, making it difficult ot attach the event listener locally.
window.addEventListener("trix-initialize", onEditorInit);
return id;
},
// TODO: validate this eslint-disable. It was automatically ignored as part of https://app.shortcut.com/homebound-team/story/40033/enable-react-hooks-exhaustive-deps-for-react-projects
// Attaching listener to the `window` to we're listening prior to render.
// The <trix-editor /> web component's `trix-initialize` event may fire before a `useEffect` hook in the component is executed, making it difficult ot attach the event listener locally.
window.addEventListener("trix-initialize", onEditorInit);
return id;
// eslint-disable-next-line react-hooks/exhaustive-deps
[],
);
}, [readOnly]);

useEffect(() => {
// If our value prop changes (without the change coming from us), reload it
Expand Down
21 changes: 20 additions & 1 deletion src/inputs/SelectField.test.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { clickAndWait } from "@homebound/rtl-utils";
import { clickAndWait, type } from "@homebound/rtl-utils";
import { fireEvent } from "@testing-library/react";
import { useState } from "react";
import { SelectField, SelectFieldProps, Value } from "src/inputs";
Expand Down Expand Up @@ -472,6 +472,25 @@ describe("SelectFieldTest", () => {
expect(onSelect.mock.calls[2][0]).toBe(undefined);
});

it("allows to add a new option", async () => {
// Given a SelectField
const onAddNew = jest.fn();
const r = await render(
<TestSelectField
label="Age"
value={undefined}
options={options}
getOptionLabel={(o) => o.name}
getOptionValue={(o) => o.id}
onAddNew={onAddNew}
/>,
);
// When we select Add New option
select(r.age, "Add New");
// Then onAddNew was called
expect(onAddNew).toHaveBeenCalledTimes(1);
});

// Used to validate the `unset` option can be applied to non-`HasIdAndName` options
type HasLabelAndValue = {
label: string;
Expand Down
2 changes: 1 addition & 1 deletion src/inputs/SelectField.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useMemo } from "react";
import { Value } from "src/inputs";
import { ComboBoxBase, ComboBoxBaseProps, unsetOption } from "src/inputs/internal/ComboBoxBase";
import { HasIdAndName, HasIdIsh, HasNameIsh, Optional } from "src/types";
import { HasIdIsh, HasNameIsh, Optional } from "src/types";
import { defaultOptionLabel, defaultOptionValue } from "src/utils/options";

export interface SelectFieldProps<O, V extends Value>
Expand Down
2 changes: 2 additions & 0 deletions src/inputs/TextField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ export interface TextFieldProps<X> extends BeamTextFieldProps<X> {
endAdornment?: ReactNode;
startAdornment?: ReactNode;
hideErrorMessage?: boolean;
/** Allow focusing without selecting, i.e. to let the user keep typing after we've pre-filled text + called focus, like the Add New component. */
selectOnFocus?: boolean;
}

export function TextField<X extends Only<TextFieldXss, X>>(props: TextFieldProps<X>) {
Expand Down
5 changes: 4 additions & 1 deletion src/inputs/TextFieldBase.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ export interface TextFieldBaseProps<X>
// Replaces empty input field and placeholder with node
// IE: Multiselect renders list of selected items in the input field
unfocusedPlaceholder?: ReactNode;
/** Allow focusing without selecting, i.e. to let the user keep typing after we've pre-filled text + called focus, like the Add New component. */
selectOnFocus?: boolean;
}

// Used by both TextField and TextArea
Expand Down Expand Up @@ -97,6 +99,7 @@ export function TextFieldBase<X extends Only<TextFieldXss, X>>(props: TextFieldB
alwaysShowHelperText = false,
fullWidth = fieldProps?.fullWidth ?? false,
unfocusedPlaceholder,
selectOnFocus = true,
} = props;

const typeScale = fieldProps?.typeScale ?? (inputProps.readOnly && labelStyle !== "hidden" ? "smMd" : "sm");
Expand Down Expand Up @@ -188,7 +191,7 @@ export function TextFieldBase<X extends Only<TextFieldXss, X>>(props: TextFieldB
}

const onFocusChained = chain((e: FocusEvent<HTMLInputElement> | FocusEvent<HTMLTextAreaElement>) => {
e.target.select();
if (selectOnFocus) e.target.select();
}, onFocus);

// Simulate clicking `ElementType` when using an unfocused placeholder
Expand Down
Loading

0 comments on commit a65c619

Please sign in to comment.