Skip to content

Commit

Permalink
feat: Support 'fullWidth' prop on all text fields (#979)
Browse files Browse the repository at this point in the history
Adds support for a `PresentationFieldProps.fullWidth` property. The
following components will respect the property:
- FormLines
- TextField
- TextAreaField
- RichTextField
- SelectField
- MultiSelectField
- DateField
- NumberField
- TreeSelectField

Updates the `FormLines` component to default to the `"lg"` size instead
of `"full"`. This will hopefully minimize the impact on existing layouts
as the previous default width for each field within `FormLines` was
550px. By using the `lg` size, we'll continue to default the fields
within `FormLines` to 550px.
  • Loading branch information
Brandon authored Dec 7, 2023
1 parent bb3b5fa commit 6bf26ac
Show file tree
Hide file tree
Showing 24 changed files with 258 additions and 39 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@
"@homebound/eslint-config": "^1.8.0",
"@homebound/rtl-react-router-utils": "1.0.3",
"@homebound/rtl-utils": "^2.65.0",
"@homebound/truss": "^1.131.0",
"@homebound/truss": "^1.132.0",
"@homebound/tsconfig": "^1.0.3",
"@semantic-release/exec": "^6.0.3",
"@semantic-release/git": "^10.0.1",
Expand Down
16 changes: 16 additions & 0 deletions src/Css.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1133,6 +1133,22 @@ class CssBuilder<T extends Properties> {
fd(value: Properties["flexDirection"]) {
return this.add("flexDirection", value);
}
/** Sets `flexWrap: "wrap"`. */
get fww() {
return this.add("flexWrap", "wrap");
}
/** Sets `flexWrap: "wrap-reverse"`. */
get fwr() {
return this.add("flexWrap", "wrap-reverse");
}
/** Sets `flexWrap: "nowrap"`. */
get fwnw() {
return this.add("flexWrap", "nowrap");
}
/** Sets `flexWrap: value`. */
flexWrap(value: Properties["flexWrap"]) {
return this.add("flexWrap", value);
}

// float
/** Sets `float: "left"`. */
Expand Down
2 changes: 1 addition & 1 deletion src/components/Pagination.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ function RenderPagination({ totalRows }: { totalRows: number }) {
const page = useState({ pageNumber: 1, pageSize: 100 });
return (
<>
<FormLines labelStyle="left">
<FormLines labelStyle="left" width="full">
<StaticField label="Number of items" value={String(totalRows)} />
<StaticField label="Current page" value={String(page[0].pageNumber)} />
</FormLines>
Expand Down
2 changes: 2 additions & 0 deletions src/components/PresentationContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ export interface PresentationFieldProps {
visuallyDisabled?: false;
// If set error messages will be rendered as tooltips rather than below the field
errorInTooltip?: true;
/** Allow the fields to grow to the width of its container. By default, fields will extend up to 550px */
fullWidth?: boolean;
}

export type PresentationContextProps = {
Expand Down
152 changes: 142 additions & 10 deletions src/forms/FormLines.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Meta } from "@storybook/react";
import { FieldGroup, FormDivider, FormLines } from "src/forms/FormLines";
import { SelectField } from "src/inputs";
import { DateField, MultiSelectField, RichTextField, SelectField, TextAreaField, TreeSelectField } from "src/inputs";
import { NumberField } from "src/inputs/NumberField";
import { Switch } from "src/inputs/Switch";
import { TextField } from "src/inputs/TextField";
Expand All @@ -13,33 +13,123 @@ export default {
export function FlatList() {
return (
<FormLines>
<TextField label="First" value="first" onChange={() => {}} />
<TextField label="Last" value="last" onChange={() => {}} />
<TextField label="Text Field" value="first" onChange={noop} />
<NumberField label="Number Field" value={1} onChange={noop} />
<DateField label="Date FIeld" value={undefined} onChange={noop} />
<SelectField<Options, number>
label="Single Select Field"
value={1}
options={[
{ id: 1, name: "Green" },
{ id: 2, name: "Red" },
]}
onSelect={noop}
/>
<MultiSelectField<Options, number>
label="Multiselect Field"
values={[1]}
options={[
{ id: 1, name: "Soccer" },
{ id: 2, name: "Basketball" },
{ id: 3, name: "Football" },
]}
onSelect={noop}
/>
<TreeSelectField
values={[]}
onSelect={noop}
options={[{ id: "1", name: "One" }]}
label="Tree Select Field"
getOptionValue={(o) => o.id}
getOptionLabel={(o) => o.name}
/>
<TextAreaField label="Text Area Field" value="" onChange={noop} />
<RichTextField label="Rich Text Field" value="" onChange={noop} />
</FormLines>
);
}

export function SmallFlatList() {
return (
<FormLines width="sm">
<TextField label="First" value="first" onChange={() => {}} />
<TextField label="Last" value="last" onChange={() => {}} />
<TextField label="Text Field" value="first" onChange={noop} />
<NumberField label="Number Field" value={1} onChange={noop} />
<DateField label="Date FIeld" value={undefined} onChange={noop} />
<SelectField<Options, number>
label="Single Select Field"
value={1}
options={[
{ id: 1, name: "Green" },
{ id: 2, name: "Red" },
]}
onSelect={noop}
/>
<MultiSelectField<Options, number>
label="Multiselect Field"
values={[1]}
options={[
{ id: 1, name: "Soccer" },
{ id: 2, name: "Basketball" },
{ id: 3, name: "Football" },
]}
onSelect={noop}
/>
<TreeSelectField
values={[]}
onSelect={noop}
options={[{ id: "1", name: "One" }]}
label="Tree Select Field"
getOptionValue={(o) => o.id}
getOptionLabel={(o) => o.name}
/>
<TextAreaField label="Text Area Field" value="" onChange={noop} />
<RichTextField label="Rich Text Field" value="" onChange={noop} />
</FormLines>
);
}

export function FullFlatList() {
return (
<FormLines width="full">
<TextField label="First" value="first" onChange={() => {}} />
<TextField label="Last" value="last" onChange={() => {}} />
<TextField label="Text Field" value="first" onChange={noop} />
<NumberField label="Number Field" value={1} onChange={noop} />
<DateField label="Date FIeld" value={undefined} onChange={noop} />
<SelectField<Options, number>
label="Single Select Field"
value={1}
options={[
{ id: 1, name: "Green" },
{ id: 2, name: "Red" },
]}
onSelect={noop}
/>
<MultiSelectField<Options, number>
label="Multiselect Field"
values={[1]}
options={[
{ id: 1, name: "Soccer" },
{ id: 2, name: "Basketball" },
{ id: 3, name: "Football" },
]}
onSelect={noop}
/>
<TreeSelectField
values={[]}
onSelect={noop}
options={[{ id: "1", name: "One" }]}
label="Tree Select Field"
getOptionValue={(o) => o.id}
getOptionLabel={(o) => o.name}
/>
<TextAreaField label="Text Area Field" value="" onChange={noop} />
<RichTextField label="Rich Text Field" value="" onChange={noop} />
</FormLines>
);
}

export function SideBySideMedium() {
export function SideBySideFull() {
return (
<FormLines>
<FormLines width="full">
<FieldGroup>
<TextField label="First" value="first" onChange={() => {}} />
<TextField label="Middle" value="middle" onChange={() => {}} />
Expand Down Expand Up @@ -80,7 +170,7 @@ export function SideBySideMedium() {
}
export function SideBySideLarge() {
return (
<FormLines width="lg">
<FormLines>
<FieldGroup>
<TextField label="First" value="first" onChange={() => {}} />
<TextField label="Middle" value="middle" onChange={() => {}} />
Expand Down Expand Up @@ -129,6 +219,48 @@ export function SideBySideLarge() {
</FormLines>
);
}

export function SideBySideMedium() {
return (
<FormLines width="md">
<FieldGroup>
<TextField label="First" value="first" onChange={() => {}} />
<TextField label="Middle" value="middle" onChange={() => {}} />
</FieldGroup>
<TextField label="Address" value="123 Main" onChange={() => {}} />
<FieldGroup>
<Switch label="Primary" labelStyle="form" selected={false} onChange={() => {}} />
<Switch label="Signatory" labelStyle="form" selected={true} onChange={() => {}} />
</FieldGroup>
<FieldGroup>
<TextField label="Title" value="Engineer" onChange={() => {}} />
<span />
</FieldGroup>
<FieldGroup>
<TextField label="First" value="first" onChange={() => {}} />
<TextField label="Middle" value="middle" onChange={() => {}} />
<TextField label="Last" value="last" onChange={() => {}} />
</FieldGroup>
<FieldGroup widths={[2, 1, 2]}>
<TextField label="First" value="first" onChange={() => {}} />
<TextField label="Middle" value="M" onChange={() => {}} />
<TextField label="Last" value="last" onChange={() => {}} />
</FieldGroup>
<FieldGroup>
<NumberField label="Qty" value={1} onChange={() => {}} />
<SelectField<Options, number>
label="Unit of Measure"
value={1}
options={[
{ id: 1, name: "Each" },
{ id: 2, name: "Square Feet" },
]}
onSelect={noop}
/>
</FieldGroup>
</FormLines>
);
}
export function SideBySideSmall() {
return (
<FormLines width="sm">
Expand Down
3 changes: 2 additions & 1 deletion src/forms/FormLines.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,15 @@ export interface FormLinesProps {
* (see the `FieldGroup` component), where they will be laid out side-by-side.
*/
export function FormLines(props: FormLinesProps) {
const { children, width = "full", labelSuffix, labelStyle, compact } = props;
const { children, width = "lg", labelSuffix, labelStyle, compact } = props;
let firstFormHeading = true;

// Only overwrite `fieldProps` if new values are explicitly set. Ensures we only set to `undefined` if explicitly set.
const newFieldProps = {
...("labelSuffix" in props ? { labelSuffix } : {}),
...("labelStyle" in props ? { labelStyle } : {}),
...("compact" in props ? { compact } : {}),
...(width === "full" ? { fullWidth: true } : {}),
};

return (
Expand Down
14 changes: 9 additions & 5 deletions src/inputs/Autocomplete.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,25 @@ import { within } from "@storybook/testing-library";
import { useState } from "react";
import { useFilter } from "react-aria";
import { Css } from "src/Css";
import { Autocomplete } from "src/inputs/Autocomplete";
import { Autocomplete, AutocompleteProps } from "src/inputs/Autocomplete";

export default {
component: Autocomplete,
} as Meta;

export const Example: StoryFn = Template.bind({});
export const Example: StoryFn<AutocompleteProps<any>> = Template.bind({});
Example.parameters = { chromatic: { disableSnapshot: true } };

export const MenuOpen: StoryFn = Template.bind({});
export const FullWidth: StoryFn<AutocompleteProps<any>> = Template.bind({});
FullWidth.args = { fullWidth: true };

export const MenuOpen: StoryFn<AutocompleteProps<any>> = Template.bind({});
MenuOpen.play = async ({ canvasElement }: { canvasElement: HTMLElement }) => {
const canvas = within(canvasElement);
canvas.getByTestId("heroes").focus();
};

function Template() {
function Template(props: AutocompleteProps<any>) {
const { contains } = useFilter({ sensitivity: "base" });
const [value, setValue] = useState<string>();
const allOptions = [
Expand All @@ -30,9 +33,10 @@ function Template() {
{ label: "Black Widow", imgSrc: "/black-widow.jpg" },
];
const [options, setOptions] = useState(allOptions);

console.log("props", props);
return (
<Autocomplete<TestOption>
{...props}
label="Heroes"
labelStyle="hidden"
getOptionValue={(o) => o.label}
Expand Down
4 changes: 2 additions & 2 deletions src/inputs/Autocomplete.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ import { ListBox } from "src/inputs/internal/ListBox";
import { TextFieldBase, TextFieldBaseProps } from "src/inputs/TextFieldBase";
import { Value, valueToKey } from "src/inputs/Value";

interface AutocompleteProps<T>
export interface AutocompleteProps<T>
extends Pick<PresentationFieldProps, "labelStyle">,
Pick<TextFieldBaseProps<any>, "label" | "clearable" | "startAdornment"> {
Pick<TextFieldBaseProps<any>, "label" | "clearable" | "startAdornment" | "fullWidth"> {
onSelect: (item: T) => void;
/** A function that returns how to render the an option in the menu. If not set, `getOptionLabel` will be used */
getOptionMenuLabel?: (o: T) => ReactNode;
Expand Down
3 changes: 2 additions & 1 deletion src/inputs/DateFields/DateField.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import { samples, withDimensions } from "src/utils/sb";

export default {
component: Button,
decorators: [withDimensions()],
parameters: {
design: {
type: "figma",
Expand Down Expand Up @@ -61,6 +60,7 @@ export function DateFields() {
disabledDays={[{ before: jan1 }, { after: jan10 }]}
/>,
],
["Full Width", <TestDateField label="Date" fullWidth />],
);
}

Expand All @@ -79,6 +79,7 @@ export function DatePickerOpen() {
</>
);
}
DatePickerOpen.decorators = [withDimensions()];

function TestDateField(props: Omit<DateFieldProps, "value" | "onChange" | "onBlur" | "onFocus">) {
const [value, onChange] = useState<Date | undefined>(jan1);
Expand Down
5 changes: 4 additions & 1 deletion src/inputs/DateFields/DateFieldBase.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,10 @@ import { maybeCall, useTestIds } from "src/utils";
import { defaultTestId } from "src/utils/defaultTestId";

export interface DateFieldBaseProps
extends Pick<TextFieldBaseProps<Properties>, "borderless" | "visuallyDisabled" | "labelStyle" | "compact"> {
extends Pick<
TextFieldBaseProps<Properties>,
"borderless" | "visuallyDisabled" | "labelStyle" | "compact" | "fullWidth"
> {
label: string;
/** Called when the component loses focus */
onBlur?: () => void;
Expand Down
1 change: 1 addition & 0 deletions src/inputs/DateFields/DateRangeField.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ export function Example() {
label="Error message"
errorMsg={rangeInitUndefined ? undefined : "Required"}
/>
<DateRangeField {...commonProps} fullWidth label="Full Width Field" />
</div>
);
}
5 changes: 5 additions & 0 deletions src/inputs/MultiSelectField.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,11 @@ export function MultiSelectFields() {
)}
/>
</div>

<div css={Css.df.fdc.gap2.$}>
<h1 css={Css.lg.$}>Full Width</h1>
<TestMultiSelectField fullWidth label="Favorite Icon" values={[options[2].id]} options={options} />
</div>
</div>
);
}
Expand Down
5 changes: 5 additions & 0 deletions src/inputs/NumberField.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,11 @@ export function NumberFieldStyles() {
<h1 css={Css.lg.$}>Without grouping</h1>
<TestNumberField value={123456789} label="No grouping" useGrouping={false} />
</div>

<div css={Css.df.fdc.gap2.$}>
<h1 css={Css.lg.$}>Full Width</h1>
<TestNumberField value={0} label="Age" fullWidth />
</div>
</div>
);
}
Expand Down
2 changes: 1 addition & 1 deletion src/inputs/NumberField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { TextFieldBase } from "./TextFieldBase";
export type NumberFieldType = "cents" | "dollars" | "percent" | "basisPoints" | "days";

// exported for testing purposes
export interface NumberFieldProps extends Pick<PresentationFieldProps, "labelStyle"> {
export interface NumberFieldProps extends Pick<PresentationFieldProps, "labelStyle" | "fullWidth"> {
label: string;
/** If set, the label will be defined as 'aria-label` on the input element */
type?: NumberFieldType;
Expand Down
Loading

0 comments on commit 6bf26ac

Please sign in to comment.