Skip to content

Commit

Permalink
Merge branch 'main' into table-card-styles
Browse files Browse the repository at this point in the history
  • Loading branch information
Brandon authored Dec 14, 2023
2 parents f6556cf + 3da849d commit 4f163c4
Show file tree
Hide file tree
Showing 4 changed files with 91 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.328.0",
"version": "2.329.0",
"author": "Homebound",
"license": "MIT",
"main": "dist/index.js",
Expand Down
35 changes: 27 additions & 8 deletions src/components/Label.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import React, { LabelHTMLAttributes } from "react";
import React, { LabelHTMLAttributes, ReactNode } from "react";
import { VisuallyHidden } from "react-aria";
import { Css } from "src/Css";
import { Css, Font, Only, Palette, Xss } from "src/Css";
import { Icon } from "src";

interface LabelProps {
type LabelXss = Font | "color";

interface LabelProps<X> {
// We don't usually have `fooProps`-style props, but this is for/from react-aria
labelProps?: LabelHTMLAttributes<HTMLLabelElement>;
label: string;
Expand All @@ -11,22 +14,38 @@ interface LabelProps {
hidden?: boolean;
contrast?: boolean;
multiline?: boolean;
tooltip?: ReactNode;
// Removes margin bottom if true - This is different from InlineLabel. InlineLabel expects to be rendered visually within the field element. Rather just on the same line.
inline?: boolean;
xss?: X;
}

/** An internal helper component for rendering form labels. */
export const Label = React.memo((props: LabelProps) => {
const { labelProps, label, hidden, suffix, contrast = false, ...others } = props;
function LabelComponent<X extends Only<Xss<LabelXss>, X>>(props: LabelProps<X>) {
const { labelProps, label, hidden, suffix, contrast = false, tooltip, inline, xss, ...others } = props;
const labelEl = (
<label {...labelProps} {...others} css={Css.dib.sm.gray700.mbPx(4).if(contrast).white.$}>
<label
{...labelProps}
{...others}
css={{ ...Css.dif.aic.gap1.sm.gray700.mbPx(inline ? 0 : 4).if(contrast).white.$, ...xss }}
>
{label}
{suffix && ` ${suffix}`}
{tooltip && (
<span css={Css.fs0.$}>
<Icon icon="infoCircle" tooltip={tooltip} inc={2} color={contrast ? Palette.White : Palette.Gray700} />
</span>
)}
</label>
);
return hidden ? <VisuallyHidden>{labelEl}</VisuallyHidden> : labelEl;
});
}

export const Label = React.memo(LabelComponent) as typeof LabelComponent;

type InlineLabelProps = Omit<LabelProps<unknown>, "xss" | "inline">;
/** Used for showing labels within text fields. */
export function InlineLabel({ labelProps, label, contrast, multiline = false, ...others }: LabelProps) {
export function InlineLabel({ labelProps, label, contrast, multiline = false, ...others }: InlineLabelProps) {
return (
<label
{...labelProps}
Expand Down
8 changes: 8 additions & 0 deletions src/inputs/Switch.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,14 @@ export const LabelStyles = () => {
<SwitchComponent label="Example Label" onChange={setSelected} selected={selected} labelStyle="hidden" />
<h2 css={Css.baseMd.mb1.mt3.pt2.bt.bGray200.$}>Left</h2>
<SwitchComponent label="Example Label" onChange={setSelected} selected={selected} labelStyle="left" />
<h2 css={Css.baseMd.mb1.mt3.pt2.bt.bGray200.$}>Centered</h2>
<SwitchComponent
label="Example Label"
tooltip="Tooltip example"
onChange={setSelected}
selected={selected}
labelStyle="centered"
/>
</div>
);
};
Expand Down
115 changes: 55 additions & 60 deletions src/inputs/Switch.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { ReactNode, useRef } from "react";
import { useFocusRing, useHover, useSwitch, VisuallyHidden } from "react-aria";
import { maybeTooltip, resolveTooltip } from "src/components";
import { resolveTooltip } from "src/components";
import { Label } from "src/components/Label";
import { Css, Palette } from "src/Css";
import { Icon } from "../components/Icon";
Expand All @@ -16,7 +16,7 @@ export interface SwitchProps {
/** Input label */
label: string;
/** Where to put the label. */
labelStyle?: "form" | "inline" | "filter" | "hidden" | "left"; // TODO: Update `labelStyle` to make consistent with other `labelStyle` properties in the library
labelStyle?: "form" | "inline" | "filter" | "hidden" | "left" | "centered"; // TODO: Update `labelStyle` to make consistent with other `labelStyle` properties in the library
/** Whether or not to hide the label */
hideLabel?: boolean;
/** Handler when the interactive element state changes. */
Expand All @@ -25,6 +25,8 @@ export interface SwitchProps {
selected: boolean;
/** Whether to include icons like the check mark */
withIcon?: boolean;
/** Adds tooltip for the switch */
tooltip?: ReactNode;
}

export function Switch(props: SwitchProps) {
Expand All @@ -46,74 +48,67 @@ export function Switch(props: SwitchProps) {
const { inputProps } = useSwitch({ ...ariaProps, "aria-label": label }, state, ref);
const { isFocusVisible: isKeyboardFocus, focusProps } = useFocusRing(otherProps);
const { hoverProps, isHovered } = useHover(ariaProps);
const tooltip = resolveTooltip(disabled);
const tooltip = resolveTooltip(disabled, props.tooltip);

return maybeTooltip({
title: tooltip,
placement: "top",
children: (
<label
{...hoverProps}
return (
<div
{...hoverProps}
css={{
...Css.relative.cursorPointer.df.w("max-content").selectNone.$,
...(labelStyle === "form" && Css.fdc.$),
...(labelStyle === "left" && Css.w100.aic.$),
...(labelStyle === "inline" && Css.gap2.aic.$),
...(labelStyle === "filter" && Css.jcsb.gap1.aic.wa.sm.$),
...(labelStyle === "centered" && Css.fdc.aic.$),
...(isDisabled && Css.cursorNotAllowed.gray400.$),
}}
>
{labelStyle !== "inline" && labelStyle !== "hidden" && (
<div css={Css.if(labelStyle === "left").w50.$}>
<Label
label={label}
tooltip={tooltip}
xss={Css.if(labelStyle === "filter").gray900.$}
inline={labelStyle === "left" || labelStyle === "filter"}
/>
</div>
)}
{/* Background */}
<div
aria-hidden="true"
css={{
...Css.relative.cursorPointer.df.w("max-content").smMd.selectNone.$,
...(labelStyle === "form" && Css.fdc.$),
...(labelStyle === "left" && Css.w100.fdr.$),
...(labelStyle === "inline" && Css.gap2.aic.$),
...(labelStyle === "filter" && Css.jcsb.gap1.aic.wa.sm.$),
...(isDisabled && Css.cursorNotAllowed.gray400.$),
...Css.wPx(40).hPx(toggleHeight(compact)).bgGray200.br12.relative.transition.$,
...(isHovered && switchHoverStyles),
...(isKeyboardFocus && switchFocusStyles),
...(isDisabled && Css.bgGray300.$),
...(isSelected && Css.bgBlue700.$),
...(isSelected && isHovered && switchSelectedHoverStyles),
}}
aria-label={label}
>
{(labelStyle === "form" || labelStyle === "left") && (
<div css={Css.if(labelStyle === "left").w50.$}>
<Label label={label} />
</div>
)}
{labelStyle === "filter" && <span>{label}</span>}
{/* Background */}
{/* Circle */}
<div
aria-hidden="true"
css={{
...Css.wPx(40).hPx(toggleHeight(compact)).bgGray200.br12.relative.transition.$,
...(isHovered && switchHoverStyles),
...(isKeyboardFocus && switchFocusStyles),
...(isDisabled && Css.bgGray300.$),
...(isSelected && Css.bgBlue700.$),
...(isSelected && isHovered && switchSelectedHoverStyles),
...switchCircleDefaultStyles(compact),
...(isDisabled && Css.bgGray100.$),
...(isSelected && switchCircleSelectedStyles(compact)),
}}
>
{/* Circle */}
<div
css={{
...switchCircleDefaultStyles(compact),
...(isDisabled && Css.bgGray100.$),
...(isSelected && switchCircleSelectedStyles(compact)),
}}
>
{/* Icon */}
{withIcon && (
<Icon icon={isSelected ? "check" : "x"} color={isSelected ? Palette.Blue700 : Palette.Gray400} />
)}
</div>
{/* Icon */}
{withIcon && (
<Icon icon={isSelected ? "check" : "x"} color={isSelected ? Palette.Blue700 : Palette.Gray400} />
)}
</div>
{/* Since we are using childGap, we must wrap the label in an element and
</div>
{/* Since we are using childGap, we must wrap the label in an element and
match the height of the icon for horizontal alignment */}
{labelStyle === "inline" && (
<span
css={{
// LineHeight is conditionally applied to handle compact version text alignment
...Css.if(compact).add("lineHeight", "1").$,
}}
>
{label}
</span>
)}
<VisuallyHidden>
<input ref={ref} {...inputProps} {...focusProps} />
</VisuallyHidden>
</label>
),
});
{labelStyle === "inline" && (
<Label label={label} tooltip={tooltip} inline xss={Css.smMd.gray900.if(compact).add("lineHeight", "1").$} />
)}
<VisuallyHidden>
<input ref={ref} {...inputProps} {...focusProps} />
</VisuallyHidden>
</div>
);
}

/** Styles */
Expand Down

0 comments on commit 4f163c4

Please sign in to comment.