Skip to content

Commit

Permalink
feat: palette fill as a presentation prop; getInputStylePalette fn prop;
Browse files Browse the repository at this point in the history
  • Loading branch information
apattersonATX-HB committed Dec 6, 2024
1 parent c9620e9 commit faf018f
Show file tree
Hide file tree
Showing 4 changed files with 96 additions and 17 deletions.
6 changes: 5 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";

export interface PresentationFieldProps {
numberAlignment?: "left" | "right";
/** Sets the label position or visibility. Defaults to "above" */
Expand All @@ -23,10 +25,12 @@ 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 */
inputStylePalette?: InputStylePalette;
}

export type PresentationContextProps = {
fieldProps?: PresentationFieldProps;
fieldProps?: Omit<PresentationFieldProps, "inputStylePalette">;
gridTableStyle?: GridStyle;
// Defines whether content should be allowed to wrap or not. `undefined` is treated as true.
wrap?: boolean;
Expand Down
44 changes: 43 additions & 1 deletion src/inputs/SelectField.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,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 +62,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 +232,39 @@ export const Contrast = Template.bind({});
// @ts-ignore
Contrast.args = { compact: true, contrast: true };

// @ts-ignore
function getInputStylePalette(v) {
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;
}

const standardColoredSelectArgs = {
options: coloredOptions,
// @ts-ignore
getInputStylePalette,
};

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

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

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

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

export function PerfTest() {
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];
}
}
16 changes: 15 additions & 1 deletion src/inputs/internal/ComboBoxBase.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { useButton, useComboBox, useFilter, useOverlayPosition } from "react-ari
import { Item, useComboBoxState, useMultipleSelectionState } from "react-stately";
import { resolveTooltip } from "src/components";
import { Popover } from "src/components/internal";
import { PresentationFieldProps, usePresentationContext } from "src/components/PresentationContext";
import { InputStylePalette, PresentationFieldProps, usePresentationContext } from "src/components/PresentationContext";
import { Css } from "src/Css";
import { ComboBoxInput } from "src/inputs/internal/ComboBoxInput";
import { ListBox } from "src/inputs/internal/ListBox";
Expand All @@ -20,6 +20,9 @@ export interface ComboBoxBaseProps<O, V extends Value> extends BeamFocusableProp
getOptionMenuLabel?: (opt: O, isUnsetOpt?: boolean, isAddNewOption?: boolean) => string | ReactNode;
getOptionValue: (opt: O) => V;
getOptionLabel: (opt: O) => string;
// TODO: explain yourself
// updates input style based on the option(s) selected if `PresentationFieldProps.inputStylePalette` is not set
getInputStylePalette?: (values: V[]) => InputStylePalette;
/** The current value; it can be `undefined`, even if `V` cannot be. */
values: V[] | undefined;
onSelect: (values: V[], opts: O[]) => void;
Expand Down Expand Up @@ -92,6 +95,8 @@ export function ComboBoxBase<O, V extends Value>(props: ComboBoxBaseProps<O, V>)
disabledOptions,
borderless,
unsetLabel,
inputStylePalette: propsInputStylePalette,
getInputStylePalette,
getOptionLabel: propOptionLabel,
getOptionValue: propOptionValue,
getOptionMenuLabel: propOptionMenuLabel,
Expand Down Expand Up @@ -147,6 +152,14 @@ export function ComboBoxBase<O, V extends Value>(props: ComboBoxBaseProps<O, V>)
);

const values = useMemo(() => propValues ?? [], [propValues]);
const inputStylePalette = useMemo(() => {
if (propsInputStylePalette) {
return propsInputStylePalette;
} else if (getInputStylePalette) {
return getInputStylePalette(values);
}
return undefined;
}, [propsInputStylePalette, getInputStylePalette, values]);

const selectedOptionsRef = useRef(options.filter((o) => values.includes(getOptionValue(o))));
const selectedOptions = useMemo(() => {
Expand Down Expand Up @@ -379,6 +392,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

0 comments on commit faf018f

Please sign in to comment.