Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: palette fill as a presentation prop; getInputStylePalette fn prop; #1092

Merged
merged 7 commits into from
Dec 9, 2024
7 changes: 6 additions & 1 deletion src/components/PresentationContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { createContext, PropsWithChildren, useContext, useMemo } from "react";
import { GridStyle } from "src/components/Table";
import { Typography } from "src/Css";

export type InputStylePalette = "success" | "warning" | "caution" | "info";
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mirrors our coloredChips and does leave room for more bespoke customization in the future if the need arises

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice! I like giving the users "logical options" and keeping the physical color palette/colors internally within the component.


export interface PresentationFieldProps {
numberAlignment?: "left" | "right";
/** Sets the label position or visibility. Defaults to "above" */
Expand All @@ -23,10 +25,13 @@ export interface PresentationFieldProps {
errorInTooltip?: true;
/** Allow the fields to grow to the width of its container. By default, fields will extend up to 550px */
fullWidth?: boolean;
/** Changes bg and hoverBg; Takes priority over `contrast`; Useful when showing many fields w/in a table that require user attention; In no way should be used as a replacement for error/focus state */
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great docs!

inputStylePalette?: InputStylePalette;
Comment on lines +28 to +29
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like this since there's been a lot of discussion around how we can show to users what changes they've made at a glance, mostly in dynamic schedules. With this we can pass something in based on a form state for example if asked

}

export type PresentationContextProps = {
fieldProps?: PresentationFieldProps;
/** `inputStylePalette` omitted because it is too dependent on the individual field use case to be controlled at this level */
fieldProps?: Omit<PresentationFieldProps, "inputStylePalette">;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not something we want set at a context level

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense, that it's not a context-level thing, but then wonder if we can find a better spot for declaring it, then PresentationFieldProps?

Hm, I guess PresentationFieldProps does seem to be "common across text fields and select fields", and it isn't used by other things that aren't getting supported in this PR... 🤔 so sgtm...

Could make your PR comment an in-source comment 🤷

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"common across text fields and select fields",
Yup! Thats why its here. Comment updated

gridTableStyle?: GridStyle;
// Defines whether content should be allowed to wrap or not. `undefined` is treated as true.
wrap?: boolean;
Expand Down
49 changes: 47 additions & 2 deletions src/inputs/SelectField.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Meta } from "@storybook/react";
import { within } from "@storybook/test";
import { useState } from "react";
import { GridColumn, GridTable, Icon, IconKey, simpleHeader, SimpleHeaderAndData } from "src/components";
import { InputStylePalette } from "src/components/PresentationContext";
import { Css } from "src/Css";
import { SelectField, SelectFieldProps } from "src/inputs/SelectField";
import { Value } from "src/inputs/Value";
Expand Down Expand Up @@ -31,14 +32,22 @@ type TestOption = {
icon?: IconKey;
};

const options: TestOption[] = [
const standardOptions: TestOption[] = [
{ id: "1", name: "Download", icon: "download" },
{ id: "2", name: "Camera", icon: "camera" },
{ id: "3", name: "Info Circle", icon: "infoCircle" },
{ id: "4", name: "Calendar", icon: "calendar" },
{ id: "5", name: "Dollar dollar bill, ya'll! ".repeat(5), icon: "dollar" },
];

const coloredOptions: TestOption[] = [
{ id: "1", name: "Download (SUCCESS style palette when selected)", icon: "download" },
{ id: "2", name: "Camera (CAUTION style palette when selected)", icon: "camera" },
{ id: "3", name: "Info Circle (WARNING style palette when selected)", icon: "infoCircle" },
{ id: "4", name: "Calendar (INFO style palette when selected)", icon: "calendar" },
{ id: "5", name: "Dollar dollar bill, ya'll! (NO EXTRA style palette when selected)", icon: "dollar" },
];

const optionsWithNumericIds: { id: number; name: string }[] = [
{ id: 1, name: "One" },
{ id: 2, name: "Two" },
Expand All @@ -54,6 +63,7 @@ const booleanOptions = [

function Template(args: SelectFieldProps<any, any>) {
const loadTestOptions: TestOption[] = zeroTo(1000).map((i) => ({ id: String(i), name: `Project ${i}` }));
const options = (args?.options as TestOption[]) ?? standardOptions;

return (
<div css={Css.df.fdc.gap5.p2.if(args.contrast === true).white.bgGray800.$}>
Expand Down Expand Up @@ -223,6 +233,33 @@ export const Contrast = Template.bind({});
// @ts-ignore
Contrast.args = { compact: true, contrast: true };

// @ts-ignore
function getInputStylePalette(v): InputStylePalette | undefined {
if (v?.includes(1) || v?.includes("1")) return "success";
if (v?.includes(2) || v?.includes("2")) return "caution";
if (v?.includes(3) || v?.includes("3")) return "warning";
if (v?.includes(4) || v?.includes("4")) return "info";
return undefined;
}

export const Colored = Template.bind({});
// @ts-ignore
Colored.args = { options: coloredOptions };

export const ColoredContrast = Template.bind({});
// @ts-ignore
ColoredContrast.args = {
contrast: true,
options: coloredOptions,
};

export const ColoredCompact = Template.bind({});
// @ts-ignore
ColoredCompact.args = {
compact: true,
options: coloredOptions,
};

const loadTestOptions: TestOption[] = zeroTo(1000).map((i) => ({ id: String(i), name: `Project ${i}` }));

export function PerfTest() {
Expand Down Expand Up @@ -371,15 +408,23 @@ function TestSelectField<T extends object, V extends Value>(
props: Optional<Omit<SelectFieldProps<T, V>, "onSelect">, "getOptionValue" | "getOptionLabel">,
): JSX.Element {
const [selectedOption, setSelectedOption] = useState<V | undefined>(props.value);
const [inputStylePalette, setInputStylePalette] = useState<InputStylePalette | undefined>();

// @ts-ignore: Hacking around type props within the testSelectField instead of the SB Template
const shouldUseStylePalette: boolean = props.options === coloredOptions;

return (
<div css={Css.df.$}>
<SelectField<T, V>
// The `as any` is due to something related to https://github.com/emotion-js/emotion/issues/2169
// We may have to redo the conditional getOptionValue/getOptionLabel
{...(props as any)}
inputStylePalette={shouldUseStylePalette ? inputStylePalette : undefined}
value={selectedOption}
onSelect={setSelectedOption}
onSelect={(v, opt) => {
setSelectedOption(v);
setInputStylePalette(getInputStylePalette(v));
}}
errorMsg={
selectedOption !== undefined || props.disabled
? ""
Expand Down
47 changes: 33 additions & 14 deletions src/inputs/TextFieldBase.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { chain, mergeProps, useFocusWithin, useHover } from "react-aria";
import { Icon, IconButton, maybeTooltip } from "src/components";
import { HelperText } from "src/components/HelperText";
import { InlineLabel, Label } from "src/components/Label";
import { usePresentationContext } from "src/components/PresentationContext";
import { InputStylePalette, usePresentationContext } from "src/components/PresentationContext";
import { BorderHoverChild, BorderHoverParent } from "src/components/Table/components/Row";
import { Css, Only, Palette } from "src/Css";
import { getLabelSuffix } from "src/forms/labelUtils";
Expand Down Expand Up @@ -42,6 +42,7 @@ export interface TextFieldBaseProps<X>
| "visuallyDisabled"
| "fullWidth"
| "xss"
| "inputStylePalette"
>,
Partial<Pick<BeamTextFieldProps<X>, "onChange">> {
labelProps?: LabelHTMLAttributes<HTMLLabelElement>;
Expand Down Expand Up @@ -103,6 +104,7 @@ export function TextFieldBase<X extends Only<TextFieldXss, X>>(props: TextFieldB
fullWidth = fieldProps?.fullWidth ?? false,
unfocusedPlaceholder,
selectOnFocus = true,
inputStylePalette,
} = props;

const typeScale = fieldProps?.typeScale ?? (inputProps.readOnly && labelStyle !== "hidden" ? "smMd" : "sm");
Expand All @@ -121,14 +123,16 @@ export function TextFieldBase<X extends Only<TextFieldXss, X>>(props: TextFieldB
const fieldHeight = 40;
const compactFieldHeight = 32;

const [bgColor, hoverBgColor, disabledBgColor] = contrast
? [Palette.Gray700, Palette.Gray600, Palette.Gray700]
: borderOnHover
? // Use transparent backgrounds to blend with the table row hover color
[Palette.Transparent, Palette.Blue100, Palette.Gray100]
: borderless && !compound
? [Palette.Gray100, Palette.Gray200, Palette.Gray200]
: [Palette.White, Palette.Gray100, Palette.Gray100];
const [bgColor, hoverBgColor, disabledBgColor] = inputStylePalette
? getInputStylePalette(inputStylePalette)
: contrast
? [Palette.Gray700, Palette.Gray600, Palette.Gray700]
: borderOnHover
? // Use transparent backgrounds to blend with the table row hover color
[Palette.Transparent, Palette.Blue100, Palette.Gray100]
: borderless && !compound
? [Palette.Gray100, Palette.Gray200, Palette.Gray200]
: [Palette.White, Palette.Gray100, Palette.Gray100];

const fieldMaxWidth = getFieldWidth(fullWidth);

Expand All @@ -137,7 +141,7 @@ export function TextFieldBase<X extends Only<TextFieldXss, X>>(props: TextFieldB
inputWrapper: {
...Css[typeScale].df.aic.br4.px1.w100
.bgColor(bgColor)
.gray900.if(contrast)
.gray900.if(contrast && !inputStylePalette)
.white.if(labelStyle === "left")
.w(labelLeftFieldWidth).$,
// When borderless then perceived vertical alignments are misaligned. As there is no longer a border, then the field looks oddly indented.
Expand All @@ -150,7 +154,7 @@ export function TextFieldBase<X extends Only<TextFieldXss, X>>(props: TextFieldB
// Do not add borders to compound fields. A compound field is responsible for drawing its own borders
...(!compound ? Css.ba.$ : {}),
...(borderOnHover && Css.br4.ba.bcTransparent.add("transition", "border-color 200ms").$),
...(borderOnHover && Css.if(isHovered).bgBlue100.ba.bcBlue300.$),
...(borderOnHover && Css.if(isHovered).bgColor(hoverBgColor).ba.bcBlue300.$),
...{
// Highlight the field when hovering over the row in a table, unless some other edit component (including ourselves) is hovered
[`.${BorderHoverParent}:hover:not(:has(.${BorderHoverChild}:hover)) &`]: Css.ba.bcBlue300.$,
Expand All @@ -167,7 +171,7 @@ export function TextFieldBase<X extends Only<TextFieldXss, X>>(props: TextFieldB
},
inputWrapperReadOnly: {
...Css[typeScale].df.aic.w100.gray900
.if(contrast)
.if(contrast && !inputStylePalette)
.white.if(labelStyle === "left")
.w(labelLeftFieldWidth).$,
// If we are hiding the label, then we are typically in a table. Keep the `mh` in this case to ensure editable and non-editable fields in a single table row line up properly
Expand All @@ -179,14 +183,14 @@ export function TextFieldBase<X extends Only<TextFieldXss, X>>(props: TextFieldB
input: {
...Css.w100.mw0.outline0.fg1.bgColor(bgColor).$,
// Not using Truss's inline `if` statement here because `addIn` properties do not respect the if statement.
...(contrast && Css.addIn("&::selection", Css.bgGray800.$).$),
...(contrast && !inputStylePalette && Css.addIn("&::selection", Css.bgGray800.$).$),
// Make the background transparent when highlighting the field on hover
...(borderOnHover && Css.bgTransparent.$),
// For "multiline" fields we add top and bottom padding of 7px for compact, or 11px for non-compact, to properly match the height of the single line fields
...(multiline ? Css.br4.pyPx(compact ? 7 : 11).add("resize", "none").$ : Css.truncate.$),
},
hover: Css.bgColor(hoverBgColor).if(contrast).bcGray600.$,
focus: Css.bcBlue700.if(contrast).bcBlue500.if(borderOnHover).bgBlue100.bcBlue500.$,
focus: Css.bcBlue700.if(contrast).bcBlue500.if(borderOnHover).bgColor(hoverBgColor).bcBlue500.$,
disabled: visuallyDisabled
? Css.cursorNotAllowed.gray600.bgColor(disabledBgColor).if(contrast).gray500.$
: Css.cursorNotAllowed.$,
Expand Down Expand Up @@ -370,3 +374,18 @@ export function TextFieldBase<X extends Only<TextFieldXss, X>>(props: TextFieldB
</>
);
}

function getInputStylePalette(inputStylePalette: InputStylePalette): [Palette, Palette, Palette] {
switch (inputStylePalette) {
case "success":
return [Palette.Green50, Palette.Green100, Palette.Green50];
case "caution":
return [Palette.Yellow50, Palette.Yellow100, Palette.Yellow50];
case "warning":
return [Palette.Red50, Palette.Red100, Palette.Red50];
case "info":
return [Palette.Blue50, Palette.Blue100, Palette.Blue50];
default:
return [Palette.White, Palette.Gray100, Palette.Gray100];
}
}
3 changes: 3 additions & 0 deletions src/inputs/internal/ComboBoxBase.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ export function ComboBoxBase<O, V extends Value>(props: ComboBoxBaseProps<O, V>)
disabledOptions,
borderless,
unsetLabel,
inputStylePalette: propsInputStylePalette,
getOptionLabel: propOptionLabel,
getOptionValue: propOptionValue,
getOptionMenuLabel: propOptionMenuLabel,
Expand Down Expand Up @@ -147,6 +148,7 @@ export function ComboBoxBase<O, V extends Value>(props: ComboBoxBaseProps<O, V>)
);

const values = useMemo(() => propValues ?? [], [propValues]);
const inputStylePalette = useMemo(() => propsInputStylePalette, [propsInputStylePalette]);

const selectedOptionsRef = useRef(options.filter((o) => values.includes(getOptionValue(o))));
const selectedOptions = useMemo(() => {
Expand Down Expand Up @@ -379,6 +381,7 @@ export function ComboBoxBase<O, V extends Value>(props: ComboBoxBaseProps<O, V>)
<div css={Css.df.fdc.w100.maxw(fieldMaxWidth).if(labelStyle === "left").maxw100.$} ref={comboBoxRef}>
<ComboBoxInput
{...otherProps}
inputStylePalette={inputStylePalette}
fullWidth={fullWidth}
buttonProps={buttonProps}
buttonRef={triggerRef}
Expand Down
Loading