#Best practice way for adding a class name to field labels on focus

1 messages · Page 1 of 1 (latest)

thorny osprey
#

I want the label for Mantine fields to visually indicate when their corresponding field is focused (e.g. by turning blue).
This is the code I wrote:

const MyAutocompleteCustomClassNames = {
  inputWrapperLabelFocused: `${CUSTOM_CLASSNAME_PREFIX}-InputWrapper-label-focused`,
  autocompleteLabelFocused: `${CUSTOM_CLASSNAME_PREFIX}-Autocomplete-label-focused`,
} as const;

const UnstyledMyAutocomplete: React.FunctionComponent<
  React.ComponentProps<typeof Autocomplete>
> = ({
  onFocus: propOnFocus,
  onBlur: propOnBlur,
  classNames: propClassNames,
  ...restProps
}) => {
  const [focused, setFocused] = useState(false)

  propOnFocus = propOnFocus ?? (() => {})
  propOnBlur = propOnBlur ?? (() => {})

  const onFocus: NonNullable<
    React.ComponentProps<typeof Autocomplete>["onFocus"]
  > = (event) => {
    setFocused(true);
    propOnFocus(event);
  };

  const onBlur: NonNullable<
    React.ComponentProps<typeof Autocomplete>["onBlur"]
  > = (event) => {
    setFocused(false);
    propOnBlur(event);
  };

  const ownClassNames = focused
    ? {
        label: clsx(
          MyAutocompleteCustomClassNames.inputWrapperLabelFocused,
          MyAutocompleteCustomClassNames.autocompleteLabelFocused
        ),
      }
    : {};

  const classNamesMergeFn = R.mergeDeepWith(clsx, ownClassNames)

  let classNames: NonNullable<
    React.ComponentProps<typeof Autocomplete>["classNames"]
  >;
  if (propClassNames === undefined) {
    classNames = ownClassNames;
  } else if (typeof propClassNames === "function") {
    classNames = R.compose(classNamesMergeFn, propClassNames);
  } else {
    classNames = classNamesMergeFn(propClassNames);
  }

  return (
    <Autocomplete
      onFocus={onFocus}
      onBlur={onBlur}
      classNames={classNames}
      {...restProps}
    />
  );
};

const MyAutocomplete = styled(UnstyledMyAutocomplete)({
  [`& .${MyAutocompleteCustomClassNames.autocompleteLabelFocused}`]: {
    color: "var(--mantine-color-blue-filled)",
  },
});
#

My questions:

  • Is there a better and/or more concise way of writing this component? (I do think it takes too many lines for what it does)
  • My code augments only the <Autocomplete/> component, is there a way to apply this label focus behaviour to all Mantine labelled field components globally, or do I have no choice but to apply it on a per-component basis?
finite quiver
thorny osprey
thorny osprey
#

@finite quiver Some specific questions:

  1. In const [focused, setFocused] = useState(false) I set the initial focus state to false. Is this potentially problematic? Maybe the input already has focus and this initial state causes it to lose focus? Should I be reading some value to determine what the intial state should be?

  2. In my particular example, classNames has a dependency on the internal focus state. But what if classNames were to have a dependency on the value of the autocomplete? I could of course add in a useState just like I did for focused, but the problem is that it makes the value an internal state of the component - that is to say: my 'wrapper' component would no longer allow users to control it externally. How could I work around this?

  3. How could I extract out the classNames merge logic and properly type it? I've made a separate thread for this as well (uses styles instead of classNames, but it's the same underlying concept): #1270996880905470004 message .

finite quiver
thorny osprey
# finite quiver 1. It is fine 2. https://mantine.dev/hooks/use-uncontrolled/ 3. Whichever way yo...

Could you provide a more detailed answer for (3)?

This is my best effort, but it produces a lot of type errors:

type StylesObject<Payload extends FactoryPayload> = Exclude<
  Styles<Payload>,
  Function
>;

export const mergeStyles = <Payload extends FactoryPayload>(
  ownStyles: StylesObject<Payload>,
  stylesProp: Styles<Payload> | undefined = {},
) => {
  const stylesMergeFn = R.mergeDeepRight(ownStyles) as (
    arg0: StylesObject<Payload>,
  ) => StylesObject<Payload>;
  return typeof stylesProp === "function"
    ? R.compose(stylesMergeFn, stylesProp)
    : stylesMergeFn(stylesProp);
};

function withStyles<
  Payload extends FactoryPayload,
  Component extends MantineComponent<Payload>,
>(styles: Styles<Payload>, Component: Component): Component {
  return ({ styles: stylesProp, ...restProps }) =>
    (
      <Component styles={mergeStyles(styles, stylesProp)} {...restProps} />
    );
}

export default withStyles;
finite quiver
thorny osprey
thorny osprey
finite quiver
thorny osprey
finite quiver
# thorny osprey Why is that? I've found myself having to add in the merge classNames/ merge styl...

Because usually people try to implement something like this are ending with either a solution that does not work (your code has several issues that I can tell from a brief look – missing forwardRef, inaccurate types usage with StylesObject, etc.) or with a solution that is not maintainable (it might require 100+ lines of code just for types to achieve somewhat good typesafety and then there might be edge cases).

thorny osprey
thorny osprey
# finite quiver In your hoc you need: 1. Accept component as prop and type it as generic 2. Get ...

Was this what you had in mind?

type StylesObject<T extends string> = Partial<Record<T, CSSProperties>>;

type Styles<T extends string> = (
  theme: MantineTheme,
  props: unknown,
  ctx: unknown,
) => StylesObject<T>;

export const mergeStyles = <T extends string>(
  ownStyles: StylesObject<T>,
  stylesProp: Styles<T> | undefined,
) => {
  const stylesMergeFn = R.mergeDeepRight(ownStyles) as (
    arg0: StylesObject<T>,
  ) => StylesObject<T>;
  return typeof stylesProp === "function"
    ? R.compose(stylesMergeFn, stylesProp)
    : stylesMergeFn(stylesProp ?? {});
};

function withStyles<
  Component extends keyof JSX.IntrinsicElements | JSXElementConstructor<any>,
>(
  styles: ComponentProps<Component>["styles"],
  Component: Component,
): FunctionComponent<ComponentProps<Component>> {
  return ({ styles: stylesProp, ...restProps }: ComponentProps<Component>) => (
    <Component styles={mergeStyles(styles, stylesProp)} {...restProps} />
  );
}

export default withStyles;
finite quiver
#

I do not know, if it works for youm then it's fine

thorny osprey
#

Thank you!

thorny osprey
# finite quiver Because usually people try to implement something like this are ending with eith...

Is this what you were looking for with forwardRef()?

function withStyles<
  Component extends keyof JSX.IntrinsicElements | JSXElementConstructor<any>,
>(
  styles: ComponentProps<Component>["styles"],
  Component: Component,
): FunctionComponent<ComponentProps<Component>> {
  return forwardRef(
    ({ styles: stylesProp, ...restProps }: ComponentProps<Component>, ref) => (
      <Component
        styles={mergeStyles(styles, stylesProp)}
        ref={ref}
        {...(restProps as any)}
      />
    ),
  );
}