123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241 |
- import {Component, createRef} from 'react';
- import styled from '@emotion/styled';
- import {IconEdit} from 'sentry/icons';
- import space from 'sentry/styles/space';
- import {callIfFunction} from 'sentry/utils/callIfFunction';
- type Props = {
- name: string;
- className?: string;
- disabled?: boolean;
- placeholder?: string;
- required?: boolean;
- style?: React.CSSProperties;
- value?: string;
- } & React.DOMAttributes<HTMLInputElement>;
- type State = {
- isFocused: boolean;
- isHovering: boolean;
- };
- /**
- * InputInline is a cool pattern and @doralchan has confirmed that this has more
- * than 50% chance of being reused elsewhere in the app. However, adding it as a
- * form component has too much overhead for Discover2, so it'll be kept outside
- * for now.
- *
- * The props for this component take some cues from InputField.tsx
- *
- * The implementation uses HTMLDivElement with `contentEditable="true"`. This is
- * because we need the width to expand along with the content inside. There
- * isn't a way to easily do this with HTMLInputElement, especially with fonts
- * which are not fixed-width.
- *
- * If you are expecting the usual HTMLInputElement, this may have some quirky
- * behaviours that'll need your help to improve.
- *
- * TODO(leedongwei): Add to storybook
- * TODO(leedongwei): Add some tests
- */
- class InputInline extends Component<Props, State> {
- /**
- * HACK(leedongwei): ContentEditable does not have the property `value`. We
- * coerce its `innerText` to `value` so it will have similar behaviour as a
- * HTMLInputElement
- *
- * We probably need to attach this to every DOMAttribute event...
- */
- static setValueOnEvent(
- event: React.FormEvent<HTMLDivElement>
- ): React.FormEvent<HTMLInputElement> {
- const text: string =
- (event.target as HTMLDivElement).innerText ||
- (event.currentTarget as HTMLDivElement).innerText;
- (event.target as HTMLInputElement).value = text;
- (event.currentTarget as HTMLInputElement).value = text;
- return event as React.FormEvent<HTMLInputElement>;
- }
- state: State = {
- isFocused: false,
- isHovering: false,
- };
- componentWillUnmount() {
- window.clearTimeout(this.onFocusSelectAllTimeout);
- }
- onFocusSelectAllTimeout: number | undefined = undefined;
- private refInput = createRef<HTMLDivElement>();
- /**
- * Used by the parent to blur/focus on the Input
- */
- blur = () => {
- if (this.refInput.current) {
- this.refInput.current.blur();
- }
- };
- /**
- * Used by the parent to blur/focus on the Input
- */
- focus = () => {
- if (this.refInput.current) {
- this.refInput.current.focus();
- document.execCommand('selectAll', false, undefined);
- }
- };
- onBlur = (event: React.FocusEvent<HTMLDivElement>) => {
- this.setState({
- isFocused: false,
- isHovering: false,
- });
- callIfFunction(this.props.onBlur, InputInline.setValueOnEvent(event));
- };
- onFocus = (event: React.FocusEvent<HTMLDivElement>) => {
- this.setState({isFocused: true});
- callIfFunction(this.props.onFocus, InputInline.setValueOnEvent(event));
- window.clearTimeout(this.onFocusSelectAllTimeout);
- // Wait for the next event loop so that the content region has focus.
- this.onFocusSelectAllTimeout = window.setTimeout(
- () => document.execCommand('selectAll', false, undefined),
- 1
- );
- };
- /**
- * HACK(leedongwei): ContentEditable is not a Form element, and as such it
- * does not emit `onChange` events. This method using `onInput` and capture the
- * inner value to be passed along to an onChange function.
- */
- onChangeUsingOnInput = (event: React.FormEvent<HTMLDivElement>) => {
- callIfFunction(this.props.onChange, InputInline.setValueOnEvent(event));
- };
- onKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {
- // Might make sense to add Form submission here too
- if (event.key === 'Enter') {
- // Prevents the Enter key from inserting a line-break
- event.preventDefault();
- if (this.refInput.current) {
- this.refInput.current.blur();
- }
- }
- callIfFunction(this.props.onKeyUp, InputInline.setValueOnEvent(event));
- };
- onKeyUp = (event: React.KeyboardEvent<HTMLDivElement>) => {
- if (event.key === 'Escape' && this.refInput.current) {
- this.refInput.current.blur();
- }
- callIfFunction(this.props.onKeyUp, InputInline.setValueOnEvent(event));
- };
- onMouseEnter = () => {
- this.setState({isHovering: !this.props.disabled});
- };
- onMouseMove = () => {
- this.setState({isHovering: !this.props.disabled});
- };
- onMouseLeave = () => {
- this.setState({isHovering: false});
- };
- onClickIcon = (event: React.MouseEvent<HTMLDivElement>) => {
- if (this.props.disabled) {
- return;
- }
- if (this.refInput.current) {
- this.refInput.current.focus();
- document.execCommand('selectAll', false, undefined);
- }
- callIfFunction(this.props.onClick, InputInline.setValueOnEvent(event));
- };
- render() {
- const {value, placeholder, disabled} = this.props;
- const {isFocused} = this.state;
- const innerText = value || placeholder || '';
- return (
- <Wrapper
- style={this.props.style}
- onMouseEnter={this.onMouseEnter}
- onMouseMove={this.onMouseMove}
- onMouseLeave={this.onMouseLeave}
- >
- <Input
- {...this.props} // Pass DOMAttributes props first, extend/overwrite below
- ref={this.refInput}
- suppressContentEditableWarning
- contentEditable={!this.props.disabled}
- isHovering={this.state.isHovering}
- isDisabled={this.props.disabled}
- onBlur={this.onBlur}
- onFocus={this.onFocus}
- onInput={this.onChangeUsingOnInput}
- onChange={this.onChangeUsingOnInput} // Overwrite onChange too, just to be 100% sure
- onKeyDown={this.onKeyDown}
- onKeyUp={this.onKeyUp}
- >
- {innerText}
- </Input>
- {!isFocused && !disabled && (
- <div onClick={this.onClickIcon}>
- <StyledIconEdit />
- </div>
- )}
- </Wrapper>
- );
- }
- }
- const Wrapper = styled('div')`
- display: inline-flex;
- align-items: center;
- vertical-align: text-bottom;
- `;
- const Input = styled('div')<{
- isDisabled?: boolean;
- isHovering?: boolean;
- }>`
- min-width: 40px;
- margin: 0;
- border: 1px solid ${p => (p.isHovering ? p.theme.border : 'transparent')};
- outline: none;
- line-height: inherit;
- border-radius: ${space(0.5)};
- background: transparent;
- padding: 1px;
- &:focus,
- &:active {
- border: 1px solid ${p => (p.isDisabled ? 'transparent' : p.theme.border)};
- background-color: ${p => (p.isDisabled ? 'transparent' : p.theme.gray200)};
- }
- `;
- const StyledIconEdit = styled(IconEdit)`
- color: ${p => p.theme.gray300};
- margin-left: ${space(0.5)};
- &:hover {
- cursor: pointer;
- }
- `;
- export default InputInline;
|