@codecademy/gamut
72.3.072.3.1-alpha.f273fd.0
FieldHint.tsx+
FieldHint.tsxNew file+100
Index: FieldHint.tsx
===================================================================
--- FieldHint.tsx
+++ FieldHint.tsx
@@ -0,0 +1,100 @@
+import { Box, BoxProps, ToolTip, ToolTipProps } from '@codecademy/gamut';
+import { MiniInfoOutlineIcon } from '@codecademy/gamut-icons';
+import { css } from '@codecademy/gamut-styles';
+import styled from '@emotion/styled';
+import React from 'react';
+
+/**
+ * `FieldHint` is a small info icon meant to sit next to a form label. Hovering
+ * or focusing the icon reveals extra explanatory text in a floating bubble.
+ *
+ * It's a thin composition of Gamut's `ToolTip` (which already handles the
+ * hover/`:focus-visible` reveal, positioning, and `role="tooltip"` wiring) around
+ * a minimal icon trigger, rather than a hand-rolled popover - see the
+ * `gamut-component-first` skill for why that matters.
+ *
+ * Usage:
+ * ```tsx
+ * <FlexBox alignItems="center" gap={4}>
+ * <FormGroupLabel htmlFor="email">Email</FormGroupLabel>
+ * <FieldHint hint="We'll only use this to send order updates." label="More info about Email" />
+ * </FlexBox>
+ * ```
+ *
+ * For screen reader users who don't hover/focus the icon itself, pass `id`
+ * here and reuse it as the related field's `aria-describedby` so the hint
+ * text is announced when the field is focused too.
+ */
+export type FieldHintProps = {
+ /**
+ * The explanatory text (or rich content) shown in the floating bubble.
+ */
+ hint: ToolTipProps['info'];
+ /**
+ * Accessible name for the hint icon itself, e.g. "More info about Email".
+ * @default 'More information'
+ */
+ label?: string;
+ /**
+ * Id applied to the tooltip content. Reuse this as `aria-describedby` on
+ * the related form field so its hint is announced on field focus as well.
+ */
+ id?: string;
+ /**
+ * Additional props forwarded to the underlying `ToolTip`, e.g. `alignment`,
+ * `placement`, or `narrow`.
+ */
+ tipProps?: Omit<ToolTipProps, 'info' | 'children' | 'id'>;
+} & Omit<BoxProps, 'as' | 'children'>;
+
+const HintTrigger = styled(Box)(
+ css({
+ position: 'relative',
+ display: 'inline-flex',
+ alignItems: 'center',
+ justifyContent: 'center',
+ flexShrink: 0,
+ height: 20,
+ width: 20,
+ p: 0,
+ border: 'none',
+ borderRadius: 'circle',
+ bg: 'transparent',
+ color: 'secondary',
+ cursor: 'pointer',
+ '&:hover': {
+ bg: 'background-hover',
+ },
+ '&:focus-visible': {
+ outline: 'none',
+ },
+ // Custom focus ring drawn on a pseudo-element (rather than the native
+ // outline) so it isn't clipped by the icon's small hit area.
+ '&::before': {
+ content: '""',
+ position: 'absolute',
+ inset: -2,
+ borderRadius: 'circle',
+ border: 2,
+ borderColor: 'primary',
+ opacity: 0,
+ },
+ '&:focus-visible::before': {
+ opacity: 1,
+ },
+ })
+);
+
+export const FieldHint: React.FC<FieldHintProps> = ({
+ hint,
+ label = 'More information',
+ id,
+ tipProps,
+ ...rest
+}) => (
+ <ToolTip id={id} info={hint} {...tipProps}>
+ <HintTrigger aria-label={label} as="button" type="button" {...rest}>
+ <MiniInfoOutlineIcon aria-hidden height={16} width={16} />
+ </HintTrigger>
+ </ToolTip>
+);