index.tsx 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219
  1. import QuestionTooltip from 'sentry/components/questionTooltip';
  2. import ControlState, {ControlStateProps} from './controlState';
  3. import FieldControl, {FieldControlProps} from './fieldControl';
  4. import FieldDescription from './fieldDescription';
  5. import FieldErrorReason from './fieldErrorReason';
  6. import FieldHelp from './fieldHelp';
  7. import FieldLabel from './fieldLabel';
  8. import FieldQuestion from './fieldQuestion';
  9. import FieldRequiredBadge from './fieldRequiredBadge';
  10. import FieldWrapper, {FieldWrapperProps} from './fieldWrapper';
  11. interface InheritedFieldWrapperProps
  12. extends Pick<
  13. FieldWrapperProps,
  14. 'inline' | 'stacked' | 'highlighted' | 'hasControlState'
  15. > {}
  16. interface InheritedFieldControlProps
  17. extends Omit<
  18. FieldControlProps,
  19. 'children' | 'disabled' | 'className' | 'help' | 'errorState'
  20. > {}
  21. interface InheritedControlStateProps
  22. extends Omit<ControlStateProps, 'children' | 'error'> {}
  23. export interface FieldProps
  24. extends InheritedFieldControlProps,
  25. InheritedFieldWrapperProps,
  26. InheritedControlStateProps {
  27. // TODO(TS): Do we need this?
  28. /**
  29. * The control to render. May be given a function to render with resolved
  30. * props.
  31. */
  32. children?: React.ReactNode | ((props: ChildRenderProps) => React.ReactNode);
  33. /**
  34. * The classname of the field
  35. */
  36. className?: string;
  37. /**
  38. * The classname of the field control
  39. */
  40. controlClassName?: string;
  41. /**
  42. * Should field be disabled?
  43. */
  44. disabled?: boolean | ((props: FieldProps) => boolean);
  45. /**
  46. * Error message to display for the field
  47. */
  48. error?: string;
  49. /**
  50. * Help or description of the field
  51. */
  52. help?: React.ReactNode | React.ReactElement | ((props: FieldProps) => React.ReactNode);
  53. /**
  54. * Should the label be rendered for the field?
  55. */
  56. hideLabel?: boolean;
  57. /**
  58. * The control's `id` property
  59. */
  60. id?: string;
  61. /**
  62. * User-facing field name
  63. */
  64. label?: React.ReactNode;
  65. /**
  66. * May be used to give the field an aria-label when the field's label is a
  67. * react node.
  68. */
  69. labelText?: string;
  70. /**
  71. * Show "required" indicator
  72. */
  73. required?: boolean;
  74. /**
  75. * Displays the help element in the tooltip
  76. */
  77. showHelpInTooltip?: boolean;
  78. /**
  79. * Additional inline styles for the field
  80. */
  81. style?: React.CSSProperties;
  82. validate?: Function;
  83. /**
  84. * Should field be visible
  85. */
  86. visible?: boolean | ((props: FieldProps) => boolean);
  87. }
  88. interface ChildRenderProps extends Omit<FieldProps, 'className' | 'disabled'> {
  89. controlState: React.ReactNode;
  90. errorState: React.ReactNode | null;
  91. help: React.ReactNode;
  92. disabled?: boolean;
  93. }
  94. /**
  95. * A component to render a Field (i.e. label + help + form "control"),
  96. * generally inside of a Panel.
  97. *
  98. * This is unconnected to any Form state
  99. */
  100. function Field({
  101. className,
  102. alignRight = false,
  103. inline = true,
  104. disabled = false,
  105. required = false,
  106. visible = true,
  107. showHelpInTooltip = false,
  108. ...props
  109. }: FieldProps) {
  110. const otherProps = {
  111. alignRight,
  112. inline,
  113. disabled,
  114. required,
  115. visible,
  116. showHelpInTooltip,
  117. ...props,
  118. };
  119. const isVisible = typeof visible === 'function' ? visible(otherProps) : visible;
  120. const isDisabled = typeof disabled === 'function' ? disabled(otherProps) : disabled;
  121. if (!isVisible) {
  122. return null;
  123. }
  124. const {
  125. controlClassName,
  126. highlighted,
  127. disabledReason,
  128. error,
  129. flexibleControlStateSize,
  130. help,
  131. id,
  132. isSaving,
  133. isSaved,
  134. label,
  135. labelText,
  136. hideLabel,
  137. stacked,
  138. children,
  139. style,
  140. } = otherProps;
  141. const helpElement = typeof help === 'function' ? help(otherProps) : help;
  142. const shouldRenderLabel = !hideLabel && !!label;
  143. const controlProps = {
  144. className: controlClassName,
  145. inline,
  146. alignRight,
  147. disabled: isDisabled,
  148. disabledReason,
  149. flexibleControlStateSize,
  150. help: helpElement,
  151. errorState: error ? <FieldErrorReason>{error}</FieldErrorReason> : null,
  152. controlState: <ControlState error={error} isSaving={isSaving} isSaved={isSaved} />,
  153. };
  154. // See comments in prop types
  155. const control =
  156. typeof children === 'function' ? (
  157. children({...otherProps, ...controlProps})
  158. ) : (
  159. <FieldControl {...controlProps}>{children}</FieldControl>
  160. );
  161. // Provide an `aria-label` to the FieldDescription label if our label is a
  162. // string value. This helps with testing and accessability. Without this the
  163. // aria label contains the entire description.
  164. const ariaLabel = labelText ?? (typeof label === 'string' ? label : undefined);
  165. // The help ID is used for the input element to have an `aria-describedby`
  166. const helpId = `${id}_help`;
  167. return (
  168. <FieldWrapper
  169. className={className}
  170. inline={inline}
  171. stacked={stacked}
  172. highlighted={highlighted}
  173. hasControlState={!flexibleControlStateSize}
  174. style={style}
  175. >
  176. {(shouldRenderLabel || helpElement) && (
  177. <FieldDescription inline={inline} htmlFor={id} aria-label={ariaLabel}>
  178. {shouldRenderLabel && (
  179. <FieldLabel disabled={isDisabled}>
  180. <span>
  181. {label}
  182. {required && <FieldRequiredBadge />}
  183. </span>
  184. {helpElement && showHelpInTooltip && (
  185. <FieldQuestion>
  186. <QuestionTooltip position="top" size="sm" title={helpElement} />
  187. </FieldQuestion>
  188. )}
  189. </FieldLabel>
  190. )}
  191. {helpElement && !showHelpInTooltip && (
  192. <FieldHelp id={helpId} stacked={stacked} inline={inline}>
  193. {helpElement}
  194. </FieldHelp>
  195. )}
  196. </FieldDescription>
  197. )}
  198. {control}
  199. </FieldWrapper>
  200. );
  201. }
  202. export default Field;