src/ui/knob.tsx (251 lines of code) (raw):
/*
Copyright (c) Uber Technologies, Inc.
This source code is licensed under the MIT license found in the
LICENSE file in the root directory of this source tree.
*/
import * as React from "react";
import { Popover } from "react-tiny-popover";
import type { TPropValue, TImportsConfig } from "../types";
import Error from "./error";
import Editor from "./editor";
import { PropTypes } from "../const";
import { useHover, useValueDebounce } from "../utils";
const getTooltip = (description: string, type: string, name: string) => (
<div
style={{
backgroundColor: "white",
border: "1px solid #CCC",
padding: "8px",
borderRadius: "5px",
fontFamily: "'Helvetica Neue', Arial",
fontSize: "14px",
}}
>
<b>{name}</b>: <i>{type}</i>
<br />
<br />
{description}
</div>
);
const Spacing: React.FC<{ children: React.ReactNode; name?: string }> = ({
children,
name,
}) => {
return (
<div
data-testid={`rv-knob-${name}`}
style={{
margin: "10px 0px",
fontFamily: "'Helvetica Neue', Arial",
fontSize: "14px",
}}
>
{children}
</div>
);
};
const Label: React.FC<{
children: React.ReactNode;
tooltip: React.ReactNode;
}> = ({ children, tooltip }) => {
const [hoverRef, isHover] = useHover();
return (
<Popover
isOpen={Boolean(isHover)}
positions={"top"}
content={<div>{tooltip}</div>}
ref={hoverRef as any}
>
<label
style={{
fontWeight: 500,
lineHeight: "20px",
}}
>
{children}{" "}
<span style={{ fontSize: "12px", fontWeight: 400 }}>[?]</span>
</label>
</Popover>
);
};
const BooleanKnob: React.FC<{
tooltip: React.ReactNode;
name: string;
val: boolean;
globalSet: (val: boolean) => void;
}> = ({ tooltip, name, val, globalSet }) => {
const [hoverRef, isHover] = useHover();
return (
<Popover
isOpen={Boolean(isHover)}
positions={"top"}
content={<div>{tooltip}</div>}
ref={hoverRef as any}
>
<div
style={{
display: "flex",
alignItems: "center",
fontWeight: 500,
}}
>
<input
id={name}
type="checkbox"
style={{ marginRight: "8px", marginLeft: "0px" }}
checked={Boolean(val)}
onChange={() => {
globalSet(!val);
}}
/>
<label htmlFor={name}>
{name} <span style={{ fontSize: "12px", fontWeight: 400 }}>[?]</span>
</label>
</div>
</Popover>
);
};
const Knob: React.FC<{
name: string;
error: string | null;
description: string;
val: TPropValue;
set: (val: TPropValue) => void;
type: PropTypes;
options?: { [key: string]: string };
placeholder?: string;
enumName?: string;
imports?: TImportsConfig;
}> = ({
name,
error,
type,
val: globalVal,
set: globalSet,
options = {},
description,
placeholder,
enumName,
imports,
}) => {
const [val] = useValueDebounce<TPropValue>(globalVal, globalSet);
const getEnumValue = (opt: string) =>
imports ? `${enumName || name.toUpperCase()}.${opt}` : opt;
switch (type) {
case PropTypes.Ref:
return (
<Spacing>
<Label tooltip={getTooltip(description, type, name)}>{name}</Label>
<a
href="https://reactjs.org/docs/refs-and-the-dom.html"
target="_blank"
rel="noopener noreferrer"
style={{
fontSize: "14px",
display: "block",
}}
>
React Ref documentation
</a>
<Error msg={error} isPopup />
</Spacing>
);
case PropTypes.Boolean:
return (
<Spacing>
<BooleanKnob
tooltip={getTooltip(description, type, name)}
val={Boolean(val)}
globalSet={globalSet}
name={name}
/>
<Error msg={error} isPopup />
</Spacing>
);
case PropTypes.Enum:
const optionsKeys = Object.keys(options);
const numberOfOptions = optionsKeys.length;
return (
<Spacing>
<Label tooltip={getTooltip(description, type, name)}>{name}</Label>
{numberOfOptions < 7 ? (
<div style={{ display: "flex", flexWrap: "wrap" }}>
{Object.keys(options).map((opt) => {
const enumValue = getEnumValue(opt);
return (
<div
style={{
marginRight: "16px",
marginBottom: "8px",
display: "flex",
alignItems: "center",
}}
key={opt}
>
<input
style={{ marginRight: "8px", marginLeft: "0px" }}
type="radio"
checked={enumValue === val}
key={opt}
id={`${name}_${opt}`}
value={enumValue}
name={`radio_${name}`}
onChange={(e) => globalSet(e.target.value)}
/>
<label htmlFor={`${name}_${opt}`}>{opt}</label>
</div>
);
})}
</div>
) : (
<select
onChange={(e) => globalSet(e.target.value)}
value={String(val)}
name={name}
style={{
display: "block",
padding: "8.5px 10px",
MozAppearance: "none",
WebkitAppearance: "none",
appearance: "none",
width: "100%",
border: "1px solid #CCC",
background: `url(data:image/svg+xml;base64,PHN2ZyBpZD0iTGF5ZXJfMSIgZGF0YS1uYW1lPSJMYXllciAxIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA0Ljk1IDEwIj48ZGVmcz48c3R5bGU+LmNscy0ye2ZpbGw6IzQ0NDt9PC9zdHlsZT48L2RlZnM+PHRpdGxlPmFycm93czwvdGl0bGU+PHBvbHlnb24gY2xhc3M9ImNscy0yIiBwb2ludHM9IjEuNDEgNC42NyAyLjQ4IDMuMTggMy41NCA0LjY3IDEuNDEgNC42NyIvPjxwb2x5Z29uIGNsYXNzPSJjbHMtMiIgcG9pbnRzPSIzLjU0IDUuMzMgMi40OCA2LjgyIDEuNDEgNS4zMyAzLjU0IDUuMzMiLz48L3N2Zz4=) no-repeat 95% 50%`,
fontSize: "14px",
borderRadius: "5px",
}}
>
{Object.keys(options).map((opt) => {
const enumValue = getEnumValue(opt);
return (
<option key={`${name}_${opt}`} value={enumValue}>
{opt}
</option>
);
})}
</select>
)}
<Error msg={error} isPopup />
</Spacing>
);
case PropTypes.ReactNode:
case PropTypes.Function:
case PropTypes.Array:
case PropTypes.Object:
case PropTypes.String:
case PropTypes.Date:
case PropTypes.Number:
return (
<Spacing name={name}>
<Label tooltip={getTooltip(description, type, name)}>{name}</Label>
<Editor
onChange={(code) => {
globalSet(code);
}}
code={val ? String(val) : ""}
placeholder={placeholder}
small
/>
<Error msg={error} isPopup />
</Spacing>
);
default:
return null;
}
};
export default Knob;