Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .changeset/add-field-container-info-tooltip.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
"@dextinity/admin": minor
---

Add `infoTooltip` prop to `FieldContainer` and `Field`

Form fields sometimes need additional explanation beyond the label. `FieldContainer` (and therefore every `Field`-based component like `TextField`) now accepts an `infoTooltip` prop that renders an info icon with a tooltip next to the label, mirroring the existing `infoTooltip` prop on `FormSection`.

**Example**

```tsx
<TextField name="slug" label="Slug" infoTooltip="Used to generate the page URL" />
```
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 4 additions & 1 deletion packages/admin/admin/src/form/Field.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { FieldValidator } from "final-form";
import { type ComponentType, createElement, type ReactNode, useRef } from "react";
import { type ComponentProps, type ComponentType, createElement, type ReactNode, useRef } from "react";
import { Field as FinalFormField, type FieldMetaState, type FieldRenderProps, FormSpy, useForm } from "react-final-form";
import { FormattedMessage } from "react-intl";

Expand All @@ -26,6 +26,7 @@ export interface FieldProps<FieldValue = any, T extends HTMLElement = HTMLElemen
name: string;
label?: ReactNode;
helperText?: ReactNode;
infoTooltip?: ComponentProps<typeof FieldContainer>["infoTooltip"];
component?: ComponentType<any> | string;
children?: (props: FieldRenderProps<FieldValue, T>) => ReactNode;
required?: boolean;
Expand All @@ -45,6 +46,7 @@ export function Field<FieldValue = any, FieldElement extends HTMLElement = HTMLE
name,
label,
helperText,
infoTooltip,
required,
validate,
validateWarning,
Expand Down Expand Up @@ -91,6 +93,7 @@ export function Field<FieldValue = any, FieldElement extends HTMLElement = HTMLE
error={shouldShowError(meta) && (meta.error || meta.submitError)}
warning={shouldShowWarning(meta) && meta.data?.warning}
helperText={helperText}
infoTooltip={infoTooltip}
variant={variant}
fullWidth={fullWidth}
scrollTo={shouldScrollToField(meta)}
Expand Down
47 changes: 46 additions & 1 deletion packages/admin/admin/src/form/FieldContainer.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
import { Info } from "@dextinity/admin-icons";
import { FormControl, FormHelperText, FormLabel, formLabelClasses, inputBaseClasses, svgIconClasses, useThemeProps } from "@mui/material";
import { type ComponentsOverrides, css, type Theme } from "@mui/material/styles";
import { type PropsWithChildren, type ReactNode, useEffect, useRef } from "react";
import { type ComponentProps, isValidElement, type PropsWithChildren, type ReactElement, type ReactNode, useEffect, useRef } from "react";

import { Tooltip } from "../common/Tooltip";
import { createComponentSlot } from "../helpers/createComponentSlot";
import type { ThemedComponentBaseProps } from "../helpers/ThemedComponentBaseProps";

export type FieldContainerProps = ThemedComponentBaseProps<{
root: typeof FormControl;
innerContainer: "div";
label: typeof FormLabel;
infoTooltip: typeof Tooltip;
inputContainer: "div";
error: typeof FormHelperText;
warning: typeof FormHelperText;
Expand All @@ -30,6 +33,10 @@ export type FieldContainerProps = ThemedComponentBaseProps<{
helperText?: ReactNode;
secondaryHelperText?: ReactNode;
helperTextIcon?: ReactNode;
infoTooltip?: ReactNode | Omit<ComponentProps<typeof Tooltip>, "children">;
iconMapping?: {
infoTooltip?: ReactElement;
};
};

export type FieldContainerClassKey =
Expand All @@ -44,6 +51,7 @@ export type FieldContainerClassKey =
| "fieldMarginNever"
| "fieldMarginOnlyIfNotLast"
| "label"
| "infoTooltip"
| "inputContainer"
| "hasError"
| "error"
Expand Down Expand Up @@ -205,6 +213,17 @@ const Label = createComponentSlot(FormLabel)<FieldContainerClassKey, OwnerState>
`,
);

const InfoTooltip = createComponentSlot(Tooltip)<FieldContainerClassKey>({
componentName: "FormFieldContainer",
slotName: "infoTooltip",
})(
({ theme }) => css`
color: ${theme.palette.text.secondary};
margin-left: 4px;
font-size: 16px;
`,
);

const InputContainer = createComponentSlot("div")<FieldContainerClassKey, OwnerState>({
componentName: "FormFieldContainer",
slotName: "inputContainer",
Expand Down Expand Up @@ -306,6 +325,22 @@ const HelperTextContent = createComponentSlot("span")<FieldContainerClassKey>({
slotName: "helperTextContent",
})();

const getInfoTooltipProps = (infoTooltip: FieldContainerProps["infoTooltip"]) => {
if (!infoTooltip) {
return null;
}

if (isValidElement(infoTooltip) || typeof infoTooltip === "string") {
return { title: infoTooltip };
}

if (typeof infoTooltip === "object") {
return infoTooltip;
}

return null;
};

export const FieldContainer = (inProps: PropsWithChildren<FieldContainerProps>) => {
const {
variant = "vertical",
Expand All @@ -320,6 +355,8 @@ export const FieldContainer = (inProps: PropsWithChildren<FieldContainerProps>)
helperText,
secondaryHelperText,
helperTextIcon,
infoTooltip,
iconMapping = {},
scrollTo = false,
fieldMargin = "onlyIfNotLast",
slotProps,
Expand All @@ -330,6 +367,9 @@ export const FieldContainer = (inProps: PropsWithChildren<FieldContainerProps>)
const hasError = !!error;
const hasWarning = !hasError && !!warning;

const { infoTooltip: infoTooltipIcon = <Info sx={{ fontSize: "inherit" }} /> } = iconMapping;
const infoTooltipProps = getInfoTooltipProps(infoTooltip);

const ref = useRef<HTMLDivElement>(null);

useEffect(() => {
Expand All @@ -355,6 +395,11 @@ export const FieldContainer = (inProps: PropsWithChildren<FieldContainerProps>)
<InnerContainer ownerState={ownerState} {...slotProps?.innerContainer}>
<Label ownerState={ownerState} disabled={disabled} {...slotProps?.label}>
{label}
{Boolean(infoTooltipProps) && (
<InfoTooltip {...infoTooltipProps} {...slotProps?.infoTooltip}>
{infoTooltipIcon}
</InfoTooltip>
)}
</Label>
<InputContainer ownerState={ownerState} {...slotProps?.inputContainer}>
{children}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,14 @@ export const FieldContainer = () => {
<TextField name="textFieldWithLabelTwo" label="TextField label" variant="vertical" />
<TextField name="testWithoutLabel" variant="vertical" placeholder="TextField without label" />
</FieldSet>
<FieldSet title="INFO TOOLTIP">
<TextField name="textFieldWithTooltip" label="TextField label" infoTooltip="Additional info about this field" />
<TextField
name="textFieldWithTooltipTitleAndDescription"
label="TextField label"
infoTooltip={{ title: "Title", description: "Additional info about this field" }}
/>
</FieldSet>
</form>
)}
/>
Expand Down
Loading